Files
Agent-System-Backend/internal/runtime/wp5_ab_test.go
T
weichen f1302ed5b3 Enable canonical versioned Go module consumption
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
2026-10-13 15:47:16 +00:00

113 lines
4.7 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 "git-ssh.u1680832.nyat.app/weichen/Agent-Native.git/runtime/llm"
core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
)
// 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]
// 2026-10-10 契约更新:封顶提示不再作为正文 Notice;最后一次回复留在正式区,
// 封顶事实以未通过态检查行收进过程区。
if last.Process || last.Text != "答案 v4。" {
t.Fatalf("final text = %+v, want the model's own last reply", last)
}
if !hasProcessNote(rc.assistant, "本轮回复未通过终轮检查") {
t.Fatalf("cap check note missing: %+v", rc.assistant)
}
// 理由经 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))
}
}