1package subagents
2
3import (
4 "context"
5 "errors"
6 "net"
7 "os"
8 "path/filepath"
9 "runtime"
10 "strings"
11 "sync"
12 "testing"
13 "time"
14)
15
16// newTestSupervisor builds a Supervisor rooted in t.TempDir and configured
17// with the Runner factory controlled by the test.
18func newTestSupervisor(t *testing.T, mk func(a *Agent) Runner) *Supervisor {
19 t.Helper()
20 root := t.TempDir()
21 return New(Config{
22 Root: root,
23 RepoRoot: root,
24 NewRunner: mk,
25 })
26}
27
28func TestUnqualifiedModelID(t *testing.T) {
29 for _, tc := range []struct {
30 name string
31 provider string
32 model string
33 want string
34 }{
35 {name: "matching provider", provider: "openai-codex", model: "openai-codex/gpt-5.6-sol", want: "gpt-5.6-sol"},
36 {name: "matching provider with whitespace", provider: "openai-codex", model: " openai-codex/gpt-5.6-sol ", want: "gpt-5.6-sol"},
37 {name: "other slash-containing model ID", provider: "openrouter", model: "anthropic/claude-sonnet-4-5", want: "anthropic/claude-sonnet-4-5"},
38 {name: "no provider", model: "/custom-model", want: "/custom-model"},
39 } {
40 t.Run(tc.name, func(t *testing.T) {
41 if got := unqualifiedModelID(tc.provider, tc.model); got != tc.want {
42 t.Fatalf("unqualifiedModelID(%q, %q) = %q, want %q", tc.provider, tc.model, got, tc.want)
43 }
44 })
45 }
46}
47
48func TestSpawnTimeoutDoesNotLimitResumableWorkerLifetime(t *testing.T) {
49 observedDeadline := make(chan bool, 1)
50 f := newTestSupervisor(t, func(a *Agent) Runner {
51 return RunnerFunc(func(ctx context.Context, _ Sink) error {
52 _, hasDeadline := ctx.Deadline()
53 observedDeadline <- hasDeadline
54 <-ctx.Done()
55 return ctx.Err()
56 })
57 })
58 a, err := f.SpawnReq(context.Background(), SpawnRequest{
59 Task: "remain resumable",
60 Timeout: time.Minute,
61 })
62 if err != nil {
63 t.Fatal(err)
64 }
65 if hasDeadline := <-observedDeadline; hasDeadline {
66 t.Fatal("worker lifetime context has a deadline; turn timeout must not expire an idle resumable worker")
67 }
68 if a.Timeout != time.Minute {
69 t.Fatalf("agent timeout = %s, want %s", a.Timeout, time.Minute)
70 }
71 f.StopAll()
72 a.Wait()
73}
74
75func TestSpawnRunsAndCompletes(t *testing.T) {
76 ran := make(chan string, 1)
77 f := newTestSupervisor(t, func(a *Agent) Runner {
78 return RunnerFunc(func(ctx context.Context, sink Sink) error {
79 ran <- a.Task
80 sink.Activity("hello")
81 sink.Transcript("line one")
82 sink.Transcript("line two")
83 return nil
84 })
85 })
86 a, err := f.Spawn(context.Background(), "do a thing")
87 if err != nil {
88 t.Fatal(err)
89 }
90 select {
91 case got := <-ran:
92 if got != "do a thing" {
93 t.Fatalf("runner got task %q; want %q", got, "do a thing")
94 }
95 case <-time.After(time.Second):
96 t.Fatal("runner did not start")
97 }
98 a.Wait()
99 if a.Status() != StatusDone {
100 t.Fatalf("status %s; want done", a.Status())
101 }
102 if got := a.Transcript(); len(got) != 2 || got[0] != "line one" || got[1] != "line two" {
103 t.Fatalf("transcript = %q", got)
104 }
105 if !strings.Contains(a.ID, "do-a-thing") {
106 t.Fatalf("id %q missing slug", a.ID)
107 }
108 // Every agent shares the host's RepoRoot.
109 if a.Dir != f.cfg.RepoRoot {
110 t.Fatalf("dir = %q; want repo root %q", a.Dir, f.cfg.RepoRoot)
111 }
112}
113
114// TestSpawnAgentSharesRepoRoot verifies the only-mode-we-support:
115// every spawned agent points its cwd at the parent zut's RepoRoot.
116func TestSpawnAgentSharesRepoRoot(t *testing.T) {
117 f := newTestSupervisor(t, func(a *Agent) Runner {
118 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
119 })
120 a, err := f.Spawn(context.Background(), "share me")
121 if err != nil {
122 t.Fatal(err)
123 }
124 a.Wait()
125 if a.Dir != f.cfg.RepoRoot {
126 t.Fatalf("Dir = %q; want RepoRoot %q", a.Dir, f.cfg.RepoRoot)
127 }
128}
129
130func TestSetRepoRootAffectsSubsequentSpawns(t *testing.T) {
131 f := newTestSupervisor(t, func(a *Agent) Runner {
132 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
133 })
134 want := t.TempDir()
135 f.SetRepoRoot(want)
136 a, err := f.Spawn(context.Background(), "use new root")
137 if err != nil {
138 t.Fatal(err)
139 }
140 a.Wait()
141 if a.Dir != want {
142 t.Fatalf("Dir = %q; want updated RepoRoot %q", a.Dir, want)
143 }
144}
145
146func TestSpawnEmptyTaskFails(t *testing.T) {
147 f := newTestSupervisor(t, func(a *Agent) Runner {
148 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
149 })
150 if _, err := f.Spawn(context.Background(), " "); err == nil {
151 t.Fatal("expected error on empty task")
152 }
153}
154
155func TestRunnerErrorMarksFailed(t *testing.T) {
156 wantErr := errors.New("boom")
157 f := newTestSupervisor(t, func(a *Agent) Runner {
158 return RunnerFunc(func(ctx context.Context, sink Sink) error { return wantErr })
159 })
160 a, _ := f.Spawn(context.Background(), "explode")
161 a.Wait()
162 if a.Status() != StatusFailed {
163 t.Fatalf("status %s; want failed", a.Status())
164 }
165 if !errors.Is(a.Err(), wantErr) {
166 t.Fatalf("err = %v", a.Err())
167 }
168 if !strings.Contains(a.Activity(), "boom") {
169 t.Fatalf("activity = %q; want it to mention the error", a.Activity())
170 }
171}
172
173func TestStopCancelsRunningAgent(t *testing.T) {
174 started := make(chan struct{})
175 f := newTestSupervisor(t, func(a *Agent) Runner {
176 return RunnerFunc(func(ctx context.Context, sink Sink) error {
177 close(started)
178 <-ctx.Done()
179 return ctx.Err()
180 })
181 })
182 a, _ := f.Spawn(context.Background(), "long")
183 <-started
184 if err := f.Stop(a.ID); err != nil {
185 t.Fatal(err)
186 }
187 a.Wait()
188 if a.Status() != StatusKilled {
189 t.Fatalf("status = %s; want killed", a.Status())
190 }
191}
192
193func TestStopPublishesShutdownOriginBeforeConcurrentFinalization(t *testing.T) {
194 started := make(chan struct{})
195 finish := make(chan struct{})
196 finalizing := make(chan struct{})
197 f := newTestSupervisor(t, func(a *Agent) Runner {
198 return RunnerFunc(func(context.Context, Sink) error {
199 close(started)
200 <-finish
201 close(finalizing)
202 return nil
203 })
204 })
205 a, err := f.Spawn(context.Background(), "finish while stopping")
206 if err != nil {
207 t.Fatal(err)
208 }
209 <-started
210
211 // Hold lifecycle finalization after Runner.Run returns. Shutdown must publish
212 // its origin before exposing StatusKilled, without relying on finalization
213 // ordering to fill the durable result correctly.
214 a.lifecycleMu.Lock()
215 lifecycleLocked := true
216 finishClosed := false
217 defer func() {
218 if !finishClosed {
219 close(finish)
220 }
221 if lifecycleLocked {
222 a.lifecycleMu.Unlock()
223 }
224 }()
225 stopDone := make(chan error, 1)
226 go func() { stopDone <- f.stop(context.Background(), a.ID, ShutdownOriginSession) }()
227
228 deadline := time.Now().Add(time.Second)
229 for a.Status() != StatusKilled {
230 if time.Now().After(deadline) {
231 t.Fatal("shutdown did not publish killed status")
232 }
233 runtime.Gosched()
234 }
235 a.mu.Lock()
236 origin := a.shutdownOrigin
237 a.mu.Unlock()
238 if origin != ShutdownOriginSession {
239 t.Fatalf("shutdown origin = %q when killed became observable, want %q", origin, ShutdownOriginSession)
240 }
241
242 close(finish)
243 finishClosed = true
244 <-finalizing
245 deadline = time.Now().Add(time.Second)
246 for {
247 a.mu.Lock()
248 finalizationStarted := !a.finished.IsZero()
249 a.mu.Unlock()
250 if finalizationStarted {
251 break
252 }
253 if time.Now().After(deadline) {
254 t.Fatal("runner finalization did not start")
255 }
256 runtime.Gosched()
257 }
258 a.lifecycleMu.Unlock()
259 lifecycleLocked = false
260 if err := <-stopDone; err != nil {
261 t.Fatal(err)
262 }
263 a.Wait()
264
265 result := a.Result()
266 if result == nil || result.Status != ResultCanceled || result.ShutdownOrigin != ShutdownOriginSession {
267 t.Fatalf("final result = %#v, want attributed session cancellation", result)
268 }
269 if result.Error == nil || result.Error.Code != "shutdown" {
270 t.Fatalf("final result error = %#v, want shutdown", result.Error)
271 }
272}
273
274func TestStopByRootSessionLeavesOtherSessionsRunning(t *testing.T) {
275 started := make(chan string, 2)
276 stopped := make(chan string, 2)
277 f := newTestSupervisor(t, func(a *Agent) Runner {
278 return RunnerFunc(func(ctx context.Context, _ Sink) error {
279 started <- a.ID
280 <-ctx.Done()
281 stopped <- a.ID
282 return ctx.Err()
283 })
284 })
285 t.Cleanup(f.StopAll)
286
287 f.SetActiveSession("session-a")
288 a, err := f.Spawn(context.Background(), "session A task")
289 if err != nil {
290 t.Fatal(err)
291 }
292 f.SetActiveSession("session-b")
293 b, err := f.Spawn(context.Background(), "session B task")
294 if err != nil {
295 t.Fatal(err)
296 }
297
298 for range 2 {
299 <-started
300 }
301 if err := f.StopByRootSession("session-a"); err != nil {
302 t.Fatal(err)
303 }
304 if got := <-stopped; got != a.ID {
305 t.Fatalf("stopped agent = %q, want session A agent %q", got, a.ID)
306 }
307 select {
308 case got := <-stopped:
309 t.Fatalf("session cleanup also stopped %q", got)
310 default:
311 }
312 if got := b.Status(); got != StatusRunning {
313 t.Fatalf("session B status = %s, want running", got)
314 }
315}
316
317func TestSupervisorShutdownCommandsIdentifyOrigin(t *testing.T) {
318 if runtime.GOOS == "windows" {
319 t.Skip("subagent inbox transport uses Unix-domain sockets")
320 }
321 for _, tc := range []struct {
322 name string
323 origin ShutdownOrigin
324 stop func(*Supervisor, *Agent) error
325 }{
326 {name: "targeted", origin: ShutdownOriginTargeted, stop: func(f *Supervisor, a *Agent) error { return f.Stop(a.ID) }},
327 {name: "session", origin: ShutdownOriginSession, stop: func(f *Supervisor, _ *Agent) error { return f.StopByRootSession("session-a") }},
328 {name: "process", origin: ShutdownOriginProcess, stop: func(f *Supervisor, _ *Agent) error { return f.StopAllContext(context.Background()) }},
329 } {
330 t.Run(tc.name, func(t *testing.T) {
331 started := make(chan struct{})
332 commands := make(chan Envelope, 1)
333 f := newTestSupervisor(t, func(a *Agent) Runner {
334 return RunnerFunc(func(ctx context.Context, _ Sink) error {
335 listener, err := Listen(a.InboxPath)
336 if err != nil {
337 return err
338 }
339 defer listener.Close()
340 close(started)
341 select {
342 case line := <-listener.Lines():
343 command, err := ParseCommand(line)
344 if err != nil {
345 return err
346 }
347 commands <- command
348 return nil
349 case <-ctx.Done():
350 return ctx.Err()
351 }
352 })
353 })
354 f.SetActiveSession("session-a")
355 a, err := f.Spawn(context.Background(), "origin test")
356 if err != nil {
357 t.Fatal(err)
358 }
359 <-started
360 if err := tc.stop(f, a); err != nil {
361 t.Fatal(err)
362 }
363 command := <-commands
364 var payload AgentShutdownPayload
365 if err := command.DecodePayload(&payload); err != nil {
366 t.Fatal(err)
367 }
368 if payload.Origin != tc.origin {
369 t.Fatalf("shutdown origin = %q, want %q", payload.Origin, tc.origin)
370 }
371 a.Wait()
372 })
373 }
374}
375
376func TestStopContextCallerCancellationDoesNotEndGracePeriod(t *testing.T) {
377 if runtime.GOOS == "windows" {
378 t.Skip("subagent inbox transport uses Unix-domain sockets")
379 }
380 const grace = 250 * time.Millisecond
381 started := make(chan struct{})
382 canceled := make(chan struct{})
383 f := New(Config{
384 Root: t.TempDir(),
385 RepoRoot: t.TempDir(),
386 Policy: SubagentPolicy{
387 CancelGracePeriod: grace,
388 },
389 NewRunner: func(a *Agent) Runner {
390 return RunnerFunc(func(ctx context.Context, sink Sink) error {
391 listener, err := Listen(a.InboxPath)
392 if err != nil {
393 return err
394 }
395 defer listener.Close()
396 close(started)
397 select {
398 case <-listener.Lines():
399 case <-ctx.Done():
400 close(canceled)
401 return ctx.Err()
402 }
403 <-ctx.Done()
404 close(canceled)
405 return ctx.Err()
406 })
407 },
408 })
409 a, err := f.Spawn(context.Background(), "graceful stop")
410 if err != nil {
411 t.Fatal(err)
412 }
413 select {
414 case <-started:
415 case <-time.After(time.Second):
416 t.Fatal("runner did not start")
417 }
418
419 ctx, cancel := context.WithCancel(context.Background())
420 if err := f.StopContext(ctx, a.ID); err != nil {
421 t.Fatal(err)
422 }
423 cancel()
424
425 early := time.NewTimer(grace / 2)
426 defer early.Stop()
427 select {
428 case <-canceled:
429 t.Fatal("agent canceled when StopContext caller context was canceled")
430 case <-early.C:
431 }
432
433 deadline := time.NewTimer(grace * 2)
434 defer deadline.Stop()
435 select {
436 case <-canceled:
437 case <-deadline.C:
438 t.Fatal("agent was not canceled after the grace period")
439 }
440 a.Wait()
441}
442
443func TestStopContextCancelsDetachedWaitWithoutHoldingOperationLock(t *testing.T) {
444 if runtime.GOOS == "windows" {
445 t.Skip("subagent inbox transport uses Unix-domain sockets")
446 }
447 path := filepath.Join(shortSocketDir(t), "agent.sock")
448 listener, err := Listen(path)
449 if err != nil {
450 t.Fatal(err)
451 }
452 defer listener.Close()
453
454 f := New(Config{
455 Root: t.TempDir(),
456 Policy: SubagentPolicy{
457 CancelGracePeriod: time.Hour,
458 },
459 })
460 a := &Agent{
461 ID: "detached-agent",
462 InboxPath: path,
463 inbox: NewInbox(path),
464 status: StatusDetached,
465 done: make(chan struct{}),
466 }
467 defer a.inbox.Close()
468 close(a.done)
469 f.mu.Lock()
470 f.agents[a.ID] = a
471 f.order = append(f.order, a.ID)
472 f.mu.Unlock()
473
474 ctx, cancel := context.WithCancel(context.Background())
475 defer cancel()
476 stopDone := make(chan error, 1)
477 go func() { stopDone <- f.StopContext(ctx, a.ID) }()
478
479 // The listener notification proves Stop reached its shutdown wait. The
480 // operation lock must already be available while that wait is in progress.
481 if msg := <-listener.Lines(); msg == "" {
482 t.Fatal("shutdown command was empty")
483 }
484 operationReleased := make(chan struct{})
485 go func() {
486 f.operationMu.Lock()
487 close(operationReleased)
488 f.operationMu.Unlock()
489 }()
490 <-operationReleased
491
492 cancel()
493 if err := <-stopDone; !errors.Is(err, context.Canceled) {
494 t.Fatalf("StopContext error = %v; want context.Canceled", err)
495 }
496 if got := a.Status(); got != StatusDetached {
497 t.Fatalf("status after canceled stop = %s; want detached", got)
498 }
499}
500
501func TestStopDetachedWorkerClosesWaiters(t *testing.T) {
502 if runtime.GOOS == "windows" {
503 t.Skip("subagent inbox transport uses Unix-domain sockets")
504 }
505 path := filepath.Join(shortSocketDir(t), "agent.sock")
506 listener, err := Listen(path)
507 if err != nil {
508 t.Fatal(err)
509 }
510 t.Cleanup(func() { _ = listener.Close() })
511
512 root := t.TempDir()
513 f := New(Config{Root: root, RepoRoot: root})
514 stateDir := f.agentStateDir("detached-agent")
515 if err := os.MkdirAll(stateDir, 0o700); err != nil {
516 t.Fatal(err)
517 }
518 a := &Agent{
519 ID: "detached-agent",
520 stateDir: stateDir,
521 InboxPath: path,
522 inbox: NewInbox(path),
523 status: StatusDetached,
524 done: make(chan struct{}),
525 }
526 defer a.inbox.Close()
527 f.mu.Lock()
528 f.agents[a.ID] = a
529 f.order = append(f.order, a.ID)
530 f.mu.Unlock()
531
532 stopped := make(chan error, 1)
533 go func() { stopped <- f.StopContext(context.Background(), a.ID) }()
534 select {
535 case msg := <-listener.Lines():
536 if msg == "" {
537 t.Fatal("shutdown command was empty")
538 }
539 case <-time.After(time.Second):
540 t.Fatal("detached worker did not receive shutdown")
541 }
542 if err := listener.Close(); err != nil {
543 t.Fatal(err)
544 }
545 select {
546 case err := <-stopped:
547 if err != nil {
548 t.Fatalf("stop detached worker: %v", err)
549 }
550 case <-time.After(time.Second):
551 t.Fatal("detached worker stop did not finish")
552 }
553 select {
554 case <-a.done:
555 case <-time.After(time.Second):
556 t.Fatal("detached worker stop did not close waiters")
557 }
558 result, err := readTurnResult(stateDir)
559 if err != nil {
560 t.Fatal(err)
561 }
562 if result.AgentID != a.ID || result.Status != ResultCanceled {
563 t.Fatalf("detached worker result = agent %q status %q, want agent %q status %q", result.AgentID, result.Status, a.ID, ResultCanceled)
564 }
565}
566
567func TestStopDetachedWorkerHonorsCanceledShutdownSend(t *testing.T) {
568 if runtime.GOOS == "windows" {
569 t.Skip("subagent inbox transport uses Unix-domain sockets")
570 }
571 path := filepath.Join(shortSocketDir(t), "agent.sock")
572 listener, err := Listen(path)
573 if err != nil {
574 t.Fatal(err)
575 }
576 defer listener.Close()
577
578 local, remote := net.Pipe()
579 defer remote.Close()
580 conn := &blockingWriteConn{
581 Conn: local,
582 started: make(chan struct{}),
583 release: make(chan struct{}),
584 }
585 inbox := NewInbox(path)
586 inbox.conn = conn
587 defer inbox.Close()
588
589 f := New(Config{Root: t.TempDir()})
590 a := &Agent{
591 ID: "detached-cancel",
592 InboxPath: path,
593 inbox: inbox,
594 status: StatusDetached,
595 done: make(chan struct{}),
596 }
597 f.mu.Lock()
598 f.agents[a.ID] = a
599 f.order = append(f.order, a.ID)
600 f.mu.Unlock()
601
602 ctx, cancel := context.WithCancel(context.Background())
603 stopped := make(chan error, 1)
604 go func() { stopped <- f.StopContext(ctx, a.ID) }()
605 select {
606 case <-conn.started:
607 case <-time.After(time.Second):
608 t.Fatal("detached worker shutdown send did not start")
609 }
610 cancel()
611 select {
612 case err := <-stopped:
613 if !errors.Is(err, context.Canceled) {
614 t.Fatalf("stop detached worker error = %v, want context canceled", err)
615 }
616 case <-time.After(time.Second):
617 t.Fatal("detached worker shutdown send did not honor cancellation")
618 }
619}
620
621func TestStopAfterDoneIsNoop(t *testing.T) {
622 f := newTestSupervisor(t, func(a *Agent) Runner {
623 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
624 })
625 a, _ := f.Spawn(context.Background(), "quick")
626 a.Wait()
627 if err := f.Stop(a.ID); err != nil {
628 t.Fatalf("stop after done: %v", err)
629 }
630 if a.Status() != StatusDone {
631 t.Fatalf("status flipped to %s", a.Status())
632 }
633}
634
635func TestGetPrefixMatch(t *testing.T) {
636 f := newTestSupervisor(t, func(a *Agent) Runner {
637 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
638 })
639 a, _ := f.Spawn(context.Background(), "alpha task")
640 a.Wait()
641 // Full id works.
642 if got := f.Get(a.ID); got != a {
643 t.Fatal("get by full id failed")
644 }
645 // Slug prefix works as long as it's unique.
646 if got := f.Get("alpha"); got != a {
647 t.Fatal("get by prefix failed")
648 }
649 // Bogus id returns nil.
650 if got := f.Get("zzz-nope"); got != nil {
651 t.Fatalf("expected nil; got %#v", got)
652 }
653}
654
655func TestRemoveRequiresTerminalState(t *testing.T) {
656 hold := make(chan struct{})
657 f := newTestSupervisor(t, func(a *Agent) Runner {
658 return RunnerFunc(func(ctx context.Context, sink Sink) error {
659 <-hold
660 return nil
661 })
662 })
663 a, _ := f.Spawn(context.Background(), "still going")
664 // Wait for run goroutine to flip to running.
665 for i := 0; i < 100 && a.Status() != StatusRunning; i++ {
666 time.Sleep(time.Millisecond)
667 }
668 if err := f.Remove(a.ID); err == nil {
669 t.Fatal("remove of running agent should fail")
670 }
671 close(hold)
672 a.Wait()
673 if err := f.Remove(a.ID); err != nil {
674 t.Fatalf("remove after done: %v", err)
675 }
676 if got := f.Get(a.ID); got != nil {
677 t.Fatal("agent still present after remove")
678 }
679}
680
681func TestSnapshotIsStableAcrossAccess(t *testing.T) {
682 var wg sync.WaitGroup
683 wg.Add(1)
684 f := newTestSupervisor(t, func(a *Agent) Runner {
685 return RunnerFunc(func(ctx context.Context, sink Sink) error {
686 defer wg.Done()
687 for i := 0; i < 200; i++ {
688 sink.Transcript("chunk")
689 sink.Activity("step")
690 }
691 return nil
692 })
693 })
694 a, _ := f.Spawn(context.Background(), "race")
695 // Hammer Snapshot while the runner is writing; the -race detector
696 // is the real assertion here.
697 stop := make(chan struct{})
698 go func() {
699 for {
700 select {
701 case <-stop:
702 return
703 default:
704 _ = a.Snapshot()
705 }
706 }
707 }()
708 wg.Wait()
709 a.Wait()
710 close(stop)
711 if a.Status() != StatusDone {
712 t.Fatalf("status = %s", a.Status())
713 }
714}
715
716func TestTruncateIsRuneAware(t *testing.T) {
717 input := "αβγδεζη"
718 if got := truncate(input, 6); got != "αβγ..." {
719 t.Fatalf("truncate(%q, 6) = %q; want %q", input, got, "αβγ...")
720 }
721 if got := truncate(input, 3); got != "..." {
722 t.Fatalf("truncate(%q, 3) = %q; want %q", input, got, "...")
723 }
724 if got := truncate(input, 2); got != ".." {
725 t.Fatalf("truncate(%q, 2) = %q; want %q", input, got, "..")
726 }
727}
728
729func TestTaskSlug(t *testing.T) {
730 cases := map[string]string{
731 "fix the login form": "fix-the-login-form",
732 " weird --- spaces!! ": "weird-spaces",
733 "": "agent",
734 "a-very-long-task-name-that-overflows": "a-very-long-task-name-th",
735 }
736 for in, want := range cases {
737 if got := taskSlug(in); got != want {
738 t.Errorf("taskSlug(%q) = %q; want %q", in, got, want)
739 }
740 }
741}
742
743func TestSnapshotAllSorted(t *testing.T) {
744 f := newTestSupervisor(t, func(a *Agent) Runner {
745 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
746 })
747 a1, _ := f.Spawn(context.Background(), "first")
748 // Force second spawn into a later nanosecond bucket.
749 time.Sleep(2 * time.Millisecond)
750 a2, _ := f.Spawn(context.Background(), "second")
751 a1.Wait()
752 a2.Wait()
753 snaps := f.SnapshotAll()
754 if len(snaps) != 2 {
755 t.Fatalf("want 2 snapshots; got %d", len(snaps))
756 }
757 if !snaps[0].Started.Before(snaps[1].Started) && !snaps[0].Started.Equal(snaps[1].Started) {
758 t.Fatal("snapshots not in spawn order")
759 }
760}