ci / vet-and-test (pull_request) Successful in 40s
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
108 lines
4.4 KiB
Go
108 lines
4.4 KiB
Go
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))
|
||
}
|
||
}
|