Improve D&D validation reliability

This commit is contained in:
2026-08-29 01:24:45 +00:00
parent 4da9360d74
commit 917d150279
55 changed files with 1300 additions and 565 deletions

View File

@@ -66,7 +66,7 @@ func TestExtractMapsAndOrdersCombatTurnsBySourcePosition(t *testing.T) {
func TestExtractPreservesInvalidCandidatesForValidators(t *testing.T) {
client := &fakeCombatTurnsLLMClient{response: extractionResponse{CombatTurns: []combatTurnResponse{
{
Actor: " ", TurnKind: "unsupported",
Actor: " ", TurnKind: "turn",
SourceRefs: []combatSourceRefResponse{{StartUnitID: 99, EndUnitID: 0}},
},
}}}
@@ -75,7 +75,7 @@ func TestExtractPreservesInvalidCandidatesForValidators(t *testing.T) {
t.Fatalf("Extract() error = %v, want nil for candidate values", err)
}
turn := result.Value.CombatTurns[0]
if turn.Actor != " " || turn.TurnKind != "unsupported" {
if turn.Actor != " " || turn.TurnKind != "turn" {
t.Fatalf("invalid turn fields = %#v, want preserved candidate values", turn)
}
if turn.SourceRefs[0] != (source.SourceRef{SourceID: "session-alpha", StartUnitID: 99}) {

View File

@@ -7,6 +7,8 @@ import (
"testing"
"github.com/santhosh-tekuri/jsonschema/v6"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
)
func TestLoadResponseSchemaUsesPrivateCombatShape(t *testing.T) {
@@ -37,7 +39,7 @@ func TestLoadResponseSchemaUsesPrivateCombatShape(t *testing.T) {
}
}
func TestResponseSchemaLeavesSemanticConstraintsToDeterministicValidators(t *testing.T) {
func TestResponseSchemaLeavesNonCategoricalSemanticsToDeterministicValidators(t *testing.T) {
schema, err := loadResponseSchema()
if err != nil {
t.Fatal(err)
@@ -45,7 +47,6 @@ func TestResponseSchemaLeavesSemanticConstraintsToDeterministicValidators(t *tes
semanticCandidate := validCombatResponse()
turn := semanticCandidate["combat_turns"].([]any)[0].(map[string]any)
turn["actor"] = ""
turn["turn_kind"] = "unsupported"
ref := turn["source_refs"].([]any)[0].(map[string]any)
ref["start_unit_id"] = 0
ref["end_unit_id"] = -1
@@ -54,7 +55,7 @@ func TestResponseSchemaLeavesSemanticConstraintsToDeterministicValidators(t *tes
t.Fatal(err)
}
if err := validateJSONSchema(content, schema.JSONSchema); err != nil {
t.Fatalf("private schema rejected validator-owned semantics: %v", err)
t.Fatalf("private schema rejected non-categorical validator-owned semantics: %v", err)
}
turn["source_refs"] = []any{}
content, err = json.Marshal(semanticCandidate)
@@ -79,6 +80,7 @@ func TestResponseSchemaRetainsStructuralBoundary(t *testing.T) {
{name: "wrong actor type", mutate: func(turn map[string]any) { turn["actor"] = 1 }},
{name: "unknown field", mutate: func(turn map[string]any) { turn["unexpected"] = true }},
{name: "missing source refs", mutate: func(turn map[string]any) { delete(turn, "source_refs") }},
{name: "unsupported turn kind", mutate: func(turn map[string]any) { turn["turn_kind"] = "unsupported" }},
} {
t.Run(test.name, func(t *testing.T) {
candidate := validCombatResponse()
@@ -94,6 +96,24 @@ func TestResponseSchemaRetainsStructuralBoundary(t *testing.T) {
}
}
func TestResponseSchemaAcceptsEverySupportedTurnKind(t *testing.T) {
schema, err := loadResponseSchema()
if err != nil {
t.Fatal(err)
}
for _, kind := range []string{string(dnd.CombatTurnKindTurn), string(dnd.CombatTurnKindReaction), string(dnd.CombatTurnKindLegendaryAction), string(dnd.CombatTurnKindLairAction), string(dnd.CombatTurnKindOther)} {
candidate := validCombatResponse()
candidate["combat_turns"].([]any)[0].(map[string]any)["turn_kind"] = kind
content, err := json.Marshal(candidate)
if err != nil {
t.Fatal(err)
}
if err := validateJSONSchema(content, schema.JSONSchema); err != nil {
t.Fatalf("supported turn kind %q was rejected: %v", kind, err)
}
}
}
func TestResponseSchemaJSONIsMutationSafe(t *testing.T) {
first, err := loadResponseSchema()
if err != nil {