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 } }