Files
Agent-Native/runtime/gates/gates_test.go
T
weichen 7f2c8b9ee1
ci / vet-and-test (pull_request) Successful in 50s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Sink the final gate mechanics and add the contract backend adapter
Why: N1 WP5/WP6 library side - the gate decision logic, artifact
checks, verdict parsing and progress-reminder throttling move from
the product into a gates package behind ports, and the native backend
gains a contract form so conformance has a real implementation to
drive.

Evidence: go vet clean; go test ./... 27 packages ok, including the
conformance suite (succeeded/descriptor/error-sideeffects) against
the real backend.

Impact: new runtime/gates package (ClaimGate, CheckArtifacts,
ParseVerdict, ProgressReminder, shared caps); adapters/native.Backend
gains Contract() returning an exec.ContractBackend wrapper - the
legacy facade keeps its signature so squad consumption is untouched.
Product texts (fix prompts, verifier prompts) stay injected by hosts.

Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md WP5/WP6
2026-10-07 06:14:29 +00:00

134 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gates
import (
"strings"
"testing"
"time"
)
func lookupWith(ids map[string][]byte) ArtifactLookup {
return func(id string) (string, []byte, bool) {
c, ok := ids[id]
return id + ".md", c, ok
}
}
func TestClaimGateForgedMarkerAlwaysBlocks(t *testing.T) {
block, why := ClaimGate("结果如下 [调用工具 write_file]", nil, nil)
if !block || why == "" {
t.Fatalf("block=%v why=%q", block, why)
}
}
func TestClaimGateProducedAndExistingDoNotBlock(t *testing.T) {
produced := []ArtifactRef{{ID: "art_aaaabbbbcccc"}}
if block, _ := ClaimGate("已生成 art_aaaabbbbcccc 请查收", produced, nil); block {
t.Fatal("produced reference blocked")
}
lookup := lookupWith(map[string][]byte{"art_dddd11112222": []byte("x")})
if block, _ := ClaimGate("之前的 art_dddd11112222 里有结论", nil, lookup); block {
t.Fatal("existing artifact reference blocked")
}
}
func TestClaimGateUnknownArtifactBlocks(t *testing.T) {
block, why := ClaimGate("见附件 art_000011112222", nil, lookupWith(nil))
if !block || !strings.Contains(why, "不存在") {
t.Fatalf("block=%v why=%q", block, why)
}
}
func TestClaimGateBareClaimWithoutArtifactsBlocks(t *testing.T) {
for _, body := range []string{
"已生成报告.md,请查收",
"文件已生成好了",
} {
if block, _ := ClaimGate(body, nil, nil); !block {
t.Fatalf("claim not blocked: %q", body)
}
}
// 如实说明没有生成则不拦。
if block, _ := ClaimGate("这一轮没有生成文件", nil, nil); block {
t.Fatal("honest note blocked")
}
}
func TestCheckArtifactsReasons(t *testing.T) {
lookup := func(id string) (string, []byte, bool) {
switch id {
case "ok":
return "ok.md", []byte("# 正常"), true
case "empty":
return "empty.md", nil, true
default:
return "", nil, false
}
}
scorer := func(name string, content []byte) (bool, string) { return false, "标题缺失" }
refs := []ArtifactRef{
{ID: ""},
{ID: "missing", Name: "缺失.md"},
{ID: "empty"},
{ID: "ok"},
}
reasons := CheckArtifacts(refs, lookup, scorer)
joined := strings.Join(reasons, ";")
for _, want := range []string{"缺少 ID", "缺失.md 不存在", "empty.md 内容为空", "ok.md:标题缺失"} {
if !strings.Contains(joined, want) {
t.Fatalf("reasons %q 缺 %q", joined, want)
}
}
}
func TestCheckArtifactsNilScorerSkipsQuality(t *testing.T) {
lookup := lookupWith(map[string][]byte{"a": []byte("x")})
if reasons := CheckArtifacts([]ArtifactRef{{ID: "a"}}, lookup, nil); len(reasons) != 0 {
t.Fatalf("reasons = %v", reasons)
}
}
func TestParseVerdictForms(t *testing.T) {
v, err := ParseVerdict("```json\n{\"passed\":true,\"reason\":\"ok\"}\n```")
if err != nil || !v.Passed {
t.Fatalf("v=%+v err=%v", v, err)
}
if _, err := ParseVerdict("没有 JSON"); err == nil {
t.Fatal("garbage accepted")
}
if _, err := ParseVerdict("{\"reason\":\"no passed key\"}"); err == nil {
t.Fatal("missing passed accepted")
}
}
func TestProgressReminderThrottleAndFormat(t *testing.T) {
r := &ProgressReminder{}
now := time.Now()
first := r.Remind([]string{"写第三章"}, []string{"评审"}, now)
if len(first) != 1 {
t.Fatalf("first = %v", first)
}
text := first[0]
for _, want := range []string{"已连续 3 个工具轮", "- 待办:写第三章", "- 计划:评审", "请更新计划或待办"} {
if !strings.Contains(text, want) {
t.Fatalf("text 缺 %q:\n%s", want, text)
}
}
// 内容相同且计数不同:key 不同,应继续发出直到封顶。
if got := r.Remind([]string{"写第三章"}, []string{"评审"}, now); len(got) != 1 {
t.Fatalf("second = %v", got)
}
if got := r.Remind([]string{"写第三章"}, []string{"评审"}, now); len(got) != 1 {
t.Fatalf("third = %v", got)
}
if got := r.Remind([]string{"写第三章"}, []string{"评审"}, now); got != nil {
t.Fatalf("fourth should be capped: %v", got)
}
if r.Sent() != 3 {
t.Fatalf("sent = %d", r.Sent())
}
// 无内容不发。
if got := (&ProgressReminder{}).Remind(nil, nil, now); got != nil {
t.Fatalf("empty = %v", got)
}
}