Files
audita/internal/framework/proposals/apply_fixtures_test.go

173 lines
4.9 KiB
Go

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