返回 DeepSeek-Reasonix
subagent_goal_work_duration_test.go
根目录 / internal / control / subagent_goal_work_duration_test.go
1 package control
2
3 import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 "reasonix/internal/agent"
10 "reasonix/internal/event"
11 "reasonix/internal/provider"
12 "reasonix/internal/skill"
13 "reasonix/internal/tool"
14 )
15
16 func TestSubagentSkillRecordsWorkDurationOnChildMessage(t *testing.T) {
17 sess := agent.NewSession("")
18 prov := &scriptedTurns{turns: goalToolTurn(GoalStatusComplete, "reviewed", "")}
19 exec := agent.New(prov, goalRegistry(), sess, agent.Options{}, event.Discard)
20 events := make(chan event.Event, 8)
21 c := newOwnedTestController(t, Options{
22 Executor: exec, Runner: exec,
23 Sink: event.FuncSink(func(e event.Event) {
24 if e.Kind == event.TurnDone || e.Kind == event.Notice {
25 events <- e
26 }
27 }),
28 Skills: []skill.Skill{{
29 Name: "research", Body: "RESEARCH_NOTES", RunAs: skill.RunSubagent, Scope: skill.ScopeGlobal,
30 }},
31 SkillRunner: func(_ context.Context, _ skill.Skill, _ string, _ skill.SubagentRunOptions) (string, error) {
32 return "Notes listed.", nil
33 },
34 })
35 defer c.Close()
36 c.SetGoalWithResearchMode("list the existing notes", GoalResearchOff)
37 c.SubmitInvocationDisplay(
38 "list the existing notes",
39 "list the existing notes",
40 []InvocationRequest{{Name: "research", Kind: "subagent", Offset: 0}},
41 )
42 waitForTurnDone(t, events)
43
44 var childDuration int64
45 for _, message := range c.History() {
46 if message.Role == provider.RoleAssistant && strings.Contains(message.Content, "Notes listed") {
47 childDuration = message.WorkDurationMs
48 break
49 }
50 }
51 if childDuration <= 0 {
52 t.Fatalf("persisted subagent answer work duration = %d, want positive", childDuration)
53 }
54 }
55
56 func TestSubagentSkillGoalRejectsStaleWorkDuration(t *testing.T) {
57 sess := agent.NewSession("")
58 exec := agent.New(nil, tool.NewRegistry(), sess, agent.Options{}, event.Discard)
59 events := make(chan event.Event, 8)
60 started := make(chan struct{})
61 release := make(chan struct{})
62 c := newOwnedTestController(t, Options{
63 Executor: exec,
64 Sink: event.FuncSink(func(e event.Event) {
65 if e.Kind == event.TurnDone || e.Kind == event.Notice {
66 events <- e
67 }
68 }),
69 Skills: []skill.Skill{{
70 Name: "research", Body: "RESEARCH_NOTES", RunAs: skill.RunSubagent, Scope: skill.ScopeGlobal,
71 }},
72 SkillRunner: func(_ context.Context, _ skill.Skill, _ string, _ skill.SubagentRunOptions) (string, error) {
73 close(started)
74 <-release
75 return "stale notes", nil
76 },
77 })
78 defer c.Close()
79 c.SetGoalWithResearchMode("original goal", GoalResearchOff)
80 c.SubmitInvocationDisplay(
81 "inspect the notes",
82 "inspect the notes",
83 []InvocationRequest{{Name: "research", Kind: "subagent", Offset: 0}},
84 )
85 select {
86 case <-started:
87 case <-time.After(5 * time.Second):
88 t.Fatal("subagent skill did not start")
89 }
90
91 c.SetGoalWithResearchMode("replacement goal", GoalResearchOff)
92 close(release)
93 waitForTurnDone(t, events)
94
95 if got := c.Goal(); got != "replacement goal" {
96 t.Fatalf("active Goal = %q, want replacement", got)
97 }
98 if got := c.GoalRuntime().WorkDurationMs; got != 0 {
99 t.Fatalf("stale subagent work duration polluted replacement Goal: %d", got)
100 }
101 }
102
102 lines GO