Files
Agent-System-Backend/internal/runtime/wp5_ab_test.go
T
weichen 5f9e77a500
ci / vet-and-test (pull_request) Successful in 40s
Consume the sunk gate mechanics and pin the WP5 library
Why: N1 WP5 host side - directRun.stop and the checklist reminders
now call the library gates package behind product ports, and the
nested native pin moves to the merged WP5 head.

Evidence: go vet clean; go test ./... 29 packages ok, including the
characterization suite unmodified and the new wp5_ab_test.go
(verify-cap continuation counts and gate-reason text parity).

Impact: llm.go drops the local regex family and deliverableGate;
verify.go drops checkDeliverables/parseVerdict/stripFence and the
local caps; workflow.go switches to gates.Verdict; reminders run
through gates.ProgressReminder. Behavior is pinned identical by the
existing suite plus the A/B tests. Squad reminders stay out of this
increment (adapter lacks per-member tool-turn signals - recorded as
WP6b).

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

108 lines
4.4 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 runtime
import (
"context"
"strings"
"testing"
agentllm "agent-native/runtime/llm"
core "agent-system-core"
)
// wp5_ab_test.go 是 N1 WP5 验收的 A/B 对照:门禁机制下沉后,
// 同一组脚本化模型输出的停止原因、提醒次数与工具拒绝必须与记录的
// 旧行为逐条一致。既有套件(GuardTwiceThenNotice / VerifierRejectsThenPasses /
// VerifierGarbageFailsClosed / TodoReminder 三测)在实现切换后原样通过,
// 是性格化等价的主网;本文件补齐两个此前无覆盖的验收角度。
// TestVerifyCapNoticeAfterThreeContinuations:验证器持续拒绝时,
// 续跑恰为 3 次(gates.MaxVerifyContinuations),随后以 verifyCapNotice 收尾,
// 理由逐次进入过程区。
func TestVerifyCapNoticeAfterThreeContinuations(t *testing.T) {
turns := [][]agentllm.Event{
textDeltaTurn("答案 v1。"),
textDeltaTurn("答案 v2。"),
textDeltaTurn("答案 v3。"),
textDeltaTurn("答案 v4。"),
}
var mainCalls int
base := scriptStream(turns, nil)
r := &LLMRunner{
Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
mainCalls++
return base(ctx, model, msgs, tools, opts)
},
VerifyStream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
return passedStream(`{"passed":false,"reason":"来源未标注","nextAction":"补上出处"}`)(ctx, model, msgs, tools, opts)
},
}
rc := &fakeRunCtx{ctx: context.Background(), prof: verifyOn(), verify: true}
if err := r.Execute(rc); err != nil {
t.Fatalf("Execute: %v", err)
}
if mainCalls != 4 {
t.Fatalf("main calls = %d, want 4(3 次续跑 + 收尾)", mainCalls)
}
texts := textParts(rc.assistant)
last := texts[len(texts)-1]
if last.Process || !strings.Contains(last.Text, "本轮回复未通过终轮检查") {
t.Fatalf("final notice = %+v, want non-process verify cap notice", last)
}
// 理由经 CheckPart 进过程区:3 次续跑各记一次,达上限时再记一次(与旧行为一致)。
notes := 0
for _, p := range rc.assistant {
if p.Type == core.PartCheck && p.Check != nil && !p.Check.Passed && strings.Contains(p.Check.Reason, "来源未标注") {
notes++
}
}
if notes != 4 {
t.Fatalf("verify notes = %d, want 4(3 次续跑 + 1 次封顶); checks = %+v", notes, rc.assistant)
}
// 前三个被降级的回复进了过程区;最后一次回复与收尾 Notice 留在正式区。
demoted := 0
for _, p := range texts {
if p.Process && strings.HasPrefix(p.Text, "答案 v") {
demoted++
}
}
if demoted != 3 {
t.Fatalf("demoted answers = %d, want 3; parts = %+v", demoted, texts)
}
}
// TestGateReasonTextParityAfterSink:下沉后门禁分支理由逐字一致——
// verify 开启时,声称交付无工件的理由必须原样进入过程区。
func TestGateReasonTextParityAfterSink(t *testing.T) {
turns := [][]agentllm.Event{
textDeltaTurn("文档已生成:甲.docx"),
textDeltaTurn("文档已保存:乙.docx"),
textDeltaTurn("文档已导出:丙.docx"),
}
r := &LLMRunner{Stream: scriptStream(turns, nil)}
rc := &fakeRunCtx{ctx: context.Background(), prof: verifyOn(), verify: true}
if err := r.Execute(rc); err != nil {
t.Fatalf("Execute: %v", err)
}
if !hasProcessNote(rc.assistant, "回复声称已经生成或登记了文件,但这一轮没有产生对应的工件") {
t.Fatalf("gate reason missing after sink: %+v", textParts(rc.assistant))
}
texts := textParts(rc.assistant)
last := texts[len(texts)-1]
if last.Process || !strings.Contains(last.Text, "本轮未实际生成文件") {
t.Fatalf("final notice = %+v", last)
}
// 伪造调用标记的分支理由也要逐字一致。
r2 := &LLMRunner{Stream: scriptStream([][]agentllm.Event{
textDeltaTurn("结果如下 [调用工具 write_file] 已完成"),
textDeltaTurn("结果如下 [调用工具 write_file] 已完成"),
textDeltaTurn("结果如下 [调用工具 write_file] 已完成"),
}, nil)}
rc2 := &fakeRunCtx{ctx: context.Background(), prof: verifyOn(), verify: true}
if err := r2.Execute(rc2); err != nil {
t.Fatalf("Execute: %v", err)
}
if !hasProcessNote(rc2.assistant, "回复正文里出现了工具调用标记,但本轮没有真实的工具调用") {
t.Fatalf("forged marker reason missing after sink: %+v", textParts(rc2.assistant))
}
}