Files
Agent-System-Backend/internal/runmgr/background_card_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

214 lines
6.5 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 runmgr
import (
"errors"
"strings"
"sync"
"testing"
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/cardsvc"
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/runtime"
core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
)
// newCardSpawnEnv 在后台派发环境上接线收件箱卡片服务,并捕获发出的卡片事件
// (任务快慢双速:慢通道进度卡 → 完成回投的验证环境)。
func newCardSpawnEnv(t *testing.T, runner runtime.Runner) (*runCtx, *[]string) {
t.Helper()
c, _ := newSpawnEnv(t, runner)
var mu sync.Mutex
var events []string
c.mgr.cards = cardsvc.New(c.mgr.st, func(conversationID, runID string, typ core.StreamEventType, payload any) error {
mu.Lock()
defer mu.Unlock()
events = append(events, string(typ))
return nil
})
return c, &events
}
func cardEvents(list *[]string) []string {
var out []string
for _, e := range *list {
if strings.HasPrefix(e, "card.") {
out = append(out, e)
}
}
return out
}
func TestBackgroundCardLifecycleOnSuccess(t *testing.T) {
runner := &captureRunner{parts: []core.Part{
{Type: core.PartText, Text: "深度解析完成:三份材料均相关,要点已汇总。"},
{Type: core.PartArtifactRef, Artifact: &core.ArtifactRef{ArtifactID: "art_0123456789ab", Name: "解析摘要.md"}},
}}
c, events := newCardSpawnEnv(t, runner)
c.mgr.runner = noopRunner{}
if _, err := c.SpawnBackground("agent.sub.general", "深度解析员", "逐页解析上传材料", ""); err != nil {
t.Fatal(err)
}
waitUntil(t, func() bool {
if len(systemTexts(t, c)) != 1 {
return false
}
c.mgr.convMu.Lock()
busy := c.mgr.convActive[c.run.ConversationID]
c.mgr.convMu.Unlock()
return !busy
})
cards, err := c.mgr.cards.List(c.run.Owner, c.run.ConversationID)
if err != nil {
t.Fatal(err)
}
if len(cards) != 1 {
t.Fatalf("cards = %d, want 1", len(cards))
}
card := cards[0]
if card.Kind != core.CardKindProgress || card.Title != "深度解析员" {
t.Fatalf("card = %+v", card)
}
if card.Status != core.CardDone || card.Tone != "ok" {
t.Fatalf("status = %s tone = %s, want done/ok", card.Status, card.Tone)
}
var md string
for _, n := range card.Body {
if n.Type == core.UIMarkdown {
md = n.Markdown
}
}
if !strings.Contains(md, "深度解析完成") || !strings.Contains(md, "解析摘要.md") {
t.Fatalf("result body = %q", md)
}
if card.SourceRunID == "" {
t.Fatal("card not linked to child run")
}
got := cardEvents(events)
for _, want := range []string{"card.created", "card.updated", "card.status"} {
found := false
for _, e := range got {
if e == want {
found = true
}
}
if !found {
t.Fatalf("events %v missing %s", got, want)
}
}
}
func TestBackgroundCardLifecycleOnFailure(t *testing.T) {
runner := &gateRunner{err: errors.New("boom: 材料解析失败")}
c, _ := newCardSpawnEnv(t, runner)
c.mgr.runner = noopRunner{}
if _, err := c.SpawnBackground("agent.sub.general", "深度解析员", "逐页解析上传材料", ""); err != nil {
t.Fatal(err)
}
waitUntil(t, func() bool {
if len(systemTexts(t, c)) != 1 {
return false
}
c.mgr.convMu.Lock()
busy := c.mgr.convActive[c.run.ConversationID]
c.mgr.convMu.Unlock()
return !busy
})
cards, err := c.mgr.cards.List(c.run.Owner, c.run.ConversationID)
if err != nil {
t.Fatal(err)
}
if len(cards) != 1 {
t.Fatalf("cards = %d, want 1", len(cards))
}
card := cards[0]
if card.Status != core.CardFailed || card.Tone != "danger" {
t.Fatalf("status = %s tone = %s, want failed/danger", card.Status, card.Tone)
}
var md string
statusLabel := ""
for _, n := range card.Body {
if n.Type == core.UIMarkdown {
md = n.Markdown
}
if n.Type == core.UIKV {
for _, kv := range n.KV {
if kv[0] == core.TaskSummaryKVStatus {
statusLabel = kv[1]
}
}
}
}
if !strings.Contains(md, "执行失败") {
t.Fatalf("failure body = %q", md)
}
if statusLabel != core.TaskSummaryStatusFailed {
t.Fatalf("status kv = %q, want %q", statusLabel, core.TaskSummaryStatusFailed)
}
}
func TestBackgroundCardCreateUsesL1SkeletonNotRawTask(t *testing.T) {
runner := &holdRunner{release: make(chan struct{})}
c, _ := newCardSpawnEnv(t, runner)
c.mgr.runner = noopRunner{}
raw := "逐页解析上传材料并对照验收条件" + strings.Repeat("LONGTASK", 40)
if _, err := c.SpawnBackground("agent.sub.general", "深度解析员", raw, ""); err != nil {
t.Fatal(err)
}
waitUntil(t, func() bool {
cards, err := c.mgr.cards.List(c.run.Owner, c.run.ConversationID)
return err == nil && len(cards) == 1
})
cards, err := c.mgr.cards.List(c.run.Owner, c.run.ConversationID)
if err != nil {
t.Fatal(err)
}
card := cards[0]
var md string
for _, n := range card.Body {
if n.Type == core.UIMarkdown {
md = n.Markdown
}
}
if strings.Contains(md, "LONGTASK") || strings.Contains(md, raw[:32]) {
t.Fatalf("create body still embeds raw task: %q", md)
}
if md != core.TaskSummaryNoFindingsYet {
t.Fatalf("create markdown = %q, want honest placeholder", md)
}
close(runner.release)
waitUntil(t, func() bool {
c.mgr.convMu.Lock()
busy := c.mgr.convActive[c.run.ConversationID]
c.mgr.convMu.Unlock()
return !busy
})
}
// TestBackgroundCardSkippedWithoutInbox:收件箱未启用(cards==nil)时后台派发
// 不建卡也不报错——既有 background_test 环境即此形态,这里显式断言不回归。
func TestBackgroundCardSkippedWithoutInbox(t *testing.T) {
runner := &captureRunner{parts: []core.Part{{Type: core.PartText, Text: "完成"}}}
c, _ := newSpawnEnv(t, runner)
c.mgr.runner = noopRunner{}
if c.mgr.cards != nil {
t.Fatal("env unexpectedly wired cards")
}
if _, err := c.SpawnBackground("agent.sub.general", "后台", "任务", ""); err != nil {
t.Fatal(err)
}
waitUntil(t, func() bool { return len(systemTexts(t, c)) == 1 })
}
// TestPartsTextStripsInlineThink:MiniMax 行内 <think> 块不进子代理摘要/回投卡。
func TestPartsTextStripsInlineThink(t *testing.T) {
got := partsText([]core.Part{{Type: core.PartText, Text: "<think>先读文件</think>\n正文开始\n<think>再想</think>结论"}})
if strings.Contains(got, "<think>") || strings.Contains(got, "先读文件") || strings.Contains(got, "再想") {
t.Fatalf("think leaked: %q", got)
}
if !strings.Contains(got, "正文开始") || !strings.Contains(got, "结论") {
t.Fatalf("body lost: %q", got)
}
if got := partsText([]core.Part{{Type: core.PartText, Text: "半截 <think> 未闭合保守保留"}}); !strings.Contains(got, "半截") {
t.Fatalf("unclosed think should keep text: %q", got)
}
}