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
113 lines
4.7 KiB
Go
113 lines
4.7 KiB
Go
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))
|
||
}
|
||
}
|