Refactor the D&D spells module to apply deterministic fields where appropriate
This commit is contained in:
@@ -11,7 +11,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd"
|
||||
)
|
||||
|
||||
func TestExtractReturnsRawOutputFromStructuredResponse(t *testing.T) {
|
||||
func TestExtractReturnsCanonicalOutputFromStructuredResponse(t *testing.T) {
|
||||
client := &fakeSpellsLLMClient{
|
||||
response: extractionResponse{
|
||||
SpellCasts: []spellCastResponse{
|
||||
@@ -20,7 +20,7 @@ func TestExtractReturnsRawOutputFromStructuredResponse(t *testing.T) {
|
||||
Spell: " Cure Wounds ",
|
||||
Effect: " Heals an injured ally. ",
|
||||
NarrativeDescription: " Aria restores the fighter after the fight. ",
|
||||
SourceRefs: responseSourceRefsInt("session-alpha", 1, 2),
|
||||
SourceRefs: responseSourceRefsInt("transcript", 1, 2),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -63,8 +63,8 @@ func TestExtractReturnsRawOutputFromStructuredResponse(t *testing.T) {
|
||||
if !json.Valid(result.Output.Schema.JSONSchema) {
|
||||
t.Fatalf("schema JSON is invalid or missing: %s", result.Output.Schema.JSONSchema)
|
||||
}
|
||||
if got := string(result.Output.Payload.Content); got != string(client.content) {
|
||||
t.Fatalf("content = %q, want exact raw completion content", got)
|
||||
if strings.Contains(string(result.Output.Payload.Content), "raw_marker") {
|
||||
t.Fatalf("content = %q, want canonical payload without raw completion marker", result.Output.Payload.Content)
|
||||
}
|
||||
|
||||
var payload extractionResponse
|
||||
@@ -72,7 +72,10 @@ func TestExtractReturnsRawOutputFromStructuredResponse(t *testing.T) {
|
||||
t.Fatalf("Unmarshal(Content) error = %v, want nil", err)
|
||||
}
|
||||
if len(payload.SpellCasts) != 1 || payload.SpellCasts[0].Spell != " Cure Wounds " {
|
||||
t.Fatalf("payload = %#v, want raw structured response", payload)
|
||||
t.Fatalf("payload = %#v, want structured response fields", payload)
|
||||
}
|
||||
if got := payload.SpellCasts[0].SourceRefs[0].SourceID; got != "session-alpha" {
|
||||
t.Fatalf("source_id = %q, want canonical source document ID", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +190,7 @@ func TestExtractReturnsRawOutputForEmptyResponse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractCarriesMalformedStructuredContentAsRawOutput(t *testing.T) {
|
||||
func TestExtractReturnsCanonicalOutputForMalformedStructuredResponse(t *testing.T) {
|
||||
client := &fakeSpellsLLMClient{response: extractionResponse{}}
|
||||
|
||||
result, err := New().Extract(context.Background(), extractionRequestWithClient(client))
|
||||
@@ -195,7 +198,7 @@ func TestExtractCarriesMalformedStructuredContentAsRawOutput(t *testing.T) {
|
||||
t.Fatalf("Extract() error = %v, want nil", err)
|
||||
}
|
||||
if string(result.Output.Payload.Content) != `{"spell_casts":null}` {
|
||||
t.Fatalf("content = %s, want raw structured output", result.Output.Payload.Content)
|
||||
t.Fatalf("content = %s, want canonical structured output", result.Output.Payload.Content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,10 +250,17 @@ func TestExtractRejectsInvalidRequests(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractPreservesResponseOrder(t *testing.T) {
|
||||
func TestExtractOrdersSpellCastsByEarliestSourceUnit(t *testing.T) {
|
||||
client := &fakeSpellsLLMClient{
|
||||
response: extractionResponse{
|
||||
SpellCasts: []spellCastResponse{
|
||||
{
|
||||
Caster: "Bandit Shaman",
|
||||
Spell: "Fire Bolt",
|
||||
Effect: "Burns.",
|
||||
NarrativeDescription: "Second spell.",
|
||||
SourceRefs: responseSourceRefs("session-alpha", 2, 2),
|
||||
},
|
||||
{
|
||||
Caster: "Aria",
|
||||
Spell: "Cure Wounds",
|
||||
@@ -259,11 +269,10 @@ func TestExtractPreservesResponseOrder(t *testing.T) {
|
||||
SourceRefs: responseSourceRefs("session-alpha", 1, 1),
|
||||
},
|
||||
{
|
||||
Caster: "Bandit Shaman",
|
||||
Spell: "Fire Bolt",
|
||||
Effect: "Burns.",
|
||||
NarrativeDescription: "Second spell.",
|
||||
SourceRefs: responseSourceRefs("session-alpha", 2, 2),
|
||||
Caster: "Narrator",
|
||||
Spell: "Unknown Spell",
|
||||
Effect: "No cited range.",
|
||||
NarrativeDescription: "This should sort after cited spell casts.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -277,8 +286,84 @@ func TestExtractPreservesResponseOrder(t *testing.T) {
|
||||
if err := json.Unmarshal(result.Output.Payload.Content, &payload); err != nil {
|
||||
t.Fatalf("Unmarshal(Content) error = %v, want nil", err)
|
||||
}
|
||||
if len(payload.SpellCasts) != 2 || payload.SpellCasts[0].Spell != "Cure Wounds" || payload.SpellCasts[1].Spell != "Fire Bolt" {
|
||||
t.Fatalf("spell order = %#v, want response order", payload.SpellCasts)
|
||||
if len(payload.SpellCasts) != 3 ||
|
||||
payload.SpellCasts[0].Spell != "Cure Wounds" ||
|
||||
payload.SpellCasts[1].Spell != "Fire Bolt" ||
|
||||
payload.SpellCasts[2].Spell != "Unknown Spell" {
|
||||
t.Fatalf("spell order = %#v, want earliest source-unit order with uncited spell last", payload.SpellCasts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractCanonicalizesSourceRefs(t *testing.T) {
|
||||
client := &fakeSpellsLLMClient{
|
||||
response: extractionResponse{
|
||||
SpellCasts: []spellCastResponse{
|
||||
{
|
||||
Caster: "Aria",
|
||||
Spell: "Cure Wounds",
|
||||
Effect: "Heals.",
|
||||
NarrativeDescription: "Aria heals.",
|
||||
SourceRefs: []dnd.SourceRefResponse{
|
||||
{SourceID: "gameplay_transcript", StartUnitID: dnd.UnitRefFromInt(2), EndUnitID: dnd.UnitRefFromInt(2)},
|
||||
{SourceID: "", StartUnitID: dnd.UnitRefFromInt(1), EndUnitID: dnd.UnitRefFromInt(2)},
|
||||
{SourceID: "transcript", StartUnitID: dnd.UnitRefFromInt(1), EndUnitID: dnd.UnitRefFromInt(2)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := New().Extract(context.Background(), extractionRequestWithClient(client))
|
||||
if err != nil {
|
||||
t.Fatalf("Extract() error = %v, want nil", err)
|
||||
}
|
||||
var payload extractionResponse
|
||||
if err := json.Unmarshal(result.Output.Payload.Content, &payload); err != nil {
|
||||
t.Fatalf("Unmarshal(Content) error = %v, want nil", err)
|
||||
}
|
||||
refs := payload.SpellCasts[0].SourceRefs
|
||||
if len(refs) != 2 {
|
||||
t.Fatalf("source refs = %#v, want duplicate collapsed", refs)
|
||||
}
|
||||
for _, ref := range refs {
|
||||
if ref.SourceID != "session-alpha" {
|
||||
t.Fatalf("source ref = %#v, want canonical source_id", ref)
|
||||
}
|
||||
}
|
||||
if refs[0].StartUnitID.Int() != 1 || refs[0].EndUnitID.Int() != 2 ||
|
||||
refs[1].StartUnitID.Int() != 2 || refs[1].EndUnitID.Int() != 2 {
|
||||
t.Fatalf("source refs = %#v, want sorted unit ranges", refs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractPreservesInvalidSourceRefsForValidators(t *testing.T) {
|
||||
client := &fakeSpellsLLMClient{
|
||||
response: extractionResponse{
|
||||
SpellCasts: []spellCastResponse{
|
||||
{
|
||||
Caster: "Aria",
|
||||
Spell: "Cure Wounds",
|
||||
Effect: "Heals.",
|
||||
NarrativeDescription: "Aria heals.",
|
||||
SourceRefs: []dnd.SourceRefResponse{
|
||||
{SourceID: "transcript", StartUnitID: dnd.UnitRefFromInt(99), EndUnitID: dnd.UnitRefFromString("missing")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := New().Extract(context.Background(), extractionRequestWithClient(client))
|
||||
if err != nil {
|
||||
t.Fatalf("Extract() error = %v, want nil", err)
|
||||
}
|
||||
var payload map[string][]map[string]any
|
||||
if err := json.Unmarshal(result.Output.Payload.Content, &payload); err != nil {
|
||||
t.Fatalf("Unmarshal(Content) error = %v, want nil", err)
|
||||
}
|
||||
ref := payload["spell_casts"][0]["source_refs"].([]any)[0].(map[string]any)
|
||||
if ref["source_id"] != "session-alpha" || ref["start_unit_id"] != float64(99) || ref["end_unit_id"] != "" {
|
||||
t.Fatalf("source ref = %#v, want source_id canonicalized without unit repair", ref)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user