Document combat semantics validator evaluation
This commit is contained in:
10
internal/modules/dnd/validate/scenedescriptions/combat_semantics/testdata/evaluation_cases.json
vendored
Normal file
10
internal/modules/dnd/validate/scenedescriptions/combat_semantics/testdata/evaluation_cases.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{"name":"active encounter","transcript_units":[{"id":1,"text":"Roll initiative; the goblins attack."},{"id":2,"text":"The ranger hits and deals damage."}],"proposed_kind":"combat","expected_verdict":"approved","reviewer_rationale":"Initiative and hostile actions organize the chunk."},
|
||||
{"name":"combat after setup","transcript_units":[{"id":1,"text":"They open the crypt door."},{"id":2,"text":"Skeletons attack and turns begin."}],"proposed_kind":"narrative","expected_verdict":"combat_should_be_added","reviewer_rationale":"Brief setup does not displace substantive active combat."},
|
||||
{"name":"combat aftermath","transcript_units":[{"id":1,"text":"The last enemy falls."},{"id":2,"text":"They search bodies and heal."}],"proposed_kind":"combat","expected_verdict":"combat_should_be_removed","reviewer_rationale":"Looting and healing after a completed fight are not active combat."},
|
||||
{"name":"multi phase encounter","transcript_units":[{"id":1,"text":"The dragon attacks."},{"id":2,"text":"After a rules clarification, its next turn begins."}],"proposed_kind":"combat","expected_verdict":"approved","reviewer_rationale":"A brief rules interruption does not end the encounter."},
|
||||
{"name":"planning","transcript_units":[{"id":1,"text":"They plan how to ambush the guard."}],"proposed_kind":"combat","expected_verdict":"combat_should_be_removed","reviewer_rationale":"Planning a possible fight is not active encounter play."},
|
||||
{"name":"hostile dialogue","transcript_units":[{"id":1,"text":"The captain threatens them and they argue."}],"proposed_kind":"combat","expected_verdict":"combat_should_be_removed","reviewer_rationale":"Threats and hostile dialogue alone are insufficient."},
|
||||
{"name":"recap recollection","transcript_units":[{"id":1,"text":"They recap last session's battle with the lich."}],"proposed_kind":"combat","expected_verdict":"combat_should_be_removed","reviewer_rationale":"Recounting earlier combat is not current active combat."},
|
||||
{"name":"rules discussion","transcript_units":[{"id":1,"text":"The table discusses how concentration works."}],"proposed_kind":"meta","expected_verdict":"approved","reviewer_rationale":"Sustained out-of-character rules discussion has no active encounter."}
|
||||
]
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -167,6 +168,54 @@ func TestValidatorConstructionOptionsAndMetadata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluationCasesAreStrictAndCoverVerdicts(t *testing.T) {
|
||||
content, err := os.ReadFile("testdata/evaluation_cases.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cases []struct {
|
||||
Name string `json:"name"`
|
||||
TranscriptUnits []struct {
|
||||
ID int `json:"id"`
|
||||
Text string `json:"text"`
|
||||
} `json:"transcript_units"`
|
||||
ProposedKind string `json:"proposed_kind"`
|
||||
ExpectedVerdict string `json:"expected_verdict"`
|
||||
ReviewerRationale string `json:"reviewer_rationale"`
|
||||
}
|
||||
decoder := json.NewDecoder(strings.NewReader(string(content)))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&cases); err != nil {
|
||||
t.Fatalf("decode evaluation cases: %v", err)
|
||||
}
|
||||
seenNames, seenVerdicts := map[string]bool{}, map[string]bool{}
|
||||
for _, value := range cases {
|
||||
if strings.TrimSpace(value.Name) == "" || seenNames[value.Name] || len(value.TranscriptUnits) == 0 || strings.TrimSpace(value.ProposedKind) == "" || strings.TrimSpace(value.ReviewerRationale) == "" {
|
||||
t.Fatalf("invalid evaluation case: %#v", value)
|
||||
}
|
||||
seenNames[value.Name] = true
|
||||
switch value.ProposedKind {
|
||||
case "combat", "narrative", "recap", "meta":
|
||||
default:
|
||||
t.Fatalf("unsupported proposed kind %q", value.ProposedKind)
|
||||
}
|
||||
switch value.ExpectedVerdict {
|
||||
case "approved", "combat_should_be_added", "combat_should_be_removed":
|
||||
seenVerdicts[value.ExpectedVerdict] = true
|
||||
default:
|
||||
t.Fatalf("unsupported verdict %q", value.ExpectedVerdict)
|
||||
}
|
||||
for _, unit := range value.TranscriptUnits {
|
||||
if unit.ID <= 0 || strings.TrimSpace(unit.Text) == "" {
|
||||
t.Fatalf("invalid transcript unit: %#v", unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(seenVerdicts) != 3 {
|
||||
t.Fatalf("evaluation verdicts = %#v, want all classes", seenVerdicts)
|
||||
}
|
||||
}
|
||||
|
||||
func newValidator(t *testing.T, client contracts.StructuredLLMClient) *Validator {
|
||||
t.Helper()
|
||||
validator, err := New(client, Options{})
|
||||
|
||||
Reference in New Issue
Block a user