Add proposal application fixtures

This commit is contained in:
2026-05-11 13:27:08 +00:00
parent f461922b9b
commit 5c78b1d5d9
17 changed files with 593 additions and 0 deletions

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

View File

@@ -0,0 +1,7 @@
// Package proposals provides deterministic, literal proposal preview and
// application helpers for transcript rewriting.
//
// The package only enforces structural and replacement-safety rules.
// It does not perform semantic validation; LLM-backed and deterministic
// validators are layered later in the pipeline.
package proposals

View File

@@ -0,0 +1,25 @@
{
"policy": "require_unique",
"transcript": {
"segments": [
{
"id": 1,
"speaker": "A",
"start": 0,
"end": 2,
"text": "rank rank"
}
]
},
"proposals": [
{
"proposal_index": 0,
"module_key": "homophones",
"module_instance": "homophones",
"id": 1,
"original_text": "rank",
"corrected_text": "Hrank",
"confidence": 0.9
}
]
}

View File

@@ -0,0 +1,34 @@
{
"policy": "require_unique",
"transcript": {
"segments": [
{
"id": 1,
"speaker": "Narrator",
"start": 0,
"end": 3,
"text": "status",
"categories": ["meta", "state"]
},
{
"id": 2,
"speaker": "Player",
"start": 3,
"end": 6,
"text": "rank",
"categories": ["combat"]
}
]
},
"proposals": [
{
"proposal_index": 0,
"module_key": "glossary",
"module_instance": "glossary_1",
"id": 2,
"original_text": "rank",
"corrected_text": "Hrank",
"confidence": 0.9
}
]
}

View File

@@ -0,0 +1,20 @@
{
"segments": [
{
"id": 1,
"speaker": "Narrator",
"start": 0,
"end": 3,
"text": "status",
"categories": ["meta", "state"]
},
{
"id": 2,
"speaker": "Player",
"start": 3,
"end": 6,
"text": "Hrank",
"categories": ["combat"]
}
]
}

View File

@@ -0,0 +1,25 @@
{
"policy": "require_unique",
"transcript": {
"segments": [
{
"id": 1,
"speaker": "A",
"start": 0,
"end": 1,
"text": "hello"
}
]
},
"proposals": [
{
"proposal_index": 0,
"module_key": "grammar",
"module_instance": "grammar_1",
"id": 99,
"original_text": "hello",
"corrected_text": "hi",
"confidence": 0.9
}
]
}

View File

@@ -0,0 +1,68 @@
{
"policy": "require_unique",
"transcript": {
"segments": [
{
"id": 1,
"speaker": "A",
"start": 0,
"end": 4,
"text": "foo bar rank rank"
},
{
"id": 2,
"speaker": "B",
"start": 4,
"end": 5,
"text": "hello"
}
]
},
"proposals": [
{
"proposal_index": 0,
"module_key": "grammar",
"module_instance": "grammar_1",
"id": 1,
"original_text": "foo bar",
"corrected_text": "foo-bar",
"confidence": 0.9
},
{
"proposal_index": 1,
"module_key": "grammar",
"module_instance": "grammar_1",
"id": 1,
"original_text": "foo bar",
"corrected_text": "foobar",
"confidence": 0.9
},
{
"proposal_index": 2,
"module_key": "homophones",
"module_instance": "homophones",
"id": 1,
"original_text": "rank",
"corrected_text": "Hrank",
"confidence": 0.9
},
{
"proposal_index": 3,
"module_key": "grammar",
"module_instance": "grammar_1",
"id": 99,
"original_text": "x",
"corrected_text": "y",
"confidence": 0.9
},
{
"proposal_index": 4,
"module_key": "spoken_word",
"module_instance": "spoken_word",
"id": 2,
"original_text": "hello",
"corrected_text": "hello",
"confidence": 0.9
}
]
}

View File

