@shogo-ai/worker 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +432 -0
  3. package/bin/shogo.mjs +38 -0
  4. package/dist/cli.mjs +64 -0
  5. package/package.json +62 -0
  6. package/src/cli.ts +199 -0
  7. package/src/commands/agent.ts +123 -0
  8. package/src/commands/config.ts +29 -0
  9. package/src/commands/doctor.ts +128 -0
  10. package/src/commands/login.ts +126 -0
  11. package/src/commands/logs.ts +24 -0
  12. package/src/commands/project-checkout.ts +179 -0
  13. package/src/commands/project-pull.ts +133 -0
  14. package/src/commands/project-push.ts +89 -0
  15. package/src/commands/runtime.ts +104 -0
  16. package/src/commands/start.ts +290 -0
  17. package/src/commands/status.ts +22 -0
  18. package/src/commands/stop.ts +13 -0
  19. package/src/lib/__tests__/agent-launch.test.ts +92 -0
  20. package/src/lib/__tests__/cloud-login-coverage.test.ts +210 -0
  21. package/src/lib/__tests__/cloud-login.test.ts +218 -0
  22. package/src/lib/__tests__/cloud-sync-watcher.test.ts +214 -0
  23. package/src/lib/__tests__/config.test.ts +141 -0
  24. package/src/lib/__tests__/db-doctor.test.ts +204 -0
  25. package/src/lib/__tests__/git-cloner.test.ts +382 -0
  26. package/src/lib/__tests__/runtime-manager-auto-pull.test.ts +275 -0
  27. package/src/lib/__tests__/runtime-manager-cloud-sync-gate.test.ts +119 -0
  28. package/src/lib/__tests__/runtime-manager-coverage-gaps.test.ts +768 -0
  29. package/src/lib/__tests__/runtime-manager-describe-rejection.test.ts +42 -0
  30. package/src/lib/__tests__/runtime-manager-git-pull.test.ts +207 -0
  31. package/src/lib/__tests__/runtime-manager-idle-eviction.test.ts +196 -0
  32. package/src/lib/__tests__/runtime-manager-restart-cap.test.ts +310 -0
  33. package/src/lib/__tests__/runtime-manager-tree-sitter-env.test.ts +189 -0
  34. package/src/lib/__tests__/runtime-manager-wait-for-health.test.ts +269 -0
  35. package/src/lib/__tests__/runtime-resolver.test.ts +112 -0
  36. package/src/lib/__tests__/tunnel-coverage.test.ts +1094 -0
  37. package/src/lib/__tests__/tunnel-structured-502.test.ts +101 -0
  38. package/src/lib/__tests__/worker-commands-greenfield.test.ts +463 -0
  39. package/src/lib/__tests__/worker-libs-greenfield.test.ts +355 -0
  40. package/src/lib/__tests__/worker-transport-greenfield.test.ts +176 -0
  41. package/src/lib/api-discovery.ts +36 -0
  42. package/src/lib/cloud-login.ts +320 -0
  43. package/src/lib/cloud-sync-watcher.ts +322 -0
  44. package/src/lib/config.ts +68 -0
  45. package/src/lib/db-doctor.ts +355 -0
  46. package/src/lib/device-id.ts +27 -0
  47. package/src/lib/git-cloner.ts +354 -0
  48. package/src/lib/paths.ts +53 -0
  49. package/src/lib/preflight.ts +158 -0
  50. package/src/lib/process-manager.ts +123 -0
  51. package/src/lib/resource-limits.ts +340 -0
  52. package/src/lib/runtime-install.ts +371 -0
  53. package/src/lib/runtime-manager.ts +1873 -0
  54. package/src/lib/runtime-resolver.ts +136 -0
  55. package/src/lib/transport.ts +202 -0
  56. package/src/lib/tunnel.ts +710 -0
  57. package/src/lib/user-agent.ts +28 -0
