Add proposal application fixtures
This commit is contained in:
172
internal/framework/proposals/apply_fixtures_test.go
Normal file
172
internal/framework/proposals/apply_fixtures_test.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package proposals
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
||||
)
|
||||
|
||||
type applyFixture struct {
|
||||
Policy string `json:"policy"`
|
||||
Transcript schema.Transcript `json:"transcript"`
|
||||
Proposals []EnrichedCorrectionProposal `json:"proposals"`
|
||||
}
|
||||
|
||||
type recordsPayload struct {
|
||||
AppliedChanges []AppliedChange `json:"applied_changes"`
|
||||
SkippedChanges []SkippedChange `json:"skipped_changes"`
|
||||
}
|
||||
|
||||
func TestApplyProposalsFixtureTranscriptGoldens(t *testing.T) {
|
||||
testCases := []string{
|
||||
"simple_replacement",
|
||||
"multiple_replacements_one_segment",
|
||||
"multiple_proposals_one_segment",
|
||||
"replace_all_behavior",
|
||||
"categories_metadata_preservation",
|
||||
}
|
||||
|
||||
for _, name := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
fixture := readApplyFixture(t, name+".input.json")
|
||||
policy, err := ParseReplacementPolicy(fixture.Policy)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse fixture policy: %v", err)
|
||||
}
|
||||
|
||||
result := ApplyProposals(&fixture.Transcript, fixture.Proposals, policy)
|
||||
assertJSONSemanticallyEqualFromGolden(t, result.Transcript, name+".transcript.golden.json")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyProposalsFixtureSkipCoverage(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
expectedSkipReason ProposalSkipReason
|
||||
}{
|
||||
{name: "stale_after_earlier_replacement", expectedSkipReason: SkipReasonMissingOriginalText},
|
||||
{name: "ambiguous_require_unique", expectedSkipReason: SkipReasonAmbiguousOriginal},
|
||||
{name: "missing_segment", expectedSkipReason: SkipReasonMissingSegment},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fixture := readApplyFixture(t, tc.name+".input.json")
|
||||
policy, err := ParseReplacementPolicy(fixture.Policy)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse fixture policy: %v", err)
|
||||
}
|
||||
|
||||
result := ApplyProposals(&fixture.Transcript, fixture.Proposals, policy)
|
||||
if len(result.Skipped) == 0 {
|
||||
t.Fatal("expected at least one skipped change")
|
||||
}
|
||||
|
||||
if got := result.Skipped[0].SkipReason; got != tc.expectedSkipReason {
|
||||
t.Fatalf("unexpected skip reason: got %q want %q", got, tc.expectedSkipReason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyProposalsFixtureMixedScenarioRecordGoldens(t *testing.T) {
|
||||
fixture := readApplyFixture(t, "mixed_scenarios.input.json")
|
||||
policy, err := ParseReplacementPolicy(fixture.Policy)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse fixture policy: %v", err)
|
||||
}
|
||||
|
||||
result := ApplyProposals(&fixture.Transcript, fixture.Proposals, policy)
|
||||
payload := recordsPayload{
|
||||
AppliedChanges: result.Applied,
|
||||
SkippedChanges: result.Skipped,
|
||||
}
|
||||
|
||||
// Records must be JSON-serializable for diagnostics/reporting.
|
||||
if _, err := json.Marshal(payload.AppliedChanges); err != nil {
|
||||
t.Fatalf("failed to marshal applied changes: %v", err)
|
||||
}
|
||||
if _, err := json.Marshal(payload.SkippedChanges); err != nil {
|
||||
t.Fatalf("failed to marshal skipped changes: %v", err)
|
||||
}
|
||||
if _, err := json.Marshal(payload); err != nil {
|
||||
t.Fatalf("failed to marshal combined records payload: %v", err)
|
||||
}
|
||||
|
||||
for i, skipped := range payload.SkippedChanges {
|
||||
if !isStableSkipReason(skipped.SkipReason) {
|
||||
t.Fatalf("unstable skip reason at skipped[%d]: %q", i, skipped.SkipReason)
|
||||
}
|
||||
}
|
||||
|
||||
assertJSONSemanticallyEqualFromGolden(t, payload, "mixed_scenarios.records.golden.json")
|
||||
}
|
||||
|
||||
func readApplyFixture(t *testing.T, fileName string) applyFixture {
|
||||
t.Helper()
|
||||
path := filepath.Join("testdata", fileName)
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read fixture %s: %v", path, err)
|
||||
}
|
||||
|
||||
var fixture applyFixture
|
||||
if err := json.Unmarshal(raw, &fixture); err != nil {
|
||||
t.Fatalf("failed to parse fixture %s: %v", path, err)
|
||||
}
|
||||
|
||||
return fixture
|
||||
}
|
||||
|
||||
func assertJSONSemanticallyEqualFromGolden(t *testing.T, actual any, goldenFileName string) {
|
||||
t.Helper()
|
||||
goldenPath := filepath.Join("testdata", goldenFileName)
|
||||
expectedRaw, err := os.ReadFile(goldenPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read golden file %s: %v", goldenPath, err)
|
||||
}
|
||||
|
||||
assertJSONSemanticallyEqual(t, actual, expectedRaw)
|
||||
}
|
||||
|
||||
func assertJSONSemanticallyEqual(t *testing.T, actual any, expectedJSON []byte) {
|
||||
t.Helper()
|
||||
|
||||
actualRaw, err := json.Marshal(actual)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal actual JSON: %v", err)
|
||||
}
|
||||
|
||||
var actualValue any
|
||||
if err := json.Unmarshal(actualRaw, &actualValue); err != nil {
|
||||
t.Fatalf("failed to parse marshaled actual JSON: %v", err)
|
||||
}
|
||||
|
||||
var expectedValue any
|
||||
if err := json.Unmarshal(expectedJSON, &expectedValue); err != nil {
|
||||
t.Fatalf("failed to parse expected JSON: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actualValue, expectedValue) {
|
||||
t.Fatalf("semantic JSON mismatch\nactual: %s\nexpected: %s", string(actualRaw), string(expectedJSON))
|
||||
}
|
||||
}
|
||||
|
||||
func isStableSkipReason(reason ProposalSkipReason) bool {
|
||||
switch reason {
|
||||
case
|
||||
SkipReasonMissingSegment,
|
||||
SkipReasonMissingOriginalText,
|
||||
SkipReasonAmbiguousOriginal,
|
||||
SkipReasonNoEffect,
|
||||
SkipReasonInvalidProposal:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user