Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements. Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate. Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable. Refs: Versioned dependencies U1/U3; feature/dependency-workspace
81 lines
2.6 KiB
Go
81 lines
2.6 KiB
Go
// Package conformance 提供 Recognizer 公开契约的行为验收套件。
|
||
// 消费方在自己的测试里用真实配置组装 Registry 与 Recognizer 后调用 Run,
|
||
// 断言分支、Top、槽位,并附带确定性与并发冒烟检查。
|
||
package conformance
|
||
|
||
import (
|
||
"context"
|
||
"reflect"
|
||
"sync"
|
||
"testing"
|
||
|
||
"git-ssh.u1680832.nyat.app/weichen/Agent-Intent.git/intent"
|
||
)
|
||
|
||
// Case 单条行为验收用例。ExpectTopID 为空断言 Top 为 nil;
|
||
// ExpectSlots / ExpectMissingSlots 只对 Top 断言。
|
||
type Case struct {
|
||
Name string
|
||
Input string
|
||
ExpectBranch intent.Branch
|
||
ExpectTopID string
|
||
ExpectSlots map[string]string
|
||
ExpectMissingSlots []string
|
||
}
|
||
|
||
// Run 逐条执行用例并输出标准断言失败信息;任何用例失败使 t 失败。
|
||
func Run(t *testing.T, rec intent.Recognizer, snap *intent.Snapshot, cases []Case) {
|
||
t.Helper()
|
||
for _, tc := range cases {
|
||
t.Run(tc.Name, func(t *testing.T) {
|
||
res, err := rec.Recognize(context.Background(), tc.Input, snap)
|
||
if err != nil {
|
||
t.Fatalf("recognize %q: %v", tc.Input, err)
|
||
}
|
||
if res.Branch != tc.ExpectBranch {
|
||
t.Fatalf("branch = %s, want %s", res.Branch, tc.ExpectBranch)
|
||
}
|
||
switch {
|
||
case tc.ExpectTopID == "" && res.Top != nil:
|
||
t.Fatalf("expected no top, got %+v", res.Top)
|
||
case tc.ExpectTopID != "" && res.Top == nil:
|
||
t.Fatalf("expected top %s, got nil", tc.ExpectTopID)
|
||
case tc.ExpectTopID != "" && res.Top.ID != tc.ExpectTopID:
|
||
t.Fatalf("top = %s, want %s", res.Top.ID, tc.ExpectTopID)
|
||
}
|
||
if res.Top != nil {
|
||
for k, v := range tc.ExpectSlots {
|
||
if res.Top.Slots[k] != v {
|
||
t.Fatalf("slot %s = %q, want %q", k, res.Top.Slots[k], v)
|
||
}
|
||
}
|
||
}
|
||
if len(tc.ExpectMissingSlots) > 0 && (res.Top == nil || !reflect.DeepEqual(res.Top.MissingRequiredSlots, tc.ExpectMissingSlots)) {
|
||
t.Fatalf("missing slots = %+v, want %v", res.Top, tc.ExpectMissingSlots)
|
||
}
|
||
// 确定性:同输入 + 同快照 → 同结果。
|
||
again, err := rec.Recognize(context.Background(), tc.Input, snap)
|
||
if err != nil {
|
||
t.Fatalf("second recognize: %v", err)
|
||
}
|
||
if !reflect.DeepEqual(res, again) {
|
||
t.Fatalf("nondeterministic result for %q", tc.Input)
|
||
}
|
||
})
|
||
}
|
||
// 并发冒烟:同一快照并行识别互不影响(配合 -race)。
|
||
t.Run("parallel-smoke", func(t *testing.T) {
|
||
var wg sync.WaitGroup
|
||
for _, tc := range cases {
|
||
wg.Add(1)
|
||
go func(input string) {
|
||
defer wg.Done()
|
||
if _, err := rec.Recognize(context.Background(), input, snap); err != nil {
|
||
t.Errorf("parallel recognize %q: %v", input, err)
|
||
}
|
||
}(tc.Input)
|
||
}
|
||
wg.Wait()
|
||
})
|
||
}
|