Files
Agent-Intent/cmd/intentctl/main_test.go
T
agent-exec-transport 7f3ff6a187 Make intent matching negation-aware and slots fail closed
Why: the A3 baseline exposed 6 mis-executions - semantic negations
(不要/别/不用/先不要 + action term) matched as positive intents - and
3 invalid-slot cases (multi-candidate values, charset violations,
symbols-only) that started without clarification.
Evidence: recall corpus mis_exec now 0; no regression in
expired_confirmation, generation_consistency or overall top1
(verify-misexec.json, exit=0).
Impact: compiledSpec.evaluate per-hit negation check; adjudicate
specificity tie-break; slot extraction multi-value/charset invalidity.
Refs: feature/recall-misexec-fixes
2026-10-23 03:55:47 +00:00

114 lines
4.1 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"agent-intent/intent"
)
const testSpecJSON = `{
"intents": [
{"id": "demo.run.status_query", "domain": "demo", "version": 1,
"matcher": {"includes_any": ["进度"], "excludes_any": ["配置"]},
"slots": [{"key": "run_id", "pattern": "(?:run|任务)[:# ]*([A-Za-z0-9-]+)"}],
"handler": "taskrun.query_status"},
{"id": "demo.greet.hello", "domain": "demo", "version": 1, "matcher": {"includes_any": ["你好"]}},
{"id": "demo.greet.hi", "domain": "demo", "version": 1, "matcher": {"includes_any": ["你好"]}}
]
}`
const testCorpusJSON = `{
"cases": [
{"name": "status", "input": "查询任务 42 的进度", "expect_branch": "matched", "expect_top": "demo.run.status_query", "expect_slots": {"run_id": "42"}},
{"name": "ambiguous", "input": "你好", "expect_branch": "ambiguous"},
{"name": "fallback", "input": "天气不错", "expect_branch": "fallback"}
]
}`
func writeTemp(t *testing.T, name, content string) string {
t.Helper()
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
return dir
}
func TestLintExplainEval(t *testing.T) {
dir := writeTemp(t, "specs.json", testSpecJSON)
// 评测集放在配置目录之外:lint 的目录 glob 只认配置 overlay。
corpusDir := t.TempDir()
corpus := filepath.Join(corpusDir, "corpus.json")
if err := os.WriteFile(corpus, []byte(testCorpusJSON), 0o644); err != nil {
t.Fatal(err)
}
if err := run([]string{"lint", "-root", dir, "-protected", ""}); err != nil {
t.Fatalf("lint: %v", err)
}
if err := run([]string{"explain", "-root", dir, "-protected", "", "查询任务 42 的进度"}); err != nil {
t.Fatalf("explain: %v", err)
}
if err := run([]string{"eval", "-root", dir, "-protected", "", corpus}); err != nil {
t.Fatalf("eval: %v", err)
}
}
func TestLintRejectsInvalidConfig(t *testing.T) {
dir := writeTemp(t, "specs.json", `{"intents": [{"id": "demo.x.one", "domain": "demo", "version": 1, "matcher": {}}]}`)
if err := run([]string{"lint", "-root", dir}); err == nil {
t.Fatal("expected lint failure on invalid spec")
}
}
func TestEvalFailsOnWrongExpectation(t *testing.T) {
dir := writeTemp(t, "specs.json", testSpecJSON)
corpus := filepath.Join(t.TempDir(), "corpus.json")
if err := os.WriteFile(corpus, []byte(`{"cases": [{"name": "wrong", "input": "你好", "expect_branch": "matched"}]}`), 0o644); err != nil {
t.Fatal(err)
}
if err := run([]string{"eval", "-root", dir, "-protected", "", corpus}); err == nil {
t.Fatal("expected eval failure on wrong expectation")
}
}
func TestEvalRecordOnlyWritesMetrics(t *testing.T) {
dir := writeTemp(t, "specs.json", testSpecJSON)
tmp := t.TempDir()
corpus := filepath.Join(tmp, "corpus.json")
// 「不用查进度了」在否定修复后已正确回落,这里换成仍会误执行的输入来覆盖 record_only 计数。
doc := `{"record_only": true, "cases": [
{"name": "miss", "category": "negation_reference", "input": "进度条的颜色真好看", "expect_branch": "fallback"},
{"name": "hit", "input": "天气不错", "expect_branch": "fallback"}]}`
if err := os.WriteFile(corpus, []byte(doc), 0o644); err != nil {
t.Fatal(err)
}
out := filepath.Join(tmp, "metrics.json")
if err := run([]string{"eval", "-root", dir, "-protected", "", "-metrics-out", out, corpus}); err != nil {
t.Fatalf("record_only eval should not fail: %v", err)
}
data, err := os.ReadFile(out)
if err != nil {
t.Fatal(err)
}
var s intent.EvalSummary
if err := json.Unmarshal(data, &s); err != nil {
t.Fatal(err)
}
if s.Schema != intent.EvalMetricsSchema || s.Overall.Cases != 2 || len(s.Categories) != 2 || s.Overall.MisExecutions != 1 {
t.Fatalf("summary = %+v", s)
}
strict := filepath.Join(tmp, "strict.json")
if err := os.WriteFile(strict, []byte(strings.Replace(doc, `"record_only": true, `, "", 1)), 0o644); err != nil {
t.Fatal(err)
}
if err := run([]string{"eval", "-root", dir, "-protected", "", strict}); err == nil {
t.Fatal("corpus without record_only must still fail on mismatch")
}
}