返回 DeepSeek-Reasonix
goal_test.go
根目录 / internal / control / goal_test.go
1 package control
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10 "sync/atomic"
11 "testing"
12
13 "reasonix/internal/agent"
14 "reasonix/internal/event"
15 "reasonix/internal/evidence"
16 "reasonix/internal/provider"
17 "reasonix/internal/store"
18 "reasonix/internal/tool"
19
20 _ "reasonix/internal/tool/builtin"
21 )
22
23 // goalRegistry returns a registry carrying the update_goal builtin so scripted
24 // goal turns can report dispositions through the structured tool.
25 func goalRegistry() *tool.Registry {
26 reg := tool.NewRegistry()
27 if t, ok := tool.LookupBuiltin("update_goal"); ok {
28 reg.Add(t)
29 }
30 return reg
31 }
32
33 // goalWireStatus maps FSM status values to the update_goal wire enum.
34 func goalWireStatus(status string) string {
35 switch status {
36 case GoalStatusComplete:
37 return "complete"
38 case GoalStatusBlocked:
39 return "blocked"
40 default:
41 return "continue"
42 }
43 }
44
45 // goalToolTurn models one goal turn's provider sequence: the model calls
46 // update_goal with the given disposition, then answers with text. Call IDs are
47 // unique per call so recycled scripted turns never collide in the transcript.
48 func goalToolTurn(status, reason, nextAction string) [][]provider.Chunk {
49 args, err := json.Marshal(map[string]string{"status": goalWireStatus(status), "reason": reason, "next_action": nextAction})
50 if err != nil {
51 panic(err)
52 }
53 id := fmt.Sprintf("ug-%d", goalToolCallSeq.Add(1))
54 return [][]provider.Chunk{
55 {toolCallChunk(id, "update_goal", string(args)), {Type: provider.ChunkDone}},
56 textTurn("worked on the goal"),
57 }
58 }
59
60 var goalToolCallSeq atomic.Uint64
61
62 // fakeGoalEvaluator is a scripted bounded Goal evaluator for tests.
63 type legacyEvaluatorVerdict struct {
64 Outcome string
65 Reason string
66 }
67
68 type fakeGoalEvaluator struct {
69 outcome string
70 reason string
71 err error
72 calls int
73 }
74
75 func (f *fakeGoalEvaluator) Evaluate(_ context.Context, _ struct{}) (legacyEvaluatorVerdict, error) {
76 f.calls++
77 if f.err != nil {
78 return legacyEvaluatorVerdict{}, f.err
79 }
80 return legacyEvaluatorVerdict{Outcome: f.outcome, Reason: f.reason}, nil
81 }
82
83 // flattenTurns concatenates per-goal-turn provider sequences into one flat
84 // scripted provider stream.
85 func flattenTurns(groups ...[][]provider.Chunk) [][]provider.Chunk {
86 var out [][]provider.Chunk
87 for _, g := range groups {
88 out = append(out, g...)
89 }
90 return out
91 }
92
93 // toolCallChunk builds a provider turn carrying one tool call.
94 func toolCallChunk(id, name, args string) provider.Chunk {
95 return provider.Chunk{Type: provider.ChunkToolCall, ToolCall: &provider.ToolCall{ID: id, Name: name, Arguments: args}}
96 }
97
98 func TestActiveGoalBlockCarriesTaskContractAndPausePolicy(t *testing.T) {
99 block := activeGoalBlock("fix the parser")
100 for _, want := range []string{
101 "Treat the user's goal as a task contract",
102 "Context, Request, Output format, Constraints",
103 "Pause policy",
104 "irreversible or externally visible operation",
105 "the requested scope has changed",
106 "information only the user can provide",
107 "output format and constraints are satisfied",
108 } {
109 if !strings.Contains(block, want) {
110 t.Fatalf("active goal block missing %q:\n%s", want, block)
111 }
112 }
113 if strings.Contains(block, "AutoResearch protocol") {
114 t.Fatalf("simple goal should not include AutoResearch protocol:\n%s", block)
115 }
116 }
117
118 func TestPlainInputWithStrongResearchSignalStaysNormal(t *testing.T) {
119 prov := &scriptedTurns{turns: [][]provider.Chunk{
120 textTurn("Here is the normal response."),
121 }}
122 ag := agent.New(prov, tool.NewRegistry(), agent.NewSession(""), agent.Options{}, event.Discard)
123 events := make(chan event.Event, 8)
124 c := newOwnedTestController(t, Options{
125 Runner: ag,
126 Executor: ag,
127 Sink: event.FuncSink(func(e event.Event) {
128 if e.Kind == event.TurnDone || e.Kind == event.Notice {
129 events <- e
130 }
131 }),
132 })
133
134 c.Submit("持续排查这个线上卡顿直到根因明确,并验证修复")
135 waitForTurnDone(t, events)
136
137 if prov.call != 1 {
138 t.Fatalf("provider calls = %d, want 1", prov.call)
139 }
140 first := agent.StripTransientUserBlocks(firstUserMessage(ag.Session().Messages))
141 if !strings.HasSuffix(first, "持续排查这个线上卡顿直到根因明确,并验证修复") {
142 t.Fatalf("ordinary turn should preserve the original prompt suffix: %q", first)
143 }
144 if strings.Contains(first, "<active-goal>") || strings.Contains(first, "AutoResearch protocol") {
145 t.Fatalf("ordinary prompt should not enter Goal or AutoResearch:\n%s", first)
146 }
147 if got := c.GoalStatus(); got != GoalStatusStopped {
148 t.Fatalf("GoalStatus() = %q, want stopped", got)
149 }
150 }
151
152 func TestPlainInputWithStrongResearchSignalPreservesRefsWithoutStartingGoal(t *testing.T) {
153 root := t.TempDir()
154 if err := os.WriteFile(filepath.Join(root, "notes.txt"), []byte("important referenced evidence"), 0o644); err != nil {
155 t.Fatal(err)
156 }
157 prov := &scriptedTurns{turns: [][]provider.Chunk{
158 textTurn("Referenced normal response."),
159 }}
160 ag := agent.New(prov, tool.NewRegistry(), agent.NewSession(""), agent.Options{}, event.Discard)
161 events := make(chan event.Event, 8)
162 c := newOwnedTestController(t, Options{
163 WorkspaceRoot: root,
164 Runner: ag,
165 Executor: ag,
166 Sink: event.FuncSink(func(e event.Event) {
167 if e.Kind == event.TurnDone || e.Kind == event.Notice {
168 events <- e
169 }
170 }),
171 })
172
173 c.Submit("持续排查直到根因明确,并验证 @notes.txt")
174 waitForTurnDone(t, events)
175
176 first := firstUserMessage(ag.Session().Messages)
177 for _, want := range []string{
178 "important referenced evidence",
179 } {
180 if !strings.Contains(first, want) {
181 t.Fatalf("ordinary turn with refs missing %q:\n%s", want, first)
182 }
183 }
184 if strings.Contains(first, "<active-goal>") || strings.Contains(first, "AutoResearch protocol") {
185 t.Fatalf("ordinary prompt with refs should not enter Goal or AutoResearch:\n%s", first)
186 }
187 if got := c.GoalStatus(); got != GoalStatusStopped {
188 t.Fatalf("GoalStatus() = %q, want stopped", got)
189 }
190 if _, err := os.Stat(filepath.Join(root, ".reasonix", "autoresearch")); !os.IsNotExist(err) {
191 t.Fatalf("ordinary prompt created AutoResearch state: err=%v", err)
192 }
193 }
194
195 func TestResearchGoalUsesContinuousRuntimeWithoutArchive(t *testing.T) {
196 root := t.TempDir()
197 sessionPath := filepath.Join(root, "sessions", "s.jsonl")
198 sess := agent.NewSession("sys")
199 exec := agent.New(nil, nil, sess, agent.Options{}, event.Discard)
200 c := newOwnedTestController(t, Options{WorkspaceRoot: root, SessionDir: root, Executor: exec})
201 c.Resume(sess, sessionPath)
202 c.SetGoalWithResearchMode("fix the typo and add a test", GoalResearchOn)
203 defer c.Close()
204 if got := c.GoalRuntime().TurnsLimit; got != 0 {
205 t.Fatalf("research Goal turns limit = %d, want unlimited", got)
206 }
207 // The class still drives behaviour; it just no longer mints a turn quota.
208 if got := c.goals.budgetClass; got != budgetClassResearch {
209 t.Fatalf("research Goal budget class = %q, want %q", got, budgetClassResearch)
210 }
211 if _, err := os.Stat(filepath.Join(root, ".reasonix", "autoresearch")); !os.IsNotExist(err) {
212 t.Fatalf("research Goal created legacy archive: %v", err)
213 }
214 if composed := c.Compose("continue"); strings.Contains(composed, "AutoResearch") || strings.Contains(composed, "autoresearch") {
215 t.Fatalf("Goal prompt exposes removed AutoResearch protocol:\n%s", composed)
216 }
217 }
218
219 func TestLegacyGoalSidecarMigratesToContinuousRuntimeWithoutTaskID(t *testing.T) {
220 root := t.TempDir()
221 sessionPath := filepath.Join(root, "sessions", "s.jsonl")
222 if err := os.MkdirAll(filepath.Dir(sessionPath), 0o755); err != nil {
223 t.Fatal(err)
224 }
225 writeLegacyGoalArchive(t, root, "old-task", "archive fallback should not replace sidecar goal")
226 if err := os.WriteFile(goalStatePath(sessionPath), []byte(`{"goal":"investigate runtime","status":"running","researchMode":1,"autoResearchTaskID":"old-task"}`), 0o644); err != nil {
227 t.Fatal(err)
228 }
229 sess := agent.NewSession("sys")
230 exec := agent.New(nil, nil, sess, agent.Options{}, event.Discard)
231 c := newOwnedTestController(t, Options{WorkspaceRoot: root, SessionDir: root, Executor: exec})
232 c.Resume(sess, sessionPath)
233 defer c.Close()
234 if got := c.GoalRuntime().TurnsLimit; got != 0 {
235 t.Fatalf("migrated Goal turns limit = %d, want unlimited", got)
236 }
237 if got := c.goals.budgetClass; got != budgetClassResearch {
238 t.Fatalf("migrated Goal budget class = %q, want %q", got, budgetClassResearch)
239 }
240 if got := c.Goal(); got != "investigate runtime" {
241 t.Fatalf("migrated Goal = %q, want sidecar goal", got)
242 }
243 raw, err := os.ReadFile(goalStatePath(sessionPath))
244 if err != nil {
245 t.Fatal(err)
246 }
247 var state goalState
248 if err := json.Unmarshal(raw, &state); err != nil {
249 t.Fatal(err)
250 }
251 if state.AutoResearchTaskID != "old-task" {
252 t.Fatalf("read-only restore rewrote the original sidecar: %q", state.AutoResearchTaskID)
253 }
254 }
255
256 func TestMissingExplicitLegacyTaskBlocksWithoutCreatingArchive(t *testing.T) {
257 root := t.TempDir()
258 c := newOwnedTestController(t, Options{WorkspaceRoot: root})
259 defer c.Close()
260 c.SetGoalWithResearchMode("resume .reasonix/autoresearch/missing-task/", GoalResearchOn)
261 if got := c.GoalStatus(); got != GoalStatusBlocked {
262 t.Fatalf("GoalStatus = %q, want blocked", got)
263 }
264 if _, err := os.Stat(filepath.Join(root, ".reasonix", "autoresearch")); !os.IsNotExist(err) {
265 t.Fatalf("missing legacy task created archive: %v", err)
266 }
267
268 c.SetGoal("resume .reasonix/autoresearch/missing-task/../../escape")
269 if got := c.GoalStatus(); got != GoalStatusBlocked {
270 t.Fatalf("unsafe legacy path status = %q, want blocked", got)
271 }
272 if got := c.Goal(); got != "resume .reasonix/autoresearch/missing-task/../../escape" {
273 t.Fatalf("unsafe legacy path silently resumed a truncated task: %q", got)
274 }
275 }
276
277 func TestExplicitLegacyTaskPathRestoresOriginalGoal(t *testing.T) {
278 root := t.TempDir()
279 if resolved, err := filepath.EvalSymlinks(root); err == nil {
280 root = resolved
281 }
282 taskID := "20260630-original-goal"
283 taskRoot := filepath.Join(root, ".reasonix", "autoresearch", taskID)
284 if err := os.MkdirAll(filepath.Join(taskRoot, "state"), 0o755); err != nil {
285 t.Fatal(err)
286 }
287 if err := os.MkdirAll(filepath.Join(taskRoot, "logs"), 0o755); err != nil {
288 t.Fatal(err)
289 }
290 spec := `{"task_id":"` + taskID + `","goal":"find the original root cause","allowed_operations":{"write":true},"success_criteria":[]}`
291 progress := `{"status":"running","iteration":2,"updated_at":"2026-06-30T10:00:00Z"}`
292 for name, body := range map[string]string{
293 "state/task_spec.json": spec,
294 "state/progress.json": progress,
295 "state/directions_tried.json": "[]\n",
296 "state/findings.jsonl": "",
297 "state/iteration_log.jsonl": "",
298 "logs/heartbeat.jsonl": "",
299 } {
300 if err := os.WriteFile(filepath.Join(taskRoot, name), []byte(body), 0o644); err != nil {
301 t.Fatal(err)
302 }
303 }
304 before, err := os.ReadFile(filepath.Join(taskRoot, "state", "task_spec.json"))
305 if err != nil {
306 t.Fatal(err)
307 }
308 c := newOwnedTestController(t, Options{WorkspaceRoot: root})
309 defer c.Close()
310 c.SetGoalWithResearchMode("resume .reasonix/autoresearch/"+taskID+"/", GoalResearchAuto)
311 if got := c.Goal(); got != "find the original root cause" {
312 t.Fatalf("Goal() = %q, want original archive goal", got)
313 }
314 if got := c.GoalRuntime().TurnsLimit; got != 0 {
315 t.Fatalf("turns limit = %d, want unlimited", got)
316 }
317 if got := c.goals.budgetClass; got != budgetClassResearch {
318 t.Fatalf("budget class = %q, want %q", got, budgetClassResearch)
319 }
320 if got := c.GoalStatus(); got != GoalStatusRunning {
321 t.Fatalf("status = %q", got)
322 }
323 after, err := os.ReadFile(filepath.Join(taskRoot, "state", "task_spec.json"))
324 if err != nil {
325 t.Fatal(err)
326 }
327 if string(before) != string(after) {
328 t.Fatal("archive task_spec mutated during resume")
329 }
330 }
331
332 func TestLegacySidecarEmptyGoalFilledFromArchive(t *testing.T) {
333 root := t.TempDir()
334 if resolved, err := filepath.EvalSymlinks(root); err == nil {
335 root = resolved
336 }
337 sessionPath := filepath.Join(root, "sessions", "s.jsonl")
338 if err := os.MkdirAll(filepath.Dir(sessionPath), 0o755); err != nil {
339 t.Fatal(err)
340 }
341 taskID := "fill-from-archive"
342 taskRoot := filepath.Join(root, ".reasonix", "autoresearch", taskID)
343 if err := os.MkdirAll(filepath.Join(taskRoot, "state"), 0o755); err != nil {
344 t.Fatal(err)
345 }
346 if err := os.MkdirAll(filepath.Join(taskRoot, "logs"), 0o755); err != nil {
347 t.Fatal(err)
348 }
349 for name, body := range map[string]string{
350 "state/task_spec.json": `{"task_id":"` + taskID + `","goal":"recover me from archive","allowed_operations":{"write":true},"success_criteria":[]}`,
351 "state/progress.json": `{"status":"running","updated_at":"2026-06-30T10:00:00Z"}`,
352 "state/directions_tried.json": "[]\n",
353 "state/findings.jsonl": "",
354 "state/iteration_log.jsonl": "",
355 "logs/heartbeat.jsonl": "",
356 } {
357 if err := os.WriteFile(filepath.Join(taskRoot, name), []byte(body), 0o644); err != nil {
358 t.Fatal(err)
359 }
360 }
361 if err := os.WriteFile(goalStatePath(sessionPath), []byte(`{"status":"running","researchMode":1,"autoResearchTaskID":"`+taskID+`","turnsUsed":3,"turnsLimit":40}`), 0o644); err != nil {
362 t.Fatal(err)
363 }
364 sess := agent.NewSession("sys")
365 exec := agent.New(nil, nil, sess, agent.Options{}, event.Discard)
366 c := newOwnedTestController(t, Options{WorkspaceRoot: root, SessionDir: root, Executor: exec})
367 c.Resume(sess, sessionPath)
368 defer c.Close()
369 if got := c.Goal(); got != "recover me from archive" {
370 t.Fatalf("Goal() = %q", got)
371 }
372 if got := c.GoalRuntime().TurnsUsed; got != 3 {
373 t.Fatalf("turns used = %d, want preserved 3", got)
374 }
375 raw, err := os.ReadFile(goalStatePath(sessionPath))
376 if err != nil {
377 t.Fatal(err)
378 }
379 if strings.Contains(string(raw), "autoResearchTaskID") {
380 t.Fatalf("sidecar retained task id: %s", raw)
381 }
382 }
383
384 func TestPlainInputWithWeakResearchSignalStaysNormal(t *testing.T) {
385 prov := &scriptedTurns{turns: [][]provider.Chunk{
386 textTurn("Here is a normal answer."),
387 }}
388 ag := agent.New(prov, tool.NewRegistry(), agent.NewSession(""), agent.Options{}, event.Discard)
389 events := make(chan event.Event, 4)
390 c := newOwnedTestController(t, Options{
391 Runner: ag,
392 Executor: ag,
393 Sink: event.FuncSink(func(e event.Event) {
394 if e.Kind == event.TurnDone {
395 events <- e
396 }
397 }),
398 })
399
400 c.Submit("长期来看这个模块怎么优化?")
401 waitForTurnDone(t, events)
402
403 first := firstUserMessage(ag.Session().Messages)
404 if strings.Contains(first, "<active-goal>") || strings.Contains(first, "AutoResearch protocol") {
405 t.Fatalf("ordinary prompt should stay outside Goal and AutoResearch:\n%s", first)
406 }
407 if got := c.GoalStatus(); got != GoalStatusStopped {
408 t.Fatalf("GoalStatus() = %q, want stopped", got)
409 }
410 }
411
412 func TestCancelStopsIdleGoalWithIncompleteTodos(t *testing.T) {
413 ag := agent.New(nil, nil, agent.NewSession(""), agent.Options{}, event.Discard)
414 ag.SeedTodoState([]evidence.TodoItem{{Content: "finish the migration", Status: "in_progress"}})
415 c := newOwnedTestController(t, Options{Executor: ag, Sink: event.Discard})
416 c.SetGoalWithResearchMode("finish the migration", GoalResearchOn)
417
418 c.Cancel()
419
420 if got := c.GoalStatus(); got != GoalStatusStopped {
421 t.Fatalf("GoalStatus() = %q, want stopped", got)
422 }
423 if got := c.Goal(); got != "finish the migration" {
424 t.Fatalf("Goal() = %q, want stopped goal text to remain for display/persistence", got)
425 }
426 if todos := c.Todos(); len(todos) != 0 {
427 t.Fatalf("Todos() after stopping idle goal = %+v, want executor seed ignored", todos)
428 }
429 }
430
431 // TestSessionRotationClearsActiveGoal pins the /new & /clear goal semantics:
432 // a fresh session starts with no active goal (so the old goal's text stops
433 // injecting into its first turns), while the OLD session's persisted
434 // goal-state sidecar keeps the running goal so resuming it restores the goal.
435 func TestSessionRotationClearsActiveGoal(t *testing.T) {
436 dir := t.TempDir()
437 exec := agent.New(nil, nil, agent.NewSession("sys"), agent.Options{}, event.Discard)
438 oldPath := filepath.Join(dir, "session.jsonl")
439 c := newOwnedTestController(t, Options{Executor: exec, SystemPrompt: "sys", SessionDir: dir, SessionPath: oldPath, Label: "test"})
440
441 c.SetGoal("ship the release checklist")
442 if got := c.Goal(); got != "ship the release checklist" {
443 t.Fatalf("Goal() = %q after SetGoal", got)
444 }
445 if composed := c.Compose("hello"); !strings.Contains(composed, "<active-goal>") {
446 t.Fatalf("running goal should inject into turns, composed = %q", composed)
447 }
448
449 if err := c.NewSession(); err != nil {
450 t.Fatalf("NewSession: %v", err)
451 }
452 if got := c.Goal(); got != "" {
453 t.Fatalf("Goal() after /new = %q, want empty", got)
454 }
455 if composed := c.Compose("hello"); strings.Contains(composed, "<active-goal>") {
456 t.Fatalf("old goal leaked into the fresh session's turn: %q", composed)
457 }
458 // The old session keeps its running goal on disk for /resume.
459 oldState, err := os.ReadFile(store.SessionGoalState(oldPath))
460 if err != nil {
461 t.Fatalf("read old goal state: %v", err)
462 }
463 if !strings.Contains(string(oldState), "ship the release checklist") || !strings.Contains(string(oldState), GoalStatusRunning) {
464 t.Fatalf("old session's goal state was disturbed by /new: %s", oldState)
465 }
466 // The new session's sidecar records the cleared (stopped) state, so
467 // profile restores read it as "no running goal".
468 newState, err := os.ReadFile(store.SessionGoalState(c.SessionPath()))
469 if err != nil {
470 t.Fatalf("read new goal state: %v", err)
471 }
472 if strings.Contains(string(newState), "ship the release checklist") {
473 t.Fatalf("new session's goal state carries the old goal: %s", newState)
474 }
475
476 // Same contract for /clear.
477 c.SetGoal("another goal")
478 if err := c.ClearSession(); err != nil {
479 t.Fatalf("ClearSession: %v", err)
480 }
481 if got := c.Goal(); got != "" {
482 t.Fatalf("Goal() after /clear = %q, want empty", got)
483 }
484 if composed := c.Compose("hello"); strings.Contains(composed, "<active-goal>") {
485 t.Fatalf("old goal leaked into the cleared session's turn: %q", composed)
486 }
487 }
488
489 func TestGoalSidecarRoundTripPreservesBlockedDeliveryCheckpoint(t *testing.T) {
490 dir := t.TempDir()
491 path := filepath.Join(dir, "session.jsonl")
492 exec := agent.New(nil, nil, agent.NewSession("sys"), agent.Options{}, event.Discard)
493 c := newOwnedTestController(t, Options{Executor: exec, SessionDir: dir, SessionPath: path, Label: "test"})
494 c.SetGoal("finish the delivery")
495 scopeID, _, ok := c.goals.deliveryScope()
496 if !ok || scopeID == "" {
497 t.Fatal("Goal did not allocate a delivery scope")
498 }
499 cp := evidence.DeliveryCheckpoint{
500 ScopeID: scopeID,
501 CriteriaEstablished: true,
502 WorkObserved: true,
503 MutationObserved: true,
504 PendingMutation: true,
505 }
506 statePath, data, persist := c.goals.setDeliveryCheckpoint(cp)
507 c.persistGoalState(statePath, data, persist)
508 c.stopGoal(GoalStatusBlocked)
509
510 freshExec := agent.New(nil, nil, agent.NewSession("sys"), agent.Options{}, event.Discard)
511 fresh := newOwnedTestController(t, Options{Executor: freshExec, SessionDir: dir, Label: "fresh"})
512 fresh.Resume(agent.NewSession("sys"), path)
513 if fresh.Goal() != "finish the delivery" || fresh.GoalStatus() != GoalStatusBlocked {
514 t.Fatalf("restored Goal = (%q, %q), want blocked Goal", fresh.Goal(), fresh.GoalStatus())
515 }
516 if got := freshExec.DeliveryCheckpoint(); got != cp {
517 t.Fatalf("restored checkpoint = %+v, want %+v", got, cp)
518 }
519 if !fresh.ResumeGoal() {
520 t.Fatal("ResumeGoal rejected a restored blocked Goal")
521 }
522 id, _, ok := fresh.goals.deliveryScope()
523 if !ok || id != scopeID {
524 t.Fatalf("resumed scope = %q, want %q", id, scopeID)
525 }
526 }
527
528 func TestLegacyRunningGoalSidecarAllocatesScope(t *testing.T) {
529 dir := t.TempDir()
530 path := filepath.Join(dir, "legacy.jsonl")
531 data := []byte(`{"goal":"legacy goal","status":"running"}`)
532 if err := os.WriteFile(store.SessionGoalState(path), data, 0o600); err != nil {
533 t.Fatal(err)
534 }
535 exec := agent.New(nil, nil, agent.NewSession("sys"), agent.Options{}, event.Discard)
536 c := newOwnedTestController(t, Options{Executor: exec, SessionDir: dir, Label: "test"})
537 c.Resume(agent.NewSession("sys"), path)
538 if c.goals.active() {
539 t.Fatal("restored goal automatically active")
540 }
541 if !c.ResumeGoal() {
542 t.Fatal("explicit resume failed")
543 }
544 id, task, ok := c.goals.deliveryScope()
545 if !ok || id == "" || task != "legacy goal" {
546 t.Fatalf("legacy delivery scope = (%q, %q, %v)", id, task, ok)
547 }
548 if got := exec.DeliveryCheckpoint(); got.ScopeID != id {
549 t.Fatalf("legacy checkpoint scope = %q, want %q", got.ScopeID, id)
550 }
551 }
552
552 lines GO