@@ -0,0 +1,42 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ /**
4
+ * Coverage for `WorkerRuntimeManager.describeRejection` — the
5
+ * structured-rejection hook that the {@link WorkerTunnel} reads when
6
+ * `resolveLocalUrl` returns null.
7
+ *
8
+ * The tunnel echoes the returned `code` + `message` (plus the original
9
+ * path) into the 502 body so a Studio client reading the response can
10
+ * tell exactly what the worker refused to do. This file pins the wire
11
+ * shape so a future refactor doesn't silently change the body Studio
12
+ * relies on.
13
+ */
14
+ import { describe, expect, it } from 'bun:test';
15
+ import { WorkerRuntimeManager } from '../runtime-manager.ts';
16
+
17
+ describe('WorkerRuntimeManager.describeRejection', () => {
18
+ it('flags non-/agent paths with CLI_WORKER_HAS_NO_DATA_API and the original path', () => {
19
+ const mgr = new WorkerRuntimeManager({});
20
+ const r = mgr.describeRejection('/api/projects?workspaceId=abc', 'p-1');
21
+ expect(r.code).toBe('CLI_WORKER_HAS_NO_DATA_API');
22
+ // Path in the message must be the pathname only — `?workspaceId=abc`
23
+ // is verbose and the tunnel echoes the full path separately.
24
+ expect(r.message).toContain('/api/projects');
25
+ expect(r.message).toContain('cli-worker only serves /agent/* paths');
26
+ });
27
+
28
+ it('flags /agent paths missing a project context with CLI_WORKER_NO_PROJECT_FOR_PATH', () => {
29
+ const mgr = new WorkerRuntimeManager({});
30
+ const r = mgr.describeRejection('/agent/chat', undefined);
31
+ expect(r.code).toBe('CLI_WORKER_NO_PROJECT_FOR_PATH');
32
+ expect(r.message).toContain('/agent/chat');
33
+ expect(r.message).toContain('projectId=none');
34
+ });
35
+
36
+ it('preserves the projectId when one is provided but no runtime exists', () => {
37
+ const mgr = new WorkerRuntimeManager({});
38
+ const r = mgr.describeRejection('/agent/chat', 'proj-42');
39
+ expect(r.code).toBe('CLI_WORKER_NO_PROJECT_FOR_PATH');
40
+ expect(r.message).toContain('projectId=proj-42');
41
+ });
42
+ });
@@ -0,0 +1,207 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ /**
4
+ * Tests for the GIT-mode branch of `WorkerRuntimeManager.maybeAutoPull`.
5
+ *
6
+ * Strategy:
7
+ * - Mock `../git-cloner` so `cloneProject()` doesn't actually spawn git.
8
+ * - Mock `globalThis.fetch` for the `.shogo/` SQLite top-up pass and
9
+ * the manifest endpoint.
10
+ * - Assert that:
11
+ * * a fresh project triggers `cloneProject()`
12
+ * * the `.shogo/` top-up issues a manifest+download for only those entries
13
+ * * a populated `.git/` dir skips re-clone
14
+ * * cloneProject failure flips the mode to 'files' and runs the
15
+ * file-transport fallback
16
+ */
17
+
18
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
19
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync, existsSync } from 'node:fs';
20
+ import { tmpdir } from 'node:os';
21
+ import { join } from 'node:path';
22
+
23
+ // ─── git-cloner fakes injected via `autoPull.gitOps` ────────────────
24
+ type CloneCall = { apiUrl: string; apiKey: string; projectId: string; localDir: string; shallow?: boolean };
25
+ const cloneCalls: CloneCall[] = [];
26
+ let cloneShouldThrow = false;
27
+ const gitOps = {
28
+ cloneProject: async (opts: CloneCall) => {
29
+ cloneCalls.push(opts);
30
+ if (cloneShouldThrow) throw new Error('simulated clone failure');
31
+ mkdirSync(join(opts.localDir, '.git'), { recursive: true });
32
+ writeFileSync(join(opts.localDir, 'AGENTS.md'), 'AGENT');
33
+ return { commitSha: 'deadbeef' };
34
+ },
35
+ gitIsAvailable: async () => true,
36
+ isGitRepo: (dir: string) => existsSync(join(dir, '.git')),
37
+ };
38
+
39
+ // ─── fetch shim ────────────────────────────────────────────────────
40
+ let scriptedFetch: typeof fetch | null = null;
41
+ const originalFetch = globalThis.fetch;
42
+ beforeEach(() => {
43
+ cloneCalls.length = 0;
44
+ cloneShouldThrow = false;
45
+ globalThis.fetch = ((input: any, init?: RequestInit) => {
46
+ if (!scriptedFetch) throw new Error('no scripted fetch set');
47
+ return scriptedFetch(input, init);
48
+ }) as any;
49
+ });
50
+ afterEach(() => {
51
+ globalThis.fetch = originalFetch;
52
+ scriptedFetch = null;
53
+ });
54
+
55
+ function scriptShogoTopup(files: Array<{ path: string; content: string }>) {
56
+ return (async (input: any, init?: RequestInit) => {
57
+ const url = typeof input === 'string' ? input : input.url;
58
+ if (url.includes('/workspace/manifest')) {
59
+ return new Response(
60
+ JSON.stringify({
61
+ ok: true,
62
+ projectId: 'p',
63
+ source: 's3',
64
+ generatedAt: '',
65
+ files: files.map((f) => ({ path: f.path, size: f.content.length, lastModified: null, etag: null })),
66
+ }),
67
+ { status: 200 },
68
+ );
69
+ }
70
+ if (url.includes('/s3/presign')) {
71
+ const body = JSON.parse((init?.body as string) || '{}') as { files: Array<{ path: string; action: string }> };
72
+ return new Response(
73
+ JSON.stringify({
74
+ ok: true,
75
+ urls: body.files.map((f) => ({ path: f.path, action: f.action, url: `https://dl.test/${encodeURIComponent(f.path)}` })),
76
+ }),
77
+ { status: 200 },
78
+ );
79
+ }
80
+ if (url.startsWith('https://dl.test/')) {
81
+ const path = decodeURIComponent(url.slice('https://dl.test/'.length));
82
+ const file = files.find((f) => f.path === path);
83
+ return file
84
+ ? new Response(file.content, { status: 200 })
85
+ : new Response('not found', { status: 404 });
86
+ }
87
+ return new Response('unhandled', { status: 500 });
88
+ }) as unknown as typeof fetch;
89
+ }
90
+
91
+ describe('WorkerRuntimeManager auto-pull (git mode)', () => {
92
+ let dir: string;
93
+ beforeEach(() => {
94
+ dir = mkdtempSync(join(tmpdir(), 'wrm-git-pull-'));
95
+ });
96
+ afterEach(() => {
97
+ try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
98
+ });
99
+
100
+ it('clones via git when git is available and the dir is empty', async () => {
101
+ const { WorkerRuntimeManager } = await import('../runtime-manager.ts');
102
+ scriptedFetch = scriptShogoTopup([]); // no .shogo entries
103
+ const mgr = new WorkerRuntimeManager({
104
+ autoPull: { enabled: true, projectsDir: dir, watch: false, useGit: true, gitOps },
105
+ });
106
+ const result = await mgr.ensurePulled('proj-g1', {
107
+ cloudUrl: 'https://api.test',
108
+ apiKey: 'shogo_sk_x',
109
+ });
110
+
111
+ expect(result.projectDir).toBe(join(dir, 'proj-g1'));
112
+ expect(cloneCalls.length).toBe(1);
113
+ expect(cloneCalls[0]!.projectId).toBe('proj-g1');
114
+ expect(cloneCalls[0]!.localDir).toBe(join(dir, 'proj-g1'));
115
+ expect(cloneCalls[0]!.shallow).toBe(true);
116
+ // The stub created AGENTS.md.
117
+ expect(existsSync(join(dir, 'proj-g1', 'AGENTS.md'))).toBe(true);
118
+ });
119
+
120
+ it('downloads .shogo/* via the file transport after a successful git clone', async () => {
121
+ const { WorkerRuntimeManager } = await import('../runtime-manager.ts');
122
+ scriptedFetch = scriptShogoTopup([
123
+ { path: '.shogo/db.sqlite', content: 'SQLITE-DB' },
124
+ { path: '.shogo/snapshots/2024.sqlite', content: 'SNAP' },
125
+ // non-.shogo entries in the manifest must be ignored by the top-up.
126
+ { path: 'src/App.tsx', content: 'APP' },
127
+ ]);
128
+ const mgr = new WorkerRuntimeManager({
129
+ autoPull: { enabled: true, projectsDir: dir, watch: false, useGit: true, gitOps },
130
+ });
131
+ await mgr.ensurePulled('proj-g2', {
132
+ cloudUrl: 'https://api.test',
133
+ apiKey: 'shogo_sk_x',
134
+ });
135
+
136
+ expect(cloneCalls.length).toBe(1);
137
+ const target = join(dir, 'proj-g2');
138
+ // .shogo/* was downloaded via the file transport.
139
+ expect(existsSync(join(target, '.shogo', 'db.sqlite'))).toBe(true);
140
+ expect(existsSync(join(target, '.shogo', 'snapshots', '2024.sqlite'))).toBe(true);
141
+ // src/App.tsx was NOT downloaded — git handles that.
142
+ expect(existsSync(join(target, 'src', 'App.tsx'))).toBe(false);
143
+ });
144
+
145
+ it('skips re-cloning when the local dir already has a .git/', async () => {
146
+ const { WorkerRuntimeManager } = await import('../runtime-manager.ts');
147
+ const target = join(dir, 'proj-g3');
148
+ mkdirSync(join(target, '.git'), { recursive: true });
149
+ writeFileSync(join(target, 'AGENTS.md'), 'existing');
150
+
151
+ scriptedFetch = scriptShogoTopup([]); // top-up still fires but is a no-op
152
+ const mgr = new WorkerRuntimeManager({
153
+ autoPull: { enabled: true, projectsDir: dir, watch: false, useGit: true, gitOps },
154
+ });
155
+ await mgr.ensurePulled('proj-g3', {
156
+ cloudUrl: 'https://api.test',
157
+ apiKey: 'shogo_sk_x',
158
+ });
159
+
160
+ expect(cloneCalls.length).toBe(0);
161
+ // Existing content is preserved.
162
+ expect(existsSync(join(target, 'AGENTS.md'))).toBe(true);
163
+ });
164
+
165
+ it('falls back to file transport when cloneProject throws', async () => {
166
+ const { WorkerRuntimeManager } = await import('../runtime-manager.ts');
167
+ cloneShouldThrow = true;
168
+ // Manifest serves a real file so the fallback downloadAll can produce something.
169
+ scriptedFetch = scriptShogoTopup([
170
+ { path: 'fallback.txt', content: 'FROM-FILES' },
171
+ ]);
172
+ const warnings: string[] = [];
173
+ const logger = {
174
+ log: () => {},
175
+ warn: (...args: any[]) => warnings.push(args.join(' ')),
176
+ error: () => {},
177
+ };
178
+
179
+ const mgr = new WorkerRuntimeManager({
180
+ autoPull: { enabled: true, projectsDir: dir, watch: false, useGit: true, logger, gitOps },
181
+ });
182
+ await mgr.ensurePulled('proj-g4', {
183
+ cloudUrl: 'https://api.test',
184
+ apiKey: 'shogo_sk_x',
185
+ });
186
+
187
+ expect(cloneCalls.length).toBe(1);
188
+ expect(warnings.some((w) => w.includes('git clone failed'))).toBe(true);
189
+ expect(existsSync(join(dir, 'proj-g4', 'fallback.txt'))).toBe(true);
190
+ });
191
+
192
+ it('uses file transport (not git) when useGit=false', async () => {
193
+ const { WorkerRuntimeManager } = await import('../runtime-manager.ts');
194
+ scriptedFetch = scriptShogoTopup([
195
+ { path: 'plain.txt', content: 'P' },
196
+ ]);
197
+ const mgr = new WorkerRuntimeManager({
198
+ autoPull: { enabled: true, projectsDir: dir, watch: false, useGit: false, gitOps },
199
+ });
200
+ await mgr.ensurePulled('proj-g5', {
201
+ cloudUrl: 'https://api.test',
202
+ apiKey: 'shogo_sk_x',
203
+ });
204
+ expect(cloneCalls.length).toBe(0);
205
+ expect(existsSync(join(dir, 'proj-g5', 'plain.txt'))).toBe(true);
206
+ });
207
+ });
@@ -0,0 +1,196 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ /**
4
+ * Coverage for `WorkerRuntimeManager` idle eviction.
5
+ *
6
+ * The "chat cut mid-stream" symptom in production was: the agent-proxy
7
+ * never refreshed `lastUsedAt` on a long stream, so a 16-minute Opus
8
+ * turn looked identical to a 16-minute idle slot to the manager and
9
+ * `stop()` fired mid-stream. The reaper itself is correct — it's
10
+ * `lastUsedAt` that wasn't being kept fresh by the surrounding HTTP
11
+ * code.
12
+ *
13
+ * The reproduction here pins both halves of the contract:
14
+ *
15
+ * 1. With NO `touch()` calls inside the idle window, the reaper
16
+ * DOES fire — the slot is killed, the process is SIGTERM'd, and
17
+ * the runtimes map is cleared. This is the bug surface.
18
+ *
19
+ * 2. With periodic `touch()` calls (modelling the agent-proxy
20
+ * forwarding chunks / the AI proxy receiving tool-call requests),
21
+ * the reaper NEVER fires across multiple idle windows — each
22
+ * touch resets the timer.
23
+ *
24
+ * The remaining tests pin the desktop / `SHOGO_LOCAL_MODE=true` opt-out:
25
+ * `idleMs: 0` (or any non-finite value) disables the reaper without
26
+ * ever arming a timer, so a long chat in local mode is not bounded by
27
+ * the cloud's 15-minute window even if a touch hook breaks somewhere.
28
+ */
29
+ import { describe, expect, it } from 'bun:test';
30
+ import { WorkerRuntimeManager } from '../runtime-manager.ts';
31
+
32
+ interface FakeProc {
33
+ exitCode: number | null;
34
+ signalCode: NodeJS.Signals | null;
35
+ killed: boolean;
36
+ pid: number;
37
+ kill: (signal?: NodeJS.Signals | number) => boolean;
38
+ once: (event: string, cb: (...args: unknown[]) => void) => FakeProc;
39
+ on: (event: string, cb: (...args: unknown[]) => void) => FakeProc;
40
+ }
41
+
42
+ function makeFakeProc(): FakeProc {
43
+ const proc: FakeProc = {
44
+ // `waitForExit` short-circuits when `exitCode !== null`, so a
45
+ // synchronous "already exited" fake keeps stop() from blocking on
46
+ // an exit listener that never fires.
47
+ exitCode: 0,
48
+ signalCode: null,
49
+ killed: false,
50
+ pid: 99999,
51
+ kill(_signal?: NodeJS.Signals | number) {
52
+ proc.killed = true;
53
+ return true;
54
+ },
55
+ once(_event, _cb) {
56
+ return proc;
57
+ },
58
+ on(_event, _cb) {
59
+ return proc;
60
+ },
61
+ };
62
+ return proc;
63
+ }
64
+
65
+ function insertRunningSlot(mgr: WorkerRuntimeManager, projectId: string) {
66
+ const proc = makeFakeProc();
67
+ const slot = {
68
+ projectId,
69
+ // 0 short-circuits releasePort and avoids polluting usedPorts.
70
+ agentPort: 0,
71
+ apiServerPort: 0,
72
+ status: 'running' as const,
73
+ proc,
74
+ startedAt: Date.now(),
75
+ lastUsedAt: Date.now(),
76
+ restarts: 0,
77
+ restartTimer: null,
78
+ idleTimer: null,
79
+ spawnConfig: {} as never,
80
+ startPromise: null,
81
+ };
82
+ // The runtimes map and armIdleTimer are private; tests reach in
83
+ // deliberately because the public surface (ensureRunning) requires a
84
+ // real binary on disk.
85
+ (mgr as unknown as { runtimes: Map<string, typeof slot> }).runtimes.set(
86
+ projectId,
87
+ slot,
88
+ );
89
+ return slot;
90
+ }
91
+
92
+ function armIdle(mgr: WorkerRuntimeManager, slot: ReturnType<typeof insertRunningSlot>): void {
93
+ (mgr as unknown as { armIdleTimer: (s: typeof slot) => void }).armIdleTimer(slot);
94
+ }
95
+
96
+ const SILENT = { log: () => {}, warn: () => {}, error: () => {} } as const;
97
+
98
+ describe('WorkerRuntimeManager idle eviction', () => {
99
+ it('reproduction: with no touches inside the idle window, the reaper kills the runtime (mid-stream cut)', async () => {
100
+ // 50ms idle window stands in for the production 15min — what we
101
+ // want to pin is "no `touch()` for >= idleMs ⇒ stop() fires", not
102
+ // the magnitude.
103
+ const mgr = new WorkerRuntimeManager({ idleMs: 50, logger: SILENT });
104
+ const slot = insertRunningSlot(mgr, 'proj-stream');
105
+
106
+ armIdle(mgr, slot);
107
+ expect(slot.idleTimer).not.toBeNull();
108
+
109
+ // Simulate an agent-proxy that forwards bytes for >idleMs but
110
+ // never calls `touch()` — exactly the production bug. The
111
+ // manager has no notion of in-flight HTTP, so it evicts.
112
+ await new Promise((r) => setTimeout(r, 120));
113
+
114
+ const runtimes = (mgr as unknown as { runtimes: Map<string, unknown> }).runtimes;
115
+ expect(runtimes.has('proj-stream')).toBe(false);
116
+ expect(slot.proc.killed).toBe(true);
117
+ });
118
+
119
+ it('fix: periodic touch() inside the idle window keeps the runtime alive across multiple windows', async () => {
120
+ const idleMs = 50;
121
+ const mgr = new WorkerRuntimeManager({ idleMs, logger: SILENT });
122
+ const slot = insertRunningSlot(mgr, 'proj-active');
123
+
124
+ armIdle(mgr, slot);
125
+
126
+ // Models the activity hooks the API server will install:
127
+ // - agent-proxy `touch(projectId)` on each forwarded SSE chunk,
128
+ // - AI proxy `touch(projectId)` after token decode.
129
+ // We tick every (idleMs / 5) for ~3x the idle window so the
130
+ // assertion lands well past where an unfixed reaper would fire.
131
+ const touchInterval = setInterval(() => mgr.touch('proj-active'), Math.max(5, idleMs / 5));
132
+ await new Promise((r) => setTimeout(r, idleMs * 3));
133
+ clearInterval(touchInterval);
134
+
135
+ const runtimes = (mgr as unknown as { runtimes: Map<string, unknown> }).runtimes;
136
+ expect(runtimes.has('proj-active')).toBe(true);
137
+ expect(slot.proc.killed).toBe(false);
138
+
139
+ // Sanity: once activity stops, the reaper does fire — the slot is
140
+ // not somehow "stuck alive". This pins the resume-eviction-on-idle
141
+ // half of the contract so a future regression that disables the
142
+ // reaper outright still gets caught.
143
+ await new Promise((r) => setTimeout(r, idleMs * 3));
144
+ expect(runtimes.has('proj-active')).toBe(false);
145
+ expect(slot.proc.killed).toBe(true);
146
+ });
147
+
148
+ it('touch() on an unknown projectId is a safe no-op', () => {
149
+ const mgr = new WorkerRuntimeManager({ idleMs: 50, logger: SILENT });
150
+ expect(() => mgr.touch('does-not-exist')).not.toThrow();
151
+ });
152
+
153
+ it('idleMs=0 disables the reaper (no timer is armed) — desktop opt-out', async () => {
154
+ const mgr = new WorkerRuntimeManager({ idleMs: 0, logger: SILENT });
155
+ const slot = insertRunningSlot(mgr, 'proj-local');
156
+
157
+ armIdle(mgr, slot);
158
+ expect(slot.idleTimer).toBeNull();
159
+
160
+ await new Promise((r) => setTimeout(r, 50));
161
+ const runtimes = (mgr as unknown as { runtimes: Map<string, unknown> }).runtimes;
162
+ expect(runtimes.has('proj-local')).toBe(true);
163
+ expect(slot.proc.killed).toBe(false);
164
+ });
165
+
166
+ it('idleMs=Infinity also disables the reaper', () => {
167
+ const mgr = new WorkerRuntimeManager({
168
+ idleMs: Number.POSITIVE_INFINITY,
169
+ logger: SILENT,
170
+ });
171
+ const slot = insertRunningSlot(mgr, 'proj-inf');
172
+
173
+ armIdle(mgr, slot);
174
+ expect(slot.idleTimer).toBeNull();
175
+ });
176
+
177
+ it('negative idleMs disables the reaper (defensive)', () => {
178
+ const mgr = new WorkerRuntimeManager({ idleMs: -1, logger: SILENT });
179
+ const slot = insertRunningSlot(mgr, 'proj-neg');
180
+
181
+ armIdle(mgr, slot);
182
+ expect(slot.idleTimer).toBeNull();
183
+ });
184
+
185
+ it('idleMs > 0 still arms a timer (cloud regression)', () => {
186
+ const mgr = new WorkerRuntimeManager({ idleMs: 1_000, logger: SILENT });
187
+ const slot = insertRunningSlot(mgr, 'proj-cloud');
188
+
189
+ armIdle(mgr, slot);
190
+ expect(slot.idleTimer).not.toBeNull();
191
+ if (slot.idleTimer) {
192
+ clearTimeout(slot.idleTimer);
193
+ slot.idleTimer = null;
194
+ }
195
+ });
196
+ });