Why: A parent turn should be able to finish while children keep working, and a review should keep only findings that a second pass confirms. Users also need to override a profile's tools, model, and turn cap without adding dispatch tools. Evidence: go test ./... passed. New cases cover overlay rejection, role mapping, queued notices, and dropping FAIL or unparsed findings. Impact: Notices live only in memory, and each conversation allows three background children. Squad writer steps now use the writer profile. Refs: feature/agent-harness-p3b Co-authored-by: Cursor <cursoragent@cursor.com>
109 lines
3.8 KiB
Go
109 lines
3.8 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
agentllm "agent-native/runtime/llm"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
func TestReviewKeepsPassAndDropsTheRest(t *testing.T) {
|
|
release := make(chan struct{})
|
|
var once sync.Once
|
|
var mu sync.Mutex
|
|
var entered int
|
|
var verifyCalls int
|
|
spawn := func(agentID, name, task, hint string) (core.AgentResultPayload, error) {
|
|
if agentID == reviewProfileID {
|
|
mu.Lock()
|
|
entered++
|
|
n := entered
|
|
mu.Unlock()
|
|
if n == 3 {
|
|
once.Do(func() { close(release) })
|
|
}
|
|
select {
|
|
case <-release:
|
|
case <-time.After(time.Second):
|
|
return core.AgentResultPayload{}, fmt.Errorf("reviewers did not run together")
|
|
}
|
|
switch name {
|
|
case "审查1":
|
|
return core.AgentResultPayload{Text: `[{"location":"第2段","severity":"high","issue":"无来源","evidence":"显然"},{"location":"第1段","severity":"low","issue":"错字","evidence":"的地"},{"location":"表1","severity":"medium","issue":"过期数据","evidence":"2020"}]`}, nil
|
|
case "审查2":
|
|
return core.AgentResultPayload{Text: `[{"location":"第2段","severity":"high","issue":"无来源","evidence":"显然"},{"location":"附录","severity":"medium","issue":"数字不一致","evidence":"3 与 4"}]`}, nil
|
|
default:
|
|
return core.AgentResultPayload{Text: `[{"location":"结尾","severity":"low","issue":"没有结论","evidence":"最后一句"}]`}, nil
|
|
}
|
|
}
|
|
mu.Lock()
|
|
verifyCalls++
|
|
mu.Unlock()
|
|
switch {
|
|
case strings.Contains(task, "无来源"):
|
|
return core.AgentResultPayload{Text: "核对原文确实没有来源。\nVERDICT: PASS"}, nil
|
|
case strings.Contains(task, "错字"):
|
|
return core.AgentResultPayload{Text: "核对后成立。\nVERDICT: PASS"}, nil
|
|
case strings.Contains(task, "过期数据"):
|
|
return core.AgentResultPayload{Text: "数据仍有效。\nVERDICT: FAIL"}, nil
|
|
default:
|
|
return core.AgentResultPayload{Text: "看不出对错"}, nil
|
|
}
|
|
}
|
|
out := runReview(spawn, "材料正文", "事实", 3)
|
|
if !strings.Contains(out, "无来源") || !strings.Contains(out, "错字") || strings.Contains(out, "数字不一致") || strings.Contains(out, "没有结论") || strings.Contains(out, "过期数据") {
|
|
t.Fatalf("filter = %s", out)
|
|
}
|
|
if !strings.Contains(out, "已丢弃 3 条未验证") {
|
|
t.Fatalf("dropped = %s", out)
|
|
}
|
|
if verifyCalls != 5 {
|
|
t.Fatalf("verify calls = %d, want 5 after dedupe", verifyCalls)
|
|
}
|
|
if strings.Index(out, "无来源") > strings.Index(out, "错字") {
|
|
t.Fatalf("severity order: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestDelegateBackgroundUsesSpawnBackground(t *testing.T) {
|
|
rc := &fakeRunCtx{ctx: context.Background(), bgID: "run_bg1"}
|
|
st := &directRun{rc: rc}
|
|
out := st.runTool(toolDelegate, `{"name":"检索","task":"查资料","background":true,"agent":"explore"}`)
|
|
if rc.bgCalls != 1 || !strings.Contains(out.Content, "已在后台启动 run_bg1") {
|
|
t.Fatalf("calls=%d out=%s", rc.bgCalls, out.Content)
|
|
}
|
|
}
|
|
|
|
func TestNoticeTurnPinsArtifacts(t *testing.T) {
|
|
rc := &fakeRunCtx{
|
|
ctx: context.Background(),
|
|
prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}},
|
|
history: []core.Message{{
|
|
Role: core.RoleSystem,
|
|
Parts: []core.Part{{Type: core.PartText, Text: "<system-reminder>\n后台子代理「检索」已结束。\n工件:art_0123456789ab 说明.md\n</system-reminder>"}},
|
|
}},
|
|
execArtifact: &core.Artifact{ID: "art_0123456789ab", Name: "说明.md", Kind: "document"},
|
|
}
|
|
r := &LLMRunner{Stream: scriptStream([][]agentllm.Event{{
|
|
{Kind: agentllm.KindTextDelta, Text: "检索已经完成。"},
|
|
{Kind: agentllm.KindMessageEnd},
|
|
}}, nil)}
|
|
if err := r.Execute(rc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var pinned bool
|
|
for _, p := range rc.assistant {
|
|
if p.Type == core.PartArtifactRef && p.Artifact != nil && p.Artifact.ArtifactID == "art_0123456789ab" {
|
|
pinned = true
|
|
}
|
|
}
|
|
if !pinned {
|
|
t.Fatalf("parts = %+v", rc.assistant)
|
|
}
|
|
}
|