@aiden-ade/sandbox-agent 0.1.21 → 0.1.23

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 (77) hide show
  1. package/dist/__tests__/cli-executable.test.d.ts +2 -0
  2. package/dist/__tests__/cli-executable.test.d.ts.map +1 -0
  3. package/dist/__tests__/cli-executable.test.js +83 -0
  4. package/dist/__tests__/cli-executable.test.js.map +1 -0
  5. package/dist/__tests__/cli-help.test.d.ts +2 -0
  6. package/dist/__tests__/cli-help.test.d.ts.map +1 -0
  7. package/dist/__tests__/cli-help.test.js +33 -0
  8. package/dist/__tests__/cli-help.test.js.map +1 -0
  9. package/dist/__tests__/core-agent-workflow-context.test.d.ts +2 -0
  10. package/dist/__tests__/core-agent-workflow-context.test.d.ts.map +1 -0
  11. package/dist/__tests__/core-agent-workflow-context.test.js +50 -0
  12. package/dist/__tests__/core-agent-workflow-context.test.js.map +1 -0
  13. package/dist/__tests__/daemon-socket.integration.test.d.ts +2 -0
  14. package/dist/__tests__/daemon-socket.integration.test.d.ts.map +1 -0
  15. package/dist/__tests__/daemon-socket.integration.test.js +156 -0
  16. package/dist/__tests__/daemon-socket.integration.test.js.map +1 -0
  17. package/dist/__tests__/daemon.test.d.ts +2 -0
  18. package/dist/__tests__/daemon.test.d.ts.map +1 -0
  19. package/dist/__tests__/daemon.test.js +1014 -0
  20. package/dist/__tests__/daemon.test.js.map +1 -0
  21. package/dist/__tests__/generated-images.test.d.ts +2 -0
  22. package/dist/__tests__/generated-images.test.d.ts.map +1 -0
  23. package/dist/__tests__/generated-images.test.js +50 -0
  24. package/dist/__tests__/generated-images.test.js.map +1 -0
  25. package/dist/__tests__/sandbox-workflow-context.test.d.ts +2 -0
  26. package/dist/__tests__/sandbox-workflow-context.test.d.ts.map +1 -0
  27. package/dist/__tests__/sandbox-workflow-context.test.js +66 -0
  28. package/dist/__tests__/sandbox-workflow-context.test.js.map +1 -0
  29. package/dist/aiden-system-prompt.d.ts +6 -0
  30. package/dist/aiden-system-prompt.d.ts.map +1 -0
  31. package/dist/aiden-system-prompt.js +6 -0
  32. package/dist/aiden-system-prompt.js.map +1 -0
  33. package/dist/cli-executable.d.ts +58 -0
  34. package/dist/cli-executable.d.ts.map +1 -0
  35. package/dist/cli-executable.js +301 -0
  36. package/dist/cli-executable.js.map +1 -0
  37. package/dist/cli-help.d.ts +7 -0
  38. package/dist/cli-help.d.ts.map +1 -0
  39. package/dist/cli-help.js +45 -0
  40. package/dist/cli-help.js.map +1 -0
  41. package/dist/core-agent.d.ts +6 -2
  42. package/dist/core-agent.d.ts.map +1 -1
  43. package/dist/core-agent.js +140 -21
  44. package/dist/core-agent.js.map +1 -1
  45. package/dist/daemon.d.ts +46 -0
  46. package/dist/daemon.d.ts.map +1 -0
  47. package/dist/daemon.js +876 -0
  48. package/dist/daemon.js.map +1 -0
  49. package/dist/generated-images.d.ts +4 -0
  50. package/dist/generated-images.d.ts.map +1 -0
  51. package/dist/generated-images.js +142 -0
  52. package/dist/generated-images.js.map +1 -0
  53. package/dist/index.cjs +245 -37
  54. package/dist/index.d.ts +4 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +107 -42
  57. package/dist/index.js.map +1 -1
  58. package/dist/sandbox.d.ts +6 -0
  59. package/dist/sandbox.d.ts.map +1 -1
  60. package/dist/sandbox.js +40 -10
  61. package/dist/sandbox.js.map +1 -1
  62. package/dist/updater.d.ts.map +1 -1
  63. package/dist/updater.js +0 -4
  64. package/dist/updater.js.map +1 -1
  65. package/dist/version.d.ts +1 -1
  66. package/dist/version.d.ts.map +1 -1
  67. package/dist/version.js +1 -1
  68. package/dist/version.js.map +1 -1
  69. package/dist/web-presenter.d.ts +7 -4
  70. package/dist/web-presenter.d.ts.map +1 -1
  71. package/dist/web-presenter.js +29 -4
  72. package/dist/web-presenter.js.map +1 -1
  73. package/dist/ws-client.d.ts +35 -0
  74. package/dist/ws-client.d.ts.map +1 -1
  75. package/dist/ws-client.js +32 -9
  76. package/dist/ws-client.js.map +1 -1
  77. package/package.json +2 -1
