173 lines
4.9 KiB
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
|
|
}
|
|
}
|