@@ -0,0 +1,55 @@
{
"applied_changes": [
{
"proposal_index": 0,
"module_key": "grammar",
"module_instance": "grammar_1",
"target_segment_id": 1,
"original_text": "foo bar",
"corrected_text": "foo-bar",
"replacement_count": 1
}
],
"skipped_changes": [
{
"proposal_index": 1,
"module_key": "grammar",
"module_instance": "grammar_1",
"target_segment_id": 1,
"original_text": "foo bar",
"corrected_text": "foobar",
"skip_reason": "missing_original_text",
"message": "original_text was not found in current segment text"
},
{
"proposal_index": 2,
"module_key": "homophones",
"module_instance": "homophones",
"target_segment_id": 1,
"original_text": "rank",
"corrected_text": "Hrank",
"skip_reason": "ambiguous_original_text",
"message": "original_text matched multiple spans under require_unique policy"
},
{
"proposal_index": 3,
"module_key": "grammar",
"module_instance": "grammar_1",
"target_segment_id": 99,
"original_text": "x",
"corrected_text": "y",
"skip_reason": "missing_segment",
"message": "target segment was not found"
},
{
"proposal_index": 4,
"module_key": "spoken_word",
"module_instance": "spoken_word",
"target_segment_id": 2,
"original_text": "hello",
"corrected_text": "hello",
"skip_reason": "no_effect",
"message": "original_text and corrected_text are identical"
}
]
}

View File

@@ -0,0 +1,34 @@
{
"policy": "require_unique",
"transcript": {
"segments": [
{
"id": 1,
"speaker": "A",
"start": 0,
"end": 2,
"text": "foo bar"
}
]
},
"proposals": [
{
"proposal_index": 2,
"module_key": "grammar",
"module_instance": "grammar_1",
"id": 1,
"original_text": "bar",
"corrected_text": "BAR",
"confidence": 0.9
},
{
"proposal_index": 1,
"module_key": "grammar",
"module_instance": "grammar_1",
"id": 1,
"original_text": "foo",
"corrected_text": "FOO",
"confidence": 0.9
}
]
}

View File

@@ -0,0 +1,11 @@
{
"segments": [
{
"id": 1,
"speaker": "A",
"start": 0,
"end": 2,
"text": "FOO BAR"
}
]
}

View File

@@ -0,0 +1,25 @@
{
"policy": "replace_all",
"transcript": {
"segments": [
{
"id": 1,
"speaker": "A",
"start": 0,
"end": 2,
"text": "uh uh uh"
}
]
},
"proposals": [
{
"proposal_index": 0,
"module_key": "spoken_word",
"module_instance": "spoken_word",
"id": 1,
"original_text": "uh",
"corrected_text": "um",
"confidence": 0.9
}
]
}

View File

@@ -0,0 +1,11 @@
{
"segments": [
{
"id": 1,
"speaker": "A",
"start": 0,
"end": 2,
"text": "um um um"
}
]
}

View File

@@ -0,0 +1,25 @@
{
"policy": "replace_all",
"transcript": {
"segments": [
{
"id": 1,
"speaker": "A",
"start": 0,
"end": 2,
"text": "ha-ha-ha"
}
]
},
"proposals": [
{
"proposal_index": 0,
"module_key": "spoken_word",
"module_instance": "spoken_word",
"id": 1,
"original_text": "ha",
"corrected_text": "ho",
"confidence": 0.9
}
]
}

View File

@@ -0,0 +1,11 @@
{
"segments": [
{
"id": 1,
"speaker": "A",
"start": 0,
"end": 2,
"text": "ho-ho-ho"
}
]
}

View File

@@ -0,0 +1,25 @@
{
"policy": "require_unique",
"transcript": {
"segments": [
{
"id": 1,
"speaker": "A",
"start": 0,
"end": 1.25,
"text": "gestures"
}
]
},
"proposals": [
{
"proposal_index": 0,
"module_key": "glossary",
"module_instance": "glossary_1",
"id": 1,
"original_text": "gestures",
"corrected_text": "Jesters",
"confidence": 0.95
}
]
}

View File

@@ -0,0 +1,11 @@
{
"segments": [
{
"id": 1,
"speaker": "A",
"start": 0,
"end": 1.25,
"text": "Jesters"
}
]
}

View File

@@ -0,0 +1,34 @@
{
"policy": "require_unique",
"transcript": {
"segments": [
{
"id": 1,
"speaker": "A",
"start": 0,
"end": 2,
"text": "foo bar"
}
]
},
"proposals": [
{
"proposal_index": 0,
"module_key": "grammar",
"module_instance": "grammar_1",
"id": 1,
"original_text": "foo bar",
"corrected_text": "foo-bar",
"confidence": 0.9
},
{
"proposal_index": 1,
"module_key": "grammar",
"module_instance": "grammar_1",
"id": 1,
"original_text": "foo bar",
"corrected_text": "foobar",
"confidence": 0.9
}
]
}