@@ -0,0 +1,1014 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { createServer } from "node:http";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
+ const mocks = vi.hoisted(() => ({
8
+ spawnSync: vi.fn(),
9
+ io: vi.fn(),
10
+ coreAgentConstructor: vi.fn(),
11
+ agentRun: vi.fn(),
12
+ agentKill: vi.fn(),
13
+ sendToolResponse: vi.fn(),
14
+ sendUserMessage: vi.fn(),
15
+ collectLocalAgentProviderLimits: vi.fn(),
16
+ }));
17
+ vi.mock("node:child_process", async () => {
18
+ const actual = await vi.importActual("node:child_process");
19
+ return {
20
+ ...actual,
21
+ spawnSync: mocks.spawnSync,
22
+ };
23
+ });
24
+ vi.mock("socket.io-client", () => ({
25
+ io: mocks.io,
26
+ }));
27
+ vi.mock("../core-agent.js", () => ({
28
+ CoreAgent: mocks.coreAgentConstructor,
29
+ }));
30
+ vi.mock("@aiden/shared/node/provider-limits", () => ({
31
+ collectLocalAgentProviderLimits: mocks.collectLocalAgentProviderLimits,
32
+ }));
33
+ function seedCliBinaries(baseDir) {
34
+ const binDir = join(baseDir, "bin");
35
+ mkdirSync(binDir, { recursive: true });
36
+ for (const name of ["codex", "claude", "gemini", "git"]) {
37
+ const path = join(binDir, name);
38
+ writeFileSync(path, "#!/bin/sh\n");
39
+ chmodSync(path, 0o755);
40
+ }
41
+ return binDir;
42
+ }
43
+ class TestSocket extends EventEmitter {
44
+ connected = true;
45
+ outbound = [];
46
+ disconnected = false;
47
+ emit(event, payload) {
48
+ this.outbound.push({ event, payload });
49
+ return super.emit(event, payload);
50
+ }
51
+ disconnect() {
52
+ this.disconnected = true;
53
+ }
54
+ }
55
+ async function waitForOutbound(socket, event) {
56
+ for (let attempt = 0; attempt < 100; attempt += 1) {
57
+ const item = socket.outbound.find((entry) => entry.event === event);
58
+ if (item) {
59
+ return item;
60
+ }
61
+ await new Promise((resolve) => setTimeout(resolve, 50));
62
+ }
63
+ throw new Error(`Timed out waiting for outbound event ${event}`);
64
+ }
65
+ describe("aiden-agent daemon CLI helpers", () => {
66
+ let tempDir;
67
+ let configPath;
68
+ beforeEach(() => {
69
+ vi.resetModules();
70
+ vi.unstubAllGlobals();
71
+ vi.clearAllMocks();
72
+ tempDir = mkdtempSync(join(tmpdir(), "aiden-agent-test-"));
73
+ configPath = join(tempDir, "config.json");
74
+ process.env.AIDEN_AGENT_CONFIG_PATH = configPath;
75
+ mocks.collectLocalAgentProviderLimits.mockResolvedValue({});
76
+ mocks.agentRun.mockResolvedValue(undefined);
77
+ mocks.sendToolResponse.mockReturnValue(true);
78
+ mocks.sendUserMessage.mockReturnValue(true);
79
+ mocks.coreAgentConstructor.mockImplementation(function MockCoreAgent() {
80
+ return {
81
+ run: mocks.agentRun,
82
+ kill: mocks.agentKill,
83
+ sendToolResponse: mocks.sendToolResponse,
84
+ sendUserMessage: mocks.sendUserMessage,
85
+ };
86
+ });
87
+ });
88
+ afterEach(() => {
89
+ delete process.env.AIDEN_AGENT_CONFIG_PATH;
90
+ delete process.env.AIDEN_API_URL;
91
+ delete process.env.AIDEN_WS_URL;
92
+ delete process.env.AIDEN_AGENT_PROFILE;
93
+ delete process.env.AIDEN_AGENT_ENDPOINTS_PATH;
94
+ delete process.env.AIDEN_TEAM_ID;
95
+ delete process.env.AIDEN_SETUP_TOKEN;
96
+ delete process.env.AIDEN_CODEX_PATH;
97
+ delete process.env.AIDEN_CLAUDE_PATH;
98
+ delete process.env.AIDEN_GEMINI_PATH;
99
+ rmSync(tempDir, { recursive: true, force: true });
100
+ vi.unstubAllGlobals();
101
+ vi.unstubAllEnvs();
102
+ });
103
+ it("discovers sanitized local capabilities without exposing binary paths", async () => {
104
+ const binDir = seedCliBinaries(tempDir);
105
+ process.env.PATH = binDir;
106
+ vi.stubEnv("SHELL", join(tempDir, "missing-shell"));
107
+ const { discoverCapabilities } = await import("../daemon.js");
108
+ const capabilities = discoverCapabilities();
109
+ expect(capabilities.hasGit).toBe(true);
110
+ expect(capabilities.hasTerminal).toBe(true);
111
+ expect(capabilities.supportsFilesystem).toBe(true);
112
+ expect(capabilities.agents).toHaveLength(9);
113
+ expect(capabilities.agents).toEqual(expect.arrayContaining([
114
+ expect.objectContaining({
115
+ provider: "codex_app_server",
116
+ available: true,
117
+ models: [],
118
+ }),
119
+ expect.objectContaining({
120
+ provider: "claude_cli",
121
+ available: true,
122
+ }),
123
+ ]));
124
+ expect(capabilities.agents.every((agent) => agent.version === undefined)).toBe(true);
125
+ expect(mocks.spawnSync).not.toHaveBeenCalled();
126
+ expect(JSON.stringify(capabilities)).not.toContain("/Users/");
127
+ });
128
+ it("setup registers a daemon runtime and persists only runtime config locally", async () => {
129
+ mocks.spawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
130
+ const fetchMock = vi.fn().mockResolvedValue({
131
+ ok: true,
132
+ json: async () => ({
133
+ runtime: {
134
+ id: "11111111-1111-4111-8111-111111111111",
135
+ displayName: "CI Runtime",
136
+ },
137
+ runtimeToken: "aiden_runtime_secret",
138
+ }),
139
+ });
140
+ vi.stubGlobal("fetch", fetchMock);
141
+ const { setupDaemon } = await import("../daemon.js");
142
+ await setupDaemon([
143
+ "--api-url",
144
+ "http://api.test",
145
+ "--ws-url",
146
+ "ws://ws.test",
147
+ "--team",
148
+ "22222222-2222-4222-8222-222222222222",
149
+ "--token",
150
+ "aiden_pat_secret",
151
+ "--name",
152
+ "CI Runtime",
153
+ "--legacy-local-machine-id",
154
+ "33333333-3333-4333-8333-333333333333",
155
+ ]);
156
+ expect(fetchMock).toHaveBeenCalledWith("http://api.test/public/runtimes", expect.objectContaining({
157
+ method: "POST",
158
+ headers: expect.objectContaining({
159
+ authorization: "Bearer aiden_pat_secret",
160
+ }),
161
+ }));
162
+ const body = JSON.parse(fetchMock.mock.calls[0][1].body);
163
+ expect(body).toMatchObject({
164
+ teamId: "22222222-2222-4222-8222-222222222222",
165
+ displayName: "CI Runtime",
166
+ runtimeKind: "machine",
167
+ hostKind: "daemon",
168
+ lifecycle: "durable",
169
+ ownerType: "team",
170
+ visibility: "team",
171
+ legacyLocalMachineId: "33333333-3333-4333-8333-333333333333",
172
+ });
173
+ const saved = JSON.parse(readFileSync(configPath, "utf8"));
174
+ expect(saved).toEqual({
175
+ apiUrl: "http://api.test",
176
+ wsUrl: "ws://ws.test",
177
+ endpointProfile: "custom",
178
+ teamId: "22222222-2222-4222-8222-222222222222",
179
+ runtimeId: "11111111-1111-4111-8111-111111111111",
180
+ runtimeToken: "aiden_runtime_secret",
181
+ localApiPort: 47831,
182
+ localApiToken: expect.any(String),
183
+ displayName: "CI Runtime",
184
+ });
185
+ expect(readFileSync(configPath, "utf8")).not.toContain("aiden_pat_secret");
186
+ });
187
+ it("setup uses install-script endpoint defaults when endpoint flags are omitted", async () => {
188
+ mocks.spawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
189
+ const endpointsPath = join(tempDir, "endpoints.json");
190
+ process.env.AIDEN_AGENT_ENDPOINTS_PATH = endpointsPath;
191
+ writeFileSync(endpointsPath, JSON.stringify({
192
+ apiUrl: "https://preview-api.aiden-platform.com",
193
+ wsUrl: "wss://preview-ws.aiden-platform.com",
194
+ endpointProfile: "custom",
195
+ }));
196
+ const fetchMock = vi.fn().mockResolvedValue({
197
+ ok: true,
198
+ json: async () => ({
199
+ runtime: {
200
+ id: "11111111-1111-4111-8111-111111111111",
201
+ displayName: "Installed Runtime",
202
+ },
203
+ runtimeToken: "aiden_runtime_secret",
204
+ }),
205
+ });
206
+ vi.stubGlobal("fetch", fetchMock);
207
+ const { setupDaemon } = await import("../daemon.js");
208
+ await setupDaemon(["--setup-token", "aiden_setup_secret", "--name", "Installed Runtime"]);
209
+ expect(fetchMock).toHaveBeenCalledWith("https://preview-api.aiden-platform.com/public/runtimes/register-with-setup-token", expect.objectContaining({ method: "POST" }));
210
+ const saved = JSON.parse(readFileSync(configPath, "utf8"));
211
+ expect(saved).toMatchObject({
212
+ apiUrl: "https://preview-api.aiden-platform.com",
213
+ wsUrl: "wss://preview-ws.aiden-platform.com",
214
+ endpointProfile: "custom",
215
+ runtimeId: "11111111-1111-4111-8111-111111111111",
216
+ runtimeToken: "aiden_runtime_secret",
217
+ });
218
+ });
219
+ it("setup fails fast without team and setup token", async () => {
220
+ const { setupDaemon } = await import("../daemon.js");
221
+ await expect(setupDaemon(["--team", "22222222-2222-4222-8222-222222222222"])).rejects.toThrow("setup requires --setup-token <token>");
222
+ });
223
+ it("setup can register with a short-lived runtime setup token without storing a PAT", async () => {
224
+ mocks.spawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
225
+ const fetchMock = vi.fn().mockResolvedValue({
226
+ ok: true,
227
+ json: async () => ({
228
+ runtime: {
229
+ id: "11111111-1111-4111-8111-111111111111",
230
+ displayName: "Headless CI",
231
+ },
232
+ runtimeToken: "aiden_runtime_secret",
233
+ }),
234
+ });
235
+ vi.stubGlobal("fetch", fetchMock);
236
+ const { setupDaemon } = await import("../daemon.js");
237
+ await setupDaemon([
238
+ "--api-url",
239
+ "http://api.test",
240
+ "--ws-url",
241
+ "ws://ws.test",
242
+ "--setup-token",
243
+ "aiden_setup_secret",
244
+ "--name",
245
+ "Headless CI",
246
+ ]);
247
+ expect(fetchMock).toHaveBeenCalledWith("http://api.test/public/runtimes/register-with-setup-token", expect.objectContaining({
248
+ method: "POST",
249
+ headers: { "content-type": "application/json" },
250
+ }));
251
+ const body = JSON.parse(fetchMock.mock.calls[0][1].body);
252
+ expect(body).toMatchObject({
253
+ setupToken: "aiden_setup_secret",
254
+ displayName: "Headless CI",
255
+ runtimeKind: "machine",
256
+ hostKind: "daemon",
257
+ ownerType: "team",
258
+ visibility: "team",
259
+ });
260
+ expect(body).not.toHaveProperty("token");
261
+ const saved = JSON.parse(readFileSync(configPath, "utf8"));
262
+ expect(saved.runtimeToken).toBe("aiden_runtime_secret");
263
+ expect(readFileSync(configPath, "utf8")).not.toContain("aiden_setup_secret");
264
+ });
265
+ it("setup can register a personal runtime with a setup token", async () => {
266
+ mocks.spawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
267
+ const fetchMock = vi.fn().mockResolvedValue({
268
+ ok: true,
269
+ json: async () => ({
270
+ runtime: {
271
+ id: "11111111-1111-4111-8111-111111111111",
272
+ displayName: "Personal Laptop",
273
+ },
274
+ runtimeToken: "aiden_runtime_secret",
275
+ }),
276
+ });
277
+ vi.stubGlobal("fetch", fetchMock);
278
+ const { setupDaemon } = await import("../daemon.js");
279
+ await setupDaemon([
280
+ "--api-url",
281
+ "http://api.test",
282
+ "--ws-url",
283
+ "ws://ws.test",
284
+ "--setup-token",
285
+ "aiden_setup_secret",
286
+ "--scope",
287
+ "user",
288
+ "--name",
289
+ "Personal Laptop",
290
+ ]);
291
+ const body = JSON.parse(fetchMock.mock.calls[0][1].body);
292
+ expect(body).toMatchObject({
293
+ setupToken: "aiden_setup_secret",
294
+ displayName: "Personal Laptop",
295
+ ownerType: "user",
296
+ visibility: "user",
297
+ });
298
+ });
299
+ it("setup uses production endpoints by default for installable package users", async () => {
300
+ mocks.spawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
301
+ const fetchMock = vi.fn().mockResolvedValue({
302
+ ok: true,
303
+ json: async () => ({
304
+ runtime: {
305
+ id: "11111111-1111-4111-8111-111111111111",
306
+ displayName: "Prod Runtime",
307
+ },
308
+ runtimeToken: "aiden_runtime_secret",
309
+ }),
310
+ });
311
+ vi.stubGlobal("fetch", fetchMock);
312
+ const { setupDaemon } = await import("../daemon.js");
313
+ await setupDaemon(["--setup-token", "aiden_setup_secret"]);
314
+ expect(fetchMock).toHaveBeenCalledWith("https://api.aiden-platform.com/public/runtimes/register-with-setup-token", expect.objectContaining({ method: "POST" }));
315
+ const saved = JSON.parse(readFileSync(configPath, "utf8"));
316
+ expect(saved).toMatchObject({
317
+ apiUrl: "https://api.aiden-platform.com",
318
+ wsUrl: "wss://ws.aiden-platform.com",
319
+ endpointProfile: "production",
320
+ });
321
+ });
322
+ it("setup uses localhost only when explicitly requested with local profile", async () => {
323
+ mocks.spawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
324
+ const fetchMock = vi.fn().mockResolvedValue({
325
+ ok: true,
326
+ json: async () => ({
327
+ runtime: {
328
+ id: "11111111-1111-4111-8111-111111111111",
329
+ displayName: "Local Runtime",
330
+ },
331
+ runtimeToken: "aiden_runtime_secret",
332
+ }),
333
+ });
334
+ vi.stubGlobal("fetch", fetchMock);
335
+ const { setupDaemon } = await import("../daemon.js");
336
+ await setupDaemon(["--profile", "local", "--setup-token", "aiden_setup_secret"]);
337
+ expect(fetchMock).toHaveBeenCalledWith("http://localhost:8400/public/runtimes/register-with-setup-token", expect.objectContaining({ method: "POST" }));
338
+ const saved = JSON.parse(readFileSync(configPath, "utf8"));
339
+ expect(saved).toMatchObject({
340
+ apiUrl: "http://localhost:8400",
341
+ wsUrl: "ws://localhost:8401",
342
+ endpointProfile: "local",
343
+ });
344
+ });
345
+ it("rejects startDaemon when an aiden-agent daemon is already listening", async () => {
346
+ writeFileSync(configPath, JSON.stringify({
347
+ runtimeId: "11111111-1111-4111-8111-111111111111",
348
+ runtimeToken: "aiden_runtime_secret",
349
+ wsUrl: "ws://ws.test",
350
+ localApiToken: "local-secret",
351
+ }));
352
+ const existing = createServer((req, res) => {
353
+ if (req.method === "GET" && req.url === "/status") {
354
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
355
+ configured: true,
356
+ runtimeId: "11111111-1111-4111-8111-111111111111",
357
+ wsUrl: "ws://ws.test",
358
+ connected: true,
359
+ activeRunIds: [],
360
+ localApiPort: 47831,
361
+ version: "test",
362
+ }));
363
+ return;
364
+ }
365
+ res.writeHead(404).end();
366
+ });
367
+ await new Promise((resolve) => existing.listen(0, "127.0.0.1", resolve));
368
+ const address = existing.address();
369
+ const port = typeof address === "object" && address ? address.port : 47831;
370
+ const socket = new TestSocket();
371
+ mocks.io.mockReturnValue(socket);
372
+ const { startDaemon } = await import("../daemon.js");
373
+ await expect(startDaemon(["--local-api-port", String(port)])).rejects.toThrow("already in use");
374
+ expect(socket.disconnected).toBe(true);
375
+ await new Promise((resolve, reject) => {
376
+ existing.close((error) => (error ? reject(error) : resolve()));
377
+ });
378
+ });
379
+ it("runDaemon exits successfully when the local daemon is listening with a stale token", async () => {
380
+ writeFileSync(configPath, JSON.stringify({
381
+ runtimeId: "11111111-1111-4111-8111-111111111111",
382
+ runtimeToken: "aiden_runtime_secret",
383
+ wsUrl: "ws://ws.test",
384
+ localApiToken: "stale-token",
385
+ }));
386
+ const existing = createServer((req, res) => {
387
+ if (req.method === "GET" && req.url === "/status") {
388
+ res
389
+ .writeHead(401, { "content-type": "application/json" })
390
+ .end(JSON.stringify({ error: "Unauthorized" }));
391
+ return;
392
+ }
393
+ res.writeHead(404).end();
394
+ });
395
+ await new Promise((resolve) => existing.listen(0, "127.0.0.1", resolve));
396
+ const address = existing.address();
397
+ const port = typeof address === "object" && address ? address.port : 47831;
398
+ const info = vi.spyOn(console, "info").mockImplementation(() => { });
399
+ const { runDaemon } = await import("../daemon.js");
400
+ await runDaemon(["--local-api-port", String(port)]);
401
+ expect(mocks.io).not.toHaveBeenCalled();
402
+ expect(info).toHaveBeenCalledWith("[aiden-agent] Daemon already running", expect.objectContaining({ port }));
403
+ await new Promise((resolve, reject) => {
404
+ existing.close((error) => (error ? reject(error) : resolve()));
405
+ });
406
+ });
407
+ it("runDaemon exits successfully when the local daemon is already running", async () => {
408
+ writeFileSync(configPath, JSON.stringify({
409
+ runtimeId: "11111111-1111-4111-8111-111111111111",
410
+ runtimeToken: "aiden_runtime_secret",
411
+ wsUrl: "ws://ws.test",
412
+ localApiToken: "local-secret",
413
+ }));
414
+ const existing = createServer((req, res) => {
415
+ if (req.method === "GET" && req.url === "/status") {
416
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
417
+ configured: true,
418
+ runtimeId: "11111111-1111-4111-8111-111111111111",
419
+ wsUrl: "ws://ws.test",
420
+ connected: true,
421
+ activeRunIds: [],
422
+ localApiPort: 47831,
423
+ version: "test",
424
+ }));
425
+ return;
426
+ }
427
+ res.writeHead(404).end();
428
+ });
429
+ await new Promise((resolve) => existing.listen(0, "127.0.0.1", resolve));
430
+ const address = existing.address();
431
+ const port = typeof address === "object" && address ? address.port : 47831;
432
+ const info = vi.spyOn(console, "info").mockImplementation(() => { });
433
+ const { runDaemon } = await import("../daemon.js");
434
+ await runDaemon(["--local-api-port", String(port)]);
435
+ expect(mocks.io).not.toHaveBeenCalled();
436
+ expect(info).toHaveBeenCalledWith("[aiden-agent] Daemon already running", expect.objectContaining({
437
+ runtimeId: "11111111-1111-4111-8111-111111111111",
438
+ }));
439
+ await new Promise((resolve, reject) => {
440
+ existing.close((error) => (error ? reject(error) : resolve()));
441
+ });
442
+ });
443
+ it("explains when the local daemon API port is already in use", async () => {
444
+ writeFileSync(configPath, JSON.stringify({
445
+ runtimeId: "11111111-1111-4111-8111-111111111111",
446
+ runtimeToken: "aiden_runtime_secret",
447
+ wsUrl: "ws://ws.test",
448
+ localApiToken: "local-secret",
449
+ }));
450
+ const occupied = createServer();
451
+ await new Promise((resolve) => occupied.listen(0, "127.0.0.1", resolve));
452
+ const address = occupied.address();
453
+ const port = typeof address === "object" && address ? address.port : 47831;
454
+ const socket = new TestSocket();
455
+ mocks.io.mockReturnValue(socket);
456
+ const { startDaemon } = await import("../daemon.js");
457
+ await expect(startDaemon(["--local-api-port", String(port)])).rejects.toThrow("already in use by another process");
458
+ expect(socket.disconnected).toBe(true);
459
+ await new Promise((resolve, reject) => {
460
+ occupied.close((error) => (error ? reject(error) : resolve()));
461
+ });
462
+ });
463
+ it("login starts browser device authorization, polls, and stores runtime config", async () => {
464
+ mocks.spawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
465
+ const fetchMock = vi
466
+ .fn()
467
+ .mockResolvedValueOnce({
468
+ ok: true,
469
+ json: async () => ({
470
+ deviceCode: "aiden_device_secret",
471
+ userCode: "ABCD1234",
472
+ verificationUri: "http://app.test/runtime/device?code=ABCD1234",
473
+ expiresAt: new Date(Date.now() + 60_000).toISOString(),
474
+ intervalSeconds: 0,
475
+ }),
476
+ })
477
+ .mockResolvedValueOnce({
478
+ ok: false,
479
+ status: 428,
480
+ json: async () => ({ error: "authorization_pending" }),
481
+ text: async () => JSON.stringify({ error: "authorization_pending" }),
482
+ })
483
+ .mockResolvedValueOnce({
484
+ ok: true,
485
+ status: 201,
486
+ json: async () => ({
487
+ runtime: {
488
+ id: "11111111-1111-4111-8111-111111111111",
489
+ displayName: "Browser Login VM",
490
+ teamId: "22222222-2222-4222-8222-222222222222",
491
+ },
492
+ runtimeToken: "aiden_runtime_browser_secret",
493
+ }),
494
+ });
495
+ vi.stubGlobal("fetch", fetchMock);
496
+ const info = vi.spyOn(console, "info").mockImplementation(() => { });
497
+ const { loginWithDeviceCode } = await import("../daemon.js");
498
+ await loginWithDeviceCode([
499
+ "--api-url",
500
+ "http://api.test",
501
+ "--ws-url",
502
+ "ws://ws.test",
503
+ "--name",
504
+ "Browser Login VM",
505
+ "--max-polls",
506
+ "3",
507
+ ]);
508
+ expect(fetchMock).toHaveBeenNthCalledWith(1, "http://api.test/public/runtimes/device-authorizations", expect.objectContaining({ method: "POST" }));
509
+ expect(fetchMock).toHaveBeenNthCalledWith(2, "http://api.test/public/runtimes/device-authorizations/token", expect.objectContaining({
510
+ method: "POST",
511
+ body: JSON.stringify({ deviceCode: "aiden_device_secret" }),
512
+ }));
513
+ expect(fetchMock).toHaveBeenCalledTimes(3);
514
+ expect(info.mock.calls.some((call) => String(call[0]).includes("ABCD1234"))).toBe(true);
515
+ const saved = JSON.parse(readFileSync(configPath, "utf8"));
516
+ expect(saved).toMatchObject({
517
+ apiUrl: "http://api.test",
518
+ wsUrl: "ws://ws.test",
519
+ endpointProfile: "custom",
520
+ teamId: "22222222-2222-4222-8222-222222222222",
521
+ runtimeId: "11111111-1111-4111-8111-111111111111",
522
+ runtimeToken: "aiden_runtime_browser_secret",
523
+ displayName: "Browser Login VM",
524
+ });
525
+ expect(readFileSync(configPath, "utf8")).not.toContain("aiden_device_secret");
526
+ });
527
+ it("login uses production endpoint defaults", async () => {
528
+ mocks.spawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
529
+ const fetchMock = vi
530
+ .fn()
531
+ .mockResolvedValueOnce({
532
+ ok: true,
533
+ json: async () => ({
534
+ deviceCode: "aiden_device_secret",
535
+ userCode: "ABCD1234",
536
+ verificationUri: "https://app.aiden-platform.com/runtime/device?code=ABCD1234",
537
+ expiresAt: new Date(Date.now() + 60_000).toISOString(),
538
+ intervalSeconds: 0,
539
+ }),
540
+ })
541
+ .mockResolvedValueOnce({
542
+ ok: true,
543
+ status: 201,
544
+ json: async () => ({
545
+ runtime: {
546
+ id: "11111111-1111-4111-8111-111111111111",
547
+ displayName: "Prod Login Runtime",
548
+ teamId: "22222222-2222-4222-8222-222222222222",
549
+ },
550
+ runtimeToken: "aiden_runtime_browser_secret",
551
+ }),
552
+ });
553
+ vi.stubGlobal("fetch", fetchMock);
554
+ vi.spyOn(console, "info").mockImplementation(() => { });
555
+ const { loginWithDeviceCode } = await import("../daemon.js");
556
+ await loginWithDeviceCode(["--max-polls", "1"]);
557
+ expect(fetchMock).toHaveBeenNthCalledWith(1, "https://api.aiden-platform.com/public/runtimes/device-authorizations", expect.objectContaining({ method: "POST" }));
558
+ const saved = JSON.parse(readFileSync(configPath, "utf8"));
559
+ expect(saved).toMatchObject({
560
+ apiUrl: "https://api.aiden-platform.com",
561
+ wsUrl: "wss://ws.aiden-platform.com",
562
+ endpointProfile: "production",
563
+ });
564
+ });
565
+ it("setup preserves local config when runtime registration fails", async () => {
566
+ writeFileSync(configPath, JSON.stringify({
567
+ apiUrl: "http://existing.test",
568
+ runtimeId: "existing-runtime",
569
+ runtimeToken: "existing-token",
570
+ }));
571
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
572
+ ok: false,
573
+ status: 403,
574
+ text: async () => "forbidden",
575
+ }));
576
+ const { setupDaemon } = await import("../daemon.js");
577
+ await expect(setupDaemon([
578
+ "--api-url",
579
+ "http://api.test",
580
+ "--team",
581
+ "22222222-2222-4222-8222-222222222222",
582
+ "--token",
583
+ "aiden_pat_secret",
584
+ ])).rejects.toThrow("runtime registration failed: 403 forbidden");
585
+ expect(JSON.parse(readFileSync(configPath, "utf8"))).toMatchObject({
586
+ apiUrl: "http://existing.test",
587
+ runtimeId: "existing-runtime",
588
+ runtimeToken: "existing-token",
589
+ });
590
+ });
591
+ it("daemon refuses to start without runtime identity, token, and websocket URL", async () => {
592
+ const { startDaemon } = await import("../daemon.js");
593
+ await expect(startDaemon(["--runtime-id", "11111111-1111-4111-8111-111111111111"])).rejects.toThrow("daemon requires runtimeId, runtime token, and wsUrl");
594
+ expect(mocks.io).not.toHaveBeenCalled();
595
+ });
596
+ it("daemon connects as a runtime and hands accepted executions to CoreAgent", async () => {
597
+ const socket = new TestSocket();
598
+ mocks.io.mockReturnValue(socket);
599
+ const binDir = seedCliBinaries(tempDir);
600
+ const codexPath = join(binDir, "codex");
601
+ process.env.AIDEN_CODEX_PATH = codexPath;
602
+ mocks.spawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
603
+ mocks.agentRun.mockReturnValue(new Promise(() => { }));
604
+ const { startDaemon } = await import("../daemon.js");
605
+ const controller = await startDaemon([
606
+ "--runtime-id",
607
+ "11111111-1111-4111-8111-111111111111",
608
+ "--token",
609
+ "aiden_runtime_secret",
610
+ "--ws-url",
611
+ "ws://ws.test",
612
+ "--local-api-port",
613
+ "0",
614
+ ]);
615
+ expect(mocks.io).toHaveBeenCalledWith("ws://ws.test", expect.objectContaining({
616
+ query: expect.objectContaining({
617
+ type: "runtime",
618
+ runtimeId: "11111111-1111-4111-8111-111111111111",
619
+ hostKind: "daemon",
620
+ }),
621
+ auth: { token: "aiden_runtime_secret" },
622
+ transports: ["websocket"],
623
+ }));
624
+ socket.emit("connect");
625
+ await expect(waitForOutbound(socket, "runtime.hello")).resolves.toEqual(expect.objectContaining({
626
+ event: "runtime.hello",
627
+ payload: expect.objectContaining({
628
+ capabilities: expect.objectContaining({ supportsFilesystem: true }),
629
+ metadata: expect.objectContaining({
630
+ platform: expect.any(String),
631
+ arch: expect.any(String),
632
+ agentVersion: expect.any(String),
633
+ memoryBytes: expect.any(Number),
634
+ }),
635
+ }),
636
+ }));
637
+ socket.emit("agent.execute", {
638
+ conversationId: "conversation-1",
639
+ runId: "run-1",
640
+ content: "Ship the task",
641
+ backendKind: "codex",
642
+ teamId: "team-1",
643
+ projectPath: "/tmp/workspace",
644
+ maxIterations: 7,
645
+ });
646
+ expect(socket.outbound).toEqual(expect.arrayContaining([{ event: "agent.accepted", payload: { runId: "run-1" } }]));
647
+ expect(mocks.coreAgentConstructor).toHaveBeenCalledWith(expect.any(Object), {
648
+ backendKind: "codex_app_server",
649
+ runtimeCommand: codexPath,
650
+ });
651
+ expect(mocks.agentRun).toHaveBeenCalledWith(expect.objectContaining({
652
+ task: "Ship the task",
653
+ cwd: "/tmp/workspace",
654
+ teamPath: "/tmp/workspace",
655
+ backendKind: "codex_app_server",
656
+ runtimeCommand: codexPath,
657
+ conversationId: "conversation-1",
658
+ teamId: "team-1",
659
+ maxIterations: 7,
660
+ }));
661
+ socket.emit("agent.execute", {
662
+ conversationId: "conversation-1",
663
+ runId: "run-1",
664
+ content: "Duplicate",
665
+ });
666
+ expect(socket.outbound).toEqual(expect.arrayContaining([
667
+ { event: "agent.rejected", payload: { runId: "run-1", message: "Run is already active" } },
668
+ ]));
669
+ socket.emit("agent.abort", { runId: "run-1" });
670
+ expect(mocks.agentKill).toHaveBeenCalled();
671
+ expect(socket.outbound).toEqual(expect.arrayContaining([
672
+ {
673
+ event: "agent.event",
674
+ payload: expect.objectContaining({
675
+ conversationId: "conversation-1",
676
+ runId: "run-1",
677
+ event: expect.objectContaining({
678
+ type: "query_complete",
679
+ result: expect.objectContaining({ error: "Interrupted by user", success: false }),
680
+ }),
681
+ }),
682
+ },
683
+ ]));
684
+ expect(socket.disconnected).toBe(false);
685
+ await controller.stop();
686
+ expect(socket.disconnected).toBe(true);
687
+ });
688
+ it("local daemon API reports status, protects endpoints, streams logs, and stops active runs", async () => {
689
+ const socket = new TestSocket();
690
+ mocks.io.mockReturnValue(socket);
691
+ mocks.spawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
692
+ mocks.agentRun.mockReturnValue(new Promise(() => { }));
693
+ const { startDaemon } = await import("../daemon.js");
694
+ const controller = await startDaemon([
695
+ "--runtime-id",
696
+ "11111111-1111-4111-8111-111111111111",
697
+ "--token",
698
+ "aiden_runtime_secret",
699
+ "--ws-url",
700
+ "ws://ws.test",
701
+ "--local-api-port",
702
+ "0",
703
+ ]);
704
+ try {
705
+ const baseUrl = `http://127.0.0.1:${controller.localApiPort}`;
706
+ const unauthorized = await fetch(`${baseUrl}/status`);
707
+ expect(unauthorized.status).toBe(401);
708
+ const missing = await fetch(`${baseUrl}/missing`, {
709
+ headers: { authorization: `Bearer ${controller.localApiToken}` },
710
+ });
711
+ expect(missing.status).toBe(404);
712
+ socket.emit("connect");
713
+ socket.emit("agent.execute", {
714
+ conversationId: "conversation-1",
715
+ runId: "run-local-api",
716
+ content: "Keep running",
717
+ });
718
+ const headers = { authorization: `Bearer ${controller.localApiToken}` };
719
+ const status = await fetch(`${baseUrl}/status`, { headers }).then((res) => res.json());
720
+ expect(status).toMatchObject({
721
+ configured: true,
722
+ runtimeId: "11111111-1111-4111-8111-111111111111",
723
+ connected: true,
724
+ activeRunIds: ["run-local-api"],
725
+ });
726
+ const capabilities = await fetch(`${baseUrl}/capabilities`, { headers }).then((res) => res.json());
727
+ expect(capabilities).toMatchObject({ hasTerminal: true, supportsFilesystem: true });
728
+ const logs = await fetch(`${baseUrl}/logs`, { headers }).then((res) => res.json());
729
+ expect(logs.lines.some((line) => line.includes("accepted run=run-local-api"))).toBe(true);
730
+ const stopped = await fetch(`${baseUrl}/stop`, { method: "POST", headers }).then((res) => res.json());
731
+ expect(stopped).toEqual({ ok: true });
732
+ expect(mocks.agentKill).toHaveBeenCalled();
733
+ const afterStop = await fetch(`${baseUrl}/status`, { headers }).then((res) => res.json());
734
+ expect(afterStop.activeRunIds).toEqual([]);
735
+ }
736
+ finally {
737
+ await controller.stop();
738
+ }
739
+ });
740
+ it("POST /shutdown stops the daemon and notifies whenExternallyStopped", async () => {
741
+ const socket = new TestSocket();
742
+ mocks.io.mockReturnValue(socket);
743
+ mocks.spawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
744
+ const { startDaemon } = await import("../daemon.js");
745
+ const controller = await startDaemon([
746
+ "--runtime-id",
747
+ "11111111-1111-4111-8111-111111111111",
748
+ "--token",
749
+ "aiden_runtime_secret",
750
+ "--ws-url",
751
+ "ws://ws.test",
752
+ "--local-api-port",
753
+ "0",
754
+ ]);
755
+ const onStopped = vi.fn();
756
+ controller.whenExternallyStopped = onStopped;
757
+ const baseUrl = `http://127.0.0.1:${controller.localApiPort}`;
758
+ const headers = { authorization: `Bearer ${controller.localApiToken}` };
759
+ const shutdown = await fetch(`${baseUrl}/shutdown`, { method: "POST", headers }).then((res) => res.json());
760
+ expect(shutdown).toEqual({ ok: true });
761
+ await vi.waitFor(() => expect(onStopped).toHaveBeenCalled());
762
+ expect(socket.disconnected).toBe(true);
763
+ });
764
+ it("stopDaemon calls POST /shutdown on a running local daemon", async () => {
765
+ const socket = new TestSocket();
766
+ mocks.io.mockReturnValue(socket);
767
+ mocks.spawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
768
+ const { startDaemon, stopDaemon } = await import("../daemon.js");
769
+ const controller = await startDaemon([
770
+ "--runtime-id",
771
+ "11111111-1111-4111-8111-111111111111",
772
+ "--token",
773
+ "aiden_runtime_secret",
774
+ "--ws-url",
775
+ "ws://ws.test",
776
+ "--local-api-port",
777
+ "0",
778
+ ]);
779
+ writeFileSync(configPath, JSON.stringify({
780
+ runtimeId: controller.runtimeId,
781
+ runtimeToken: "aiden_runtime_secret",
782
+ wsUrl: "ws://ws.test",
783
+ localApiPort: controller.localApiPort,
784
+ localApiToken: controller.localApiToken,
785
+ }));
786
+ const info = vi.spyOn(console, "info").mockImplementation(() => { });
787
+ await stopDaemon([]);
788
+ expect(info).toHaveBeenCalledWith("[aiden-agent] Daemon stopped", expect.objectContaining({
789
+ port: controller.localApiPort,
790
+ }));
791
+ await vi.waitFor(() => expect(socket.disconnected).toBe(true));
792
+ });
793
+ it("stopDaemon reports when no daemon is listening", async () => {
794
+ const freePort = await new Promise((resolve, reject) => {
795
+ const server = createServer();
796
+ server.listen(0, "127.0.0.1", () => {
797
+ const address = server.address();
798
+ const port = typeof address === "object" && address ? address.port : 0;
799
+ server.close((error) => (error ? reject(error) : resolve(port)));
800
+ });
801
+ });
802
+ writeFileSync(configPath, JSON.stringify({
803
+ runtimeId: "11111111-1111-4111-8111-111111111111",
804
+ runtimeToken: "aiden_runtime_secret",
805
+ wsUrl: "ws://ws.test",
806
+ localApiPort: freePort,
807
+ localApiToken: "local-secret",
808
+ }));
809
+ const info = vi.spyOn(console, "info").mockImplementation(() => { });
810
+ const { stopDaemon } = await import("../daemon.js");
811
+ await stopDaemon(["--local-api-port", String(freePort)]);
812
+ expect(info).toHaveBeenCalledWith("[aiden-agent] No daemon listening", { port: freePort });
813
+ });
814
+ it("forwards browser tool responses and appended user messages to the active CoreAgent", async () => {
815
+ const socket = new TestSocket();
816
+ mocks.io.mockReturnValue(socket);
817
+ mocks.spawnSync.mockReturnValue({ status: 1, stdout: "", stderr: "" });
818
+ mocks.agentRun.mockReturnValue(new Promise(() => { }));
819
+ const { startDaemon } = await import("../daemon.js");
820
+ const controller = await startDaemon([
821
+ "--runtime-id",
822
+ "11111111-1111-4111-8111-111111111111",
823
+ "--token",
824
+ "aiden_runtime_secret",
825
+ "--ws-url",
826
+ "ws://ws.test",
827
+ "--local-api-port",
828
+ "0",
829
+ ]);
830
+ try {
831
+ socket.emit("agent.execute", {
832
+ conversationId: "conversation-1",
833
+ runId: "run-interactive",
834
+ content: "Wait for user input",
835
+ });
836
+ socket.emit("agent.tool_response", {
837
+ runId: "run-interactive",
838
+ toolId: "tool-1",
839
+ response: "Approved",
840
+ });
841
+ socket.emit("agent.append_message", {
842
+ runId: "run-interactive",
843
+ text: "Continue with the implementation",
844
+ });
845
+ expect(mocks.sendToolResponse).toHaveBeenCalledWith("tool-1", "Approved");
846
+ expect(mocks.sendUserMessage).toHaveBeenCalledWith("Continue with the implementation");
847
+ expect(socket.outbound).toEqual(expect.arrayContaining([{ event: "agent.accepted", payload: { runId: "run-interactive" } }]));
848
+ socket.emit("agent.append_message", {
849
+ runId: "missing-run",
850
+ text: "This should be rejected",
851
+ });
852
+ socket.emit("agent.tool_response", {
853
+ runId: "missing-run",
854
+ toolId: "missing-tool",
855
+ response: "Ignored",
856
+ });
857
+ expect(socket.outbound).toEqual(expect.arrayContaining([
858
+ {
859
+ event: "agent.rejected",
860
+ payload: {
861
+ runId: "missing-run",
862
+ message: "No active agent for appended message",
863
+ },
864
+ },
865
+ ]));
866
+ const logs = await fetch(`http://127.0.0.1:${controller.localApiPort}/logs`, {
867
+ headers: { authorization: `Bearer ${controller.localApiToken}` },
868
+ }).then((res) => res.json());
869
+ expect(logs.lines.some((line) => line.includes("tool_response missed tool=missing-tool"))).toBe(true);
870
+ }
871
+ finally {
872
+ await controller.stop();
873
+ }
874
+ });
875
+ it("rotates runtime tokens and logout removes local config", async () => {
876
+ writeFileSync(configPath, JSON.stringify({
877
+ apiUrl: "http://api.test",
878
+ wsUrl: "ws://ws.test",
879
+ runtimeId: "11111111-1111-4111-8111-111111111111",
880
+ runtimeToken: "old_runtime_secret",
881
+ }));
882
+ const fetchMock = vi.fn().mockResolvedValue({
883
+ ok: true,
884
+ json: async () => ({ runtimeToken: "new_runtime_secret" }),
885
+ });
886
+ vi.stubGlobal("fetch", fetchMock);
887
+ const { rotateRuntimeToken, logoutDaemon } = await import("../daemon.js");
888
+ await rotateRuntimeToken(["--token", "aiden_pat_secret"]);
889
+ expect(fetchMock).toHaveBeenCalledWith("http://api.test/public/runtimes/11111111-1111-4111-8111-111111111111/tokens/rotate", expect.objectContaining({
890
+ method: "POST",
891
+ headers: { authorization: "Bearer aiden_pat_secret" },
892
+ }));
893
+ expect(JSON.parse(readFileSync(configPath, "utf8")).runtimeToken).toBe("new_runtime_secret");
894
+ logoutDaemon();
895
+ expect(() => readFileSync(configPath, "utf8")).toThrow();
896
+ });
897
+ it("doctor --sync pushes sanitized capabilities using the runtime token", async () => {
898
+ writeFileSync(configPath, JSON.stringify({
899
+ apiUrl: "http://api.test",
900
+ wsUrl: "ws://ws.test",
901
+ runtimeId: "11111111-1111-4111-8111-111111111111",
902
+ runtimeToken: "aiden_runtime_secret",
903
+ }));
904
+ const binDir = seedCliBinaries(tempDir);
905
+ process.env.PATH = binDir;
906
+ mocks.spawnSync.mockImplementation((command) => {
907
+ if (command.includes("codex"))
908
+ return { status: 0, stdout: "codex 1.2.3\n", stderr: "" };
909
+ if (command.includes("git"))
910
+ return { status: 0, stdout: "git version 2.50.0\n", stderr: "" };
911
+ return { status: 1, stdout: "", stderr: "not found" };
912
+ });
913
+ const fetchMock = vi.fn().mockResolvedValue({
914
+ ok: true,
915
+ json: async () => ({
916
+ capabilities: {
917
+ agents: [{ provider: "codex", available: true, version: "codex 1.2.3" }],
918
+ hasGit: true,
919
+ },
920
+ }),
921
+ });
922
+ vi.stubGlobal("fetch", fetchMock);
923
+ const info = vi.spyOn(console, "info").mockImplementation(() => { });
924
+ const { printDoctor } = await import("../daemon.js");
925
+ await printDoctor(["--sync"]);
926
+ expect(fetchMock).toHaveBeenCalledWith("http://api.test/public/runtimes/11111111-1111-4111-8111-111111111111/capabilities/self", expect.objectContaining({
927
+ method: "PATCH",
928
+ headers: {
929
+ authorization: "Bearer aiden_runtime_secret",
930
+ "content-type": "application/json",
931
+ },
932
+ }));
933
+ const body = JSON.parse(fetchMock.mock.calls[0][1].body);
934
+ expect(JSON.stringify(body.capabilities)).not.toContain("/Users/");
935
+ expect(body.metadata).toEqual(expect.objectContaining({
936
+ platform: expect.any(String),
937
+ arch: expect.any(String),
938
+ agentVersion: expect.any(String),
939
+ memoryBytes: expect.any(Number),
940
+ }));
941
+ expect(JSON.parse(info.mock.calls.at(-1)[0])).toMatchObject({ synced: true });
942
+ });
943
+ it("status explains durable runtime config separately from ephemeral sessions", async () => {
944
+ const info = vi.spyOn(console, "info").mockImplementation(() => { });
945
+ const { printStatus } = await import("../daemon.js");
946
+ await printStatus();
947
+ expect(JSON.parse(info.mock.calls.at(-1)[0])).toMatchObject({
948
+ mode: "runtime",
949
+ configured: false,
950
+ localDaemonRunning: false,
951
+ apiUrl: null,
952
+ wsUrl: null,
953
+ note: expect.stringContaining("Cloud sandbox sessions are launched by Aiden"),
954
+ warnings: [],
955
+ });
956
+ });
957
+ it("resolves the runtime config path at call time", async () => {
958
+ const { printStatus } = await import("../daemon.js");
959
+ const alternateConfigPath = join(tempDir, "alternate-config.json");
960
+ writeFileSync(alternateConfigPath, JSON.stringify({
961
+ apiUrl: "http://api.test",
962
+ wsUrl: "ws://ws.test",
963
+ endpointProfile: "custom",
964
+ runtimeId: "22222222-2222-4222-8222-222222222222",
965
+ runtimeToken: "aiden_runtime_secret",
966
+ }));
967
+ process.env.AIDEN_AGENT_CONFIG_PATH = alternateConfigPath;
968
+ const info = vi.spyOn(console, "info").mockImplementation(() => { });
969
+ await printStatus();
970
+ expect(JSON.parse(info.mock.calls.at(-1)[0])).toMatchObject({
971
+ configured: true,
972
+ runtimeId: "22222222-2222-4222-8222-222222222222",
973
+ configPath: alternateConfigPath,
974
+ localDaemonRunning: false,
975
+ });
976
+ });
977
+ it("token revoke revokes server-side tokens and removes local runtime config", async () => {
978
+ writeFileSync(configPath, JSON.stringify({
979
+ apiUrl: "http://api.test",
980
+ wsUrl: "ws://ws.test",
981
+ runtimeId: "11111111-1111-4111-8111-111111111111",
982
+ runtimeToken: "old_runtime_secret",
983
+ }));
984
+ const fetchMock = vi.fn().mockResolvedValue({
985
+ ok: true,
986
+ json: async () => ({ ok: true }),
987
+ });
988
+ vi.stubGlobal("fetch", fetchMock);
989
+ const { revokeRuntimeToken } = await import("../daemon.js");
990
+ await revokeRuntimeToken(["--token", "aiden_pat_secret"]);
991
+ expect(fetchMock).toHaveBeenCalledWith("http://api.test/public/runtimes/11111111-1111-4111-8111-111111111111/tokens/revoke", expect.objectContaining({
992
+ method: "POST",
993
+ headers: { authorization: "Bearer aiden_pat_secret" },
994
+ }));
995
+ expect(() => readFileSync(configPath, "utf8")).toThrow();
996
+ });
997
+ it("does not overwrite the runtime token when token rotation fails", async () => {
998
+ writeFileSync(configPath, JSON.stringify({
999
+ apiUrl: "http://api.test",
1000
+ wsUrl: "ws://ws.test",
1001
+ runtimeId: "11111111-1111-4111-8111-111111111111",
1002
+ runtimeToken: "old_runtime_secret",
1003
+ }));
1004
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
1005
+ ok: false,
1006
+ status: 500,
1007
+ text: async () => "boom",
1008
+ }));
1009
+ const { rotateRuntimeToken } = await import("../daemon.js");
1010
+ await expect(rotateRuntimeToken(["--token", "aiden_pat_secret"])).rejects.toThrow("runtime token rotation failed: 500 boom");
1011
+ expect(JSON.parse(readFileSync(configPath, "utf8")).runtimeToken).toBe("old_runtime_secret");
1012
+ });
1013
+ });
1014
+ //# sourceMappingURL=daemon.test.js.map