| 1 | package control |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "testing" |
| 6 | "time" |
| 7 | |
| 8 | "reasonix/internal/session" |
| 9 | ) |
| 10 | |
| 11 | func TestRecoveryCloseWaitsForTerminalFanout(t *testing.T) { |
| 12 | entered := make(chan struct{}, 1) |
| 13 | release := make(chan struct{}) |
| 14 | c := newOwnedTestController(t, Options{Sink: holdFinishingWindow(release, entered, nil)}) |
| 15 | // Always release the sink before the controller cleanup joins it. |
| 16 | t.Cleanup(func() { close(release) }) |
| 17 | started := make(chan struct{}) |
| 18 | body := make(chan struct{}) |
| 19 | c.runGuarded(func(context.Context) error { |
| 20 | close(started) |
| 21 | <-body |
| 22 | return nil |
| 23 | }) |
| 24 | <-started |
| 25 | // Force the state published by the cancellation watchdog, then hold the |
| 26 | // recovered body's terminal fanout while Close races its completion. |
| 27 | c.mu.Lock() |
| 28 | c.turns.phase = session.RuntimeRecoveryRequired |
| 29 | c.mu.Unlock() |
| 30 | close(body) |
| 31 | select { |
| 32 | case <-entered: |
| 33 | case <-time.After(5 * time.Second): |
| 34 | t.Fatal("recovery terminal fanout did not start") |
| 35 | } |
| 36 | c.Close() |
| 37 | select { |
| 38 | case <-c.closeFinalized: |
| 39 | t.Fatal("controller resources closed while recovery terminal fanout was active") |
| 40 | default: |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | func TestRecoveryCloseWaitsForWatchdogFanout(t *testing.T) { |
| 45 | entered := make(chan struct{}, 1) |
| 46 | release := make(chan struct{}) |
| 47 | c := newOwnedTestController(t, Options{Sink: holdFinishingWindow(release, entered, nil)}) |
| 48 | t.Cleanup(func() { close(release) }) |
| 49 | c.testCancelGrace = time.Millisecond |
| 50 | started := make(chan struct{}) |
| 51 | body := make(chan struct{}) |
| 52 | c.runGuarded(func(ctx context.Context) error { |
| 53 | close(started) |
| 54 | <-body |
| 55 | return ctx.Err() |
| 56 | }) |
| 57 | <-started |
| 58 | idle, _ := c.TurnIdleDone() |
| 59 | c.CancelSession() |
| 60 | select { |
| 61 | case <-entered: |
| 62 | case <-time.After(5 * time.Second): |
| 63 | close(body) |
| 64 | t.Fatal("watchdog terminal fanout did not start") |
| 65 | } |
| 66 | c.Close() |
| 67 | close(body) |
| 68 | select { |
| 69 | case <-idle: |
| 70 | case <-time.After(5 * time.Second): |
| 71 | t.Fatal("recovery body did not settle") |
| 72 | } |
| 73 | select { |
| 74 | case <-c.closeFinalized: |
| 75 | t.Fatal("controller resources closed while watchdog terminal fanout was active") |
| 76 | default: |
| 77 | } |
| 78 | } |
| 79 |