Why: I1 WP6 - eval needs per-turn command expectations to carry the SetSlot+Start acceptance case, and corpora must reproduce standalone in intentctl (the guard case has been red there since before I1 because the guard spec only existed as a test builtin). Evidence: go vet clean; go test ./... all ok; intentctl eval passes 11/11 on the v1 corpus (guard case fixed via self-declaration) and 1/1 on the new two-turn SetSlot+Start corpus. Impact: new intent/corpus.go (CorpusCase/CorpusTurn/CommandExpect, RunCorpusCase with chained expectations); intentctl eval consumes turns and corpus-declared builtin_specs and domains; report.generate spec and corpus_multiturn.json fixtures added. v1 corpus format and conformance unchanged. Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md I1 WP6
80 lines
2.4 KiB
Go
80 lines
2.4 KiB
Go
package intent
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// TestEvalMultiTurnCorpus 是 I1 验收载体:v2 多轮语料含「SetSlot 加 Start」
|
||
// 用例并通过(链式预期把第二轮转进 expectation_hit)。
|
||
func TestEvalMultiTurnCorpus(t *testing.T) {
|
||
data, err := os.ReadFile("testdata/corpus_multiturn.json")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var doc struct {
|
||
Cases []CorpusCase `json:"cases"`
|
||
}
|
||
if err := json.Unmarshal(data, &doc); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(doc.Cases) == 0 {
|
||
t.Fatal("empty corpus")
|
||
}
|
||
_, snap := buildDemoRegistry(t)
|
||
for _, tc := range doc.Cases {
|
||
result := RunCorpusCase(context.Background(), snap, tc)
|
||
if !result.Passed() {
|
||
for _, tr := range result.Turns {
|
||
for _, mm := range tr.Mismatches {
|
||
t.Errorf("case %s turn %d (%q): %s", tc.Name, tr.Index, tr.Input, mm)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestCorpusV1WrapsAsSingleTurn:v1 单轮形态经同一执行器等价运行(兼容承诺)。
|
||
func TestCorpusV1WrapsAsSingleTurn(t *testing.T) {
|
||
_, snap := buildDemoRegistry(t)
|
||
tc := CorpusCase{Name: "v1", Input: "你好呀", ExpectBranch: "matched", ExpectTop: "demo.greet.hello"}
|
||
result := RunCorpusCase(context.Background(), snap, tc)
|
||
if !result.Passed() {
|
||
t.Fatalf("v1 wrap: %+v", result.Turns)
|
||
}
|
||
if len(result.Turns) != 1 || result.Turns[0].Branch != "matched" {
|
||
t.Fatalf("turns = %+v", result.Turns)
|
||
}
|
||
}
|
||
|
||
// TestMatchCommandsFindsMismatches:命令断言的差异定位(kind/action/slot/name)。
|
||
func TestMatchCommandsFindsMismatches(t *testing.T) {
|
||
got := []Command{
|
||
{Kind: CommandStart, Action: ActionRef{Kind: ActionHandler, ID: "h"}, Slots: map[string]string{"topic": "电机"}},
|
||
}
|
||
mm := matchCommands([]CommandExpect{
|
||
{Kind: "start", ActionKind: ActionHandler, ActionID: "other"},
|
||
}, got)
|
||
if len(mm) != 1 || !strings.Contains(mm[0], "action.id") {
|
||
t.Fatalf("mm = %v", mm)
|
||
}
|
||
mm = matchCommands([]CommandExpect{
|
||
{Kind: "start", Slots: map[string]string{"topic": "水泵"}},
|
||
}, got)
|
||
if len(mm) != 1 || !strings.Contains(mm[0], "slots") {
|
||
t.Fatalf("mm = %v", mm)
|
||
}
|
||
mm = matchCommands([]CommandExpect{
|
||
{Kind: "start", ActionKind: ActionHandler, ActionID: "h", Slots: map[string]string{"topic": "电机"}},
|
||
}, got)
|
||
if len(mm) != 0 {
|
||
t.Fatalf("mm = %v", mm)
|
||
}
|
||
if mm := matchCommands([]CommandExpect{{Kind: "start"}, {Kind: "clarify"}}, got); len(mm) != 1 {
|
||
t.Fatalf("count mismatch = %v", mm)
|
||
}
|
||
}
|