codex@macbookpro
·
2026-04-03
index.test.js
1import assert from "node:assert/strict";
2import { EventEmitter } from "node:events";
3import { createServer } from "node:http";
4import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5import { createConnection } from "node:net";
6import { homedir, tmpdir } from "node:os";
7import { join } from "node:path";
8import test from "node:test";
9
10import {
11 ARTIFACTS_DIRNAME,
12 ARTIFACT_DB_FILENAME,
13 ArtifactStore
14} from "../../../packages/artifact-db/dist/index.js";
15import "./artifacts.test.js";
16import "./renewal/utils.test.js";
17import { ConductorLocalControlPlane } from "../dist/local-control-plane.js";
18import { FirefoxCommandBroker } from "../dist/firefox-bridge.js";
19import {
20 BaaInstructionCenter,
21 BaaLiveInstructionIngest,
22 InMemoryBaaInstructionDeduper,
23 InMemoryBaaLiveInstructionMessageDeduper,
24 BrowserRequestPolicyController,
25 ConductorDaemon,
26 ConductorTimedJobs,
27 ConductorRuntime,
28 DEFAULT_BAA_EXEC_INSTRUCTION_TIMEOUT_MS,
29 DEFAULT_BAA_INSTRUCTION_TIMEOUT_MS,
30 FirefoxBridgeError,
31 PersistentBaaInstructionDeduper,
32 PersistentBaaLiveInstructionMessageDeduper,
33 PersistentBaaLiveInstructionSnapshotStore,
34 createDefaultBaaInstructionPolicyTargetTools,
35 createArtifactStoreBrowserRequestPolicyPersistence,
36 createFetchControlApiClient,
37 createRenewalDispatcherRunner,
38 createRenewalProjectorRunner,
39 evaluateBaaInstructionPolicy,
40 executeBaaInstruction,
41 extractBaaInstructionBlocks,
42 handleConductorHttpRequest,
43 normalizeBaaInstruction,
44 parseBaaInstructionBlock,
45 parseConductorCliRequest,
46 routeBaaInstruction,
47 recordAssistantMessageAutomationSignal,
48 recordAutomationFailureSignal,
49 recordRenewalPayloadSignal,
50 shouldRenew,
51 writeHttpResponse
52} from "../dist/index.js";
53import { observeRenewalConversation } from "../dist/renewal/conversations.js";
54
55const BROWSER_REQUEST_WAITER_TIMEOUT_MS = 120_000;
56
57function createLeaseResult({
58 holderId,
59 holderHost = "mini",
60 term,
61 leaseExpiresAt,
62 renewedAt,
63 isLeader,
64 operation
65}) {
66 return {
67 holderId,
68 holderHost,
69 term,
70 leaseExpiresAt,
71 renewedAt,
72 isLeader,
73 operation,
74 lease: {
75 leaseName: "global",
76 holderId,
77 holderHost,
78 term,
79 leaseExpiresAt,
80 renewedAt,
81 preferredHolderId: holderId,
82 metadataJson: null
83 }
84 };
85}
86
87async function createLocalApiFixture(options = {}) {
88 const sharedToken = "local-shared-token";
89 const controlPlane = new ConductorLocalControlPlane({
90 databasePath: options.databasePath ?? ":memory:"
91 });
92 await controlPlane.initialize();
93
94 const repository = controlPlane.repository;
95 const now = 100;
96 const lease = await repository.acquireLeaderLease({
97 controllerId: "mini-main",
98 host: "mini",
99 preferred: true,
100 ttlSec: 30,
101 now
102 });
103
104 await repository.heartbeatController({
105 controllerId: "mini-main",
106 heartbeatAt: now,
107 host: "mini",
108 priority: 100,
109 role: "primary",
110 startedAt: now,
111 status: "alive",
112 version: "1.2.3"
113 });
114
115 await repository.upsertWorker({
116 workerId: "worker-shell-1",
117 controllerId: "mini-main",
118 host: "mini",
119 workerType: "shell",
120 status: "idle",
121 maxParallelism: 1,
122 currentLoad: 0,
123 lastHeartbeatAt: now,
124 capabilitiesJson: JSON.stringify({
125 kinds: ["shell"]
126 }),
127 metadataJson: null
128 });
129
130 await repository.insertTask({
131 acceptanceJson: JSON.stringify(["returns local data"]),
132 assignedControllerId: "mini-main",
133 baseRef: "main",
134 branchName: "feat/demo-task",
135 constraintsJson: JSON.stringify({
136 target_host: "mini"
137 }),
138 createdAt: now,
139 currentStepIndex: 0,
140 errorText: null,
141 finishedAt: null,
142 goal: "Validate local API reads",
143 metadataJson: JSON.stringify({
144 requested_by: "test"
145 }),
146 plannerProvider: "manual",
147 planningStrategy: "single_step",
148 priority: 50,
149 repo: "/Users/george/code/baa-conductor",
150 resultJson: null,
151 resultSummary: null,
152 source: "local_test",
153 startedAt: now,
154 status: "running",
155 targetHost: "mini",
156 taskId: "task_demo",
157 taskType: "shell",
158 title: "Demo task",
159 updatedAt: now
160 });
161
162 await repository.insertTaskStep({
163 stepId: "step_demo",
164 taskId: "task_demo",
165 stepIndex: 0,
166 stepName: "Run local smoke",
167 stepKind: "shell",
168 status: "running",
169 assignedWorkerId: "worker-shell-1",
170 assignedControllerId: "mini-main",
171 timeoutSec: 300,
172 retryLimit: 0,
173 retryCount: 0,
174 leaseExpiresAt: now + 30,
175 inputJson: JSON.stringify({
176 cmd: "echo hi"
177 }),
178 outputJson: null,
179 summary: null,
180 errorText: null,
181 createdAt: now,
182 updatedAt: now,
183 startedAt: now,
184 finishedAt: null
185 });
186
187 await repository.insertTaskRun({
188 runId: "run_demo",
189 taskId: "task_demo",
190 stepId: "step_demo",
191 workerId: "worker-shell-1",
192 controllerId: "mini-main",
193 host: "mini",
194 pid: 4321,
195 status: "running",
196 leaseExpiresAt: now + 30,
197 heartbeatAt: now,
198 logDir: "/tmp/demo-run",
199 stdoutPath: "/tmp/demo-run/stdout.log",
200 stderrPath: null,
201 workerLogPath: "/tmp/demo-run/worker.log",
202 checkpointSeq: 1,
203 exitCode: null,
204 resultJson: null,
205 errorText: null,
206 createdAt: now,
207 startedAt: now,
208 finishedAt: null
209 });
210
211 await repository.appendTaskLog({
212 taskId: "task_demo",
213 stepId: "step_demo",
214 runId: "run_demo",
215 seq: 1,
216 stream: "stdout",
217 level: "info",
218 message: "hello from local api",
219 createdAt: now
220 });
221
222 const snapshot = {
223 claudeCoded: {
224 localApiBase: null
225 },
226 codexd: {
227 localApiBase: null
228 },
229 controlApi: {
230 baseUrl: "https://control.example.test",
231 browserWsUrl: "ws://127.0.0.1:4317/ws/browser",
232 firefoxWsUrl: "ws://127.0.0.1:4317/ws/firefox",
233 hasSharedToken: true,
234 localApiBase: "http://127.0.0.1:4317",
235 usesPlaceholderToken: false
236 },
237 daemon: {
238 currentLeaderId: lease.holderId,
239 currentTerm: lease.term,
240 host: "mini",
241 lastError: null,
242 leaseExpiresAt: lease.leaseExpiresAt,
243 leaseState: "leader",
244 nodeId: "mini-main",
245 role: "primary",
246 schedulerEnabled: true
247 },
248 identity: "mini-main@mini(primary)",
249 runtime: {
250 pid: 123,
251 started: true,
252 startedAt: now
253 },
254 warnings: []
255 };
256
257 return {
258 controlPlane,
259 repository,
260 sharedToken,
261 snapshot
262 };
263}
264
265function createTimedJobRunnerContext({
266 artifactStore,
267 batchId = "timed-jobs-test-batch",
268 config = {
269 intervalMs: 5_000,
270 maxMessagesPerTick: 10,
271 maxTasksPerTick: 10,
272 settleDelayMs: 0
273 },
274 logDir = null,
275 trigger = "manual"
276} = {}) {
277 const entries = [];
278
279 return {
280 entries,
281 context: {
282 artifactStore: artifactStore ?? null,
283 batchId,
284 config,
285 controllerId: "mini-main",
286 host: "mini",
287 log: (input) => {
288 entries.push(input);
289 },
290 logDir,
291 maxMessagesPerTick: config.maxMessagesPerTick,
292 maxTasksPerTick: config.maxTasksPerTick,
293 settleDelayMs: config.settleDelayMs,
294 term: 2,
295 trigger
296 }
297 };
298}
299
300function createInstructionEnvelope({
301 blockIndex = 0,
302 params = null,
303 paramsKind = params == null
304 ? "none"
305 : typeof params === "string"
306 ? "inline_string"
307 : "inline_json",
308 target = "conductor",
309 tool = "describe"
310} = {}) {
311 return {
312 assistantMessageId: "msg-route-test",
313 blockIndex,
314 conversationId: "conv-route-test",
315 dedupeBasis: {
316 assistant_message_id: "msg-route-test",
317 block_index: blockIndex,
318 conversation_id: "conv-route-test",
319 params,
320 platform: "claude",
321 target,
322 tool,
323 version: "baa.v1"
324 },
325 dedupeKey: `dedupe:${blockIndex}:${target}:${tool}`,
326 envelopeVersion: "baa.v1",
327 instructionId: `instruction-${blockIndex}`,
328 params,
329 paramsKind,
330 platform: "claude",
331 rawBlock: "```baa\nplaceholder\n```",
332 rawInstruction: `@${target}::${tool}`,
333 target,
334 tool
335 };
336}
337
338async function startCodexdStubServer() {
339 const requests = [];
340 const sessions = [
341 {
342 sessionId: "session-demo",
343 purpose: "duplex",
344 threadId: "thread-demo",
345 status: "active",
346 endpoint: "http://127.0.0.1:0",
347 childPid: 43210,
348 createdAt: "2026-03-23T00:00:00.000Z",
349 updatedAt: "2026-03-23T00:00:00.000Z",
350 cwd: "/Users/george/code/baa-conductor",
351 model: "gpt-5.4",
352 modelProvider: "openai",
353 serviceTier: "default",
354 reasoningEffort: "medium",
355 currentTurnId: null,
356 lastTurnId: "turn-demo",
357 lastTurnStatus: "completed",
358 metadata: {
359 origin: "stub"
360 }
361 }
362 ];
363
364 const server = createServer(async (request, response) => {
365 const method = (request.method ?? "GET").toUpperCase();
366 const url = new URL(request.url ?? "/", "http://127.0.0.1");
367 let rawBody = "";
368
369 request.setEncoding("utf8");
370 for await (const chunk of request) {
371 rawBody += chunk;
372 }
373
374 const parsedBody = rawBody === "" ? null : JSON.parse(rawBody);
375 requests.push({
376 body: parsedBody,
377 method,
378 path: url.pathname
379 });
380
381 const address = server.address();
382 const port = typeof address === "object" && address ? address.port : 0;
383 const baseUrl = `http://127.0.0.1:${port}`;
384 const eventStreamUrl = `ws://127.0.0.1:${port}/v1/codexd/events`;
385
386 const writeJson = (status, payload) => {
387 response.statusCode = status;
388 response.setHeader("content-type", "application/json; charset=utf-8");
389 response.end(`${JSON.stringify(payload, null, 2)}\n`);
390 };
391
392 const currentSessions = sessions.map((session) => ({
393 ...session,
394 endpoint: baseUrl
395 }));
396
397 if (method === "GET" && url.pathname === "/v1/codexd/status") {
398 writeJson(200, {
399 ok: true,
400 data: {
401 service: {
402 configuredBaseUrl: baseUrl,
403 eventStreamPath: "/v1/codexd/events",
404 eventStreamUrl,
405 listening: true,
406 resolvedBaseUrl: baseUrl,
407 websocketClients: 0
408 },
409 snapshot: {
410 identity: {
411 daemonId: "codexd-demo",
412 nodeId: "mini",
413 repoRoot: "/Users/george/code/baa-conductor",
414 version: "1.2.3"
415 },
416 daemon: {
417 started: true,
418 startedAt: "2026-03-23T00:00:00.000Z",
419 updatedAt: "2026-03-23T00:00:01.000Z",
420 child: {
421 endpoint: baseUrl,
422 lastError: null,
423 pid: 43210,
424 status: "running",
425 strategy: "spawn"
426 }
427 },
428 sessionRegistry: {
429 updatedAt: "2026-03-23T00:00:02.000Z",
430 sessions: currentSessions
431 },
432 recentEvents: {
433 updatedAt: "2026-03-23T00:00:03.000Z",
434 events: [
435 {
436 seq: 1,
437 createdAt: "2026-03-23T00:00:03.000Z",
438 level: "info",
439 type: "session.created",
440 message: "Created session session-demo.",
441 detail: {
442 sessionId: "session-demo",
443 threadId: "thread-demo"
444 }
445 }
446 ]
447 }
448 }
449 }
450 });
451 return;
452 }
453
454 if (method === "GET" && url.pathname === "/v1/codexd/sessions") {
455 writeJson(200, {
456 ok: true,
457 data: {
458 sessions: currentSessions
459 }
460 });
461 return;
462 }
463
464 if (method === "POST" && url.pathname === "/v1/codexd/sessions") {
465 const nextSession = {
466 ...sessions[0],
467 sessionId: `session-${sessions.length + 1}`,
468 purpose: parsedBody?.purpose ?? "duplex",
469 cwd: parsedBody?.cwd ?? sessions[0].cwd,
470 model: parsedBody?.model ?? sessions[0].model,
471 updatedAt: "2026-03-23T00:00:04.000Z"
472 };
473 sessions.push(nextSession);
474 writeJson(201, {
475 ok: true,
476 data: {
477 session: {
478 ...nextSession,
479 endpoint: baseUrl
480 }
481 }
482 });
483 return;
484 }
485
486 if (method === "POST" && url.pathname === "/v1/codexd/turn") {
487 const sessionId = parsedBody?.sessionId;
488 const session = sessions.find((entry) => entry.sessionId === sessionId);
489
490 if (!session) {
491 writeJson(404, {
492 ok: false,
493 error: "not_found",
494 message: `Unknown codexd session "${sessionId}".`
495 });
496 return;
497 }
498
499 session.lastTurnId = "turn-created";
500 session.lastTurnStatus = "accepted";
501 session.updatedAt = "2026-03-23T00:00:05.000Z";
502 writeJson(202, {
503 ok: true,
504 data: {
505 accepted: true,
506 session: {
507 ...session,
508 endpoint: baseUrl
509 },
510 turnId: "turn-created"
511 }
512 });
513 return;
514 }
515
516 const sessionMatch = url.pathname.match(/^\/v1\/codexd\/sessions\/([^/]+)$/u);
517
518 if (method === "GET" && sessionMatch?.[1]) {
519 const session = sessions.find((entry) => entry.sessionId === decodeURIComponent(sessionMatch[1]));
520
521 if (!session) {
522 writeJson(404, {
523 ok: false,
524 error: "not_found",
525 message: `Unknown codexd session "${decodeURIComponent(sessionMatch[1])}".`
526 });
527 return;
528 }
529
530 writeJson(200, {
531 ok: true,
532 data: {
533 session: {
534 ...session,
535 endpoint: baseUrl
536 },
537 recentEvents: [
538 {
539 seq: 1,
540 type: "turn.completed"
541 }
542 ]
543 }
544 });
545 return;
546 }
547
548 writeJson(404, {
549 ok: false,
550 error: "not_found",
551 message: `Unknown codexd route ${method} ${url.pathname}.`
552 });
553 });
554
555 await new Promise((resolve, reject) => {
556 server.once("error", reject);
557 server.listen(0, "127.0.0.1", resolve);
558 });
559
560 const address = server.address();
561 const port = typeof address === "object" && address ? address.port : 0;
562
563 return {
564 baseUrl: `http://127.0.0.1:${port}`,
565 requests,
566 async stop() {
567 await new Promise((resolve, reject) => {
568 server.close((error) => {
569 if (error) {
570 reject(error);
571 return;
572 }
573
574 resolve();
575 });
576 server.closeAllConnections?.();
577 });
578 }
579 };
580}
581
582async function withArtifactStoreFixture(callback) {
583 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-artifact-store-"));
584 const artifactStore = new ArtifactStore({
585 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
586 databasePath: join(stateDir, ARTIFACT_DB_FILENAME),
587 publicBaseUrl: "https://conductor.makefile.so"
588 });
589
590 try {
591 return await callback({
592 artifactStore,
593 stateDir
594 });
595 } finally {
596 artifactStore.close();
597 rmSync(stateDir, {
598 force: true,
599 recursive: true
600 });
601 }
602}
603
604async function withConductorUiDistFixture(callback) {
605 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-ui-dist-"));
606 const uiDistDir = join(stateDir, "dist");
607 const assetFile = "app-shell.js";
608
609 mkdirSync(join(uiDistDir, "assets"), {
610 recursive: true
611 });
612 writeFileSync(
613 join(uiDistDir, "index.html"),
614 [
615 "<!doctype html>",
616 "<html lang=\"zh-CN\">",
617 "<head><meta charset=\"UTF-8\" /><title>Conductor UI Shell Fixture</title></head>",
618 "<body>",
619 "<div id=\"app\">fixture</div>",
620 `<script type="module" src="/app/assets/${assetFile}"></script>`,
621 "</body>",
622 "</html>"
623 ].join("")
624 );
625 writeFileSync(join(uiDistDir, "assets", assetFile), "console.log('fixture ui shell');\n");
626
627 try {
628 return await callback({
629 assetFile,
630 uiDistDir
631 });
632 } finally {
633 rmSync(stateDir, {
634 force: true,
635 recursive: true
636 });
637 }
638}
639
640function parseJsonBody(response) {
641 return JSON.parse(response.body);
642}
643
644function readBinaryBodyText(response) {
645 return typeof response.body === "string" ? response.body : Buffer.from(response.body).toString("utf8");
646}
647
648test(
649 "BAA instruction extraction and parsing supports the four parameter forms and ignores ordinary code fences",
650 () => {
651 const message = [
652 "Ignore the ordinary code block first.",
653 "```js",
654 "@conductor::exec::printf 'ignore-me'",
655 "```",
656 "",
657 "```baa",
658 "@conductor::describe",
659 "```",
660 "",
661 "```baa",
662 "@conductor::exec::printf 'inline-string'",
663 "```",
664 "",
665 "```baa",
666 '@conductor::files/read::{"cwd":"/tmp","path":"README.md"}',
667 "```",
668 "",
669 "```baa",
670 "@conductor::exec",
671 "printf 'multiline-body'",
672 "pwd",
673 "```"
674 ].join("\n");
675
676 const blocks = extractBaaInstructionBlocks(message);
677
678 assert.equal(blocks.length, 4);
679 assert.deepEqual(
680 blocks.map((block) => block.blockIndex),
681 [0, 1, 2, 3]
682 );
683
684 const parsed = blocks.map((block) => parseBaaInstructionBlock(block));
685
686 assert.equal(parsed[0].target, "conductor");
687 assert.equal(parsed[0].tool, "describe");
688 assert.equal(parsed[0].paramsKind, "none");
689 assert.equal(parsed[0].params, null);
690
691 assert.equal(parsed[1].tool, "exec");
692 assert.equal(parsed[1].paramsKind, "inline_string");
693 assert.equal(parsed[1].params, "printf 'inline-string'");
694
695 assert.equal(parsed[2].tool, "files/read");
696 assert.equal(parsed[2].paramsKind, "inline_json");
697 assert.deepEqual(parsed[2].params, {
698 cwd: "/tmp",
699 path: "README.md"
700 });
701
702 assert.equal(parsed[3].tool, "exec");
703 assert.equal(parsed[3].paramsKind, "body");
704 assert.equal(parsed[3].params, "printf 'multiline-body'\npwd");
705 }
706);
707
708test("BAA instruction extraction ignores unterminated baa blocks and keeps closed ones", () => {
709 const message = [
710 "```baa",
711 "@conductor::describe",
712 "```",
713 "",
714 "```baa",
715 "@conductor::exec::printf 'broken-tail'"
716 ].join("\n");
717
718 const blocks = extractBaaInstructionBlocks(message);
719
720 assert.equal(blocks.length, 1);
721 assert.equal(blocks[0].blockIndex, 0);
722 assert.equal(parseBaaInstructionBlock(blocks[0]).tool, "describe");
723});
724
725test("BAA instruction extraction accepts fence attributes and rejects non-baa languages", () => {
726 const message = [
727 "```baa",
728 "@conductor::describe",
729 "```",
730 "",
731 '```baa id="p4k2vt"',
732 "@conductor::status",
733 "```",
734 "",
735 "```javascript",
736 "@conductor::exec::printf 'ignore-js'",
737 "```",
738 "",
739 "```baa-ext",
740 "@conductor::exec::printf 'ignore-baa-ext'",
741 "```"
742 ].join("\n");
743
744 const blocks = extractBaaInstructionBlocks(message);
745
746 assert.equal(blocks.length, 2);
747 assert.deepEqual(
748 blocks.map((block) => block.content),
749 ["@conductor::describe", "@conductor::status"]
750 );
751 assert.equal(blocks[1].rawBlock, '```baa id="p4k2vt"\n@conductor::status\n```');
752});
753
754test("InMemoryBaaInstructionDeduper evicts the oldest keys when maxSize is exceeded", () => {
755 const deduper = new InMemoryBaaInstructionDeduper({
756 maxSize: 2
757 });
758
759 deduper.add({
760 dedupeKey: "sha256:key-1"
761 });
762 deduper.add({
763 dedupeKey: "sha256:key-2"
764 });
765 deduper.add({
766 dedupeKey: "sha256:key-3"
767 });
768
769 assert.equal(deduper.has("sha256:key-1"), false);
770 assert.equal(deduper.has("sha256:key-2"), true);
771 assert.equal(deduper.has("sha256:key-3"), true);
772});
773
774test("InMemoryBaaLiveInstructionMessageDeduper evicts the oldest keys when maxSize is exceeded", () => {
775 const deduper = new InMemoryBaaLiveInstructionMessageDeduper({
776 maxSize: 2
777 });
778
779 deduper.add("sha256:msg-1");
780 deduper.add("sha256:msg-2");
781 deduper.add("sha256:msg-3");
782
783 assert.equal(deduper.has("sha256:msg-1"), false);
784 assert.equal(deduper.has("sha256:msg-2"), true);
785 assert.equal(deduper.has("sha256:msg-3"), true);
786});
787
788test("BAA instruction normalization keeps auditable fields and stable dedupe keys", () => {
789 const source = {
790 assistantMessageId: "msg-001",
791 conversationId: "conv-001",
792 platform: "claude"
793 };
794 const left = normalizeBaaInstruction(source, {
795 blockIndex: 0,
796 params: {
797 path: "/tmp/demo.txt",
798 overwrite: true,
799 content: "hello"
800 },
801 paramsKind: "inline_json",
802 rawBlock: "```baa\n@conductor::files/write::{}\n```",
803 rawInstruction: '@conductor::files/write::{"path":"/tmp/demo.txt","overwrite":true,"content":"hello"}',
804 target: "conductor",
805 tool: "files/write"
806 });
807 const right = normalizeBaaInstruction(source, {
808 blockIndex: 0,
809 params: {
810 content: "hello",
811 path: "/tmp/demo.txt",
812 overwrite: true
813 },
814 paramsKind: "inline_json",
815 rawBlock: "```baa\n@conductor::files/write::{}\n```",
816 rawInstruction: '@conductor::files/write::{"content":"hello","path":"/tmp/demo.txt","overwrite":true}',
817 target: "conductor",
818 tool: "files/write"
819 });
820
821 assert.equal(left.dedupeKey, right.dedupeKey);
822 assert.equal(left.instructionId, right.instructionId);
823 assert.deepEqual(left.dedupeBasis, {
824 assistant_message_id: "msg-001",
825 block_index: 0,
826 conversation_id: "conv-001",
827 params: {
828 content: "hello",
829 overwrite: true,
830 path: "/tmp/demo.txt"
831 },
832 platform: "claude",
833 target: "conductor",
834 tool: "files/write",
835 version: "baa.v1"
836 });
837});
838
839test("routeBaaInstruction maps browser send/current targets to the existing local browser routes", () => {
840 for (const platform of ["claude", "chatgpt", "gemini"]) {
841 const sendRoute = routeBaaInstruction(createInstructionEnvelope({
842 params: `hello ${platform} from baa`,
843 target: `browser.${platform}`,
844 tool: "send"
845 }));
846 assert.deepEqual(sendRoute, {
847 body: {
848 prompt: `hello ${platform} from baa`
849 },
850 key: `local.browser.${platform}.send`,
851 method: "POST",
852 path: `/v1/browser/${platform}/send`,
853 requiresSharedToken: false,
854 timeoutMs: DEFAULT_BAA_INSTRUCTION_TIMEOUT_MS
855 });
856
857 const currentRoute = routeBaaInstruction(createInstructionEnvelope({
858 target: `browser.${platform}`,
859 tool: "current"
860 }));
861 assert.deepEqual(currentRoute, {
862 body: null,
863 key: `local.browser.${platform}.current`,
864 method: "GET",
865 path: `/v1/browser/${platform}/current`,
866 requiresSharedToken: false,
867 timeoutMs: DEFAULT_BAA_INSTRUCTION_TIMEOUT_MS
868 });
869 }
870});
871
872test("routeBaaInstruction applies a 60s default timeout to conductor exec", () => {
873 const execRoute = routeBaaInstruction(createInstructionEnvelope({
874 params: "printf 'exec-timeout-default'",
875 tool: "exec"
876 }));
877
878 assert.equal(execRoute.timeoutMs, DEFAULT_BAA_EXEC_INSTRUCTION_TIMEOUT_MS);
879 assert.deepEqual(execRoute.body, {
880 command: "printf 'exec-timeout-default'",
881 timeoutMs: DEFAULT_BAA_EXEC_INSTRUCTION_TIMEOUT_MS
882 });
883});
884
885test("evaluateBaaInstructionPolicy keeps the default Phase 1 policy behavior", () => {
886 const allowed = evaluateBaaInstructionPolicy(createInstructionEnvelope({
887 target: "browser.chatgpt",
888 tool: "send"
889 }));
890 const denied = evaluateBaaInstructionPolicy(createInstructionEnvelope({
891 target: "browser.chatgpt",
892 tool: "reload"
893 }));
894
895 assert.deepEqual(allowed, {
896 code: null,
897 message: null,
898 ok: true
899 });
900 assert.equal(denied.ok, false);
901 assert.equal(denied.code, "unsupported_tool");
902 assert.match(denied.message ?? "", /not supported in Phase 1/i);
903});
904
905test("executeBaaInstruction returns a structured timeout failure when the local handler stalls", async () => {
906 const { controlPlane, snapshot } = await createLocalApiFixture();
907 const instruction = createInstructionEnvelope({
908 params: "printf 'timeout-protected'",
909 tool: "exec"
910 });
911 const route = {
912 ...routeBaaInstruction(instruction),
913 timeoutMs: 20
914 };
915 let requestSignal = null;
916 let resolveAbortObserved;
917 const abortObserved = new Promise((resolve) => {
918 resolveAbortObserved = resolve;
919 });
920
921 try {
922 const result = await executeBaaInstruction(
923 instruction,
924 route,
925 {
926 repository: null,
927 sharedToken: "local-shared-token",
928 snapshotLoader: () => snapshot
929 },
930 {
931 requestHandler: (request) => {
932 requestSignal = request.signal ?? null;
933 request.signal?.addEventListener("abort", () => {
934 resolveAbortObserved();
935 }, {
936 once: true
937 });
938 return new Promise(() => {});
939 }
940 }
941 );
942
943 await abortObserved;
944
945 assert.equal(result.ok, false);
946 assert.equal(result.httpStatus, 504);
947 assert.equal(result.error, "execution_timeout");
948 assert.equal(result.message, "Local instruction execution timed out after 20ms.");
949 assert.deepEqual(result.details, {
950 timeout_ms: 20
951 });
952 assert.equal(result.route.key, "local.exec");
953 assert.ok(result.artifact);
954 assert.equal(requestSignal?.aborted, true);
955 } finally {
956 controlPlane.close();
957 }
958});
959
960test("BaaInstructionCenter applies custom policy overrides for target and tool allowlists", async () => {
961 const { controlPlane, repository, sharedToken, snapshot } = await createLocalApiFixture();
962 const targets = createDefaultBaaInstructionPolicyTargetTools();
963 const chatgptTools = targets.get("browser.chatgpt");
964 assert.ok(chatgptTools);
965 targets.delete("browser.gemini");
966 chatgptTools.add("reload");
967 const center = new BaaInstructionCenter({
968 localApiContext: {
969 fetchImpl: globalThis.fetch,
970 repository,
971 sharedToken,
972 snapshotLoader: () => snapshot
973 },
974 policy: {
975 targets
976 }
977 });
978 const message = [
979 "```baa",
980 "@conductor::describe",
981 "```",
982 "",
983 "```baa",
984 "@browser.chatgpt::reload",
985 "```",
986 "",
987 "```baa",
988 "@browser.gemini::send::hello gemini",
989 "```"
990 ].join("\n");
991
992 try {
993 const result = await center.processAssistantMessage({
994 assistantMessageId: "msg-custom-policy-1",
995 conversationId: "conv-custom-policy-1",
996 platform: "claude",
997 text: message
998 });
999
1000 assert.equal(result.status, "executed");
1001 assert.equal(result.executions.length, 1);
1002 assert.equal(result.executions[0]?.tool, "describe");
1003 assert.equal(result.denied.length, 2);
1004
1005 const expandedToolDeny = result.denied.find(
1006 (entry) => entry.instruction.target === "browser.chatgpt" && entry.instruction.tool === "reload"
1007 );
1008 assert.ok(expandedToolDeny);
1009 assert.equal(expandedToolDeny.stage, "route");
1010 assert.equal(expandedToolDeny.code, null);
1011 assert.match(expandedToolDeny.reason, /No Phase 1 route exists/i);
1012
1013 const removedTargetDeny = result.denied.find(
1014 (entry) => entry.instruction.target === "browser.gemini" && entry.instruction.tool === "send"
1015 );
1016 assert.ok(removedTargetDeny);
1017 assert.equal(removedTargetDeny.stage, "policy");
1018 assert.equal(removedTargetDeny.code, "unsupported_target");
1019 } finally {
1020 controlPlane.close();
1021 }
1022});
1023
1024test("BaaInstructionCenter runs the Phase 1 execution loop and dedupes replayed messages", async () => {
1025 const { controlPlane, repository, sharedToken, snapshot } = await createLocalApiFixture();
1026 const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-instruction-center-"));
1027 const readableFilePath = join(hostOpsDir, "seed.txt");
1028 const localApiContext = {
1029 fetchImpl: globalThis.fetch,
1030 repository,
1031 sharedToken,
1032 snapshotLoader: () => snapshot
1033 };
1034 const center = new BaaInstructionCenter({
1035 localApiContext
1036 });
1037 const message = [
1038 "Run the minimal Phase 1 toolset.",
1039 "```baa",
1040 "@conductor::describe",
1041 "```",
1042 "",
1043 "```baa",
1044 "@conductor::status",
1045 "```",
1046 "",
1047 "```baa",
1048 `@conductor::exec::{"command":"printf 'instruction-loop-ok'","cwd":${JSON.stringify(hostOpsDir)}}`,
1049 "```",
1050 "",
1051 "```baa",
1052 `@conductor::files/write::{"path":"written.txt","cwd":${JSON.stringify(hostOpsDir)},"content":"hello from baa","overwrite":true,"createParents":true}`,
1053 "```",
1054 "",
1055 "```baa",
1056 `@conductor::files/read::{"path":"seed.txt","cwd":${JSON.stringify(hostOpsDir)}}`,
1057 "```"
1058 ].join("\n");
1059
1060 writeFileSync(readableFilePath, "seed from fixture", "utf8");
1061
1062 try {
1063 const firstPass = await center.processAssistantMessage({
1064 assistantMessageId: "msg-loop-1",
1065 conversationId: "conv-loop-1",
1066 platform: "claude",
1067 text: message
1068 });
1069
1070 assert.equal(firstPass.status, "executed");
1071 assert.equal(firstPass.duplicates.length, 0);
1072 assert.equal(firstPass.instructions.length, 5);
1073 assert.equal(firstPass.executions.length, 5);
1074 assert.equal(firstPass.parseErrors.length, 0);
1075
1076 const describeExecution = firstPass.executions.find((execution) => execution.tool === "describe");
1077 assert.ok(describeExecution);
1078 assert.equal(describeExecution.ok, true);
1079 assert.equal(describeExecution.data.name, "baa-conductor-daemon");
1080
1081 const statusExecution = firstPass.executions.find((execution) => execution.tool === "status");
1082 assert.ok(statusExecution);
1083 assert.equal(statusExecution.ok, true);
1084 assert.equal(statusExecution.data.mode, "running");
1085 assert.equal(statusExecution.data.activeRuns, 1);
1086
1087 const execExecution = firstPass.executions.find((execution) => execution.tool === "exec");
1088 assert.ok(execExecution);
1089 assert.equal(execExecution.ok, true);
1090 assert.equal(execExecution.data.operation, "exec");
1091 assert.equal(execExecution.data.result.stdout, "instruction-loop-ok");
1092
1093 const writeExecution = firstPass.executions.find(
1094 (execution) => execution.tool === "files/write"
1095 );
1096 assert.ok(writeExecution);
1097 assert.equal(writeExecution.ok, true);
1098 assert.equal(writeExecution.data.operation, "files/write");
1099 assert.equal(writeExecution.data.result.created, true);
1100
1101 const readExecution = firstPass.executions.find(
1102 (execution) => execution.tool === "files/read"
1103 );
1104 assert.ok(readExecution);
1105 assert.equal(readExecution.ok, true);
1106 assert.equal(readExecution.data.operation, "files/read");
1107 assert.equal(readExecution.data.result.content, "seed from fixture");
1108
1109 const replayPass = await center.processAssistantMessage({
1110 assistantMessageId: "msg-loop-1",
1111 conversationId: "conv-loop-1",
1112 platform: "claude",
1113 text: message
1114 });
1115
1116 assert.equal(replayPass.status, "duplicate_only");
1117 assert.equal(replayPass.duplicates.length, 5);
1118 assert.equal(replayPass.executions.length, 0);
1119 assert.equal(replayPass.parseErrors.length, 0);
1120 } finally {
1121 controlPlane.close();
1122 rmSync(hostOpsDir, {
1123 force: true,
1124 recursive: true
1125 });
1126 }
1127});
1128
1129test("BaaInstructionCenter blocks ordinary instructions while system automation is paused and resumes through control instructions", async () => {
1130 const { controlPlane, repository, sharedToken, snapshot } = await createLocalApiFixture();
1131 const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-instruction-center-system-paused-"));
1132 const outputPath = join(hostOpsDir, "system-gate-restored.txt");
1133 const center = new BaaInstructionCenter({
1134 localApiContext: {
1135 fetchImpl: globalThis.fetch,
1136 repository,
1137 sharedToken,
1138 snapshotLoader: () => snapshot
1139 }
1140 });
1141 const writeMessage = [
1142 "```baa",
1143 `@conductor::files/write::${JSON.stringify({
1144 path: "system-gate-restored.txt",
1145 cwd: hostOpsDir,
1146 content: "system gate lifted",
1147 overwrite: true
1148 })}`,
1149 "```"
1150 ].join("\n");
1151
1152 try {
1153 await repository.setAutomationMode("paused", 250);
1154
1155 const blocked = await center.processAssistantMessage({
1156 assistantMessageId: "msg-system-paused-1",
1157 conversationId: "conv-system-paused-1",
1158 platform: "claude",
1159 text: writeMessage
1160 });
1161
1162 assert.equal(blocked.status, "system_paused");
1163 assert.equal(blocked.executions.length, 0);
1164 assert.equal(existsSync(outputPath), false);
1165
1166 const resume = await center.processAssistantMessage({
1167 assistantMessageId: "msg-system-paused-resume-1",
1168 conversationId: "conv-system-paused-1",
1169 platform: "claude",
1170 text: ["```baa", "@conductor::system/resume", "```"].join("\n")
1171 });
1172
1173 assert.equal(resume.status, "executed");
1174 assert.equal(resume.executions.length, 1);
1175 assert.equal(resume.executions[0]?.ok, true);
1176 assert.equal((await repository.getAutomationState())?.mode, "running");
1177
1178 const resumed = await center.processAssistantMessage({
1179 assistantMessageId: "msg-system-paused-2",
1180 conversationId: "conv-system-paused-1",
1181 platform: "claude",
1182 text: writeMessage
1183 });
1184
1185 assert.equal(resumed.status, "executed");
1186 assert.equal(resumed.executions.length, 1);
1187 assert.equal(resumed.executions[0]?.ok, true);
1188 assert.equal(readFileSync(outputPath, "utf8"), "system gate lifted");
1189 } finally {
1190 controlPlane.close();
1191 rmSync(hostOpsDir, {
1192 force: true,
1193 recursive: true
1194 });
1195 }
1196});
1197
1198test("BaaInstructionCenter executes browser.claude send/current through the Phase 1 route layer", async () => {
1199 const { controlPlane, repository, snapshot } = await createLocalApiFixture();
1200 const browser = createBrowserBridgeStub();
1201 const center = new BaaInstructionCenter({
1202 localApiContext: {
1203 ...browser.context,
1204 fetchImpl: globalThis.fetch,
1205 repository,
1206 snapshotLoader: () => snapshot
1207 }
1208 });
1209 const message = [
1210 "```baa",
1211 "@browser.claude::send::hello from instruction center",
1212 "```",
1213 "",
1214 "```baa",
1215 "@browser.claude::current",
1216 "```"
1217 ].join("\n");
1218
1219 try {
1220 const result = await center.processAssistantMessage({
1221 assistantMessageId: "msg-browser-claude-1",
1222 conversationId: "conv-browser-claude-1",
1223 platform: "claude",
1224 text: message
1225 });
1226
1227 assert.equal(result.status, "executed");
1228 assert.equal(result.denied.length, 0);
1229 assert.equal(result.executions.length, 2);
1230
1231 const sendExecution = result.executions.find((execution) => execution.tool === "send");
1232 assert.ok(sendExecution);
1233 assert.equal(sendExecution.ok, true);
1234 assert.equal(sendExecution.route.path, "/v1/browser/claude/send");
1235 assert.equal(sendExecution.data.conversation.conversation_id, "conv-1");
1236 assert.equal(sendExecution.data.response.accepted, true);
1237
1238 const currentExecution = result.executions.find((execution) => execution.tool === "current");
1239 assert.ok(currentExecution);
1240 assert.equal(currentExecution.ok, true);
1241 assert.equal(currentExecution.route.path, "/v1/browser/claude/current");
1242 assert.equal(currentExecution.data.conversation.conversation_id, "conv-1");
1243 assert.equal(currentExecution.data.messages.length, 2);
1244
1245 const completionCall = browser.calls.find(
1246 (call) =>
1247 call.kind === "apiRequest"
1248 && call.path === "/api/organizations/org-1/chat_conversations/conv-1/completion"
1249 );
1250 assert.ok(completionCall);
1251 assert.deepEqual(completionCall.body, {
1252 prompt: "hello from instruction center"
1253 });
1254
1255 const currentConversationCall = browser.calls.find(
1256 (call) =>
1257 call.kind === "apiRequest"
1258 && call.path === "/api/organizations/org-1/chat_conversations/conv-1"
1259 );
1260 assert.ok(currentConversationCall);
1261 } finally {
1262 controlPlane.close();
1263 }
1264});
1265
1266test("BaaInstructionCenter executes browser.chatgpt and browser.gemini send/current through the Phase 1 route layer", async () => {
1267 const { controlPlane, repository, snapshot } = await createLocalApiFixture();
1268 const browser = createBrowserBridgeStub();
1269 const browserState = browser.context.browserStateLoader();
1270
1271 browserState.clients[0].request_hooks.push(
1272 {
1273 account: "ops@example.com",
1274 credential_fingerprint: "fp-chatgpt-stub",
1275 platform: "chatgpt",
1276 endpoint_count: 1,
1277 endpoint_metadata: [
1278 {
1279 method: "POST",
1280 path: "/backend-api/conversation",
1281 first_seen_at: 1710000002600,
1282 last_seen_at: 1710000003600
1283 }
1284 ],
1285 endpoints: [
1286 "POST /backend-api/conversation"
1287 ],
1288 last_verified_at: 1710000003650,
1289 updated_at: 1710000003550
1290 },
1291 {
1292 account: "ops@example.com",
1293 credential_fingerprint: "fp-gemini-stub",
1294 platform: "gemini",
1295 endpoint_count: 1,
1296 endpoint_metadata: [
1297 {
1298 method: "POST",
1299 path: "/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate",
1300 first_seen_at: 1710000002700,
1301 last_seen_at: 1710000003700
1302 }
1303 ],
1304 endpoints: [
1305 "POST /_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
1306 ],
1307 last_verified_at: 1710000003750,
1308 updated_at: 1710000003650
1309 }
1310 );
1311 browserState.clients[0].shell_runtime.push(
1312 buildShellRuntime("chatgpt", {
1313 actual: {
1314 ...buildShellRuntime("chatgpt").actual,
1315 url: "https://chatgpt.com/c/conv-chatgpt-current"
1316 }
1317 }),
1318 buildShellRuntime("gemini", {
1319 actual: {
1320 ...buildShellRuntime("gemini").actual,
1321 url: "https://gemini.google.com/app/conv-gemini-current"
1322 }
1323 })
1324 );
1325 browserState.clients[0].final_messages.push(
1326 {
1327 conversation_id: "conv-chatgpt-current",
1328 observed_at: 1710000003800,
1329 page_title: "ChatGPT Current",
1330 page_url: "https://chatgpt.com/c/conv-chatgpt-current",
1331 platform: "chatgpt",
1332 raw_text: "hello from chatgpt current"
1333 },
1334 {
1335 conversation_id: "conv-gemini-current",
1336 observed_at: 1710000003900,
1337 page_title: "Gemini Current",
1338 page_url: "https://gemini.google.com/app/conv-gemini-current",
1339 platform: "gemini",
1340 raw_text: "hello from gemini current"
1341 }
1342 );
1343
1344 const center = new BaaInstructionCenter({
1345 localApiContext: {
1346 ...browser.context,
1347 browserStateLoader: () => browserState,
1348 fetchImpl: globalThis.fetch,
1349 repository,
1350 snapshotLoader: () => snapshot
1351 }
1352 });
1353 const message = [
1354 "```baa",
1355 "@browser.chatgpt::send::hello chatgpt",
1356 "```",
1357 "",
1358 "```baa",
1359 "@browser.chatgpt::current",
1360 "```",
1361 "",
1362 "```baa",
1363 "@browser.gemini::send::hello gemini",
1364 "```",
1365 "",
1366 "```baa",
1367 "@browser.gemini::current",
1368 "```"
1369 ].join("\n");
1370
1371 try {
1372 const result = await center.processAssistantMessage({
1373 assistantMessageId: "msg-browser-compat-1",
1374 conversationId: "conv-browser-compat-1",
1375 platform: "claude",
1376 text: message
1377 });
1378
1379 assert.equal(result.status, "executed");
1380 assert.equal(result.denied.length, 0);
1381 assert.equal(result.executions.length, 4);
1382
1383 const chatgptSend = result.executions.find((execution) => execution.target === "browser.chatgpt" && execution.tool === "send");
1384 assert.ok(chatgptSend);
1385 assert.equal(chatgptSend.ok, true);
1386 assert.equal(chatgptSend.route.path, "/v1/browser/chatgpt/send");
1387 assert.equal(chatgptSend.data.proxy.path, "/backend-api/conversation");
1388
1389 const chatgptCurrent = result.executions.find((execution) => execution.target === "browser.chatgpt" && execution.tool === "current");
1390 assert.ok(chatgptCurrent);
1391 assert.equal(chatgptCurrent.ok, true);
1392 assert.equal(chatgptCurrent.route.path, "/v1/browser/chatgpt/current");
1393 assert.equal(chatgptCurrent.data.conversation.conversation_id, "conv-chatgpt-current");
1394 assert.equal(chatgptCurrent.data.messages[0].text, "hello from chatgpt current");
1395 assert.equal(chatgptCurrent.data.proxy.path, "/backend-api/conversation/conv-chatgpt-current");
1396
1397 const geminiSend = result.executions.find((execution) => execution.target === "browser.gemini" && execution.tool === "send");
1398 assert.ok(geminiSend);
1399 assert.equal(geminiSend.ok, true);
1400 assert.equal(geminiSend.route.path, "/v1/browser/gemini/send");
1401 assert.equal(
1402 geminiSend.data.proxy.path,
1403 "/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
1404 );
1405
1406 const geminiCurrent = result.executions.find((execution) => execution.target === "browser.gemini" && execution.tool === "current");
1407 assert.ok(geminiCurrent);
1408 assert.equal(geminiCurrent.ok, true);
1409 assert.equal(geminiCurrent.route.path, "/v1/browser/gemini/current");
1410 assert.equal(geminiCurrent.data.conversation.conversation_id, "conv-gemini-current");
1411 assert.equal(geminiCurrent.data.messages[0].text, "hello from gemini current");
1412 assert.equal(geminiCurrent.data.proxy, null);
1413
1414 assert.ok(
1415 browser.calls.find(
1416 (call) => call.kind === "apiRequest" && call.path === "/backend-api/conversation"
1417 )
1418 );
1419 assert.ok(
1420 browser.calls.find(
1421 (call) =>
1422 call.kind === "apiRequest"
1423 && call.path === "/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
1424 )
1425 );
1426 assert.ok(
1427 browser.calls.find(
1428 (call) =>
1429 call.kind === "apiRequest"
1430 && call.path === "/backend-api/conversation/conv-chatgpt-current"
1431 )
1432 );
1433 } finally {
1434 controlPlane.close();
1435 }
1436});
1437
1438test("BaaInstructionCenter keeps supported instructions running when one instruction is denied", async () => {
1439 const { controlPlane, repository, sharedToken, snapshot } = await createLocalApiFixture();
1440 const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-instruction-center-partial-deny-"));
1441 const allowedFilePath = join(hostOpsDir, "still-runs.txt");
1442 const center = new BaaInstructionCenter({
1443 localApiContext: {
1444 fetchImpl: globalThis.fetch,
1445 repository,
1446 sharedToken,
1447 snapshotLoader: () => snapshot
1448 }
1449 });
1450 const message = [
1451 "```baa",
1452 `@conductor::files/write::{"path":"still-runs.txt","cwd":${JSON.stringify(hostOpsDir)},"content":"partial deny is open","overwrite":true}`,
1453 "```",
1454 "",
1455 "```baa",
1456 "@browser.gemini::reload",
1457 "```",
1458 "",
1459 "```baa",
1460 `@conductor::exec::{"command":"printf 'instruction-still-runs'","cwd":${JSON.stringify(hostOpsDir)}}`,
1461 "```"
1462 ].join("\n");
1463
1464 try {
1465 const result = await center.processAssistantMessage({
1466 assistantMessageId: "msg-partial-deny-1",
1467 conversationId: "conv-partial-deny-1",
1468 platform: "claude",
1469 text: message
1470 });
1471
1472 assert.equal(result.status, "executed");
1473 assert.equal(result.instructions.length, 3);
1474 assert.equal(result.duplicates.length, 0);
1475 assert.equal(result.executions.length, 2);
1476 assert.equal(result.denied.length, 1);
1477
1478 const writeExecution = result.executions.find((execution) => execution.tool === "files/write");
1479 assert.ok(writeExecution);
1480 assert.equal(writeExecution.ok, true);
1481 assert.equal(readFileSync(allowedFilePath, "utf8"), "partial deny is open");
1482
1483 const execExecution = result.executions.find((execution) => execution.tool === "exec");
1484 assert.ok(execExecution);
1485 assert.equal(execExecution.ok, true);
1486 assert.equal(execExecution.data.result.stdout, "instruction-still-runs");
1487
1488 assert.equal(result.denied[0].blockIndex, 1);
1489 assert.equal(result.denied[0].stage, "policy");
1490 assert.equal(result.denied[0].code, "unsupported_tool");
1491 assert.equal(result.denied[0].instruction.target, "browser.gemini");
1492 assert.equal(result.denied[0].instruction.tool, "reload");
1493 assert.match(result.denied[0].reason, /not supported in Phase 1/i);
1494 } finally {
1495 controlPlane.close();
1496 rmSync(hostOpsDir, {
1497 force: true,
1498 recursive: true
1499 });
1500 }
1501});
1502
1503test("BaaInstructionCenter skips malformed blocks and keeps later valid blocks running", async () => {
1504 const { controlPlane, repository, sharedToken, snapshot } = await createLocalApiFixture();
1505 const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-instruction-center-parse-isolation-"));
1506 const outputPath = join(hostOpsDir, "after-parse-error.txt");
1507 const center = new BaaInstructionCenter({
1508 localApiContext: {
1509 fetchImpl: globalThis.fetch,
1510 repository,
1511 sharedToken,
1512 snapshotLoader: () => snapshot
1513 }
1514 });
1515 const message = [
1516 "```baa",
1517 '@conductor::files/write::{"path":"broken.txt"',
1518 "```",
1519 "",
1520 "```baa",
1521 `@conductor::files/write::{"path":"after-parse-error.txt","cwd":${JSON.stringify(hostOpsDir)},"content":"still-runs","overwrite":true}`,
1522 "```"
1523 ].join("\n");
1524
1525 try {
1526 const result = await center.processAssistantMessage({
1527 assistantMessageId: "msg-parse-isolation-1",
1528 conversationId: "conv-parse-isolation-1",
1529 platform: "claude",
1530 text: message
1531 });
1532
1533 assert.equal(result.status, "executed");
1534 assert.equal(result.instructions.length, 1);
1535 assert.equal(result.duplicates.length, 0);
1536 assert.equal(result.denied.length, 0);
1537 assert.equal(result.executions.length, 1);
1538 assert.equal(result.parseErrors.length, 1);
1539 assert.equal(result.parseErrors[0].blockIndex, 0);
1540 assert.equal(result.parseErrors[0].stage, "parse");
1541 assert.match(result.parseErrors[0].message, /Failed to parse inline JSON params/i);
1542 assert.equal(result.executions[0].tool, "files/write");
1543 assert.equal(result.executions[0].ok, true);
1544 assert.equal(readFileSync(outputPath, "utf8"), "still-runs");
1545 } finally {
1546 controlPlane.close();
1547 rmSync(hostOpsDir, {
1548 force: true,
1549 recursive: true
1550 });
1551 }
1552});
1553
1554test("BaaInstructionCenter returns denied_only when every pending instruction is denied", async () => {
1555 const { controlPlane, repository, sharedToken, snapshot } = await createLocalApiFixture();
1556 const center = new BaaInstructionCenter({
1557 localApiContext: {
1558 fetchImpl: globalThis.fetch,
1559 repository,
1560 sharedToken,
1561 snapshotLoader: () => snapshot
1562 }
1563 });
1564 const message = [
1565 "```baa",
1566 "@browser.chatgpt::reload",
1567 "```",
1568 "",
1569 "```baa",
1570 "@browser.gemini::reload",
1571 "```"
1572 ].join("\n");
1573
1574 try {
1575 const result = await center.processAssistantMessage({
1576 assistantMessageId: "msg-denied-only-1",
1577 conversationId: "conv-denied-only-1",
1578 platform: "claude",
1579 text: message
1580 });
1581
1582 assert.equal(result.status, "denied_only");
1583 assert.equal(result.executions.length, 0);
1584 assert.equal(result.denied.length, 2);
1585 assert.deepEqual(
1586 result.denied.map((entry) => ({
1587 code: entry.code,
1588 target: entry.instruction.target,
1589 tool: entry.instruction.tool
1590 })),
1591 [
1592 {
1593 code: "unsupported_tool",
1594 target: "browser.chatgpt",
1595 tool: "reload"
1596 },
1597 {
1598 code: "unsupported_tool",
1599 target: "browser.gemini",
1600 tool: "reload"
1601 }
1602 ]
1603 );
1604 } finally {
1605 controlPlane.close();
1606 }
1607});
1608
1609test("BaaLiveInstructionIngest ignores plain messages, dedupes replayed browser final messages, tolerates missing conversation ids, and surfaces partial denies", async () => {
1610 const { controlPlane, repository, sharedToken, snapshot } = await createLocalApiFixture();
1611 const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-live-instruction-ingest-"));
1612 const dedupeFilePath = join(hostOpsDir, "dedupe-count.txt");
1613 const ingest = new BaaLiveInstructionIngest({
1614 localApiContext: {
1615 fetchImpl: globalThis.fetch,
1616 repository,
1617 sharedToken,
1618 snapshotLoader: () => snapshot
1619 },
1620 now: () => 1_710_000_100_000
1621 });
1622 const executableMessage = [
1623 "```baa",
1624 `@conductor::exec::{"command":"printf 'live-hit\\n' >> dedupe-count.txt","cwd":${JSON.stringify(hostOpsDir)}}`,
1625 "```"
1626 ].join("\n");
1627
1628 try {
1629 const ignored = await ingest.ingestAssistantFinalMessage({
1630 assistantMessageId: "msg-live-plain",
1631 conversationId: null,
1632 observedAt: 1_710_000_001_000,
1633 platform: "chatgpt",
1634 source: "browser.final_message",
1635 text: [
1636 "plain markdown must stay inert",
1637 "```js",
1638 "console.log('no baa');",
1639 "```"
1640 ].join("\n")
1641 });
1642
1643 assert.equal(ignored.summary.status, "ignored_no_instructions");
1644 assert.equal(ignored.summary.execution_count, 0);
1645 assert.equal(ignored.summary.conversation_id, null);
1646 assert.equal(ignored.summary.parse_error_count, 0);
1647
1648 const firstPass = await ingest.ingestAssistantFinalMessage({
1649 assistantMessageId: "msg-live-exec",
1650 conversationId: null,
1651 observedAt: 1_710_000_002_000,
1652 platform: "chatgpt",
1653 source: "browser.final_message",
1654 text: executableMessage
1655 });
1656
1657 assert.equal(firstPass.summary.status, "executed");
1658 assert.equal(firstPass.summary.execution_count, 1);
1659 assert.equal(firstPass.summary.execution_ok_count, 1);
1660 assert.equal(firstPass.summary.instruction_tools[0], "conductor::exec");
1661 assert.equal(firstPass.summary.conversation_id, null);
1662 assert.equal(firstPass.summary.parse_error_count, 0);
1663 assert.equal(readFileSync(dedupeFilePath, "utf8"), "live-hit\n");
1664
1665 const replayPass = await ingest.ingestAssistantFinalMessage({
1666 assistantMessageId: "msg-live-exec",
1667 conversationId: "conv-should-not-matter",
1668 observedAt: 1_710_000_003_000,
1669 platform: "chatgpt",
1670 source: "browser.final_message",
1671 text: executableMessage
1672 });
1673
1674 assert.equal(replayPass.summary.status, "duplicate_message");
1675 assert.equal(replayPass.summary.execution_count, 0);
1676 assert.equal(replayPass.summary.parse_error_count, 0);
1677 assert.equal(readFileSync(dedupeFilePath, "utf8"), "live-hit\n");
1678
1679 const partialDeny = await ingest.ingestAssistantFinalMessage({
1680 assistantMessageId: "msg-live-partial-deny",
1681 conversationId: null,
1682 observedAt: 1_710_000_004_000,
1683 platform: "chatgpt",
1684 source: "browser.final_message",
1685 text: [
1686 "```baa",
1687 `@conductor::exec::{"command":"printf 'partial-live\\n' >> dedupe-count.txt","cwd":${JSON.stringify(hostOpsDir)}}`,
1688 "```",
1689 "",
1690 "```baa",
1691 "@conductor::files/write::should route fail",
1692 "```"
1693 ].join("\n")
1694 });
1695
1696 assert.equal(partialDeny.summary.status, "executed");
1697 assert.equal(partialDeny.summary.execution_count, 1);
1698 assert.equal(partialDeny.summary.execution_ok_count, 1);
1699 assert.equal(partialDeny.summary.error_stage, null);
1700 assert.equal(partialDeny.summary.parse_error_count, 0);
1701 assert.ok(partialDeny.processResult);
1702 assert.equal(partialDeny.processResult.executions.length, 1);
1703 assert.equal(partialDeny.processResult.denied.length, 1);
1704 assert.equal(partialDeny.processResult.parseErrors.length, 0);
1705 assert.equal(partialDeny.processResult.denied[0].blockIndex, 1);
1706 assert.equal(partialDeny.processResult.denied[0].stage, "route");
1707 assert.equal(partialDeny.processResult.denied[0].code, null);
1708 assert.equal(partialDeny.processResult.denied[0].instruction.tool, "files/write");
1709 assert.match(partialDeny.processResult.denied[0].reason, /requires JSON object params/i);
1710 assert.equal(readFileSync(dedupeFilePath, "utf8"), "live-hit\npartial-live\n");
1711
1712 const deniedOnly = await ingest.ingestAssistantFinalMessage({
1713 assistantMessageId: "msg-live-denied-only",
1714 conversationId: null,
1715 observedAt: 1_710_000_005_000,
1716 platform: "chatgpt",
1717 source: "browser.final_message",
1718 text: [
1719 "```baa",
1720 "@browser.chatgpt::reload",
1721 "```"
1722 ].join("\n")
1723 });
1724
1725 assert.equal(deniedOnly.summary.status, "denied_only");
1726 assert.equal(deniedOnly.summary.execution_count, 0);
1727 assert.equal(deniedOnly.summary.execution_ok_count, 0);
1728 assert.equal(deniedOnly.summary.error_stage, null);
1729 assert.equal(deniedOnly.summary.parse_error_count, 0);
1730 assert.ok(deniedOnly.processResult);
1731 assert.equal(deniedOnly.processResult.executions.length, 0);
1732 assert.equal(deniedOnly.processResult.denied.length, 1);
1733 assert.equal(deniedOnly.processResult.parseErrors.length, 0);
1734 assert.equal(deniedOnly.processResult.denied[0].stage, "policy");
1735 assert.equal(deniedOnly.processResult.denied[0].code, "unsupported_tool");
1736
1737 assert.equal(ingest.getSnapshot().last_ingest?.assistant_message_id, "msg-live-denied-only");
1738 assert.equal(ingest.getSnapshot().last_execute?.status, "denied_only");
1739 } finally {
1740 controlPlane.close();
1741 rmSync(hostOpsDir, {
1742 force: true,
1743 recursive: true
1744 });
1745 }
1746});
1747
1748test("BaaLiveInstructionIngest records parse errors while later valid blocks still execute", async () => {
1749 const { controlPlane, repository, sharedToken, snapshot } = await createLocalApiFixture();
1750 const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-live-instruction-parse-isolation-"));
1751 const outputPath = join(hostOpsDir, "parse-isolation.txt");
1752 const ingest = new BaaLiveInstructionIngest({
1753 localApiContext: {
1754 fetchImpl: globalThis.fetch,
1755 repository,
1756 sharedToken,
1757 snapshotLoader: () => snapshot
1758 },
1759 now: () => 1_710_000_110_000
1760 });
1761
1762 try {
1763 const result = await ingest.ingestAssistantFinalMessage({
1764 assistantMessageId: "msg-live-parse-isolation",
1765 conversationId: "conv-live-parse-isolation",
1766 observedAt: 1_710_000_111_000,
1767 platform: "chatgpt",
1768 source: "browser.final_message",
1769 text: [
1770 "```baa",
1771 '@conductor::files/write::{"path":"broken.txt"',
1772 "```",
1773 "",
1774 "```baa",
1775 `@conductor::exec::{"command":"printf 'parse-live\\n' >> parse-isolation.txt","cwd":${JSON.stringify(hostOpsDir)}}`,
1776 "```"
1777 ].join("\n")
1778 });
1779
1780 assert.equal(result.summary.status, "executed");
1781 assert.equal(result.summary.error_stage, null);
1782 assert.equal(result.summary.parse_error_count, 1);
1783 assert.equal(result.summary.parse_errors.length, 1);
1784 assert.equal(result.summary.parse_errors[0].block_index, 0);
1785 assert.equal(result.summary.parse_errors[0].stage, "parse");
1786 assert.match(result.summary.parse_errors[0].message, /Failed to parse inline JSON params/i);
1787 assert.equal(result.summary.instruction_count, 1);
1788 assert.equal(result.summary.execution_count, 1);
1789 assert.equal(result.summary.execution_ok_count, 1);
1790 assert.ok(result.processResult);
1791 assert.equal(result.processResult.instructions.length, 1);
1792 assert.equal(result.processResult.executions.length, 1);
1793 assert.equal(result.processResult.parseErrors.length, 1);
1794 assert.equal(result.processResult.parseErrors[0].blockIndex, 0);
1795 assert.equal(result.processResult.parseErrors[0].stage, "parse");
1796 assert.equal(readFileSync(outputPath, "utf8"), "parse-live\n");
1797 assert.equal(ingest.getSnapshot().last_ingest?.parse_error_count, 1);
1798 assert.equal(ingest.getSnapshot().last_execute?.parse_error_count, 1);
1799 } finally {
1800 controlPlane.close();
1801 rmSync(hostOpsDir, {
1802 force: true,
1803 recursive: true
1804 });
1805 }
1806});
1807
1808function createManualTimerScheduler() {
1809 let now = 0;
1810 let nextId = 1;
1811 const timers = new Map();
1812
1813 const pickNextTimerId = (deadline) => {
1814 let selectedId = null;
1815 let selectedDueAt = Number.POSITIVE_INFINITY;
1816
1817 for (const [timerId, timer] of timers.entries()) {
1818 if (timer.dueAt > deadline) {
1819 continue;
1820 }
1821
1822 if (
1823 selectedId === null
1824 || timer.dueAt < selectedDueAt
1825 || (timer.dueAt === selectedDueAt && timerId < selectedId)
1826 ) {
1827 selectedId = timerId;
1828 selectedDueAt = timer.dueAt;
1829 }
1830 }
1831
1832 return selectedId;
1833 };
1834
1835 return {
1836 advanceBy(durationMs) {
1837 const deadline = now + Math.max(0, durationMs);
1838
1839 while (true) {
1840 const timerId = pickNextTimerId(deadline);
1841 if (timerId == null) {
1842 break;
1843 }
1844
1845 const timer = timers.get(timerId);
1846 timers.delete(timerId);
1847 now = timer.dueAt;
1848 timer.handler();
1849 }
1850
1851 now = deadline;
1852 },
1853 clearTimeout(timerId) {
1854 timers.delete(timerId);
1855 },
1856 now: () => now,
1857 setTimeout(handler, timeoutMs) {
1858 const timerId = nextId++;
1859 timers.set(timerId, {
1860 dueAt: now + Math.max(0, timeoutMs),
1861 handler
1862 });
1863 return timerId;
1864 }
1865 };
1866}
1867
1868function createManualIntervalScheduler() {
1869 let nextId = 1;
1870 const intervals = new Map();
1871
1872 return {
1873 clearInterval(intervalId) {
1874 intervals.delete(intervalId);
1875 },
1876 fireAll() {
1877 for (const interval of [...intervals.values()]) {
1878 interval.handler();
1879 }
1880 },
1881 getActiveCount() {
1882 return intervals.size;
1883 },
1884 setInterval(handler, intervalMs) {
1885 const intervalId = nextId++;
1886 intervals.set(intervalId, {
1887 handler,
1888 intervalMs
1889 });
1890 return intervalId;
1891 }
1892 };
1893}
1894
1895function readJsonlEntries(dirPath) {
1896 const fileNames = readdirSync(dirPath).filter((name) => name.endsWith(".jsonl")).sort();
1897 assert.ok(fileNames.length > 0, `expected at least one jsonl file in ${dirPath}`);
1898 return readFileSync(join(dirPath, fileNames[0]), "utf8")
1899 .trim()
1900 .split("\n")
1901 .filter(Boolean)
1902 .map((line) => JSON.parse(line));
1903}
1904
1905async function waitForJsonlEntries(dirPath, predicate = null, timeoutMs = 1_000) {
1906 const deadline = Date.now() + timeoutMs;
1907 let lastError = null;
1908
1909 while (Date.now() < deadline) {
1910 try {
1911 const entries = readJsonlEntries(dirPath);
1912
1913 if (predicate == null || predicate(entries)) {
1914 return entries;
1915 }
1916 } catch (error) {
1917 lastError = error;
1918 }
1919
1920 await new Promise((resolve) => setTimeout(resolve, 10));
1921 }
1922
1923 if (lastError != null) {
1924 throw lastError;
1925 }
1926
1927 return readJsonlEntries(dirPath);
1928}
1929
1930async function flushAsyncWork() {
1931 await Promise.resolve();
1932 await Promise.resolve();
1933}
1934
1935function getPolicyPlatformSnapshot(policy, platform) {
1936 return policy.getSnapshot().platforms.find((entry) => entry.platform === platform) ?? null;
1937}
1938
1939function getPolicyTargetSnapshot(policy, clientId, platform) {
1940 return policy.getSnapshot().targets.find(
1941 (entry) => entry.clientId === clientId && entry.platform === platform
1942 ) ?? null;
1943}
1944
1945async function readResponseBodyText(response) {
1946 let text = response.body;
1947
1948 if (response.streamBody != null) {
1949 for await (const chunk of response.streamBody) {
1950 text += chunk;
1951 }
1952 }
1953
1954 return text;
1955}
1956
1957function parseSseFrames(text) {
1958 return String(text || "")
1959 .split(/\n\n+/u)
1960 .map((chunk) => chunk.trim())
1961 .filter(Boolean)
1962 .map((chunk) => {
1963 const lines = chunk.split("\n");
1964 const eventLine = lines.find((line) => line.startsWith("event:"));
1965 const dataLines = lines
1966 .filter((line) => line.startsWith("data:"))
1967 .map((line) => line.slice(5).trimStart());
1968
1969 return {
1970 data: JSON.parse(dataLines.join("\n")),
1971 event: eventLine ? eventLine.slice(6).trim() : null
1972 };
1973 });
1974}
1975
1976function getShellRuntimeDefaults(platform) {
1977 switch (platform) {
1978 case "chatgpt":
1979 return {
1980 actualUrl: "https://chatgpt.com/c/http",
1981 shellUrl: "https://chatgpt.com/",
1982 title: "ChatGPT HTTP"
1983 };
1984 case "gemini":
1985 return {
1986 actualUrl: "https://gemini.google.com/app",
1987 shellUrl: "https://gemini.google.com/",
1988 title: "Gemini HTTP"
1989 };
1990 case "claude":
1991 default:
1992 return {
1993 actualUrl: "https://claude.ai/chats/http",
1994 shellUrl: "https://claude.ai/",
1995 title: "Claude HTTP"
1996 };
1997 }
1998}
1999
2000function buildShellRuntime(platform, overrides = {}) {
2001 const defaults = getShellRuntimeDefaults(platform);
2002
2003 return {
2004 platform,
2005 desired: {
2006 exists: true,
2007 shell_url: defaults.shellUrl,
2008 source: "integration",
2009 reason: "test",
2010 updated_at: 1710000002000,
2011 last_action: "tab_open",
2012 last_action_at: 1710000002100
2013 },
2014 actual: {
2015 exists: true,
2016 tab_id: 321,
2017 url: defaults.actualUrl,
2018 title: defaults.title,
2019 window_id: 91,
2020 active: true,
2021 status: "complete",
2022 discarded: false,
2023 hidden: false,
2024 healthy: true,
2025 issue: null,
2026 last_seen_at: 1710000002200,
2027 last_ready_at: 1710000002300,
2028 candidate_tab_id: null,
2029 candidate_url: null
2030 },
2031 drift: {
2032 aligned: true,
2033 needs_restore: false,
2034 unexpected_actual: false,
2035 reason: "aligned"
2036 },
2037 ...overrides
2038 };
2039}
2040
2041function sendPluginActionResult(socket, input) {
2042 const shellRuntime = input.shell_runtime ?? (input.platform ? [buildShellRuntime(input.platform)] : []);
2043 const results =
2044 input.results
2045 ?? shellRuntime.map((runtime) => ({
2046 delivery_ack: input.deliveryAck ?? null,
2047 ok: true,
2048 platform: runtime.platform,
2049 restored: input.restored ?? false,
2050 shell_runtime: runtime,
2051 skipped: input.skipped ?? null,
2052 tab_id: runtime.actual.tab_id
2053 }));
2054
2055 socket.send(
2056 JSON.stringify({
2057 type: "action_result",
2058 requestId: input.requestId,
2059 action: input.action,
2060 command_type: input.commandType ?? input.type ?? input.action,
2061 accepted: input.accepted ?? true,
2062 completed: input.completed ?? true,
2063 failed: input.failed ?? false,
2064 reason: input.reason ?? null,
2065 target: {
2066 platform: input.platform ?? null,
2067 requested_platform: input.platform ?? null
2068 },
2069 result: {
2070 actual_count: shellRuntime.filter((runtime) => runtime.actual.exists).length,
2071 desired_count: shellRuntime.filter((runtime) => runtime.desired.exists).length,
2072 drift_count: shellRuntime.filter((runtime) => runtime.drift.aligned === false).length,
2073 failed_count: results.filter((entry) => entry.ok === false).length,
2074 ok_count: results.filter((entry) => entry.ok).length,
2075 platform_count: shellRuntime.length,
2076 restored_count: results.filter((entry) => entry.restored === true).length,
2077 skipped_reasons: results.map((entry) => entry.skipped).filter(Boolean)
2078 },
2079 results,
2080 shell_runtime: shellRuntime
2081 })
2082 );
2083}
2084
2085function buildDeliveryAck(statusCode, overrides = {}) {
2086 const resolvedStatusCode = Number.isFinite(Number(statusCode)) ? Number(statusCode) : null;
2087 return {
2088 confirmed_at: overrides.confirmed_at ?? overrides.confirmedAt ?? 1710000000250,
2089 failed: overrides.failed ?? resolvedStatusCode !== 200,
2090 level: overrides.level ?? (resolvedStatusCode == null ? 0 : 1),
2091 reason:
2092 overrides.reason
2093 ?? (resolvedStatusCode != null && resolvedStatusCode !== 200 ? `downstream_status_${resolvedStatusCode}` : null),
2094 status_code: resolvedStatusCode
2095 };
2096}
2097
2098function buildProxyDeliveryActionResult(options = {}) {
2099 const platform = options.platform ?? "claude";
2100 const shellRuntime = options.shell_runtime ?? [buildShellRuntime(platform)];
2101 const deliveryAck = options.deliveryAck ?? null;
2102 const results = options.results ?? shellRuntime.map((entry) => ({
2103 delivery_ack: deliveryAck,
2104 ok: true,
2105 platform: entry.platform,
2106 restored: false,
2107 shell_runtime: entry,
2108 skipped: null,
2109 tab_id: entry.actual.tab_id
2110 }));
2111
2112 return {
2113 accepted: options.accepted ?? true,
2114 action: "proxy_delivery",
2115 completed: options.completed ?? true,
2116 failed: options.failed ?? false,
2117 reason: options.reason ?? null,
2118 received_at: options.receivedAt ?? 1710000000300,
2119 request_id: options.requestId ?? "proxy-delivery-result",
2120 result: {
2121 actual_count: shellRuntime.filter((entry) => entry.actual.exists).length,
2122 desired_count: shellRuntime.filter((entry) => entry.desired.exists).length,
2123 drift_count: shellRuntime.filter((entry) => entry.drift.aligned === false).length,
2124 failed_count: results.filter((entry) => entry.ok === false).length,
2125 ok_count: results.filter((entry) => entry.ok).length,
2126 platform_count: shellRuntime.length,
2127 restored_count: results.filter((entry) => entry.restored === true).length,
2128 skipped_reasons: results.map((entry) => entry.skipped).filter(Boolean)
2129 },
2130 results,
2131 shell_runtime: shellRuntime,
2132 target: {
2133 client_id: options.clientId ?? `firefox-${platform}`,
2134 connection_id: options.connectionId ?? `conn-firefox-${platform}`,
2135 platform,
2136 requested_client_id: options.clientId ?? `firefox-${platform}`,
2137 requested_platform: platform
2138 },
2139 type: "browser.proxy_delivery"
2140 };
2141}
2142
2143function createBrowserBridgeStub() {
2144 const calls = [];
2145 const buildActionDispatch = ({
2146 action,
2147 clientId,
2148 connectionId = "conn-firefox-claude",
2149 dispatchedAt,
2150 platform = null,
2151 reason = null,
2152 type
2153 }) => {
2154 const shellRuntime = platform ? [buildShellRuntime(platform)] : [buildShellRuntime("claude")];
2155 const requestId = `${type}-stub-${calls.length + 1}`;
2156
2157 return {
2158 clientId,
2159 connectionId,
2160 dispatchedAt,
2161 requestId,
2162 result: Promise.resolve({
2163 accepted: true,
2164 action,
2165 completed: true,
2166 failed: false,
2167 reason,
2168 received_at: dispatchedAt + 25,
2169 request_id: requestId,
2170 result: {
2171 actual_count: shellRuntime.filter((entry) => entry.actual.exists).length,
2172 desired_count: shellRuntime.filter((entry) => entry.desired.exists).length,
2173 drift_count: shellRuntime.filter((entry) => entry.drift.aligned === false).length,
2174 failed_count: 0,
2175 ok_count: shellRuntime.length,
2176 platform_count: shellRuntime.length,
2177 restored_count: 0,
2178 skipped_reasons: []
2179 },
2180 results: shellRuntime.map((entry) => ({
2181 ok: true,
2182 platform: entry.platform,
2183 restored: false,
2184 shell_runtime: entry,
2185 skipped: null,
2186 tab_id: entry.actual.tab_id
2187 })),
2188 shell_runtime: shellRuntime,
2189 target: {
2190 client_id: clientId,
2191 connection_id: connectionId,
2192 platform,
2193 requested_client_id: clientId,
2194 requested_platform: platform
2195 },
2196 type
2197 }),
2198 type
2199 };
2200 };
2201 const browserState = {
2202 active_client_id: "firefox-claude",
2203 active_connection_id: "conn-firefox-claude",
2204 client_count: 1,
2205 clients: [
2206 {
2207 client_id: "firefox-claude",
2208 connected_at: 1710000000000,
2209 connection_id: "conn-firefox-claude",
2210 credentials: [
2211 {
2212 account: "ops@example.com",
2213 account_captured_at: 1710000000500,
2214 account_last_seen_at: 1710000000900,
2215 platform: "claude",
2216 captured_at: 1710000001000,
2217 credential_fingerprint: "fp-claude-stub",
2218 freshness: "fresh",
2219 header_count: 3,
2220 last_seen_at: 1710000001100
2221 }
2222 ],
2223 final_messages: [],
2224 last_action_result: {
2225 accepted: true,
2226 action: "plugin_status",
2227 completed: true,
2228 failed: false,
2229 reason: null,
2230 received_at: 1710000003600,
2231 request_id: "plugin_status-stub-1",
2232 result: {
2233 actual_count: 1,
2234 desired_count: 1,
2235 drift_count: 0,
2236 failed_count: 0,
2237 ok_count: 1,
2238 platform_count: 1,
2239 restored_count: 0,
2240 skipped_reasons: []
2241 },
2242 results: [
2243 {
2244 ok: true,
2245 platform: "claude",
2246 restored: false,
2247 shell_runtime: buildShellRuntime("claude"),
2248 skipped: null,
2249 tab_id: 88
2250 }
2251 ],
2252 shell_runtime: [buildShellRuntime("claude")],
2253 target: {
2254 client_id: "firefox-claude",
2255 connection_id: "conn-firefox-claude",
2256 platform: "claude",
2257 requested_client_id: "firefox-claude",
2258 requested_platform: "claude"
2259 },
2260 type: "plugin_status"
2261 },
2262 last_message_at: 1710000002000,
2263 node_category: "proxy",
2264 node_platform: "firefox",
2265 node_type: "browser",
2266 request_hooks: [
2267 {
2268 account: "ops@example.com",
2269 credential_fingerprint: "fp-claude-stub",
2270 platform: "claude",
2271 endpoint_count: 3,
2272 endpoint_metadata: [
2273 {
2274 method: "GET",
2275 path: "/api/organizations",
2276 first_seen_at: 1710000001500,
2277 last_seen_at: 1710000002500
2278 }
2279 ],
2280 endpoints: [
2281 "GET /api/organizations",
2282 "GET /api/organizations/{id}/chat_conversations/{id}",
2283 "POST /api/organizations/{id}/chat_conversations/{id}/completion"
2284 ],
2285 last_verified_at: 1710000003500,
2286 updated_at: 1710000003000
2287 }
2288 ],
2289 shell_runtime: [buildShellRuntime("claude")]
2290 }
2291 ],
2292 ws_path: "/ws/browser",
2293 ws_url: "ws://127.0.0.1:4317/ws/browser"
2294 };
2295
2296 const buildApiResponse = ({ body, clientId = "firefox-claude", error = null, id = null, status = 200 }) => ({
2297 body,
2298 clientId,
2299 connectionId: "conn-firefox-claude",
2300 error,
2301 id: id ?? `browser-${calls.length + 1}`,
2302 ok: error == null && status < 400,
2303 respondedAt: 1710000004000 + calls.length,
2304 status
2305 });
2306
2307 const buildApiStream = ({ events, input }) => ({
2308 clientId: input.clientId || "firefox-claude",
2309 connectionId: "conn-firefox-claude",
2310 requestId: input.id || `browser-stream-${calls.length + 1}`,
2311 streamId: input.streamId || input.id || `browser-stream-${calls.length + 1}`,
2312 cancel(reason = null) {
2313 calls.push({
2314 id: input.id,
2315 kind: "streamCancel",
2316 reason
2317 });
2318 },
2319 async *[Symbol.asyncIterator]() {
2320 for (const event of events) {
2321 yield event;
2322 }
2323 }
2324 });
2325
2326 return {
2327 calls,
2328 context: {
2329 browserBridge: {
2330 async apiRequest(input) {
2331 calls.push({
2332 ...input,
2333 kind: "apiRequest"
2334 });
2335
2336 if (input.path === "/api/organizations") {
2337 return buildApiResponse({
2338 id: input.id,
2339 body: {
2340 organizations: [
2341 {
2342 uuid: "org-1",
2343 name: "Claude Org",
2344 is_default: true
2345 }
2346 ]
2347 }
2348 });
2349 }
2350
2351 if (input.path === "/api/organizations/org-1/chat_conversations") {
2352 return buildApiResponse({
2353 id: input.id,
2354 body: {
2355 chat_conversations: [
2356 {
2357 uuid: "conv-1",
2358 name: "Current Claude Chat",
2359 updated_at: "2026-03-24T12:00:00.000Z",
2360 selected: true
2361 }
2362 ]
2363 }
2364 });
2365 }
2366
2367 if (input.path === "/api/organizations/org-1/chat_conversations/conv-1") {
2368 return buildApiResponse({
2369 id: input.id,
2370 body: {
2371 conversation: {
2372 uuid: "conv-1",
2373 name: "Current Claude Chat",
2374 updated_at: "2026-03-24T12:00:00.000Z"
2375 },
2376 messages: [
2377 {
2378 uuid: "msg-user-1",
2379 sender: "human",
2380 text: "hello claude"
2381 },
2382 {
2383 uuid: "msg-assistant-1",
2384 sender: "assistant",
2385 content: [
2386 {
2387 text: "hello from claude"
2388 }
2389 ]
2390 }
2391 ]
2392 }
2393 });
2394 }
2395
2396 if (input.path === "/api/organizations/org-1/chat_conversations/conv-1/completion") {
2397 return buildApiResponse({
2398 id: input.id,
2399 body: {
2400 accepted: true,
2401 conversation_uuid: "conv-1"
2402 },
2403 status: 202
2404 });
2405 }
2406
2407 if (input.path === "/api/stream-buffered-smoke") {
2408 return buildApiResponse({
2409 id: input.id,
2410 body: [
2411 "event: completion",
2412 'data: {"type":"completion","completion":"Hello "}',
2413 "",
2414 "event: completion",
2415 'data: {"type":"completion","completion":"world"}',
2416 "",
2417 "event: completion",
2418 'data: {"type":"completion","completion":"","id":"chatcompl_01-buffered","stop":"\\n\\nHuman:","log_id":"log_01-buffered","messageLimit":{"type":"within_limit","resetsAt":"2026-03-28T00:00:00.000Z"}}',
2419 ""
2420 ].join("\n")
2421 });
2422 }
2423
2424 if (input.path === "/backend-api/conversation-buffered-smoke") {
2425 return buildApiResponse({
2426 id: input.id,
2427 body: [
2428 "event: message",
2429 'data: {"message":{"id":"msg-chatgpt-buffered-1","author":{"role":"assistant"},"content":{"content_type":"text","parts":["Buffered ChatGPT answer"]}}}',
2430 ""
2431 ].join("\n")
2432 });
2433 }
2434
2435 if (input.path === "/backend-api/conversation") {
2436 return buildApiResponse({
2437 id: input.id,
2438 body: {
2439 conversation_id: "conv-chatgpt-send",
2440 message: {
2441 id: "msg-chatgpt-send"
2442 },
2443 accepted: true
2444 },
2445 status: 202
2446 });
2447 }
2448
2449 if (input.path === "/backend-api/conversation/conv-chatgpt-current") {
2450 return buildApiResponse({
2451 id: input.id,
2452 body: {
2453 conversation_id: "conv-chatgpt-current",
2454 title: "Current ChatGPT Chat"
2455 }
2456 });
2457 }
2458
2459 if (input.path === "/backend-api/models") {
2460 return buildApiResponse({
2461 id: input.id,
2462 body: {
2463 models: [
2464 {
2465 slug: "gpt-5.4"
2466 }
2467 ]
2468 }
2469 });
2470 }
2471
2472 if (
2473 input.path
2474 === "/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
2475 ) {
2476 return buildApiResponse({
2477 id: input.id,
2478 body: {
2479 conversation_id: "conv-gemini-current",
2480 accepted: true
2481 },
2482 status: 202
2483 });
2484 }
2485
2486 throw new Error(`unexpected browser proxy path: ${input.path}`);
2487 },
2488 cancelApiRequest(input = {}) {
2489 calls.push({
2490 ...input,
2491 kind: "cancelApiRequest"
2492 });
2493
2494 return {
2495 clientId: input.clientId || "firefox-claude",
2496 connectionId: "conn-firefox-claude",
2497 dispatchedAt: 1710000004500,
2498 reason: input.reason || null,
2499 requestId: input.requestId || "browser-cancel",
2500 streamId: input.streamId || null,
2501 type: "request_cancel"
2502 };
2503 },
2504 dispatchPluginAction(input = {}) {
2505 calls.push({
2506 ...input,
2507 kind: "dispatchPluginAction"
2508 });
2509
2510 return buildActionDispatch({
2511 action: input.action || "plugin_status",
2512 clientId: input.clientId || "firefox-claude",
2513 dispatchedAt: 1710000004750,
2514 platform: input.platform || null,
2515 reason: input.reason || null,
2516 type: input.action || "plugin_status"
2517 });
2518 },
2519 openTab(input = {}) {
2520 calls.push({
2521 ...input,
2522 kind: "openTab"
2523 });
2524
2525 return buildActionDispatch({
2526 action: "tab_open",
2527 clientId: input.clientId || "firefox-claude",
2528 dispatchedAt: 1710000005000,
2529 platform: input.platform || null,
2530 type: "open_tab"
2531 });
2532 },
2533 reload(input = {}) {
2534 calls.push({
2535 ...input,
2536 kind: "reload"
2537 });
2538
2539 return buildActionDispatch({
2540 action: input.platform ? "tab_reload" : "controller_reload",
2541 clientId: input.clientId || "firefox-claude",
2542 dispatchedAt: 1710000006000,
2543 platform: input.platform || null,
2544 reason: input.reason || null,
2545 type: "reload"
2546 });
2547 },
2548 requestCredentials(input = {}) {
2549 calls.push({
2550 ...input,
2551 kind: "requestCredentials"
2552 });
2553
2554 return buildActionDispatch({
2555 action: "request_credentials",
2556 clientId: input.clientId || "firefox-claude",
2557 dispatchedAt: 1710000007000,
2558 platform: input.platform || null,
2559 reason: input.reason || null,
2560 type: "request_credentials"
2561 });
2562 },
2563 streamRequest(input) {
2564 calls.push({
2565 ...input,
2566 kind: "streamRequest"
2567 });
2568
2569 return buildApiStream({
2570 input,
2571 events: [
2572 {
2573 clientId: input.clientId || "firefox-claude",
2574 connectionId: "conn-firefox-claude",
2575 meta: {
2576 path: input.path
2577 },
2578 openedAt: 1710000008000,
2579 requestId: input.id || "browser-stream-1",
2580 status: 200,
2581 streamId: input.streamId || input.id || "browser-stream-1",
2582 type: "stream_open"
2583 },
2584 {
2585 clientId: input.clientId || "firefox-claude",
2586 connectionId: "conn-firefox-claude",
2587 data: {
2588 type: "content_block_delta",
2589 delta: {
2590 type: "text_delta",
2591 text: "hello from claude stream"
2592 }
2593 },
2594 event: "message",
2595 raw: 'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"hello from claude stream"}}',
2596 receivedAt: 1710000008001,
2597 requestId: input.id || "browser-stream-1",
2598 seq: 1,
2599 streamId: input.streamId || input.id || "browser-stream-1",
2600 type: "stream_event"
2601 },
2602 {
2603 clientId: input.clientId || "firefox-claude",
2604 connectionId: "conn-firefox-claude",
2605 endedAt: 1710000008002,
2606 partial: {
2607 buffered_bytes: 120,
2608 event_count: 1,
2609 last_seq: 1,
2610 opened: true
2611 },
2612 requestId: input.id || "browser-stream-1",
2613 status: 200,
2614 streamId: input.streamId || input.id || "browser-stream-1",
2615 type: "stream_end"
2616 }
2617 ]
2618 });
2619 }
2620 },
2621 browserStateLoader: () => browserState
2622 }
2623 };
2624}
2625
2626async function withMockedPlatform(platform, callback) {
2627 const descriptor = Object.getOwnPropertyDescriptor(process, "platform");
2628
2629 assert.ok(descriptor);
2630
2631 Object.defineProperty(process, "platform", {
2632 configurable: true,
2633 enumerable: descriptor.enumerable ?? true,
2634 value: platform
2635 });
2636
2637 try {
2638 return await callback();
2639 } finally {
2640 Object.defineProperty(process, "platform", descriptor);
2641 }
2642}
2643
2644function assertEmptyExecResultShape(result) {
2645 assert.equal(result.stdout, "");
2646 assert.equal(result.stderr, "");
2647 assert.equal(result.exitCode, null);
2648 assert.equal(result.signal, null);
2649 assert.equal(result.durationMs, 0);
2650 assert.equal(typeof result.startedAt, "string");
2651 assert.equal(typeof result.finishedAt, "string");
2652 assert.equal(result.timedOut, false);
2653}
2654
2655function createWebSocketMessageQueue(socket) {
2656 const messages = [];
2657 const waiters = [];
2658
2659 const onMessage = (event) => {
2660 let payload = null;
2661
2662 try {
2663 payload = JSON.parse(event.data);
2664 } catch {
2665 return;
2666 }
2667
2668 const waiterIndex = waiters.findIndex((waiter) => waiter.predicate(payload));
2669
2670 if (waiterIndex >= 0) {
2671 const [waiter] = waiters.splice(waiterIndex, 1);
2672
2673 if (waiter) {
2674 clearTimeout(waiter.timer);
2675 waiter.resolve(payload);
2676 }
2677
2678 return;
2679 }
2680
2681 messages.push(payload);
2682 };
2683
2684 const onClose = () => {
2685 while (waiters.length > 0) {
2686 const waiter = waiters.shift();
2687
2688 if (waiter) {
2689 clearTimeout(waiter.timer);
2690 waiter.reject(new Error("websocket closed before the expected message arrived"));
2691 }
2692 }
2693 };
2694
2695 socket.addEventListener("message", onMessage);
2696 socket.addEventListener("close", onClose);
2697
2698 return {
2699 async next(predicate, timeoutMs = 5_000) {
2700 const existingIndex = messages.findIndex((message) => predicate(message));
2701
2702 if (existingIndex >= 0) {
2703 const [message] = messages.splice(existingIndex, 1);
2704 return message;
2705 }
2706
2707 return await new Promise((resolve, reject) => {
2708 const timer = setTimeout(() => {
2709 const waiterIndex = waiters.findIndex((waiter) => waiter.timer === timer);
2710
2711 if (waiterIndex >= 0) {
2712 waiters.splice(waiterIndex, 1);
2713 }
2714
2715 reject(new Error("timed out waiting for websocket message"));
2716 }, timeoutMs);
2717
2718 waiters.push({
2719 predicate,
2720 reject,
2721 resolve,
2722 timer
2723 });
2724 });
2725 },
2726 stop() {
2727 socket.removeEventListener("message", onMessage);
2728 socket.removeEventListener("close", onClose);
2729 onClose();
2730 }
2731 };
2732}
2733
2734async function waitForWebSocketOpen(socket) {
2735 if (socket.readyState === WebSocket.OPEN) {
2736 return;
2737 }
2738
2739 await new Promise((resolve, reject) => {
2740 const onOpen = () => {
2741 socket.removeEventListener("error", onError);
2742 resolve();
2743 };
2744 const onError = () => {
2745 socket.removeEventListener("open", onOpen);
2746 reject(new Error("websocket failed to open"));
2747 };
2748
2749 socket.addEventListener("open", onOpen, {
2750 once: true
2751 });
2752 socket.addEventListener("error", onError, {
2753 once: true
2754 });
2755 });
2756}
2757
2758async function waitForWebSocketClose(socket) {
2759 if (socket.readyState === WebSocket.CLOSED) {
2760 return {
2761 code: null,
2762 reason: null
2763 };
2764 }
2765
2766 return await new Promise((resolve) => {
2767 socket.addEventListener("close", (event) => {
2768 resolve({
2769 code: event.code ?? null,
2770 reason: event.reason ?? null
2771 });
2772 }, {
2773 once: true
2774 });
2775 });
2776}
2777
2778async function waitForCondition(assertion, timeoutMs = 2_000, intervalMs = 50) {
2779 const deadline = Date.now() + timeoutMs;
2780 let lastError = null;
2781
2782 while (Date.now() < deadline) {
2783 try {
2784 return await assertion();
2785 } catch (error) {
2786 lastError = error;
2787 await new Promise((resolve) => setTimeout(resolve, intervalMs));
2788 }
2789 }
2790
2791 throw lastError ?? new Error("timed out waiting for condition");
2792}
2793
2794async function expectQueueTimeout(queue, predicate, timeoutMs = 400) {
2795 await assert.rejects(
2796 () => queue.next(predicate, timeoutMs),
2797 /timed out waiting for websocket message/u
2798 );
2799}
2800
2801class MockWritableResponse extends EventEmitter {
2802 constructor(onWrite) {
2803 super();
2804 this.destroyed = false;
2805 this.endCalls = [];
2806 this.headers = {};
2807 this.onWrite = onWrite;
2808 this.statusCode = 200;
2809 this.writableEnded = false;
2810 this.writeCalls = [];
2811 }
2812
2813 setHeader(name, value) {
2814 this.headers[name] = value;
2815 }
2816
2817 write(chunk) {
2818 this.writeCalls.push(chunk);
2819 return this.onWrite(chunk, this.writeCalls.length, this);
2820 }
2821
2822 end(chunk) {
2823 if (chunk !== undefined) {
2824 this.endCalls.push(chunk);
2825 }
2826
2827 this.writableEnded = true;
2828 }
2829}
2830
2831async function assertSettlesWithin(promise, timeoutMs = 250) {
2832 let timeoutId = null;
2833
2834 try {
2835 return await Promise.race([
2836 promise,
2837 new Promise((_, reject) => {
2838 timeoutId = setTimeout(() => {
2839 reject(new Error(`expected promise to settle within ${timeoutMs}ms`));
2840 }, timeoutMs);
2841 })
2842 ]);
2843 } finally {
2844 if (timeoutId != null) {
2845 clearTimeout(timeoutId);
2846 }
2847 }
2848}
2849
2850async function fetchJson(url, init) {
2851 const response = await fetch(url, init);
2852 const text = await response.text();
2853
2854 return {
2855 payload: text === "" ? null : JSON.parse(text),
2856 response,
2857 text
2858 };
2859}
2860
2861async function connectBrowserBridgeClient(wsUrl, clientId, nodePlatform = "firefox") {
2862 const socket = new WebSocket(wsUrl);
2863 const queue = createWebSocketMessageQueue(socket);
2864
2865 await waitForWebSocketOpen(socket);
2866 socket.send(
2867 JSON.stringify({
2868 type: "hello",
2869 clientId,
2870 nodeType: "browser",
2871 nodeCategory: "proxy",
2872 nodePlatform
2873 })
2874 );
2875
2876 const helloAck = await queue.next(
2877 (message) => message.type === "hello_ack" && message.clientId === clientId
2878 );
2879 const initialSnapshot = await queue.next(
2880 (message) => message.type === "state_snapshot" && message.reason === "hello"
2881 );
2882 const credentialRequest = await queue.next(
2883 (message) => message.type === "request_credentials" && message.reason === "hello"
2884 );
2885
2886 return {
2887 credentialRequest,
2888 helloAck,
2889 initialSnapshot,
2890 queue,
2891 socket
2892 };
2893}
2894
2895async function connectFirefoxBridgeClient(wsUrl, clientId) {
2896 return await connectBrowserBridgeClient(wsUrl, clientId, "firefox");
2897}
2898
2899test("writeHttpResponse stops waiting for body backpressure when the client closes", async () => {
2900 const response = new MockWritableResponse((_chunk, writeCount, writableResponse) => {
2901 assert.equal(writeCount, 1);
2902 queueMicrotask(() => {
2903 writableResponse.destroyed = true;
2904 writableResponse.emit("close");
2905 });
2906
2907 return false;
2908 });
2909
2910 await assertSettlesWithin(
2911 writeHttpResponse(response, {
2912 body: "preface",
2913 headers: {
2914 "content-type": "text/plain; charset=utf-8"
2915 },
2916 status: 200,
2917 streamBody: (async function* () {
2918 yield "tail";
2919 })()
2920 })
2921 );
2922
2923 assert.deepEqual(response.writeCalls, ["preface"]);
2924 assert.equal(response.writableEnded, false);
2925 assert.equal(response.listenerCount("drain"), 0);
2926 assert.equal(response.listenerCount("close"), 0);
2927 assert.equal(response.listenerCount("error"), 0);
2928});
2929
2930test("writeHttpResponse stops waiting for stream backpressure when the client closes", async () => {
2931 const response = new MockWritableResponse((_chunk, writeCount, writableResponse) => {
2932 assert.equal(writeCount, 1);
2933 queueMicrotask(() => {
2934 writableResponse.destroyed = true;
2935 writableResponse.emit("close");
2936 });
2937
2938 return false;
2939 });
2940
2941 await assertSettlesWithin(
2942 writeHttpResponse(response, {
2943 body: "",
2944 headers: {
2945 "content-type": "text/event-stream; charset=utf-8"
2946 },
2947 status: 200,
2948 streamBody: (async function* () {
2949 yield "chunk-1";
2950 yield "chunk-2";
2951 })()
2952 })
2953 );
2954
2955 assert.deepEqual(response.writeCalls, ["chunk-1"]);
2956 assert.equal(response.writableEnded, false);
2957 assert.equal(response.listenerCount("drain"), 0);
2958 assert.equal(response.listenerCount("close"), 0);
2959 assert.equal(response.listenerCount("error"), 0);
2960});
2961
2962test("writeHttpResponse continues streaming after drain and finishes the response", async () => {
2963 const response = new MockWritableResponse((_chunk, writeCount, writableResponse) => {
2964 if (writeCount === 1) {
2965 queueMicrotask(() => {
2966 writableResponse.emit("drain");
2967 });
2968
2969 return false;
2970 }
2971
2972 return true;
2973 });
2974
2975 await assertSettlesWithin(
2976 writeHttpResponse(response, {
2977 body: "preface",
2978 headers: {
2979 "content-type": "text/event-stream; charset=utf-8"
2980 },
2981 status: 200,
2982 streamBody: (async function* () {
2983 yield "chunk-1";
2984 yield "chunk-2";
2985 })()
2986 })
2987 );
2988
2989 assert.deepEqual(response.writeCalls, ["preface", "chunk-1", "chunk-2"]);
2990 assert.equal(response.writableEnded, true);
2991 assert.equal(response.listenerCount("drain"), 0);
2992 assert.equal(response.listenerCount("close"), 0);
2993 assert.equal(response.listenerCount("error"), 0);
2994});
2995
2996async function withRuntimeFixture(callback) {
2997 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-fixture-"));
2998 const runtime = new ConductorRuntime(
2999 {
3000 nodeId: "mini-main",
3001 host: "mini",
3002 role: "primary",
3003 controlApiBase: "https://control.example.test",
3004 localApiBase: "http://127.0.0.1:0",
3005 sharedToken: "replace-me",
3006 paths: {
3007 runsDir: "/tmp/runs",
3008 stateDir
3009 }
3010 },
3011 {
3012 autoStartLoops: false,
3013 now: () => 100
3014 }
3015 );
3016
3017 try {
3018 return await callback({
3019 runtime,
3020 stateDir
3021 });
3022 } finally {
3023 await runtime.stop();
3024 rmSync(stateDir, {
3025 force: true,
3026 recursive: true
3027 });
3028 }
3029}
3030
3031async function assertLocalApiListenerClosed(baseUrl) {
3032 const { hostname, port } = new URL(baseUrl);
3033
3034 await new Promise((resolve, reject) => {
3035 const socket = createConnection({
3036 host: hostname,
3037 port: Number(port)
3038 });
3039
3040 socket.once("connect", () => {
3041 socket.destroy();
3042 reject(new Error(`expected local API listener to be closed: ${baseUrl}`));
3043 });
3044 socket.once("error", (error) => {
3045 socket.destroy();
3046
3047 if (error && typeof error === "object" && "code" in error && error.code === "ECONNREFUSED") {
3048 resolve();
3049 return;
3050 }
3051
3052 reject(error);
3053 });
3054 });
3055}
3056
3057test("start enters leader state and allows scheduler work only for the lease holder", async () => {
3058 const heartbeatRequests = [];
3059 const leaseRequests = [];
3060 let currentNow = 100;
3061 const daemon = new ConductorDaemon(
3062 {
3063 nodeId: "mini-main",
3064 host: "mini",
3065 role: "primary",
3066 controlApiBase: "https://control.example.test"
3067 },
3068 {
3069 autoStartLoops: false,
3070 client: {
3071 async acquireLeaderLease(request) {
3072 leaseRequests.push(request);
3073
3074 return createLeaseResult({
3075 holderId: "mini-main",
3076 term: 4,
3077 leaseExpiresAt: 130,
3078 renewedAt: 100,
3079 isLeader: true,
3080 operation: "acquire"
3081 });
3082 },
3083 async sendControllerHeartbeat(request) {
3084 heartbeatRequests.push(request);
3085 }
3086 },
3087 now: () => currentNow
3088 }
3089 );
3090
3091 const state = await daemon.start();
3092 assert.equal(state, "leader");
3093 assert.equal(heartbeatRequests.length, 1);
3094 assert.equal(leaseRequests.length, 1);
3095 assert.equal(daemon.canSchedule(), true);
3096 assert.equal(daemon.getNextLeaseOperation(), "renew");
3097
3098 let executed = false;
3099 const decision = await daemon.runSchedulerPass(async () => {
3100 executed = true;
3101 });
3102
3103 assert.equal(decision, "scheduled");
3104 assert.equal(executed, true);
3105
3106 currentNow = 131;
3107 assert.equal(daemon.canSchedule(), false);
3108});
3109
3110test("standby responses keep scheduler disabled", async () => {
3111 const daemon = new ConductorDaemon(
3112 {
3113 nodeId: "mac-standby",
3114 host: "mac",
3115 role: "standby",
3116 controlApiBase: "https://control.example.test"
3117 },
3118 {
3119 autoStartLoops: false,
3120 client: {
3121 async acquireLeaderLease() {
3122 return createLeaseResult({
3123 holderId: "mini-main",
3124 term: 7,
3125 leaseExpiresAt: 230,
3126 renewedAt: 200,
3127 isLeader: false,
3128 operation: "acquire"
3129 });
3130 },
3131 async sendControllerHeartbeat() {}
3132 },
3133 now: () => 200
3134 }
3135 );
3136
3137 const state = await daemon.start();
3138 assert.equal(state, "standby");
3139 assert.equal(daemon.canSchedule(), false);
3140 assert.equal(daemon.getStatusSnapshot().schedulerEnabled, false);
3141
3142 let executed = false;
3143 const decision = await daemon.runSchedulerPass(async () => {
3144 executed = true;
3145 });
3146
3147 assert.equal(decision, "skipped_not_leader");
3148 assert.equal(executed, false);
3149});
3150
3151test("repeated renew failures degrade the daemon after the configured threshold", async () => {
3152 const calls = [];
3153 let currentNow = 100;
3154 const daemon = new ConductorDaemon(
3155 {
3156 nodeId: "mini-main",
3157 host: "mini",
3158 role: "primary",
3159 controlApiBase: "https://control.example.test",
3160 renewFailureThreshold: 2
3161 },
3162 {
3163 autoStartLoops: false,
3164 client: {
3165 async acquireLeaderLease(request) {
3166 calls.push(request);
3167
3168 if (calls.length === 1) {
3169 return createLeaseResult({
3170 holderId: "mini-main",
3171 term: 1,
3172 leaseExpiresAt: 130,
3173 renewedAt: 100,
3174 isLeader: true,
3175 operation: "acquire"
3176 });
3177 }
3178
3179 throw new Error("lease endpoint timeout");
3180 },
3181 async sendControllerHeartbeat() {}
3182 },
3183 now: () => currentNow
3184 }
3185 );
3186
3187 await daemon.start();
3188 assert.equal(daemon.getStatusSnapshot().leaseState, "leader");
3189
3190 currentNow = 110;
3191 await assert.rejects(() => daemon.runLeaseCycle(), /lease endpoint timeout/);
3192 assert.equal(daemon.getStatusSnapshot().leaseState, "leader");
3193 assert.equal(daemon.getStatusSnapshot().consecutiveRenewFailures, 1);
3194
3195 currentNow = 115;
3196 await assert.rejects(() => daemon.runLeaseCycle(), /lease endpoint timeout/);
3197 assert.equal(daemon.getStatusSnapshot().leaseState, "degraded");
3198 assert.equal(daemon.getStatusSnapshot().consecutiveRenewFailures, 2);
3199});
3200
3201test("parseConductorCliRequest reads timed-jobs defaults from env and allows CLI overrides", () => {
3202 const request = parseConductorCliRequest(
3203 [
3204 "start",
3205 "--run-once",
3206 "--timed-jobs-interval-ms",
3207 "6000",
3208 "--timed-jobs-max-tasks-per-tick",
3209 "9"
3210 ],
3211 {
3212 BAA_NODE_ID: "mini-main",
3213 BAA_CONDUCTOR_HOST: "mini",
3214 BAA_CONDUCTOR_ROLE: "primary",
3215 BAA_CONDUCTOR_PUBLIC_API_BASE: "https://public.example.test/",
3216 BAA_SHARED_TOKEN: "replace-me",
3217 BAA_TIMED_JOBS_INTERVAL_MS: "10000",
3218 BAA_TIMED_JOBS_MAX_MESSAGES_PER_TICK: "11",
3219 BAA_TIMED_JOBS_MAX_TASKS_PER_TICK: "12",
3220 BAA_TIMED_JOBS_SETTLE_DELAY_MS: "15000"
3221 }
3222 );
3223
3224 assert.equal(request.action, "start");
3225
3226 if (request.action !== "start") {
3227 throw new Error("expected start action");
3228 }
3229
3230 assert.equal(request.config.timedJobsIntervalMs, 6000);
3231 assert.equal(request.config.timedJobsMaxMessagesPerTick, 11);
3232 assert.equal(request.config.timedJobsMaxTasksPerTick, 9);
3233 assert.equal(request.config.timedJobsSettleDelayMs, 15000);
3234});
3235
3236test("ConductorTimedJobs runs registered runners on leader ticks and writes JSONL logs", async () => {
3237 const logsDir = mkdtempSync(join(tmpdir(), "baa-timed-jobs-leader-"));
3238 const daemon = new ConductorDaemon(
3239 {
3240 nodeId: "mini-main",
3241 host: "mini",
3242 role: "primary",
3243 controlApiBase: "https://control.example.test"
3244 },
3245 {
3246 autoStartLoops: false,
3247 client: {
3248 async acquireLeaderLease() {
3249 return createLeaseResult({
3250 holderId: "mini-main",
3251 term: 4,
3252 leaseExpiresAt: 130,
3253 renewedAt: 100,
3254 isLeader: true,
3255 operation: "acquire"
3256 });
3257 },
3258 async sendControllerHeartbeat() {}
3259 },
3260 now: () => 100
3261 }
3262 );
3263 const observedRuns = [];
3264 const timedJobs = new ConductorTimedJobs(
3265 {
3266 intervalMs: 5_000,
3267 maxMessagesPerTick: 10,
3268 maxTasksPerTick: 8,
3269 settleDelayMs: 12_000
3270 },
3271 {
3272 autoStart: false,
3273 logDir: logsDir,
3274 schedule: (work) => daemon.runSchedulerPass(work)
3275 }
3276 );
3277
3278 timedJobs.registerRunner({
3279 name: "renewal.projector",
3280 async run(context) {
3281 observedRuns.push({
3282 batchId: context.batchId,
3283 maxMessagesPerTick: context.maxMessagesPerTick,
3284 settleDelayMs: context.settleDelayMs,
3285 term: context.term
3286 });
3287 context.log({
3288 stage: "scan_window",
3289 result: "ok",
3290 details: {
3291 cursor: "message:1"
3292 }
3293 });
3294 return {
3295 result: "ok",
3296 details: {
3297 projected_messages: 0
3298 }
3299 };
3300 }
3301 });
3302
3303 try {
3304 await daemon.start();
3305 await timedJobs.start();
3306
3307 const tick = await timedJobs.runTick("manual");
3308
3309 assert.equal(tick.decision, "scheduled");
3310 assert.equal(observedRuns.length, 1);
3311 assert.equal(observedRuns[0].maxMessagesPerTick, 10);
3312 assert.equal(observedRuns[0].settleDelayMs, 12_000);
3313 assert.equal(observedRuns[0].term, 4);
3314
3315 await timedJobs.stop();
3316
3317 const entries = await waitForJsonlEntries(
3318 logsDir,
3319 (items) => items.some((entry) => entry.runner === "renewal.projector" && entry.stage === "job_projected")
3320 );
3321 assert.ok(
3322 entries.find(
3323 (entry) =>
3324 entry.runner === "renewal.projector"
3325 && entry.stage === "runner_started"
3326 && entry.result === "running"
3327 )
3328 );
3329 assert.ok(
3330 entries.find(
3331 (entry) =>
3332 entry.runner === "renewal.projector"
3333 && entry.stage === "scan_window"
3334 && entry.cursor === "message:1"
3335 )
3336 );
3337
3338 const completedEntry = entries.find(
3339 (entry) => entry.runner === "renewal.projector" && entry.stage === "runner_completed"
3340 );
3341 assert.ok(completedEntry);
3342 assert.equal(completedEntry.result, "ok");
3343 assert.equal(typeof completedEntry.batch_id, "string");
3344 assert.equal(typeof completedEntry.duration_ms, "number");
3345 assert.equal(completedEntry.error, null);
3346 } finally {
3347 await timedJobs.stop();
3348 rmSync(logsDir, {
3349 force: true,
3350 recursive: true
3351 });
3352 }
3353});
3354
3355test("ConductorTimedJobs does not block ticks on pending async log writes and drains them on stop", async () => {
3356 const logsDir = mkdtempSync(join(tmpdir(), "baa-timed-jobs-async-log-"));
3357 const pendingWrites = [];
3358 const timedJobs = new ConductorTimedJobs(
3359 {
3360 intervalMs: 5_000,
3361 maxMessagesPerTick: 10,
3362 maxTasksPerTick: 8,
3363 settleDelayMs: 12_000
3364 },
3365 {
3366 appendFileImpl: async (filePath, data) => {
3367 await new Promise((resolve) => {
3368 pendingWrites.push(() => {
3369 writeFileSync(filePath, data, {
3370 flag: "a"
3371 });
3372 resolve();
3373 });
3374 });
3375 },
3376 autoStart: false,
3377 logDir: logsDir,
3378 schedule: async (work) => {
3379 await work({
3380 controllerId: "mini-main",
3381 host: "mini",
3382 term: 4
3383 });
3384 return "scheduled";
3385 }
3386 }
3387 );
3388 let stopCompleted = false;
3389
3390 timedJobs.registerRunner({
3391 name: "renewal.projector",
3392 async run(context) {
3393 context.log({
3394 stage: "scan_window",
3395 result: "ok",
3396 details: {
3397 cursor: "message:1"
3398 }
3399 });
3400
3401 return {
3402 result: "ok"
3403 };
3404 }
3405 });
3406
3407 try {
3408 await timedJobs.start();
3409
3410 const tick = await Promise.race([
3411 timedJobs.runTick("manual"),
3412 new Promise((_, reject) => {
3413 setTimeout(() => reject(new Error("timed out waiting for timed-jobs tick")), 1_000);
3414 })
3415 ]);
3416
3417 assert.equal(tick.decision, "scheduled");
3418 assert.ok(pendingWrites.length > 0);
3419
3420 const stopPromise = timedJobs.stop();
3421 void stopPromise.then(() => {
3422 stopCompleted = true;
3423 });
3424
3425 await flushAsyncWork();
3426 assert.equal(stopCompleted, false);
3427
3428 while (!stopCompleted) {
3429 const releaseWrite = pendingWrites.shift();
3430
3431 if (releaseWrite == null) {
3432 await flushAsyncWork();
3433 continue;
3434 }
3435
3436 releaseWrite();
3437 await flushAsyncWork();
3438 }
3439
3440 await stopPromise;
3441
3442 const entries = await waitForJsonlEntries(
3443 logsDir,
3444 (items) => items.some((entry) => entry.runner === "timed-jobs.framework" && entry.stage === "stopped")
3445 );
3446 assert.ok(
3447 entries.find(
3448 (entry) => entry.runner === "timed-jobs.framework" && entry.stage === "started"
3449 )
3450 );
3451 assert.ok(
3452 entries.find(
3453 (entry) => entry.runner === "renewal.projector" && entry.stage === "scan_window"
3454 )
3455 );
3456 assert.ok(
3457 entries.find(
3458 (entry) => entry.runner === "timed-jobs.framework" && entry.stage === "stopped"
3459 )
3460 );
3461 } finally {
3462 while (pendingWrites.length > 0) {
3463 pendingWrites.shift()();
3464 await flushAsyncWork();
3465 }
3466
3467 await timedJobs.stop();
3468 rmSync(logsDir, {
3469 force: true,
3470 recursive: true
3471 });
3472 }
3473});
3474
3475test("renewal projector scans settled messages with cursor semantics and skips ineligible conversations", async () => {
3476 const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-projector-"));
3477 const logsDir = mkdtempSync(join(tmpdir(), "baa-renewal-projector-logs-"));
3478 const controlPlane = new ConductorLocalControlPlane({
3479 databasePath: join(rootDir, "control-plane.db")
3480 });
3481 await controlPlane.initialize();
3482 const repository = controlPlane.repository;
3483 const artifactStore = new ArtifactStore({
3484 artifactDir: join(rootDir, "artifacts"),
3485 databasePath: join(rootDir, "artifact.db"),
3486 publicBaseUrl: "https://conductor.makefile.so"
3487 });
3488 const nowMs = Date.UTC(2026, 2, 30, 10, 0, 0);
3489 let timedJobs = null;
3490
3491 try {
3492 await artifactStore.upsertLocalConversation({
3493 automationStatus: "manual",
3494 localConversationId: "lc_manual",
3495 platform: "claude"
3496 });
3497 await artifactStore.upsertConversationLink({
3498 clientId: "firefox-manual",
3499 linkId: "link_manual",
3500 localConversationId: "lc_manual",
3501 observedAt: nowMs - 9_000,
3502 pageUrl: "https://claude.ai/chat/conv_manual",
3503 platform: "claude",
3504 remoteConversationId: "conv_manual",
3505 targetKind: "browser.proxy_delivery",
3506 targetPayload: {
3507 clientId: "firefox-manual",
3508 tabId: 1
3509 }
3510 });
3511 const manualMessage = await artifactStore.insertMessage({
3512 conversationId: "conv_manual",
3513 id: "msg_manual",
3514 observedAt: nowMs - 9_000,
3515 platform: "claude",
3516 rawText: "manual conversation should not project",
3517 role: "assistant"
3518 });
3519
3520 await artifactStore.upsertLocalConversation({
3521 automationStatus: "paused",
3522 localConversationId: "lc_paused",
3523 pausedAt: nowMs - 8_000,
3524 platform: "claude"
3525 });
3526 await artifactStore.upsertConversationLink({
3527 clientId: "firefox-paused",
3528 linkId: "link_paused",
3529 localConversationId: "lc_paused",
3530 observedAt: nowMs - 8_000,
3531 pageUrl: "https://claude.ai/chat/conv_paused",
3532 platform: "claude",
3533 remoteConversationId: "conv_paused",
3534 targetKind: "browser.proxy_delivery",
3535 targetPayload: {
3536 clientId: "firefox-paused",
3537 tabId: 2
3538 }
3539 });
3540 const pausedMessage = await artifactStore.insertMessage({
3541 conversationId: "conv_paused",
3542 id: "msg_paused",
3543 observedAt: nowMs - 8_000,
3544 platform: "claude",
3545 rawText: "paused conversation should not project",
3546 role: "assistant"
3547 });
3548
3549 await artifactStore.upsertLocalConversation({
3550 automationStatus: "auto",
3551 cooldownUntil: nowMs + 30_000,
3552 localConversationId: "lc_cooldown",
3553 platform: "claude"
3554 });
3555 await artifactStore.upsertConversationLink({
3556 clientId: "firefox-cooldown",
3557 linkId: "link_cooldown",
3558 localConversationId: "lc_cooldown",
3559 observedAt: nowMs - 7_000,
3560 pageUrl: "https://claude.ai/chat/conv_cooldown",
3561 platform: "claude",
3562 remoteConversationId: "conv_cooldown",
3563 targetKind: "browser.proxy_delivery",
3564 targetPayload: {
3565 clientId: "firefox-cooldown",
3566 tabId: 3
3567 }
3568 });
3569 const cooldownMessage = await artifactStore.insertMessage({
3570 conversationId: "conv_cooldown",
3571 id: "msg_cooldown",
3572 observedAt: nowMs - 7_000,
3573 platform: "claude",
3574 rawText: "cooldown conversation should not project",
3575 role: "assistant"
3576 });
3577
3578 await artifactStore.upsertLocalConversation({
3579 automationStatus: "auto",
3580 localConversationId: "lc_auto",
3581 platform: "claude"
3582 });
3583 await artifactStore.upsertConversationLink({
3584 clientId: "firefox-auto",
3585 linkId: "link_auto",
3586 localConversationId: "lc_auto",
3587 observedAt: nowMs - 6_000,
3588 pageTitle: "Claude Auto",
3589 pageUrl: "https://claude.ai/chat/conv_auto",
3590 platform: "claude",
3591 remoteConversationId: "conv_auto",
3592 routeParams: {
3593 conversationId: "conv_auto"
3594 },
3595 routePath: "/chat/conv_auto",
3596 routePattern: "/chat/:conversationId",
3597 targetId: "client:firefox-auto",
3598 targetKind: "browser.proxy_delivery",
3599 targetPayload: {
3600 clientId: "firefox-auto",
3601 tabId: 4
3602 }
3603 });
3604 const autoMessage = await artifactStore.insertMessage({
3605 conversationId: "conv_auto",
3606 id: "msg_auto",
3607 observedAt: nowMs - 6_000,
3608 platform: "claude",
3609 rawText: "eligible auto conversation should project",
3610 role: "assistant"
3611 });
3612
3613 await artifactStore.upsertLocalConversation({
3614 automationStatus: "auto",
3615 localConversationId: "lc_non_proxy_target",
3616 platform: "claude"
3617 });
3618 await artifactStore.upsertConversationLink({
3619 clientId: "firefox-non-proxy-target",
3620 linkId: "link_non_proxy_target",
3621 localConversationId: "lc_non_proxy_target",
3622 observedAt: nowMs - 5_000,
3623 pageUrl: "https://claude.ai/chat/conv_non_proxy_target",
3624 platform: "claude",
3625 remoteConversationId: "conv_non_proxy_target",
3626 targetId: "tab:5",
3627 targetKind: "browser.shell_page",
3628 targetPayload: {
3629 clientId: "firefox-non-proxy-target",
3630 tabId: 5
3631 }
3632 });
3633 const nonProxyTargetMessage = await artifactStore.insertMessage({
3634 conversationId: "conv_non_proxy_target",
3635 id: "msg_non_proxy_target",
3636 observedAt: nowMs - 5_000,
3637 platform: "claude",
3638 rawText: "non proxy delivery target should not project",
3639 role: "assistant"
3640 });
3641
3642 await artifactStore.upsertLocalConversation({
3643 automationStatus: "auto",
3644 localConversationId: "lc_shell_page",
3645 platform: "claude"
3646 });
3647 await artifactStore.upsertConversationLink({
3648 clientId: "firefox-shell-page",
3649 linkId: "link_shell_page",
3650 localConversationId: "lc_shell_page",
3651 observedAt: nowMs - 4_000,
3652 pageUrl: "https://claude.ai/chat/conv_shell_page",
3653 platform: "claude",
3654 remoteConversationId: "conv_shell_page",
3655 targetId: "tab:6",
3656 targetKind: "browser.proxy_delivery",
3657 targetPayload: {
3658 clientId: "firefox-shell-page",
3659 shellPage: true,
3660 tabId: 6
3661 }
3662 });
3663 const shellPageMessage = await artifactStore.insertMessage({
3664 conversationId: "conv_shell_page",
3665 id: "msg_shell_page",
3666 observedAt: nowMs - 4_000,
3667 platform: "claude",
3668 rawText: "shell page route should not project",
3669 role: "assistant"
3670 });
3671
3672 await artifactStore.upsertLocalConversation({
3673 automationStatus: "auto",
3674 localConversationId: "lc_missing_tab_id",
3675 platform: "claude"
3676 });
3677 await artifactStore.upsertConversationLink({
3678 clientId: "firefox-missing-tab-id",
3679 linkId: "link_missing_tab_id",
3680 localConversationId: "lc_missing_tab_id",
3681 observedAt: nowMs - 3_000,
3682 pageUrl: "https://claude.ai/chat/conv_missing_tab_id",
3683 platform: "claude",
3684 remoteConversationId: "conv_missing_tab_id",
3685 targetKind: "browser.proxy_delivery",
3686 targetPayload: {
3687 clientId: "firefox-missing-tab-id"
3688 }
3689 });
3690 const missingTabIdMessage = await artifactStore.insertMessage({
3691 conversationId: "conv_missing_tab_id",
3692 id: "msg_missing_tab_id",
3693 observedAt: nowMs - 3_000,
3694 platform: "claude",
3695 rawText: "missing tab id but conversation route should still project",
3696 role: "assistant"
3697 });
3698
3699 timedJobs = new ConductorTimedJobs(
3700 {
3701 intervalMs: 5_000,
3702 maxMessagesPerTick: 10,
3703 maxTasksPerTick: 10,
3704 settleDelayMs: 1_000
3705 },
3706 {
3707 artifactStore,
3708 autoStart: false,
3709 logDir: logsDir,
3710 schedule: async (work) => {
3711 await work({
3712 controllerId: "mini-main",
3713 host: "mini",
3714 term: 2
3715 });
3716 return "scheduled";
3717 }
3718 }
3719 );
3720 timedJobs.registerRunner(
3721 createRenewalProjectorRunner({
3722 now: () => nowMs,
3723 repository
3724 })
3725 );
3726
3727 await timedJobs.start();
3728 const tick = await timedJobs.runTick("manual");
3729 assert.equal(tick.decision, "scheduled");
3730
3731 const jobs = await artifactStore.listRenewalJobs({});
3732 assert.equal(jobs.length, 2);
3733 assert.deepEqual(
3734 jobs.map((job) => job.messageId).sort(),
3735 [autoMessage.id, missingTabIdMessage.id].sort()
3736 );
3737
3738 const autoJob = jobs.find((job) => job.messageId === autoMessage.id);
3739 const routeOnlyJob = jobs.find((job) => job.messageId === missingTabIdMessage.id);
3740 assert.ok(autoJob);
3741 assert.ok(routeOnlyJob);
3742 assert.equal(autoJob.status, "pending");
3743 assert.equal(autoJob.payloadKind, "json");
3744 assert.equal(typeof autoJob.logPath, "string");
3745 assert.match(autoJob.logPath, /\.jsonl$/u);
3746 assert.equal(routeOnlyJob.status, "pending");
3747 assert.equal(routeOnlyJob.payloadKind, "json");
3748
3749 const payload = JSON.parse(autoJob.payload);
3750 assert.equal(payload.template, "summary_with_link");
3751 assert.match(payload.text, /\[renewal\] Context summary:/u);
3752 assert.equal(payload.sourceMessage.id, autoMessage.id);
3753 assert.equal(payload.linkUrl, "https://claude.ai/chat/conv_auto");
3754
3755 const targetSnapshot = JSON.parse(autoJob.targetSnapshot);
3756 assert.equal(targetSnapshot.target.kind, "browser.proxy_delivery");
3757 assert.equal(targetSnapshot.route.pattern, "/chat/:conversationId");
3758 assert.equal(targetSnapshot.target.payload.tabId, 4);
3759 assert.equal(targetSnapshot.target.id, "client:firefox-auto");
3760
3761 const routeOnlyTargetSnapshot = JSON.parse(routeOnlyJob.targetSnapshot);
3762 assert.equal(routeOnlyTargetSnapshot.target.kind, "browser.proxy_delivery");
3763 assert.equal(routeOnlyTargetSnapshot.pageUrl, "https://claude.ai/chat/conv_missing_tab_id");
3764 assert.equal(routeOnlyTargetSnapshot.target.payload.clientId, "firefox-missing-tab-id");
3765 assert.equal(routeOnlyTargetSnapshot.target.payload.tabId, undefined);
3766
3767 const cursorState = await repository.getSystemState("renewal.projector.cursor");
3768 assert.ok(cursorState);
3769 assert.equal(cursorState.updatedAt, nowMs);
3770 assert.deepEqual(JSON.parse(cursorState.valueJson), {
3771 message_id: missingTabIdMessage.id,
3772 observed_at: missingTabIdMessage.observedAt
3773 });
3774
3775 const entries = await waitForJsonlEntries(
3776 logsDir,
3777 (items) => items.filter((entry) => entry.runner === "renewal.projector" && entry.stage === "job_projected").length >= 2
3778 );
3779 assert.equal(
3780 entries.filter((entry) => entry.runner === "renewal.projector" && entry.stage === "job_projected").length,
3781 2
3782 );
3783 assert.ok(
3784 entries.find(
3785 (entry) => entry.runner === "renewal.projector" && entry.stage === "message_skipped" && entry.result === "automation_manual"
3786 )
3787 );
3788 assert.ok(
3789 entries.find(
3790 (entry) => entry.runner === "renewal.projector" && entry.stage === "message_skipped" && entry.result === "automation_paused"
3791 )
3792 );
3793 assert.ok(
3794 entries.find(
3795 (entry) => entry.runner === "renewal.projector" && entry.stage === "message_skipped" && entry.result === "cooldown_active"
3796 )
3797 );
3798 const routeUnavailableEntries = entries.filter(
3799 (entry) => entry.runner === "renewal.projector" && entry.stage === "message_skipped" && entry.result === "route_unavailable"
3800 );
3801 assert.ok(
3802 routeUnavailableEntries.find(
3803 (entry) =>
3804 entry.message_id === nonProxyTargetMessage.id
3805 && entry.route_unavailable_reason === "target_kind_not_proxy_delivery"
3806 )
3807 );
3808 assert.ok(
3809 routeUnavailableEntries.find(
3810 (entry) =>
3811 entry.message_id === shellPageMessage.id
3812 && entry.route_unavailable_reason === "shell_page"
3813 )
3814 );
3815 assert.ok(
3816 entries.find(
3817 (entry) =>
3818 entry.runner === "renewal.projector"
3819 && entry.stage === "scan_completed"
3820 && entry.cursor_after === `message:${missingTabIdMessage.observedAt}:${missingTabIdMessage.id}`
3821 )
3822 );
3823
3824 await timedJobs.stop();
3825 assert.equal(manualMessage.id, "msg_manual");
3826 assert.equal(pausedMessage.id, "msg_paused");
3827 assert.equal(cooldownMessage.id, "msg_cooldown");
3828 } finally {
3829 await timedJobs?.stop();
3830 artifactStore.close();
3831 rmSync(rootDir, {
3832 force: true,
3833 recursive: true
3834 });
3835 rmSync(logsDir, {
3836 force: true,
3837 recursive: true
3838 });
3839 }
3840});
3841
3842test("renewal projector restores cursor from valueJson even when legacy system_state.updated_at is second-based", async () => {
3843 const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-projector-cursor-restore-legacy-"));
3844 const stateDir = join(rootDir, "state");
3845 const logsDir = mkdtempSync(join(tmpdir(), "baa-renewal-projector-cursor-restore-legacy-logs-"));
3846 const localApiFixture = await createLocalApiFixture({
3847 databasePath: join(rootDir, "control-plane.sqlite")
3848 });
3849 const artifactStore = new ArtifactStore({
3850 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
3851 databasePath: join(stateDir, ARTIFACT_DB_FILENAME),
3852 publicBaseUrl: "https://artifacts.example.test"
3853 });
3854 const nowMs = Date.UTC(2026, 2, 30, 12, 0, 0);
3855 let timedJobs = null;
3856
3857 try {
3858 await artifactStore.upsertLocalConversation({
3859 automationStatus: "auto",
3860 localConversationId: "lc_restore_cursor",
3861 platform: "claude",
3862 updatedAt: nowMs - 60_000
3863 });
3864 await artifactStore.upsertConversationLink({
3865 clientId: "firefox-restore-cursor",
3866 linkId: "link_restore_cursor",
3867 localConversationId: "lc_restore_cursor",
3868 observedAt: nowMs - 60_000,
3869 pageUrl: "https://claude.ai/chat/conv_restore_cursor",
3870 platform: "claude",
3871 remoteConversationId: "conv_restore_cursor",
3872 targetId: "tab:17",
3873 targetKind: "browser.proxy_delivery",
3874 targetPayload: {
3875 clientId: "firefox-restore-cursor",
3876 tabId: 17
3877 }
3878 });
3879
3880 const firstMessage = await artifactStore.insertMessage({
3881 conversationId: "conv_restore_cursor",
3882 id: "msg_restore_cursor_first",
3883 observedAt: nowMs - 30_000,
3884 platform: "claude",
3885 rawText: "already processed before unit migration",
3886 role: "assistant"
3887 });
3888 const secondMessage = await artifactStore.insertMessage({
3889 conversationId: "conv_restore_cursor",
3890 id: "msg_restore_cursor_second",
3891 observedAt: nowMs - 20_000,
3892 platform: "claude",
3893 rawText: "should project after restoring legacy cursor",
3894 role: "assistant"
3895 });
3896
3897 await localApiFixture.repository.putSystemState({
3898 stateKey: "renewal.projector.cursor",
3899 updatedAt: Math.floor(nowMs / 1000),
3900 valueJson: JSON.stringify({
3901 message_id: firstMessage.id,
3902 observed_at: firstMessage.observedAt
3903 })
3904 });
3905
3906 timedJobs = new ConductorTimedJobs(
3907 {
3908 intervalMs: 5_000,
3909 maxMessagesPerTick: 10,
3910 maxTasksPerTick: 10,
3911 settleDelayMs: 0
3912 },
3913 {
3914 artifactStore,
3915 autoStart: false,
3916 logDir: logsDir,
3917 schedule: async (work) => {
3918 await work({
3919 controllerId: "mini-main",
3920 host: "mini",
3921 term: 2
3922 });
3923 return "scheduled";
3924 }
3925 }
3926 );
3927 timedJobs.registerRunner(
3928 createRenewalProjectorRunner({
3929 now: () => nowMs,
3930 repository: localApiFixture.repository
3931 })
3932 );
3933
3934 await timedJobs.start();
3935 const tick = await timedJobs.runTick("manual");
3936 assert.equal(tick.decision, "scheduled");
3937
3938 const jobs = await artifactStore.listRenewalJobs({});
3939 assert.equal(jobs.length, 1);
3940 assert.equal(jobs[0].messageId, secondMessage.id);
3941
3942 const cursorState = await localApiFixture.repository.getSystemState("renewal.projector.cursor");
3943 assert.ok(cursorState);
3944 assert.equal(cursorState.updatedAt, nowMs);
3945 assert.deepEqual(JSON.parse(cursorState.valueJson), {
3946 message_id: secondMessage.id,
3947 observed_at: secondMessage.observedAt
3948 });
3949
3950 await timedJobs.stop();
3951 } finally {
3952 await timedJobs?.stop();
3953 artifactStore.close();
3954 localApiFixture.controlPlane.close();
3955 rmSync(rootDir, {
3956 force: true,
3957 recursive: true
3958 });
3959 rmSync(logsDir, {
3960 force: true,
3961 recursive: true
3962 });
3963 }
3964});
3965
3966test("shouldRenew keeps route_unavailable while exposing structured route failure details", async () => {
3967 const nowMs = Date.UTC(2026, 2, 30, 10, 5, 0);
3968 const baseCandidate = {
3969 conversation: {
3970 automationStatus: "auto",
3971 cooldownUntil: null
3972 },
3973 link: {
3974 isActive: true,
3975 targetId: "tab:42",
3976 targetKind: "browser.proxy_delivery",
3977 targetPayload: JSON.stringify({
3978 clientId: "firefox-route-detail",
3979 tabId: 42
3980 })
3981 },
3982 message: {
3983 conversationId: "conv_route_detail",
3984 id: "msg_route_detail",
3985 observedAt: nowMs - 1_000,
3986 organizationId: null,
3987 pageTitle: null,
3988 pageUrl: null,
3989 platform: "claude",
3990 rawText: "plain renewal candidate",
3991 role: "assistant",
3992 staticPath: "msg/msg_route_detail.txt",
3993 summary: null
3994 }
3995 };
3996
3997 const cases = [
3998 {
3999 expectedReason: "inactive_link",
4000 link: {
4001 isActive: false
4002 }
4003 },
4004 {
4005 expectedReason: "target_kind_not_proxy_delivery",
4006 link: {
4007 targetKind: "browser.shell_page"
4008 }
4009 },
4010 {
4011 expectedReason: "shell_page",
4012 link: {
4013 targetPayload: JSON.stringify({
4014 clientId: "firefox-route-detail",
4015 shellPage: true,
4016 tabId: 42
4017 })
4018 }
4019 },
4020 {
4021 expectedReason: "missing_delivery_context",
4022 link: {
4023 targetId: null,
4024 targetPayload: JSON.stringify({
4025 clientId: "firefox-route-detail"
4026 })
4027 }
4028 }
4029 ];
4030
4031 for (const [index, testCase] of cases.entries()) {
4032 const decision = await shouldRenew({
4033 candidate: {
4034 ...baseCandidate,
4035 link: {
4036 ...baseCandidate.link,
4037 ...testCase.link
4038 },
4039 message: {
4040 ...baseCandidate.message,
4041 id: `${baseCandidate.message.id}_${index}`
4042 }
4043 },
4044 now: nowMs,
4045 store: {
4046 async listRenewalJobs() {
4047 assert.fail("route-unavailable branches should short-circuit before duplicate-job lookup");
4048 }
4049 }
4050 });
4051
4052 assert.equal(decision.eligible, false);
4053 assert.equal(decision.reason, "route_unavailable");
4054 assert.equal(decision.routeUnavailableReason, testCase.expectedReason);
4055 }
4056});
4057
4058test("shouldRenew skips assistant messages that contain baa instruction blocks", async () => {
4059 const decision = await shouldRenew({
4060 candidate: {
4061 conversation: {
4062 automationStatus: "auto",
4063 cooldownUntil: null
4064 },
4065 link: {
4066 isActive: true,
4067 targetId: "tab:42",
4068 targetKind: "browser.proxy_delivery",
4069 targetPayload: JSON.stringify({
4070 clientId: "firefox-instruction-message",
4071 tabId: 42
4072 })
4073 },
4074 message: {
4075 conversationId: "conv_instruction_message",
4076 id: "msg_instruction_message",
4077 observedAt: Date.UTC(2026, 2, 30, 10, 6, 0),
4078 organizationId: null,
4079 pageTitle: null,
4080 pageUrl: null,
4081 platform: "claude",
4082 rawText: "```baa\n@conductor::describe\n```",
4083 role: "assistant",
4084 staticPath: "msg/msg_instruction_message.txt",
4085 summary: null
4086 }
4087 },
4088 now: Date.UTC(2026, 2, 30, 10, 6, 30),
4089 store: {
4090 async listRenewalJobs() {
4091 assert.fail("instruction_message branches should short-circuit before duplicate-job lookup");
4092 }
4093 }
4094 });
4095
4096 assert.equal(decision.eligible, false);
4097 assert.equal(decision.reason, "instruction_message");
4098});
4099
4100test("automation signal helpers pause repeated messages, repeated renewals, and consecutive failures", async () => {
4101 const rootDir = mkdtempSync(join(tmpdir(), "baa-automation-signal-helpers-"));
4102 const artifactStore = new ArtifactStore({
4103 artifactDir: join(rootDir, ARTIFACTS_DIRNAME),
4104 databasePath: join(rootDir, ARTIFACT_DB_FILENAME)
4105 });
4106 const baseConversation = await artifactStore.upsertLocalConversation({
4107 automationStatus: "auto",
4108 localConversationId: "lc_signal_helpers",
4109 platform: "claude",
4110 updatedAt: Date.UTC(2026, 2, 30, 10, 10, 0)
4111 });
4112
4113 try {
4114 let conversation = baseConversation;
4115
4116 for (let index = 0; index < 3; index += 1) {
4117 conversation = await recordAssistantMessageAutomationSignal({
4118 conversation,
4119 observedAt: Date.UTC(2026, 2, 30, 10, 10, index),
4120 rawText: "same assistant final message",
4121 store: artifactStore
4122 });
4123 }
4124
4125 assert.equal(conversation.automationStatus, "paused");
4126 assert.equal(conversation.pauseReason, "repeated_message");
4127
4128 conversation = await artifactStore.upsertLocalConversation({
4129 automationStatus: "auto",
4130 consecutiveFailureCount: 0,
4131 lastError: null,
4132 lastMessageFingerprint: null,
4133 lastRenewalFingerprint: null,
4134 localConversationId: conversation.localConversationId,
4135 pauseReason: null,
4136 pausedAt: null,
4137 platform: conversation.platform,
4138 repeatedMessageCount: 0,
4139 repeatedRenewalCount: 0,
4140 updatedAt: Date.UTC(2026, 2, 30, 10, 11, 0)
4141 });
4142
4143 for (let index = 0; index < 3; index += 1) {
4144 conversation = await recordRenewalPayloadSignal({
4145 conversation,
4146 observedAt: Date.UTC(2026, 2, 30, 10, 11, index),
4147 payloadText: "[renewal] repeated payload",
4148 store: artifactStore
4149 });
4150 }
4151
4152 assert.equal(conversation.automationStatus, "paused");
4153 assert.equal(conversation.pauseReason, "repeated_renewal");
4154
4155 conversation = await artifactStore.upsertLocalConversation({
4156 automationStatus: "auto",
4157 consecutiveFailureCount: 0,
4158 lastError: null,
4159 lastMessageFingerprint: null,
4160 lastRenewalFingerprint: null,
4161 localConversationId: conversation.localConversationId,
4162 pauseReason: null,
4163 pausedAt: null,
4164 platform: conversation.platform,
4165 repeatedMessageCount: 0,
4166 repeatedRenewalCount: 0,
4167 updatedAt: Date.UTC(2026, 2, 30, 10, 12, 0)
4168 });
4169
4170 for (let index = 0; index < 3; index += 1) {
4171 conversation = await recordAutomationFailureSignal({
4172 conversation,
4173 errorMessage: "browser_action_timeout",
4174 observedAt: Date.UTC(2026, 2, 30, 10, 12, index),
4175 store: artifactStore
4176 });
4177 }
4178
4179 assert.equal(conversation.automationStatus, "paused");
4180 assert.equal(conversation.pauseReason, "execution_failure");
4181 assert.equal(conversation.lastError, "browser_action_timeout");
4182 } finally {
4183 artifactStore.close();
4184 rmSync(rootDir, {
4185 force: true,
4186 recursive: true
4187 });
4188 }
4189});
4190
4191test("renewal dispatcher sends due pending jobs through browser.proxy_delivery and marks them done", async () => {
4192 const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-dispatcher-success-"));
4193 const stateDir = join(rootDir, "state");
4194 const logsDir = mkdtempSync(join(tmpdir(), "baa-renewal-dispatcher-success-logs-"));
4195 const artifactStore = new ArtifactStore({
4196 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
4197 databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
4198 });
4199 const nowMs = Date.UTC(2026, 2, 30, 12, 0, 0);
4200 const browserCalls = [];
4201 const runner = createRenewalDispatcherRunner({
4202 browserBridge: {
4203 proxyDelivery(input) {
4204 browserCalls.push(input);
4205 return {
4206 clientId: input.clientId || "firefox-chatgpt",
4207 connectionId: "conn-firefox-chatgpt",
4208 dispatchedAt: nowMs,
4209 requestId: "proxy-dispatch-1",
4210 result: Promise.resolve(buildProxyDeliveryActionResult({
4211 clientId: input.clientId || "firefox-chatgpt",
4212 connectionId: "conn-firefox-chatgpt",
4213 deliveryAck: buildDeliveryAck(200, {
4214 confirmed_at: nowMs + 40
4215 }),
4216 platform: input.platform,
4217 receivedAt: nowMs + 50,
4218 requestId: "proxy-dispatch-1"
4219 })),
4220 type: "browser.proxy_delivery"
4221 };
4222 }
4223 },
4224 now: () => nowMs
4225 });
4226
4227 try {
4228 await artifactStore.insertMessage({
4229 conversationId: "conv_dispatch_success",
4230 id: "msg_dispatch_success",
4231 observedAt: nowMs - 60_000,
4232 platform: "chatgpt",
4233 rawText: "renewal dispatcher success message",
4234 role: "assistant"
4235 });
4236 await artifactStore.upsertLocalConversation({
4237 automationStatus: "auto",
4238 localConversationId: "lc_dispatch_success",
4239 platform: "chatgpt",
4240 updatedAt: nowMs - 30_000
4241 });
4242 await artifactStore.upsertConversationLink({
4243 clientId: "firefox-chatgpt",
4244 linkId: "link_dispatch_success",
4245 localConversationId: "lc_dispatch_success",
4246 observedAt: nowMs - 30_000,
4247 pageTitle: "Dispatch Success",
4248 pageUrl: "https://chatgpt.com/c/conv_dispatch_success",
4249 platform: "chatgpt",
4250 remoteConversationId: "conv_dispatch_success",
4251 routeParams: {
4252 conversationId: "conv_dispatch_success"
4253 },
4254 routePath: "/c/conv_dispatch_success",
4255 routePattern: "/c/:conversationId",
4256 targetId: "tab:17",
4257 targetKind: "browser.proxy_delivery",
4258 targetPayload: {
4259 clientId: "firefox-chatgpt",
4260 conversationId: "conv_dispatch_success",
4261 pageUrl: "https://chatgpt.com/c/conv_dispatch_success",
4262 tabId: 17
4263 }
4264 });
4265 await artifactStore.insertRenewalJob({
4266 jobId: "job_dispatch_success",
4267 localConversationId: "lc_dispatch_success",
4268 messageId: "msg_dispatch_success",
4269 nextAttemptAt: nowMs,
4270 payload: JSON.stringify({
4271 kind: "renewal.message",
4272 sourceMessage: {
4273 id: "msg_dispatch_success"
4274 },
4275 template: "summary_with_link",
4276 text: "[renewal] keepalive",
4277 version: 1
4278 }),
4279 payloadKind: "json",
4280 targetSnapshot: {
4281 stale: true
4282 }
4283 });
4284
4285 const { context, entries } = createTimedJobRunnerContext({
4286 artifactStore,
4287 logDir: logsDir
4288 });
4289 const result = await runner.run(context);
4290 const job = await artifactStore.getRenewalJob("job_dispatch_success");
4291 const conversation = await artifactStore.getLocalConversation("lc_dispatch_success");
4292
4293 assert.equal(result.result, "ok");
4294 assert.equal(browserCalls.length, 1);
4295 assert.equal(browserCalls[0].messageText, "[renewal] keepalive");
4296 assert.equal(browserCalls[0].conversationId, "conv_dispatch_success");
4297 assert.equal(browserCalls[0].pageUrl, "https://chatgpt.com/c/conv_dispatch_success");
4298 assert.equal(browserCalls[0].tabId, null);
4299 assert.equal(job.status, "done");
4300 assert.equal(job.attemptCount, 1);
4301 assert.equal(job.lastError, null);
4302 assert.equal(job.nextAttemptAt, null);
4303 assert.equal(typeof job.finishedAt, "number");
4304 assert.equal(JSON.parse(job.targetSnapshot).target.kind, "browser.proxy_delivery");
4305 assert.equal(conversation.cooldownUntil, nowMs + 60_000);
4306 assert.equal(conversation.updatedAt, nowMs);
4307 assert.ok(entries.find((entry) => entry.stage === "job_attempt_started"));
4308 assert.ok(
4309 entries.find(
4310 (entry) =>
4311 entry.stage === "job_completed"
4312 && entry.result === "attempt_succeeded"
4313 && entry.details?.downstream_status_code === 200
4314 )
4315 );
4316 } finally {
4317 artifactStore.close();
4318 rmSync(rootDir, {
4319 force: true,
4320 recursive: true
4321 });
4322 rmSync(logsDir, {
4323 force: true,
4324 recursive: true
4325 });
4326 }
4327});
4328
4329test("renewal dispatcher adds inter-job jitter before consecutive dispatches and logs it", async () => {
4330 const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-dispatcher-jitter-"));
4331 const stateDir = join(rootDir, "state");
4332 const artifactStore = new ArtifactStore({
4333 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
4334 databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
4335 });
4336 const dispatchTimes = [];
4337 const randomValues = [0, 1];
4338 let randomIndex = 0;
4339 const runner = createRenewalDispatcherRunner({
4340 browserBridge: {
4341 proxyDelivery(input) {
4342 const dispatchedAt = Date.now();
4343 dispatchTimes.push(dispatchedAt);
4344
4345 return {
4346 clientId: input.clientId || "firefox-chatgpt",
4347 connectionId: "conn-firefox-chatgpt",
4348 dispatchedAt,
4349 requestId: `proxy-dispatch-jitter-${dispatchTimes.length}`,
4350 result: Promise.resolve(buildProxyDeliveryActionResult({
4351 clientId: input.clientId || "firefox-chatgpt",
4352 connectionId: "conn-firefox-chatgpt",
4353 deliveryAck: buildDeliveryAck(200, {
4354 confirmed_at: dispatchedAt + 4
4355 }),
4356 platform: input.platform,
4357 receivedAt: dispatchedAt + 5,
4358 requestId: `proxy-dispatch-jitter-${dispatchTimes.length}`
4359 })),
4360 type: "browser.proxy_delivery"
4361 };
4362 }
4363 },
4364 interJobJitterMaxMs: 45,
4365 interJobJitterMinMs: 25,
4366 random: () => randomValues[randomIndex++] ?? 0.5
4367 });
4368 const baseNowMs = Date.now() - 1_000;
4369
4370 try {
4371 for (let index = 1; index <= 3; index += 1) {
4372 const conversationId = `conv_dispatch_jitter_${index}`;
4373 const localConversationId = `lc_dispatch_jitter_${index}`;
4374 const messageId = `msg_dispatch_jitter_${index}`;
4375 const jobId = `job_dispatch_jitter_${index}`;
4376
4377 await artifactStore.insertMessage({
4378 conversationId,
4379 id: messageId,
4380 observedAt: baseNowMs - (index * 1_000),
4381 platform: "chatgpt",
4382 rawText: `renewal dispatcher jitter message ${index}`,
4383 role: "assistant"
4384 });
4385 await artifactStore.upsertLocalConversation({
4386 automationStatus: "auto",
4387 localConversationId,
4388 platform: "chatgpt",
4389 updatedAt: baseNowMs - 500
4390 });
4391 await artifactStore.upsertConversationLink({
4392 clientId: "firefox-chatgpt",
4393 linkId: `link_dispatch_jitter_${index}`,
4394 localConversationId,
4395 observedAt: baseNowMs - 500,
4396 pageTitle: `Dispatch Jitter ${index}`,
4397 pageUrl: `https://chatgpt.com/c/${conversationId}`,
4398 platform: "chatgpt",
4399 remoteConversationId: conversationId,
4400 routeParams: {
4401 conversationId
4402 },
4403 routePath: `/c/${conversationId}`,
4404 routePattern: "/c/:conversationId",
4405 targetId: `tab:${30 + index}`,
4406 targetKind: "browser.proxy_delivery",
4407 targetPayload: {
4408 clientId: "firefox-chatgpt",
4409 conversationId,
4410 pageUrl: `https://chatgpt.com/c/${conversationId}`,
4411 tabId: 30 + index
4412 }
4413 });
4414 await artifactStore.insertRenewalJob({
4415 jobId,
4416 localConversationId,
4417 messageId,
4418 nextAttemptAt: baseNowMs,
4419 payload: `[renewal] jitter ${index}`,
4420 payloadKind: "text"
4421 });
4422 }
4423
4424 const { context, entries } = createTimedJobRunnerContext({
4425 artifactStore,
4426 config: {
4427 intervalMs: 5_000,
4428 maxMessagesPerTick: 10,
4429 maxTasksPerTick: 10,
4430 settleDelayMs: 0
4431 }
4432 });
4433 const result = await runner.run(context);
4434 const jitterEntries = entries.filter((entry) => entry.stage === "job_dispatch_jitter");
4435 const startedEntries = entries.filter((entry) => entry.stage === "job_attempt_started");
4436
4437 assert.equal(result.result, "ok");
4438 assert.equal(result.details.successful_jobs, 3);
4439 assert.equal(dispatchTimes.length, 3);
4440 assert.ok(
4441 dispatchTimes[1] - dispatchTimes[0] >= 20,
4442 `expected second dispatch gap >= 20ms, got ${dispatchTimes[1] - dispatchTimes[0]}ms`
4443 );
4444 assert.ok(
4445 dispatchTimes[2] - dispatchTimes[1] >= 35,
4446 `expected third dispatch gap >= 35ms, got ${dispatchTimes[2] - dispatchTimes[1]}ms`
4447 );
4448 assert.deepEqual(
4449 jitterEntries.map((entry) => entry.details?.jitter_ms),
4450 [25, 45]
4451 );
4452 assert.deepEqual(
4453 jitterEntries.map((entry) => entry.details?.dispatch_sequence_in_tick),
4454 [2, 3]
4455 );
4456 assert.deepEqual(
4457 startedEntries.map((entry) => entry.details?.inter_job_jitter_ms),
4458 [0, 25, 45]
4459 );
4460 } finally {
4461 artifactStore.close();
4462 rmSync(rootDir, {
4463 force: true,
4464 recursive: true
4465 });
4466 }
4467});
4468
4469test("renewal dispatcher classifies downstream proxy_delivery statuses for retry and terminal failure", async () => {
4470 const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-dispatcher-http-status-"));
4471 const stateDir = join(rootDir, "state");
4472 const artifactStore = new ArtifactStore({
4473 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
4474 databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
4475 });
4476 const nowMs = Date.UTC(2026, 2, 30, 12, 30, 0);
4477 const browserCalls = [];
4478 const runner = createRenewalDispatcherRunner({
4479 browserBridge: {
4480 proxyDelivery(input) {
4481 browserCalls.push(input);
4482 const statusCode = input.conversationId === "conv_dispatch_http_retry" ? 429 : 401;
4483
4484 return {
4485 clientId: input.clientId || "firefox-chatgpt",
4486 connectionId: "conn-firefox-chatgpt",
4487 dispatchedAt: nowMs,
4488 requestId: `proxy-http-${statusCode}`,
4489 result: Promise.resolve(buildProxyDeliveryActionResult({
4490 clientId: input.clientId || "firefox-chatgpt",
4491 connectionId: "conn-firefox-chatgpt",
4492 deliveryAck: buildDeliveryAck(statusCode, {
4493 confirmed_at: nowMs + 10
4494 }),
4495 platform: input.platform,
4496 receivedAt: nowMs + 25,
4497 requestId: `proxy-http-${statusCode}`
4498 })),
4499 type: "browser.proxy_delivery"
4500 };
4501 }
4502 },
4503 now: () => nowMs,
4504 retryBaseDelayMs: 1,
4505 retryMaxDelayMs: 1
4506 });
4507
4508 try {
4509 const definitions = [
4510 {
4511 conversationId: "conv_dispatch_http_retry",
4512 expectedStatus: "pending",
4513 jobId: "job_dispatch_http_retry",
4514 lastError: "downstream_status_429",
4515 localConversationId: "lc_dispatch_http_retry",
4516 messageId: "msg_dispatch_http_retry",
4517 pageTitle: "HTTP Retry Renewal",
4518 tabId: 41
4519 },
4520 {
4521 conversationId: "conv_dispatch_http_fail",
4522 expectedStatus: "failed",
4523 jobId: "job_dispatch_http_fail",
4524 lastError: "downstream_status_401",
4525 localConversationId: "lc_dispatch_http_fail",
4526 messageId: "msg_dispatch_http_fail",
4527 pageTitle: "HTTP Fail Renewal",
4528 tabId: 42
4529 }
4530 ];
4531
4532 for (const definition of definitions) {
4533 await artifactStore.insertMessage({
4534 conversationId: definition.conversationId,
4535 id: definition.messageId,
4536 observedAt: nowMs - 60_000,
4537 platform: "chatgpt",
4538 rawText: `${definition.conversationId} renewal message`,
4539 role: "assistant"
4540 });
4541 await artifactStore.upsertLocalConversation({
4542 automationStatus: "auto",
4543 localConversationId: definition.localConversationId,
4544 platform: "chatgpt",
4545 updatedAt: nowMs - 30_000
4546 });
4547 await artifactStore.upsertConversationLink({
4548 clientId: "firefox-chatgpt",
4549 linkId: `link_${definition.localConversationId}`,
4550 localConversationId: definition.localConversationId,
4551 observedAt: nowMs - 30_000,
4552 pageTitle: definition.pageTitle,
4553 pageUrl: `https://chatgpt.com/c/${definition.conversationId}`,
4554 platform: "chatgpt",
4555 remoteConversationId: definition.conversationId,
4556 routeParams: {
4557 conversationId: definition.conversationId
4558 },
4559 routePath: `/c/${definition.conversationId}`,
4560 routePattern: "/c/:conversationId",
4561 targetId: `tab:${definition.tabId}`,
4562 targetKind: "browser.proxy_delivery",
4563 targetPayload: {
4564 clientId: "firefox-chatgpt",
4565 conversationId: definition.conversationId,
4566 pageUrl: `https://chatgpt.com/c/${definition.conversationId}`,
4567 tabId: definition.tabId
4568 }
4569 });
4570 await artifactStore.insertRenewalJob({
4571 jobId: definition.jobId,
4572 localConversationId: definition.localConversationId,
4573 maxAttempts: 2,
4574 messageId: definition.messageId,
4575 nextAttemptAt: nowMs,
4576 payload: "[renewal] http confirmation",
4577 payloadKind: "text"
4578 });
4579 }
4580
4581 const { context, entries } = createTimedJobRunnerContext({
4582 artifactStore
4583 });
4584 const result = await runner.run(context);
4585 const retryJob = await artifactStore.getRenewalJob("job_dispatch_http_retry");
4586 const failedJob = await artifactStore.getRenewalJob("job_dispatch_http_fail");
4587
4588 assert.equal(result.result, "ok");
4589 assert.equal(result.details.retried_jobs, 1);
4590 assert.equal(result.details.failed_jobs, 1);
4591 assert.equal(browserCalls.length, 2);
4592 assert.equal(retryJob.status, "pending");
4593 assert.equal(retryJob.attemptCount, 1);
4594 assert.equal(retryJob.lastError, "downstream_status_429");
4595 assert.equal(retryJob.nextAttemptAt, nowMs + 1);
4596 assert.equal(failedJob.status, "failed");
4597 assert.equal(failedJob.attemptCount, 1);
4598 assert.equal(failedJob.lastError, "downstream_status_401");
4599 assert.equal(failedJob.nextAttemptAt, null);
4600 assert.ok(
4601 entries.find(
4602 (entry) =>
4603 entry.stage === "job_retry_scheduled"
4604 && entry.result === "downstream_status_429"
4605 && entry.details?.downstream_status_code === 429
4606 )
4607 );
4608 assert.ok(
4609 entries.find(
4610 (entry) =>
4611 entry.stage === "job_failed"
4612 && entry.result === "downstream_status_401"
4613 && entry.details?.downstream_status_code === 401
4614 )
4615 );
4616 } finally {
4617 artifactStore.close();
4618 rmSync(rootDir, {
4619 force: true,
4620 recursive: true
4621 });
4622 }
4623});
4624
4625test("renewal dispatcher uses short retries for ChatGPT cold-start template misses and logs warmup recovery", async () => {
4626 const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-dispatcher-chatgpt-cold-start-"));
4627 const stateDir = join(rootDir, "state");
4628 const artifactStore = new ArtifactStore({
4629 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
4630 databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
4631 });
4632 const coldStartReason = "delivery.template_missing: missing ChatGPT send template; send one real ChatGPT message first";
4633 let nowMs = Date.UTC(2026, 2, 30, 12, 45, 0);
4634 const browserCalls = [];
4635 const runner = createRenewalDispatcherRunner({
4636 browserBridge: {
4637 proxyDelivery(input) {
4638 browserCalls.push({
4639 ...input,
4640 calledAt: nowMs
4641 });
4642 const requestId = `proxy-chatgpt-cold-start-${browserCalls.length}`;
4643
4644 if (browserCalls.length === 1) {
4645 return {
4646 clientId: input.clientId || "firefox-chatgpt",
4647 connectionId: "conn-firefox-chatgpt",
4648 dispatchedAt: nowMs,
4649 requestId,
4650 result: Promise.resolve(buildProxyDeliveryActionResult({
4651 accepted: true,
4652 clientId: input.clientId || "firefox-chatgpt",
4653 connectionId: "conn-firefox-chatgpt",
4654 failed: true,
4655 platform: input.platform,
4656 reason: coldStartReason,
4657 receivedAt: nowMs + 20,
4658 requestId,
4659 results: []
4660 })),
4661 type: "browser.proxy_delivery"
4662 };
4663 }
4664
4665 return {
4666 clientId: input.clientId || "firefox-chatgpt",
4667 connectionId: "conn-firefox-chatgpt",
4668 dispatchedAt: nowMs,
4669 requestId,
4670 result: Promise.resolve(buildProxyDeliveryActionResult({
4671 clientId: input.clientId || "firefox-chatgpt",
4672 connectionId: "conn-firefox-chatgpt",
4673 deliveryAck: buildDeliveryAck(200, {
4674 confirmed_at: nowMs + 30
4675 }),
4676 platform: input.platform,
4677 receivedAt: nowMs + 40,
4678 requestId
4679 })),
4680 type: "browser.proxy_delivery"
4681 };
4682 }
4683 },
4684 now: () => nowMs,
4685 random: () => 0.5,
4686 retryBaseDelayMs: 30_000,
4687 retryMaxDelayMs: 30_000
4688 });
4689
4690 try {
4691 await artifactStore.insertMessage({
4692 conversationId: "conv_dispatch_chatgpt_cold_start",
4693 id: "msg_dispatch_chatgpt_cold_start",
4694 observedAt: nowMs - 60_000,
4695 platform: "chatgpt",
4696 rawText: "renewal dispatcher cold start message",
4697 role: "assistant"
4698 });
4699 await artifactStore.upsertLocalConversation({
4700 automationStatus: "auto",
4701 localConversationId: "lc_dispatch_chatgpt_cold_start",
4702 platform: "chatgpt",
4703 updatedAt: nowMs - 30_000
4704 });
4705 await artifactStore.upsertConversationLink({
4706 clientId: "firefox-chatgpt",
4707 linkId: "link_dispatch_chatgpt_cold_start",
4708 localConversationId: "lc_dispatch_chatgpt_cold_start",
4709 observedAt: nowMs - 30_000,
4710 pageTitle: "ChatGPT Cold Start",
4711 pageUrl: "https://chatgpt.com/c/conv_dispatch_chatgpt_cold_start",
4712 platform: "chatgpt",
4713 remoteConversationId: "conv_dispatch_chatgpt_cold_start",
4714 routeParams: {
4715 conversationId: "conv_dispatch_chatgpt_cold_start"
4716 },
4717 routePath: "/c/conv_dispatch_chatgpt_cold_start",
4718 routePattern: "/c/:conversationId",
4719 targetId: "tab:61",
4720 targetKind: "browser.proxy_delivery",
4721 targetPayload: {
4722 clientId: "firefox-chatgpt",
4723 conversationId: "conv_dispatch_chatgpt_cold_start",
4724 pageUrl: "https://chatgpt.com/c/conv_dispatch_chatgpt_cold_start",
4725 tabId: 61
4726 }
4727 });
4728 await artifactStore.insertRenewalJob({
4729 jobId: "job_dispatch_chatgpt_cold_start",
4730 localConversationId: "lc_dispatch_chatgpt_cold_start",
4731 maxAttempts: 3,
4732 messageId: "msg_dispatch_chatgpt_cold_start",
4733 nextAttemptAt: nowMs,
4734 payload: "[renewal] cold start recovery",
4735 payloadKind: "text"
4736 });
4737
4738 const firstTick = createTimedJobRunnerContext({
4739 artifactStore
4740 });
4741 const firstResult = await runner.run(firstTick.context);
4742 const pendingJob = await artifactStore.getRenewalJob("job_dispatch_chatgpt_cold_start");
4743
4744 assert.equal(firstResult.result, "ok");
4745 assert.equal(firstResult.details.retried_jobs, 1);
4746 assert.equal(browserCalls.length, 1);
4747 assert.equal(pendingJob.status, "pending");
4748 assert.equal(pendingJob.attemptCount, 1);
4749 assert.equal(pendingJob.lastError, coldStartReason);
4750 assert.equal(pendingJob.nextAttemptAt, nowMs + 5_000);
4751 assert.ok(
4752 firstTick.entries.find(
4753 (entry) =>
4754 entry.stage === "chatgpt_cold_start_delivery"
4755 && entry.result === "waiting_for_template_warmup"
4756 && entry.details?.retry_base_delay_ms === 5_000
4757 && entry.details?.retry_delay_ms === 5_000
4758 )
4759 );
4760 assert.ok(
4761 firstTick.entries.find(
4762 (entry) =>
4763 entry.stage === "job_retry_scheduled"
4764 && entry.result === coldStartReason
4765 && entry.details?.cold_start_waiting_for_template === true
4766 && entry.details?.retry_base_delay_ms === 5_000
4767 )
4768 );
4769
4770 nowMs = pendingJob.nextAttemptAt;
4771
4772 const secondTick = createTimedJobRunnerContext({
4773 artifactStore
4774 });
4775 const secondResult = await runner.run(secondTick.context);
4776 const completedJob = await artifactStore.getRenewalJob("job_dispatch_chatgpt_cold_start");
4777
4778 assert.equal(secondResult.result, "ok");
4779 assert.equal(secondResult.details.successful_jobs, 1);
4780 assert.equal(browserCalls.length, 2);
4781 assert.equal(completedJob.status, "done");
4782 assert.equal(completedJob.attemptCount, 2);
4783 assert.equal(completedJob.lastError, null);
4784 assert.equal(completedJob.nextAttemptAt, null);
4785 assert.ok(
4786 secondTick.entries.find(
4787 (entry) =>
4788 entry.stage === "chatgpt_template_warmup"
4789 && entry.result === "template_warmup_completed"
4790 && entry.details?.previous_error === coldStartReason
4791 )
4792 );
4793 assert.ok(
4794 secondTick.entries.find(
4795 (entry) =>
4796 entry.stage === "job_completed"
4797 && entry.result === "attempt_succeeded"
4798 && entry.details?.recovered_from_cold_start === true
4799 )
4800 );
4801 } finally {
4802 artifactStore.close();
4803 rmSync(rootDir, {
4804 force: true,
4805 recursive: true
4806 });
4807 }
4808});
4809
4810test("renewal dispatcher success writes cooldownUntil and blocks projector from projecting follow-up messages during cooldown", async () => {
4811 const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-dispatcher-cooldown-chain-"));
4812 const stateDir = join(rootDir, "state");
4813 const localApiFixture = await createLocalApiFixture({
4814 databasePath: join(rootDir, "control-plane.sqlite")
4815 });
4816 const artifactStore = new ArtifactStore({
4817 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
4818 databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
4819 });
4820 let nowMs = Date.UTC(2026, 2, 30, 14, 0, 0);
4821 const browserCalls = [];
4822 const projector = createRenewalProjectorRunner({
4823 now: () => nowMs,
4824 repository: localApiFixture.repository
4825 });
4826 const dispatcher = createRenewalDispatcherRunner({
4827 browserBridge: {
4828 proxyDelivery(input) {
4829 browserCalls.push(input);
4830 return {
4831 clientId: input.clientId || "firefox-claude",
4832 connectionId: "conn-firefox-claude",
4833 dispatchedAt: nowMs,
4834 requestId: "proxy-chain-1",
4835 result: Promise.resolve(buildProxyDeliveryActionResult({
4836 clientId: input.clientId || "firefox-claude",
4837 connectionId: "conn-firefox-claude",
4838 deliveryAck: buildDeliveryAck(200, {
4839 confirmed_at: nowMs + 40
4840 }),
4841 platform: input.platform,
4842 receivedAt: nowMs + 50,
4843 requestId: "proxy-chain-1"
4844 })),
4845 type: "browser.proxy_delivery"
4846 };
4847 }
4848 },
4849 now: () => nowMs,
4850 successCooldownMs: 60_000
4851 });
4852
4853 try {
4854 await artifactStore.upsertLocalConversation({
4855 automationStatus: "auto",
4856 localConversationId: "lc_dispatch_chain",
4857 platform: "claude",
4858 updatedAt: nowMs - 60_000
4859 });
4860 await artifactStore.upsertConversationLink({
4861 clientId: "firefox-claude",
4862 linkId: "link_dispatch_chain",
4863 localConversationId: "lc_dispatch_chain",
4864 observedAt: nowMs - 60_000,
4865 pageTitle: "Dispatch Cooldown Chain",
4866 pageUrl: "https://claude.ai/chat/conv_dispatch_chain",
4867 platform: "claude",
4868 remoteConversationId: "conv_dispatch_chain",
4869 routeParams: {
4870 conversationId: "conv_dispatch_chain"
4871 },
4872 routePath: "/chat/conv_dispatch_chain",
4873 routePattern: "/chat/:conversationId",
4874 targetId: "tab:21",
4875 targetKind: "browser.proxy_delivery",
4876 targetPayload: {
4877 clientId: "firefox-claude",
4878 conversationId: "conv_dispatch_chain",
4879 pageUrl: "https://claude.ai/chat/conv_dispatch_chain",
4880 tabId: 21
4881 }
4882 });
4883 await artifactStore.insertMessage({
4884 conversationId: "conv_dispatch_chain",
4885 id: "msg_dispatch_chain_seed",
4886 observedAt: nowMs - 30_000,
4887 platform: "claude",
4888 rawText: "first renewal candidate should dispatch successfully",
4889 role: "assistant"
4890 });
4891
4892 const initialProjectorTick = createTimedJobRunnerContext({
4893 artifactStore,
4894 config: {
4895 intervalMs: 10_000,
4896 maxMessagesPerTick: 10,
4897 maxTasksPerTick: 10,
4898 settleDelayMs: 0
4899 }
4900 });
4901 const projected = await projector.run(initialProjectorTick.context);
4902 const projectedJobs = await artifactStore.listRenewalJobs({});
4903
4904 assert.equal(projected.result, "ok");
4905 assert.equal(projected.details.projected_jobs, 1);
4906 assert.equal(projectedJobs.length, 1);
4907 assert.equal(projectedJobs[0].messageId, "msg_dispatch_chain_seed");
4908
4909 const initialDispatcherTick = createTimedJobRunnerContext({
4910 artifactStore,
4911 config: {
4912 intervalMs: 10_000,
4913 maxMessagesPerTick: 10,
4914 maxTasksPerTick: 10,
4915 settleDelayMs: 0
4916 }
4917 });
4918 const dispatched = await dispatcher.run(initialDispatcherTick.context);
4919 const conversationAfterSuccess = await artifactStore.getLocalConversation("lc_dispatch_chain");
4920
4921 assert.equal(dispatched.result, "ok");
4922 assert.equal(dispatched.details.successful_jobs, 1);
4923 assert.equal(browserCalls.length, 1);
4924 assert.ok(conversationAfterSuccess);
4925 assert.equal(conversationAfterSuccess.cooldownUntil, nowMs + 60_000);
4926
4927 nowMs += 10_000;
4928 await artifactStore.insertMessage({
4929 conversationId: "conv_dispatch_chain",
4930 id: "msg_dispatch_chain_followup",
4931 observedAt: nowMs - 1_000,
4932 platform: "claude",
4933 rawText: "follow-up message should be skipped by cooldown",
4934 role: "assistant"
4935 });
4936
4937 const cooldownProjectorTick = createTimedJobRunnerContext({
4938 artifactStore,
4939 config: {
4940 intervalMs: 10_000,
4941 maxMessagesPerTick: 10,
4942 maxTasksPerTick: 10,
4943 settleDelayMs: 0
4944 }
4945 });
4946 const cooldownProjected = await projector.run(cooldownProjectorTick.context);
4947 const followupJobs = await artifactStore.listRenewalJobs({
4948 messageId: "msg_dispatch_chain_followup"
4949 });
4950
4951 assert.equal(cooldownProjected.result, "ok");
4952 assert.equal(cooldownProjected.details.projected_jobs, 0);
4953 assert.equal(followupJobs.length, 0);
4954 assert.ok(
4955 cooldownProjectorTick.entries.find(
4956 (entry) =>
4957 entry.stage === "message_skipped"
4958 && entry.result === "cooldown_active"
4959 && entry.details?.message_id === "msg_dispatch_chain_followup"
4960 )
4961 );
4962 } finally {
4963 artifactStore.close();
4964 localApiFixture.controlPlane.close();
4965 rmSync(rootDir, {
4966 force: true,
4967 recursive: true
4968 });
4969 }
4970});
4971
4972test("renewal dispatcher adds retry jitter so same-attempt failures do not reschedule to the same timestamp", async () => {
4973 const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-dispatcher-retry-jitter-"));
4974 const stateDir = join(rootDir, "state");
4975 const artifactStore = new ArtifactStore({
4976 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
4977 databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
4978 });
4979 const nowMs = Date.UTC(2026, 2, 30, 13, 30, 0);
4980 const randomValues = [0, 1];
4981 let randomIndex = 0;
4982 const runner = createRenewalDispatcherRunner({
4983 browserBridge: {
4984 proxyDelivery() {
4985 assert.fail("jobs missing active links should not call browser.proxy_delivery");
4986 }
4987 },
4988 now: () => nowMs,
4989 random: () => randomValues[randomIndex++] ?? 0.5,
4990 retryBaseDelayMs: 1_000,
4991 retryJitterFactor: 0.3,
4992 retryMaxDelayMs: 10_000
4993 });
4994
4995 try {
4996 for (let index = 1; index <= 2; index += 1) {
4997 const conversationId = `conv_dispatch_retry_jitter_${index}`;
4998 const localConversationId = `lc_dispatch_retry_jitter_${index}`;
4999 const messageId = `msg_dispatch_retry_jitter_${index}`;
5000
5001 await artifactStore.insertMessage({
5002 conversationId,
5003 id: messageId,
5004 observedAt: nowMs - (index * 1_000),
5005 platform: "claude",
5006 rawText: `renewal dispatcher retry jitter message ${index}`,
5007 role: "assistant"
5008 });
5009 await artifactStore.upsertLocalConversation({
5010 automationStatus: "auto",
5011 localConversationId,
5012 platform: "claude",
5013 updatedAt: nowMs - 500
5014 });
5015 await artifactStore.insertRenewalJob({
5016 jobId: `job_dispatch_retry_jitter_${index}`,
5017 localConversationId,
5018 maxAttempts: 3,
5019 messageId,
5020 nextAttemptAt: nowMs,
5021 payload: `[renewal] retry jitter ${index}`,
5022 payloadKind: "text"
5023 });
5024 }
5025
5026 const { context, entries } = createTimedJobRunnerContext({
5027 artifactStore
5028 });
5029 const result = await runner.run(context);
5030 const firstJob = await artifactStore.getRenewalJob("job_dispatch_retry_jitter_1");
5031 const secondJob = await artifactStore.getRenewalJob("job_dispatch_retry_jitter_2");
5032 const retryEntries = entries.filter((entry) => entry.stage === "job_retry_scheduled");
5033
5034 assert.equal(result.result, "ok");
5035 assert.equal(result.details.retried_jobs, 2);
5036 assert.equal(firstJob.status, "pending");
5037 assert.equal(secondJob.status, "pending");
5038 assert.equal(firstJob.nextAttemptAt, nowMs + 700);
5039 assert.equal(secondJob.nextAttemptAt, nowMs + 1_300);
5040 assert.notEqual(firstJob.nextAttemptAt, secondJob.nextAttemptAt);
5041 assert.deepEqual(
5042 retryEntries.map((entry) => entry.details?.retry_base_delay_ms),
5043 [1_000, 1_000]
5044 );
5045 assert.deepEqual(
5046 retryEntries.map((entry) => entry.details?.retry_jitter_ms),
5047 [-300, 300]
5048 );
5049 assert.deepEqual(
5050 retryEntries.map((entry) => entry.details?.retry_delay_ms),
5051 [700, 1_300]
5052 );
5053 } finally {
5054 artifactStore.close();
5055 rmSync(rootDir, {
5056 force: true,
5057 recursive: true
5058 });
5059 }
5060});
5061
5062test("renewal dispatcher defers paused jobs and retries transient proxy failures until failed", async () => {
5063 const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-dispatcher-retry-"));
5064 const stateDir = join(rootDir, "state");
5065 const artifactStore = new ArtifactStore({
5066 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
5067 databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
5068 });
5069 let nowMs = Date.UTC(2026, 2, 30, 13, 0, 0);
5070 const browserCalls = [];
5071 const retryRunner = createRenewalDispatcherRunner({
5072 browserBridge: {
5073 proxyDelivery(input) {
5074 browserCalls.push(input);
5075 return {
5076 clientId: input.clientId || "firefox-claude",
5077 connectionId: "conn-firefox-claude",
5078 dispatchedAt: nowMs,
5079 requestId: `proxy-retry-${browserCalls.length}`,
5080 result: Promise.resolve({
5081 accepted: false,
5082 action: "proxy_delivery",
5083 completed: true,
5084 failed: true,
5085 reason: "no_active_client",
5086 received_at: nowMs + 25,
5087 request_id: `proxy-retry-${browserCalls.length}`,
5088 result: {
5089 actual_count: 0,
5090 desired_count: 1,
5091 drift_count: 0,
5092 failed_count: 1,
5093 ok_count: 0,
5094 platform_count: 1,
5095 restored_count: 0,
5096 skipped_reasons: ["no_active_client"]
5097 },
5098 results: [],
5099 shell_runtime: [],
5100 target: {
5101 client_id: input.clientId || "firefox-claude",
5102 connection_id: "conn-firefox-claude",
5103 platform: input.platform,
5104 requested_client_id: input.clientId || "firefox-claude",
5105 requested_platform: input.platform
5106 },
5107 type: "browser.proxy_delivery"
5108 }),
5109 type: "browser.proxy_delivery"
5110 };
5111 }
5112 },
5113 now: () => nowMs,
5114 retryBaseDelayMs: 1,
5115 retryMaxDelayMs: 1
5116 });
5117
5118 try {
5119 await artifactStore.insertMessage({
5120 conversationId: "conv_dispatch_paused",
5121 id: "msg_dispatch_paused",
5122 observedAt: nowMs - 60_000,
5123 platform: "claude",
5124 rawText: "renewal dispatcher paused message",
5125 role: "assistant"
5126 });
5127 await artifactStore.insertMessage({
5128 conversationId: "conv_dispatch_retry",
5129 id: "msg_dispatch_retry",
5130 observedAt: nowMs - 55_000,
5131 platform: "claude",
5132 rawText: "renewal dispatcher retry message",
5133 role: "assistant"
5134 });
5135 await artifactStore.upsertLocalConversation({
5136 automationStatus: "paused",
5137 localConversationId: "lc_dispatch_paused",
5138 pausedAt: nowMs - 5_000,
5139 platform: "claude"
5140 });
5141 await artifactStore.upsertLocalConversation({
5142 automationStatus: "auto",
5143 localConversationId: "lc_dispatch_retry",
5144 platform: "claude"
5145 });
5146 await artifactStore.upsertConversationLink({
5147 clientId: "firefox-claude",
5148 linkId: "link_dispatch_paused",
5149 localConversationId: "lc_dispatch_paused",
5150 observedAt: nowMs - 20_000,
5151 pageTitle: "Paused Renewal",
5152 pageUrl: "https://claude.ai/chat/conv_dispatch_paused",
5153 platform: "claude",
5154 remoteConversationId: "conv_dispatch_paused",
5155 routeParams: {
5156 conversationId: "conv_dispatch_paused"
5157 },
5158 routePath: "/chat/conv_dispatch_paused",
5159 routePattern: "/chat/:conversationId",
5160 targetId: "tab:11",
5161 targetKind: "browser.proxy_delivery",
5162 targetPayload: {
5163 clientId: "firefox-claude",
5164 conversationId: "conv_dispatch_paused",
5165 pageUrl: "https://claude.ai/chat/conv_dispatch_paused",
5166 tabId: 11
5167 }
5168 });
5169 await artifactStore.upsertConversationLink({
5170 clientId: "firefox-claude",
5171 linkId: "link_dispatch_retry",
5172 localConversationId: "lc_dispatch_retry",
5173 observedAt: nowMs - 20_000,
5174 pageTitle: "Retry Renewal",
5175 pageUrl: "https://claude.ai/chat/conv_dispatch_retry",
5176 platform: "claude",
5177 remoteConversationId: "conv_dispatch_retry",
5178 routeParams: {
5179 conversationId: "conv_dispatch_retry"
5180 },
5181 routePath: "/chat/conv_dispatch_retry",
5182 routePattern: "/chat/:conversationId",
5183 targetId: "tab:12",
5184 targetKind: "browser.proxy_delivery",
5185 targetPayload: {
5186 clientId: "firefox-claude",
5187 conversationId: "conv_dispatch_retry",
5188 pageUrl: "https://claude.ai/chat/conv_dispatch_retry",
5189 tabId: 12
5190 }
5191 });
5192 await artifactStore.insertRenewalJob({
5193 jobId: "job_dispatch_paused",
5194 localConversationId: "lc_dispatch_paused",
5195 messageId: "msg_dispatch_paused",
5196 nextAttemptAt: nowMs,
5197 payload: "[renewal] paused",
5198 payloadKind: "text"
5199 });
5200 await artifactStore.insertRenewalJob({
5201 jobId: "job_dispatch_retry",
5202 localConversationId: "lc_dispatch_retry",
5203 maxAttempts: 2,
5204 messageId: "msg_dispatch_retry",
5205 nextAttemptAt: nowMs,
5206 payload: "[renewal] retry",
5207 payloadKind: "text"
5208 });
5209
5210 const pausedContext = createTimedJobRunnerContext({
5211 artifactStore,
5212 config: {
5213 intervalMs: 5_000,
5214 maxMessagesPerTick: 10,
5215 maxTasksPerTick: 10,
5216 settleDelayMs: 0
5217 }
5218 });
5219 const firstResult = await retryRunner.run(pausedContext.context);
5220 const pausedJob = await artifactStore.getRenewalJob("job_dispatch_paused");
5221 const retryAfterFirstAttempt = await artifactStore.getRenewalJob("job_dispatch_retry");
5222
5223 assert.equal(firstResult.result, "ok");
5224 assert.equal(pausedJob.status, "pending");
5225 assert.equal(pausedJob.attemptCount, 0);
5226 assert.equal(pausedJob.nextAttemptAt, nowMs + 10_000);
5227 assert.equal(retryAfterFirstAttempt.status, "pending");
5228 assert.equal(retryAfterFirstAttempt.attemptCount, 1);
5229 assert.equal(retryAfterFirstAttempt.lastError, "no_active_client");
5230 assert.equal(retryAfterFirstAttempt.nextAttemptAt, nowMs + 1);
5231 assert.equal(browserCalls.length, 1);
5232 assert.ok(
5233 pausedContext.entries.find((entry) => entry.stage === "job_deferred" && entry.result === "automation_paused")
5234 );
5235 assert.ok(
5236 pausedContext.entries.find((entry) => entry.stage === "job_retry_scheduled" && entry.result === "no_active_client")
5237 );
5238
5239 nowMs = retryAfterFirstAttempt.nextAttemptAt;
5240 const secondContext = createTimedJobRunnerContext({
5241 artifactStore
5242 });
5243 const secondResult = await retryRunner.run(secondContext.context);
5244 const retryAfterSecondAttempt = await artifactStore.getRenewalJob("job_dispatch_retry");
5245
5246 assert.equal(secondResult.result, "ok");
5247 assert.equal(retryAfterSecondAttempt.status, "failed");
5248 assert.equal(retryAfterSecondAttempt.attemptCount, 2);
5249 assert.equal(retryAfterSecondAttempt.lastError, "no_active_client");
5250 assert.equal(retryAfterSecondAttempt.nextAttemptAt, null);
5251 assert.equal(browserCalls.length, 2);
5252 assert.ok(
5253 secondContext.entries.find((entry) => entry.stage === "job_failed" && entry.result === "no_active_client")
5254 );
5255 } finally {
5256 artifactStore.close();
5257 rmSync(rootDir, {
5258 force: true,
5259 recursive: true
5260 });
5261 }
5262});
5263
5264test("renewal dispatcher defers jobs when the local conversation execution lock is busy", async () => {
5265 const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-dispatcher-busy-"));
5266 const artifactStore = new ArtifactStore({
5267 artifactDir: join(rootDir, ARTIFACTS_DIRNAME),
5268 databasePath: join(rootDir, ARTIFACT_DB_FILENAME)
5269 });
5270 const nowMs = Date.UTC(2026, 2, 30, 13, 30, 0);
5271 let browserCalls = 0;
5272 const runner = createRenewalDispatcherRunner({
5273 browserBridge: {
5274 proxyDelivery() {
5275 browserCalls += 1;
5276 throw new Error("proxyDelivery should not run while the instruction lock is held");
5277 }
5278 },
5279 now: () => nowMs
5280 });
5281
5282 try {
5283 await artifactStore.insertMessage({
5284 conversationId: "conv_dispatch_busy",
5285 id: "msg_dispatch_busy",
5286 observedAt: nowMs - 60_000,
5287 platform: "claude",
5288 rawText: "renewal dispatcher busy message",
5289 role: "assistant"
5290 });
5291 await artifactStore.upsertLocalConversation({
5292 automationStatus: "auto",
5293 executionState: "instruction_running",
5294 localConversationId: "lc_dispatch_busy",
5295 platform: "claude",
5296 updatedAt: nowMs - 1_000
5297 });
5298 await artifactStore.upsertConversationLink({
5299 clientId: "firefox-claude",
5300 linkId: "link_dispatch_busy",
5301 localConversationId: "lc_dispatch_busy",
5302 observedAt: nowMs - 20_000,
5303 pageTitle: "Busy Renewal",
5304 pageUrl: "https://claude.ai/chat/conv_dispatch_busy",
5305 platform: "claude",
5306 remoteConversationId: "conv_dispatch_busy",
5307 routeParams: {
5308 conversationId: "conv_dispatch_busy"
5309 },
5310 routePath: "/chat/conv_dispatch_busy",
5311 routePattern: "/chat/:conversationId",
5312 targetId: "tab:14",
5313 targetKind: "browser.proxy_delivery",
5314 targetPayload: {
5315 clientId: "firefox-claude",
5316 conversationId: "conv_dispatch_busy",
5317 pageUrl: "https://claude.ai/chat/conv_dispatch_busy",
5318 tabId: 14
5319 }
5320 });
5321 await artifactStore.insertRenewalJob({
5322 jobId: "job_dispatch_busy",
5323 localConversationId: "lc_dispatch_busy",
5324 messageId: "msg_dispatch_busy",
5325 nextAttemptAt: nowMs,
5326 payload: "[renewal] busy",
5327 payloadKind: "text"
5328 });
5329
5330 const busyContext = createTimedJobRunnerContext({
5331 artifactStore
5332 });
5333 const result = await runner.run(busyContext.context);
5334 const deferredJob = await artifactStore.getRenewalJob("job_dispatch_busy");
5335
5336 assert.equal(result.result, "ok");
5337 assert.equal(browserCalls, 0);
5338 assert.equal(deferredJob.status, "pending");
5339 assert.equal(deferredJob.attemptCount, 0);
5340 assert.equal(deferredJob.nextAttemptAt, nowMs + 10_000);
5341 assert.ok(
5342 busyContext.entries.find((entry) => entry.stage === "job_deferred" && entry.result === "automation_busy")
5343 );
5344 } finally {
5345 artifactStore.close();
5346 rmSync(rootDir, {
5347 force: true,
5348 recursive: true
5349 });
5350 }
5351});
5352
5353test("renewal dispatcher records timeout failures with a distinct timeout result and timeout_ms", async () => {
5354 const rootDir = mkdtempSync(join(tmpdir(), "baa-renewal-dispatcher-timeout-"));
5355 const stateDir = join(rootDir, "state");
5356 const artifactStore = new ArtifactStore({
5357 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
5358 databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
5359 });
5360 let nowMs = Date.UTC(2026, 2, 30, 14, 0, 0);
5361 const timeoutMs = 250;
5362 let requestCount = 0;
5363 const runner = createRenewalDispatcherRunner({
5364 browserBridge: {
5365 proxyDelivery(input) {
5366 requestCount += 1;
5367 const requestId = `proxy-timeout-${requestCount}`;
5368
5369 return {
5370 clientId: input.clientId || "firefox-claude",
5371 connectionId: "conn-firefox-claude",
5372 dispatchedAt: nowMs,
5373 requestId,
5374 result: Promise.reject(
5375 new FirefoxBridgeError(
5376 "action_timeout",
5377 `Firefox client "${input.clientId || "firefox-claude"}" did not report action_result "${requestId}" within ${timeoutMs}ms.`,
5378 {
5379 clientId: input.clientId || "firefox-claude",
5380 connectionId: "conn-firefox-claude",
5381 requestId,
5382 timeoutMs
5383 }
5384 )
5385 ),
5386 type: "browser.proxy_delivery"
5387 };
5388 }
5389 },
5390 executionTimeoutMs: timeoutMs,
5391 now: () => nowMs,
5392 retryBaseDelayMs: 1,
5393 retryMaxDelayMs: 1
5394 });
5395
5396 try {
5397 await artifactStore.insertMessage({
5398 conversationId: "conv_dispatch_timeout",
5399 id: "msg_dispatch_timeout",
5400 observedAt: nowMs - 60_000,
5401 platform: "claude",
5402 rawText: "renewal dispatcher timeout message",
5403 role: "assistant"
5404 });
5405 await artifactStore.upsertLocalConversation({
5406 automationStatus: "auto",
5407 localConversationId: "lc_dispatch_timeout",
5408 platform: "claude"
5409 });
5410 await artifactStore.upsertConversationLink({
5411 clientId: "firefox-claude",
5412 linkId: "link_dispatch_timeout",
5413 localConversationId: "lc_dispatch_timeout",
5414 observedAt: nowMs - 20_000,
5415 pageTitle: "Timeout Renewal",
5416 pageUrl: "https://claude.ai/chat/conv_dispatch_timeout",
5417 platform: "claude",
5418 remoteConversationId: "conv_dispatch_timeout",
5419 routeParams: {
5420 conversationId: "conv_dispatch_timeout"
5421 },
5422 routePath: "/chat/conv_dispatch_timeout",
5423 routePattern: "/chat/:conversationId",
5424 targetId: "tab:13",
5425 targetKind: "browser.proxy_delivery",
5426 targetPayload: {
5427 clientId: "firefox-claude",
5428 conversationId: "conv_dispatch_timeout",
5429 pageUrl: "https://claude.ai/chat/conv_dispatch_timeout",
5430 tabId: 13
5431 }
5432 });
5433 await artifactStore.insertRenewalJob({
5434 jobId: "job_dispatch_timeout",
5435 localConversationId: "lc_dispatch_timeout",
5436 maxAttempts: 2,
5437 messageId: "msg_dispatch_timeout",
5438 nextAttemptAt: nowMs,
5439 payload: "[renewal] timeout",
5440 payloadKind: "text"
5441 });
5442
5443 const firstContext = createTimedJobRunnerContext({
5444 artifactStore
5445 });
5446 const firstResult = await runner.run(firstContext.context);
5447 const retryJob = await artifactStore.getRenewalJob("job_dispatch_timeout");
5448
5449 assert.equal(firstResult.result, "ok");
5450 assert.equal(retryJob.status, "pending");
5451 assert.equal(retryJob.attemptCount, 1);
5452 assert.equal(retryJob.lastError, "browser_action_timeout");
5453 assert.equal(retryJob.nextAttemptAt, nowMs + 1);
5454 assert.ok(
5455 firstContext.entries.find(
5456 (entry) =>
5457 entry.stage === "job_retry_scheduled"
5458 && entry.result === "browser_action_timeout"
5459 && entry.details?.error_code === "action_timeout"
5460 && entry.details?.timeout_ms === timeoutMs
5461 )
5462 );
5463
5464 nowMs = retryJob.nextAttemptAt;
5465 const secondContext = createTimedJobRunnerContext({
5466 artifactStore
5467 });
5468 const secondResult = await runner.run(secondContext.context);
5469 const failedJob = await artifactStore.getRenewalJob("job_dispatch_timeout");
5470
5471 assert.equal(secondResult.result, "ok");
5472 assert.equal(failedJob.status, "failed");
5473 assert.equal(failedJob.attemptCount, 2);
5474 assert.equal(failedJob.lastError, "browser_action_timeout");
5475 assert.equal(failedJob.nextAttemptAt, null);
5476 assert.ok(
5477 secondContext.entries.find(
5478 (entry) =>
5479 entry.stage === "job_failed"
5480 && entry.result === "browser_action_timeout"
5481 && entry.details?.error_code === "action_timeout"
5482 && entry.details?.timeout_ms === timeoutMs
5483 )
5484 );
5485 } finally {
5486 artifactStore.close();
5487 rmSync(rootDir, {
5488 force: true,
5489 recursive: true
5490 });
5491 }
5492});
5493
5494test("ConductorTimedJobs keeps standby runners idle and clears interval handles on stop", async () => {
5495 const logsDir = mkdtempSync(join(tmpdir(), "baa-timed-jobs-standby-"));
5496 const intervalScheduler = createManualIntervalScheduler();
5497 const daemon = new ConductorDaemon(
5498 {
5499 nodeId: "mac-standby",
5500 host: "mac",
5501 role: "standby",
5502 controlApiBase: "https://control.example.test"
5503 },
5504 {
5505 autoStartLoops: false,
5506 client: {
5507 async acquireLeaderLease() {
5508 return createLeaseResult({
5509 holderId: "mini-main",
5510 term: 7,
5511 leaseExpiresAt: 230,
5512 renewedAt: 200,
5513 isLeader: false,
5514 operation: "acquire"
5515 });
5516 },
5517 async sendControllerHeartbeat() {}
5518 },
5519 now: () => 200
5520 }
5521 );
5522 let runCount = 0;
5523 const timedJobs = new ConductorTimedJobs(
5524 {
5525 intervalMs: 5_000,
5526 maxMessagesPerTick: 10,
5527 maxTasksPerTick: 10,
5528 settleDelayMs: 10_000
5529 },
5530 {
5531 autoStart: true,
5532 clearIntervalImpl: intervalScheduler.clearInterval,
5533 logDir: logsDir,
5534 schedule: (work) => daemon.runSchedulerPass(work),
5535 setIntervalImpl: intervalScheduler.setInterval
5536 }
5537 );
5538
5539 timedJobs.registerRunner({
5540 name: "renewal.dispatcher",
5541 async run() {
5542 runCount += 1;
5543 return {
5544 result: "ok"
5545 };
5546 }
5547 });
5548
5549 try {
5550 await daemon.start();
5551 await timedJobs.start();
5552
5553 assert.equal(intervalScheduler.getActiveCount(), 1);
5554
5555 const tick = await timedJobs.runTick("manual");
5556 assert.equal(tick.decision, "skipped_not_leader");
5557 assert.equal(runCount, 0);
5558
5559 await timedJobs.stop();
5560 assert.equal(intervalScheduler.getActiveCount(), 0);
5561
5562 const entries = await waitForJsonlEntries(logsDir);
5563 assert.ok(
5564 entries.find(
5565 (entry) =>
5566 entry.runner === "renewal.dispatcher"
5567 && entry.stage === "runner_skipped"
5568 && entry.result === "skipped_not_leader"
5569 )
5570 );
5571 } finally {
5572 await timedJobs.stop();
5573 rmSync(logsDir, {
5574 force: true,
5575 recursive: true
5576 });
5577 }
5578});
5579
5580test("ConductorRuntime skips timed-jobs ticks while system automation is paused", async () => {
5581 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-system-paused-state-"));
5582 const logsDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-system-paused-logs-"));
5583 const runtime = new ConductorRuntime(
5584 {
5585 nodeId: "mini-main",
5586 host: "mini",
5587 role: "primary",
5588 controlApiBase: "https://control.example.test",
5589 localApiBase: "http://127.0.0.1:0",
5590 sharedToken: "replace-me",
5591 timedJobsIntervalMs: 50,
5592 paths: {
5593 logsDir,
5594 runsDir: "/tmp/runs",
5595 stateDir
5596 }
5597 },
5598 {
5599 now: () => 250
5600 }
5601 );
5602
5603 try {
5604 const snapshot = await runtime.start();
5605 const baseUrl = snapshot.controlApi.localApiBase;
5606 const pauseResponse = await fetch(`${baseUrl}/v1/system/pause`, {
5607 body: JSON.stringify({
5608 reason: "pause_before_timed_jobs_tick",
5609 requested_by: "integration_test"
5610 }),
5611 headers: {
5612 "content-type": "application/json"
5613 },
5614 method: "POST"
5615 });
5616 assert.equal(pauseResponse.status, 200);
5617
5618 const timedJobsEntries = await waitForJsonlEntries(
5619 join(logsDir, "timed-jobs"),
5620 (items) =>
5621 items.some((entry) =>
5622 entry.runner === "timed-jobs.framework"
5623 && entry.stage === "tick_completed"
5624 && entry.result === "skipped_system_paused"
5625 )
5626 && items.some((entry) =>
5627 entry.runner === "renewal.projector"
5628 && entry.stage === "runner_skipped"
5629 && entry.result === "skipped_system_paused"
5630 )
5631 && items.some((entry) =>
5632 entry.runner === "renewal.dispatcher"
5633 && entry.stage === "runner_skipped"
5634 && entry.result === "skipped_system_paused"
5635 )
5636 );
5637
5638 assert.ok(
5639 timedJobsEntries.find((entry) =>
5640 entry.runner === "timed-jobs.framework"
5641 && entry.stage === "tick_completed"
5642 && entry.result === "skipped_system_paused"
5643 )
5644 );
5645 assert.ok(
5646 timedJobsEntries.find((entry) =>
5647 entry.runner === "renewal.projector"
5648 && entry.stage === "runner_skipped"
5649 && entry.result === "skipped_system_paused"
5650 )
5651 );
5652 assert.ok(
5653 timedJobsEntries.find((entry) =>
5654 entry.runner === "renewal.dispatcher"
5655 && entry.stage === "runner_skipped"
5656 && entry.result === "skipped_system_paused"
5657 )
5658 );
5659 } finally {
5660 await runtime.stop();
5661 rmSync(stateDir, {
5662 force: true,
5663 recursive: true
5664 });
5665 rmSync(logsDir, {
5666 force: true,
5667 recursive: true
5668 });
5669 }
5670});
5671
5672test("createFetchControlApiClient unwraps control-api envelopes and sends bearer auth", async () => {
5673 const observedRequests = [];
5674 const client = createFetchControlApiClient(
5675 "https://control.example.test/",
5676 async (url, init) => {
5677 observedRequests.push({
5678 url,
5679 init
5680 });
5681
5682 return new Response(
5683 JSON.stringify({
5684 ok: true,
5685 request_id: "req-1",
5686 data: {
5687 holder_id: "mini-main",
5688 holder_host: "mini",
5689 term: 3,
5690 lease_expires_at: 130,
5691 renewed_at: 100,
5692 is_leader: true,
5693 operation: "renew",
5694 lease: {
5695 lease_name: "global",
5696 holder_id: "mini-main",
5697 holder_host: "mini",
5698 term: 3,
5699 lease_expires_at: 130,
5700 renewed_at: 100,
5701 preferred_holder_id: "mini-main",
5702 metadata_json: null
5703 }
5704 }
5705 }),
5706 {
5707 status: 200,
5708 headers: {
5709 "content-type": "application/json"
5710 }
5711 }
5712 );
5713 },
5714 {
5715 bearerToken: "secret-token"
5716 }
5717 );
5718
5719 const result = await client.acquireLeaderLease({
5720 controllerId: "mini-main",
5721 host: "mini",
5722 ttlSec: 30,
5723 preferred: true,
5724 now: 100
5725 });
5726
5727 assert.equal(observedRequests.length, 1);
5728 assert.equal(String(observedRequests[0].url), "https://control.example.test/v1/leader/acquire");
5729 assert.equal(
5730 new Headers(observedRequests[0].init.headers).get("authorization"),
5731 "Bearer secret-token"
5732 );
5733 assert.deepEqual(JSON.parse(String(observedRequests[0].init.body)), {
5734 controller_id: "mini-main",
5735 host: "mini",
5736 ttl_sec: 30,
5737 preferred: true,
5738 now: 100
5739 });
5740 assert.equal(result.holderId, "mini-main");
5741 assert.equal(result.holderHost, "mini");
5742 assert.equal(result.term, 3);
5743 assert.equal(result.operation, "renew");
5744 assert.equal(result.isLeader, true);
5745});
5746
5747test("parseConductorCliRequest merges launchd env defaults with CLI overrides", () => {
5748 const request = parseConductorCliRequest(["start", "--role", "standby", "--run-once"], {
5749 BAA_NODE_ID: "mini-main",
5750 BAA_CONDUCTOR_HOST: "mini",
5751 BAA_CONDUCTOR_ROLE: "primary",
5752 BAA_CONDUCTOR_PUBLIC_API_BASE: "https://public.example.test/",
5753 BAA_CODE_ROOT_DIR: "/tmp/code-root/",
5754 BAA_CODEXD_LOCAL_API_BASE: "http://127.0.0.1:4323/",
5755 BAA_CONDUCTOR_LOCAL_API: "http://127.0.0.1:4317/",
5756 BAA_SHARED_TOKEN: "replace-me",
5757 BAA_RUNS_DIR: "/tmp/runs"
5758 });
5759
5760 assert.equal(request.action, "start");
5761
5762 if (request.action !== "start") {
5763 throw new Error("expected start action");
5764 }
5765
5766 assert.equal(request.runOnce, true);
5767 assert.equal(request.config.role, "standby");
5768 assert.equal(request.config.nodeId, "mini-main");
5769 assert.equal(request.config.publicApiBase, "https://public.example.test");
5770 assert.equal(request.config.controlApiBase, "https://public.example.test");
5771 assert.equal(request.config.codeRootDir, "/tmp/code-root");
5772 assert.equal(request.config.codexdLocalApiBase, "http://127.0.0.1:4323");
5773 assert.equal(request.config.localApiBase, "http://127.0.0.1:4317");
5774 assert.equal(request.config.paths.runsDir, "/tmp/runs");
5775});
5776
5777test("parseConductorCliRequest prefers the canonical public API env name over the legacy alias", () => {
5778 const request = parseConductorCliRequest(["config"], {
5779 BAA_NODE_ID: "mini-main",
5780 BAA_CONDUCTOR_HOST: "mini",
5781 BAA_CONDUCTOR_ROLE: "primary",
5782 BAA_CONDUCTOR_PUBLIC_API_BASE: "https://public.example.test/",
5783 BAA_CONTROL_API_BASE: "https://legacy.example.test/",
5784 BAA_SHARED_TOKEN: "replace-me"
5785 });
5786
5787 assert.equal(request.action, "config");
5788
5789 if (request.action !== "config") {
5790 throw new Error("expected config action");
5791 }
5792
5793 assert.equal(request.config.publicApiBase, "https://public.example.test");
5794 assert.equal(request.config.controlApiBase, "https://public.example.test");
5795});
5796
5797test("parseConductorCliRequest prefers --public-api-base over --control-api-base", () => {
5798 const request = parseConductorCliRequest(
5799 [
5800 "config",
5801 "--control-api-base",
5802 "https://legacy-cli.example.test/",
5803 "--public-api-base",
5804 "https://public-cli.example.test/"
5805 ],
5806 {
5807 BAA_NODE_ID: "mini-main",
5808 BAA_CONDUCTOR_HOST: "mini",
5809 BAA_CONDUCTOR_ROLE: "primary",
5810 BAA_SHARED_TOKEN: "replace-me"
5811 }
5812 );
5813
5814 assert.equal(request.action, "config");
5815
5816 if (request.action !== "config") {
5817 throw new Error("expected config action");
5818 }
5819
5820 assert.equal(request.config.publicApiBase, "https://public-cli.example.test");
5821 assert.equal(request.config.controlApiBase, "https://public-cli.example.test");
5822});
5823
5824test("parseConductorCliRequest allows an explicitly listed Tailscale local API host", () => {
5825 const request = parseConductorCliRequest(["config"], {
5826 BAA_NODE_ID: "mini-main",
5827 BAA_CONDUCTOR_HOST: "mini",
5828 BAA_CONDUCTOR_ROLE: "primary",
5829 BAA_CONTROL_API_BASE: "https://control.example.test/",
5830 BAA_CONDUCTOR_LOCAL_API: "http://100.71.210.78:4317/",
5831 BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS: "100.71.210.78",
5832 BAA_SHARED_TOKEN: "replace-me"
5833 });
5834
5835 assert.equal(request.action, "config");
5836
5837 if (request.action !== "config") {
5838 throw new Error("expected config action");
5839 }
5840
5841 assert.equal(request.config.localApiBase, "http://100.71.210.78:4317");
5842 assert.deepEqual(request.config.localApiAllowedHosts, ["100.71.210.78"]);
5843});
5844
5845test("parseConductorCliRequest rejects unlisted or non-Tailscale local API hosts", () => {
5846 assert.throws(
5847 () =>
5848 parseConductorCliRequest(["config"], {
5849 BAA_NODE_ID: "mini-main",
5850 BAA_CONDUCTOR_HOST: "mini",
5851 BAA_CONDUCTOR_ROLE: "primary",
5852 BAA_CONTROL_API_BASE: "https://control.example.test/",
5853 BAA_CONDUCTOR_LOCAL_API: "http://100.71.210.78:4317/",
5854 BAA_SHARED_TOKEN: "replace-me"
5855 }),
5856 /explicitly allowed Tailscale 100\.x host/
5857 );
5858
5859 assert.throws(
5860 () =>
5861 parseConductorCliRequest(["config"], {
5862 BAA_NODE_ID: "mini-main",
5863 BAA_CONDUCTOR_HOST: "mini",
5864 BAA_CONDUCTOR_ROLE: "primary",
5865 BAA_CONTROL_API_BASE: "https://control.example.test/",
5866 BAA_CONDUCTOR_LOCAL_API: "http://100.71.210.78:4317/",
5867 BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS: "192.168.1.50",
5868 BAA_SHARED_TOKEN: "replace-me"
5869 }),
5870 /localApiAllowedHosts/
5871 );
5872});
5873
5874test("handleConductorHttpRequest keeps degraded runtimes observable but not ready", async () => {
5875 const snapshot = {
5876 claudeCoded: {
5877 localApiBase: null
5878 },
5879 codexd: {
5880 localApiBase: null
5881 },
5882 daemon: {
5883 nodeId: "mini-main",
5884 host: "mini",
5885 role: "primary",
5886 leaseState: "degraded",
5887 schedulerEnabled: false,
5888 currentLeaderId: null,
5889 currentTerm: null,
5890 leaseExpiresAt: null,
5891 lastHeartbeatAt: 100,
5892 lastLeaseOperation: "acquire",
5893 nextLeaseOperation: "acquire",
5894 consecutiveRenewFailures: 2,
5895 lastError: "lease endpoint timeout"
5896 },
5897 identity: "mini-main@mini(primary)",
5898 controlApi: {
5899 baseUrl: "https://control.example.test",
5900 localApiBase: "http://127.0.0.1:4317",
5901 hasSharedToken: true,
5902 usesPlaceholderToken: false
5903 },
5904 runtime: {
5905 pid: 123,
5906 started: true,
5907 startedAt: 100
5908 },
5909 startupChecklist: [],
5910 warnings: []
5911 };
5912
5913 const readyResponse = await handleConductorHttpRequest(
5914 {
5915 method: "GET",
5916 path: "/readyz"
5917 },
5918 {
5919 repository: null,
5920 snapshotLoader: () => snapshot
5921 }
5922 );
5923 assert.equal(readyResponse.status, 503);
5924 assert.equal(readyResponse.body, "not_ready\n");
5925
5926 const roleResponse = await handleConductorHttpRequest(
5927 {
5928 method: "GET",
5929 path: "/rolez"
5930 },
5931 {
5932 repository: null,
5933 snapshotLoader: () => snapshot
5934 }
5935 );
5936 assert.equal(roleResponse.status, 200);
5937 assert.equal(roleResponse.body, "standby\n");
5938});
5939
5940test("handleConductorHttpRequest serves the migrated local business endpoints from the local repository", async () => {
5941 const { repository, sharedToken, snapshot } = await createLocalApiFixture();
5942 const browser = createBrowserBridgeStub();
5943 const codexd = await startCodexdStubServer();
5944 const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-conductor-local-host-http-"));
5945 const authorizedHeaders = {
5946 authorization: `Bearer ${sharedToken}`
5947 };
5948 snapshot.codexd.localApiBase = codexd.baseUrl;
5949 const browserState = browser.context.browserStateLoader();
5950 browserState.clients[0].request_hooks.push(
5951 {
5952 account: "ops@example.com",
5953 credential_fingerprint: "fp-chatgpt-stub",
5954 platform: "chatgpt",
5955 endpoint_count: 1,
5956 endpoint_metadata: [
5957 {
5958 method: "POST",
5959 path: "/backend-api/conversation",
5960 first_seen_at: 1710000002600,
5961 last_seen_at: 1710000003600
5962 }
5963 ],
5964 endpoints: [
5965 "POST /backend-api/conversation"
5966 ],
5967 last_verified_at: 1710000003650,
5968 updated_at: 1710000003550
5969 },
5970 {
5971 account: "ops@example.com",
5972 credential_fingerprint: "fp-gemini-stub",
5973 platform: "gemini",
5974 endpoint_count: 1,
5975 endpoint_metadata: [
5976 {
5977 method: "POST",
5978 path: "/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate",
5979 first_seen_at: 1710000002700,
5980 last_seen_at: 1710000003700
5981 }
5982 ],
5983 endpoints: [
5984 "POST /_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
5985 ],
5986 last_verified_at: 1710000003750,
5987 updated_at: 1710000003650
5988 }
5989 );
5990 browserState.clients[0].shell_runtime.push(
5991 buildShellRuntime("chatgpt", {
5992 actual: {
5993 ...buildShellRuntime("chatgpt").actual,
5994 url: "https://chatgpt.com/c/conv-chatgpt-current"
5995 }
5996 }),
5997 buildShellRuntime("gemini", {
5998 actual: {
5999 ...buildShellRuntime("gemini").actual,
6000 url: "https://gemini.google.com/app/conv-gemini-current"
6001 }
6002 })
6003 );
6004 browserState.clients[0].final_messages.push(
6005 {
6006 conversation_id: "conv-chatgpt-current",
6007 observed_at: 1710000003800,
6008 page_title: "ChatGPT Current",
6009 page_url: "https://chatgpt.com/c/conv-chatgpt-current",
6010 platform: "chatgpt",
6011 raw_text: "hello from chatgpt current"
6012 },
6013 {
6014 conversation_id: "conv-gemini-current",
6015 observed_at: 1710000003900,
6016 page_title: "Gemini Current",
6017 page_url: "https://gemini.google.com/app/conv-gemini-current",
6018 platform: "gemini",
6019 raw_text: "hello from gemini current"
6020 }
6021 );
6022 const localApiContext = {
6023 ...browser.context,
6024 browserStateLoader: () => browserState,
6025 codexdLocalApiBase: codexd.baseUrl,
6026 fetchImpl: globalThis.fetch,
6027 repository,
6028 sharedToken,
6029 snapshotLoader: () => snapshot
6030 };
6031 const versionedLocalApiContext = {
6032 ...localApiContext,
6033 version: "1.2.3"
6034 };
6035
6036 try {
6037 const describeResponse = await handleConductorHttpRequest(
6038 {
6039 method: "GET",
6040 path: "/describe"
6041 },
6042 versionedLocalApiContext
6043 );
6044 assert.equal(describeResponse.status, 200);
6045 const describePayload = parseJsonBody(describeResponse);
6046 assert.equal(describePayload.ok, true);
6047 assert.equal(describePayload.data.name, "baa-conductor-daemon");
6048 assert.equal(describePayload.data.system.mode, "running");
6049 assert.equal(describePayload.data.describe_endpoints.business.path, "/describe/business");
6050 assert.equal(describePayload.data.describe_endpoints.control.path, "/describe/control");
6051 assert.equal(describePayload.data.codex.enabled, true);
6052 assert.equal(describePayload.data.codex.target_base_url, codexd.baseUrl);
6053 assert.equal(describePayload.data.browser.route_prefix, "/v1/browser");
6054 assert.equal(describePayload.data.browser.action_contract.route.path, "/v1/browser/actions");
6055 assert.equal(describePayload.data.browser.request_contract.route.path, "/v1/browser/request");
6056 assert.equal(
6057 describePayload.data.browser.cancel_contract.route.path,
6058 "/v1/browser/request/cancel"
6059 );
6060 assert.equal(describePayload.data.host_operations.enabled, true);
6061 assert.equal(describePayload.data.host_operations.auth.header, "Authorization: Bearer <BAA_SHARED_TOKEN>");
6062 assert.equal(describePayload.data.host_operations.auth.configured, true);
6063 assert.deepEqual(
6064 describePayload.data.browser.routes.map((route) => route.path),
6065 [
6066 "/v1/browser",
6067 "/v1/browser/actions",
6068 "/v1/browser/request",
6069 "/v1/browser/request/cancel"
6070 ]
6071 );
6072 assert.deepEqual(
6073 describePayload.data.browser.request_contract.supported_platforms,
6074 ["claude", "chatgpt", "gemini"]
6075 );
6076 assert.deepEqual(
6077 describePayload.data.browser.action_contract.supported_platforms,
6078 ["claude", "chatgpt"]
6079 );
6080 const legacyClaudeOpen = describePayload.data.browser.legacy_routes.find(
6081 (route) => route.path === "/v1/browser/claude/open"
6082 );
6083 assert.equal(legacyClaudeOpen.lifecycle, "legacy");
6084 assert.equal(legacyClaudeOpen.legacy_replacement_path, "/v1/browser/actions");
6085 assert.deepEqual(
6086 describePayload.data.browser.legacy_helper_platforms,
6087 ["claude", "chatgpt", "gemini"]
6088 );
6089 assert.ok(
6090 describePayload.data.browser.legacy_routes.find(
6091 (route) => route.path === "/v1/browser/chatgpt/send"
6092 )
6093 );
6094 assert.ok(
6095 describePayload.data.browser.legacy_routes.find(
6096 (route) => route.path === "/v1/browser/gemini/current"
6097 )
6098 );
6099 assert.doesNotMatch(JSON.stringify(describePayload.data.codex.routes), /\/v1\/codex\/runs/u);
6100 assert.doesNotMatch(JSON.stringify(describePayload.data.capabilities.read_endpoints), /\/v1\/runs/u);
6101
6102 const businessDescribeResponse = await handleConductorHttpRequest(
6103 {
6104 method: "GET",
6105 path: "/describe/business"
6106 },
6107 versionedLocalApiContext
6108 );
6109 assert.equal(businessDescribeResponse.status, 200);
6110 const businessDescribePayload = parseJsonBody(businessDescribeResponse);
6111 assert.equal(businessDescribePayload.data.surface, "business");
6112 assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/tasks/u);
6113 assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/codex/u);
6114 assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/browser\/request/u);
6115 assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/browser\/request\/cancel/u);
6116 assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/browser\/claude\/current/u);
6117 assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/browser\/chatgpt\/send/u);
6118 assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/browser\/chatgpt\/current/u);
6119 assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/browser\/gemini\/send/u);
6120 assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/browser\/gemini\/current/u);
6121 assert.doesNotMatch(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/browser\/actions/u);
6122 assert.doesNotMatch(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/browser\/claude\/open/u);
6123 assert.doesNotMatch(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/system\/pause/u);
6124 assert.doesNotMatch(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/exec"/u);
6125 assert.doesNotMatch(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/runs/u);
6126 assert.equal(businessDescribePayload.data.codex.backend, "independent_codexd");
6127 assert.equal(businessDescribePayload.data.browser.request_contract.route.path, "/v1/browser/request");
6128 assert.deepEqual(
6129 businessDescribePayload.data.browser.request_contract.supported_platforms,
6130 ["claude", "chatgpt", "gemini"]
6131 );
6132 assert.match(
6133 JSON.stringify(businessDescribePayload.data.browser.request_contract.supported_response_modes),
6134 /"sse"/u
6135 );
6136
6137 const controlDescribeResponse = await handleConductorHttpRequest(
6138 {
6139 method: "GET",
6140 path: "/describe/control"
6141 },
6142 versionedLocalApiContext
6143 );
6144 assert.equal(controlDescribeResponse.status, 200);
6145 const controlDescribePayload = parseJsonBody(controlDescribeResponse);
6146 assert.equal(controlDescribePayload.data.surface, "control");
6147 assert.match(JSON.stringify(controlDescribePayload.data.endpoints), /\/v1\/browser/u);
6148 assert.match(JSON.stringify(controlDescribePayload.data.endpoints), /\/v1\/browser\/actions/u);
6149 assert.match(JSON.stringify(controlDescribePayload.data.endpoints), /\/v1\/browser\/claude\/open/u);
6150 assert.match(JSON.stringify(controlDescribePayload.data.endpoints), /\/v1\/system\/pause/u);
6151 assert.match(JSON.stringify(controlDescribePayload.data.endpoints), /\/v1\/exec/u);
6152 assert.doesNotMatch(JSON.stringify(controlDescribePayload.data.endpoints), /\/v1\/tasks/u);
6153 assert.equal(controlDescribePayload.data.codex.target_base_url, codexd.baseUrl);
6154 assert.equal(controlDescribePayload.data.browser.action_contract.route.path, "/v1/browser/actions");
6155 assert.equal(controlDescribePayload.data.browser.action_contract.response_body.accepted, "布尔值;插件是否接受了该动作。");
6156 assert.deepEqual(
6157 controlDescribePayload.data.browser.action_contract.supported_platforms,
6158 ["claude", "chatgpt"]
6159 );
6160 assert.equal(controlDescribePayload.data.host_operations.auth.header, "Authorization: Bearer <BAA_SHARED_TOKEN>");
6161
6162 const healthResponse = await handleConductorHttpRequest(
6163 {
6164 method: "GET",
6165 path: "/health"
6166 },
6167 versionedLocalApiContext
6168 );
6169 assert.equal(healthResponse.status, 200);
6170 assert.equal(parseJsonBody(healthResponse).data.status, "ok");
6171
6172 const capabilitiesResponse = await handleConductorHttpRequest(
6173 {
6174 method: "GET",
6175 path: "/v1/capabilities"
6176 },
6177 versionedLocalApiContext
6178 );
6179 assert.equal(capabilitiesResponse.status, 200);
6180 const capabilitiesPayload = parseJsonBody(capabilitiesResponse);
6181 assert.equal(capabilitiesPayload.data.codex.backend, "independent_codexd");
6182 assert.equal(capabilitiesPayload.data.browser.route_prefix, "/v1/browser");
6183 assert.match(JSON.stringify(capabilitiesPayload.data.read_endpoints), /\/v1\/codex/u);
6184 assert.match(JSON.stringify(capabilitiesPayload.data.read_endpoints), /\/v1\/browser/u);
6185 assert.match(JSON.stringify(capabilitiesPayload.data.write_endpoints), /\/v1\/browser\/actions/u);
6186 assert.match(JSON.stringify(capabilitiesPayload.data.write_endpoints), /\/v1\/browser\/request/u);
6187 assert.doesNotMatch(JSON.stringify(capabilitiesPayload.data.read_endpoints), /\/v1\/runs/u);
6188
6189 const browserStatusResponse = await handleConductorHttpRequest(
6190 {
6191 method: "GET",
6192 path: "/v1/browser"
6193 },
6194 localApiContext
6195 );
6196 assert.equal(browserStatusResponse.status, 200);
6197 const browserStatusPayload = parseJsonBody(browserStatusResponse);
6198 assert.equal(browserStatusPayload.data.bridge.client_count, 1);
6199 assert.equal(browserStatusPayload.data.current_client.client_id, "firefox-claude");
6200 assert.equal(browserStatusPayload.data.current_client.shell_runtime[0].platform, "claude");
6201 assert.equal(browserStatusPayload.data.current_client.last_action_result.action, "plugin_status");
6202 assert.equal(browserStatusPayload.data.claude.ready, true);
6203 assert.equal(browserStatusPayload.data.claude.shell_runtime.platform, "claude");
6204 assert.equal(browserStatusPayload.data.records[0].live.credentials.account, "ops@example.com");
6205 assert.equal(browserStatusPayload.data.records[0].live.shell_runtime.platform, "claude");
6206 assert.equal(browserStatusPayload.data.records[0].status, "fresh");
6207
6208 const browserActionsResponse = await handleConductorHttpRequest(
6209 {
6210 body: JSON.stringify({
6211 action: "tab_open",
6212 client_id: "firefox-claude",
6213 platform: "claude"
6214 }),
6215 method: "POST",
6216 path: "/v1/browser/actions"
6217 },
6218 localApiContext
6219 );
6220 assert.equal(browserActionsResponse.status, 200);
6221 const browserActionPayload = parseJsonBody(browserActionsResponse);
6222 assert.equal(browserActionPayload.data.action, "tab_open");
6223 assert.equal(browserActionPayload.data.accepted, true);
6224 assert.equal(browserActionPayload.data.completed, true);
6225 assert.equal(browserActionPayload.data.shell_runtime[0].platform, "claude");
6226
6227 const browserPluginActionResponse = await handleConductorHttpRequest(
6228 {
6229 body: JSON.stringify({
6230 action: "plugin_status",
6231 client_id: "firefox-claude"
6232 }),
6233 method: "POST",
6234 path: "/v1/browser/actions"
6235 },
6236 localApiContext
6237 );
6238 assert.equal(browserPluginActionResponse.status, 200);
6239 const browserPluginActionPayload = parseJsonBody(browserPluginActionResponse);
6240 assert.equal(browserPluginActionPayload.data.action, "plugin_status");
6241 assert.equal(browserPluginActionPayload.data.result.platform_count, 1);
6242
6243 const browserReconnectActionResponse = await handleConductorHttpRequest(
6244 {
6245 body: JSON.stringify({
6246 action: "ws_reconnect",
6247 client_id: "firefox-claude",
6248 disconnect_ms: 2500,
6249 repeat_count: 3,
6250 repeat_interval_ms: 750
6251 }),
6252 method: "POST",
6253 path: "/v1/browser/actions"
6254 },
6255 localApiContext
6256 );
6257 assert.equal(browserReconnectActionResponse.status, 200);
6258 const browserReconnectActionPayload = parseJsonBody(browserReconnectActionResponse);
6259 assert.equal(browserReconnectActionPayload.data.action, "ws_reconnect");
6260 const browserReconnectCall = browser.calls[browser.calls.length - 1];
6261 assert.equal(browserReconnectCall.kind, "dispatchPluginAction");
6262 assert.equal(browserReconnectCall.disconnectMs, 2500);
6263 assert.equal(browserReconnectCall.repeatCount, 3);
6264 assert.equal(browserReconnectCall.repeatIntervalMs, 750);
6265
6266 const browserRequestResponse = await handleConductorHttpRequest(
6267 {
6268 body: JSON.stringify({
6269 platform: "claude",
6270 prompt: "hello generic browser request"
6271 }),
6272 method: "POST",
6273 path: "/v1/browser/request"
6274 },
6275 localApiContext
6276 );
6277 assert.equal(browserRequestResponse.status, 200);
6278 const browserRequestPayload = parseJsonBody(browserRequestResponse);
6279 assert.equal(browserRequestPayload.data.organization.organization_id, "org-1");
6280 assert.equal(browserRequestPayload.data.conversation.conversation_id, "conv-1");
6281 assert.equal(browserRequestPayload.data.request_mode, "claude_prompt");
6282 assert.equal(
6283 browserRequestPayload.data.proxy.path,
6284 "/api/organizations/org-1/chat_conversations/conv-1/completion"
6285 );
6286 assert.equal(browserRequestPayload.data.policy.target_client_id, "firefox-claude");
6287
6288 const bufferedSseResponse = await handleConductorHttpRequest(
6289 {
6290 body: JSON.stringify({
6291 platform: "claude",
6292 method: "GET",
6293 path: "/api/stream-buffered-smoke",
6294 requestId: "browser-buffered-sse-123"
6295 }),
6296 method: "POST",
6297 path: "/v1/browser/request"
6298 },
6299 localApiContext
6300 );
6301 assert.equal(bufferedSseResponse.status, 200);
6302 const bufferedSsePayload = parseJsonBody(bufferedSseResponse);
6303 assert.equal(bufferedSsePayload.data.request_mode, "api_request");
6304 assert.equal(bufferedSsePayload.data.proxy.path, "/api/stream-buffered-smoke");
6305 assert.equal(bufferedSsePayload.data.response.content_type, "text/event-stream");
6306 assert.equal(bufferedSsePayload.data.response.events.length, 3);
6307 assert.equal(bufferedSsePayload.data.response.events[0].event, "completion");
6308 assert.equal(bufferedSsePayload.data.response.events[0].data.type, "completion");
6309 assert.equal(bufferedSsePayload.data.response.events[0].data.completion, "Hello ");
6310 assert.equal(bufferedSsePayload.data.response.events[1].data.completion, "world");
6311 assert.equal(bufferedSsePayload.data.response.events[2].data.completion, "");
6312 assert.equal(bufferedSsePayload.data.response.events[2].data.id, "chatcompl_01-buffered");
6313 assert.equal(bufferedSsePayload.data.response.events[2].data.stop, "\n\nHuman:");
6314 assert.equal(bufferedSsePayload.data.response.full_text, "Hello world");
6315
6316 const chatgptBufferedSseResponse = await handleConductorHttpRequest(
6317 {
6318 body: JSON.stringify({
6319 platform: "chatgpt",
6320 method: "GET",
6321 path: "/backend-api/conversation-buffered-smoke",
6322 requestId: "browser-chatgpt-buffered-sse-123"
6323 }),
6324 method: "POST",
6325 path: "/v1/browser/request"
6326 },
6327 localApiContext
6328 );
6329 assert.equal(chatgptBufferedSseResponse.status, 200);
6330 const chatgptBufferedSsePayload = parseJsonBody(chatgptBufferedSseResponse);
6331 assert.equal(chatgptBufferedSsePayload.data.request_mode, "api_request");
6332 assert.equal(chatgptBufferedSsePayload.data.proxy.path, "/backend-api/conversation-buffered-smoke");
6333 assert.equal(chatgptBufferedSsePayload.data.response.content_type, "text/event-stream");
6334 assert.equal(chatgptBufferedSsePayload.data.response.events[0].event, "message");
6335 assert.equal(
6336 chatgptBufferedSsePayload.data.response.events[0].data.message.content.parts[0],
6337 "Buffered ChatGPT answer"
6338 );
6339 assert.equal(chatgptBufferedSsePayload.data.response.full_text, "Buffered ChatGPT answer");
6340
6341 const chatgptBufferedResponse = await handleConductorHttpRequest(
6342 {
6343 body: JSON.stringify({
6344 platform: "chatgpt",
6345 method: "GET",
6346 path: "/backend-api/models",
6347 requestId: "browser-chatgpt-buffered-123"
6348 }),
6349 method: "POST",
6350 path: "/v1/browser/request"
6351 },
6352 localApiContext
6353 );
6354 assert.equal(chatgptBufferedResponse.status, 200);
6355 const chatgptBufferedPayload = parseJsonBody(chatgptBufferedResponse);
6356 assert.equal(chatgptBufferedPayload.data.request_mode, "api_request");
6357 assert.equal(chatgptBufferedPayload.data.proxy.path, "/backend-api/models");
6358 assert.equal(chatgptBufferedPayload.data.proxy.request_id, "browser-chatgpt-buffered-123");
6359 assert.equal(chatgptBufferedPayload.data.response.models[0].slug, "gpt-5.4");
6360 assert.equal(chatgptBufferedPayload.data.policy.platform, "chatgpt");
6361
6362 const geminiBufferedResponse = await handleConductorHttpRequest(
6363 {
6364 body: JSON.stringify({
6365 conversationId: "conv-gemini-current",
6366 platform: "gemini",
6367 prompt: "hello generic gemini relay",
6368 requestId: "browser-gemini-buffered-123",
6369 path: "/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
6370 }),
6371 method: "POST",
6372 path: "/v1/browser/request"
6373 },
6374 localApiContext
6375 );
6376 assert.equal(geminiBufferedResponse.status, 200);
6377 const geminiBufferedPayload = parseJsonBody(geminiBufferedResponse);
6378 assert.equal(geminiBufferedPayload.data.request_mode, "api_request");
6379 assert.equal(
6380 geminiBufferedPayload.data.proxy.path,
6381 "/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
6382 );
6383 assert.equal(geminiBufferedPayload.data.proxy.request_id, "browser-gemini-buffered-123");
6384 assert.equal(geminiBufferedPayload.data.response.conversation_id, "conv-gemini-current");
6385 assert.equal(geminiBufferedPayload.data.policy.platform, "gemini");
6386 const geminiBufferedCall = browser.calls.find(
6387 (entry) => entry.kind === "apiRequest" && entry.id === "browser-gemini-buffered-123"
6388 );
6389 assert.ok(geminiBufferedCall);
6390 assert.equal(geminiBufferedCall.conversationId, "conv-gemini-current");
6391
6392 const chatgptLegacySendResponse = await handleConductorHttpRequest(
6393 {
6394 body: JSON.stringify({
6395 prompt: "legacy helper chatgpt"
6396 }),
6397 method: "POST",
6398 path: "/v1/browser/chatgpt/send"
6399 },
6400 localApiContext
6401 );
6402 assert.equal(chatgptLegacySendResponse.status, 200);
6403 const chatgptLegacySendPayload = parseJsonBody(chatgptLegacySendResponse);
6404 assert.equal(chatgptLegacySendPayload.data.platform, "chatgpt");
6405 assert.equal(chatgptLegacySendPayload.data.proxy.path, "/backend-api/conversation");
6406
6407 const chatgptLegacyCurrentResponse = await handleConductorHttpRequest(
6408 {
6409 method: "GET",
6410 path: "/v1/browser/chatgpt/current"
6411 },
6412 localApiContext
6413 );
6414 assert.equal(chatgptLegacyCurrentResponse.status, 200);
6415 const chatgptLegacyCurrentPayload = parseJsonBody(chatgptLegacyCurrentResponse);
6416 assert.equal(chatgptLegacyCurrentPayload.data.conversation.conversation_id, "conv-chatgpt-current");
6417 assert.equal(
6418 chatgptLegacyCurrentPayload.data.proxy.path,
6419 "/backend-api/conversation/conv-chatgpt-current"
6420 );
6421
6422 const geminiLegacySendResponse = await handleConductorHttpRequest(
6423 {
6424 body: JSON.stringify({
6425 prompt: "legacy helper gemini"
6426 }),
6427 method: "POST",
6428 path: "/v1/browser/gemini/send"
6429 },
6430 localApiContext
6431 );
6432 assert.equal(geminiLegacySendResponse.status, 200);
6433 const geminiLegacySendPayload = parseJsonBody(geminiLegacySendResponse);
6434 assert.equal(geminiLegacySendPayload.data.platform, "gemini");
6435 assert.equal(
6436 geminiLegacySendPayload.data.proxy.path,
6437 "/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
6438 );
6439
6440 const geminiLegacyCurrentResponse = await handleConductorHttpRequest(
6441 {
6442 method: "GET",
6443 path: "/v1/browser/gemini/current"
6444 },
6445 localApiContext
6446 );
6447 assert.equal(geminiLegacyCurrentResponse.status, 200);
6448 const geminiLegacyCurrentPayload = parseJsonBody(geminiLegacyCurrentResponse);
6449 assert.equal(geminiLegacyCurrentPayload.data.conversation.conversation_id, "conv-gemini-current");
6450 assert.equal(geminiLegacyCurrentPayload.data.proxy, null);
6451
6452 const browserStreamResponse = await handleConductorHttpRequest(
6453 {
6454 body: JSON.stringify({
6455 platform: "claude",
6456 prompt: "hello browser stream",
6457 responseMode: "sse",
6458 requestId: "browser-stream-123"
6459 }),
6460 method: "POST",
6461 path: "/v1/browser/request"
6462 },
6463 localApiContext
6464 );
6465 assert.equal(browserStreamResponse.status, 200);
6466 assert.equal(browserStreamResponse.headers["content-type"], "text/event-stream; charset=utf-8");
6467 const browserStreamText = await readResponseBodyText(browserStreamResponse);
6468 const browserStreamFrames = parseSseFrames(browserStreamText);
6469 assert.deepEqual(
6470 browserStreamFrames.map((frame) => frame.event),
6471 ["stream_open", "stream_event", "stream_end"]
6472 );
6473 assert.equal(browserStreamFrames[0].data.request_id, "browser-stream-123");
6474 assert.equal(browserStreamFrames[1].data.seq, 1);
6475 assert.equal(browserStreamFrames[2].data.stream_id, "browser-stream-123");
6476
6477 const browserRequestCancelResponse = await handleConductorHttpRequest(
6478 {
6479 body: JSON.stringify({
6480 platform: "claude",
6481 request_id: "browser-stream-123"
6482 }),
6483 method: "POST",
6484 path: "/v1/browser/request/cancel"
6485 },
6486 localApiContext
6487 );
6488 assert.equal(browserRequestCancelResponse.status, 200);
6489 const browserRequestCancelPayload = parseJsonBody(browserRequestCancelResponse);
6490 assert.equal(browserRequestCancelPayload.data.status, "cancel_requested");
6491 assert.equal(browserRequestCancelPayload.data.type, "request_cancel");
6492
6493 const browserOpenResponse = await handleConductorHttpRequest(
6494 {
6495 body: JSON.stringify({
6496 client_id: "firefox-claude"
6497 }),
6498 method: "POST",
6499 path: "/v1/browser/claude/open"
6500 },
6501 localApiContext
6502 );
6503 assert.equal(browserOpenResponse.status, 200);
6504 assert.equal(parseJsonBody(browserOpenResponse).data.client_id, "firefox-claude");
6505
6506 const controllersResponse = await handleConductorHttpRequest(
6507 {
6508 method: "GET",
6509 path: "/v1/controllers?limit=5"
6510 },
6511 localApiContext
6512 );
6513 const controllersPayload = parseJsonBody(controllersResponse);
6514 assert.equal(controllersPayload.data.count, 1);
6515 assert.equal(controllersPayload.data.controllers[0].controller_id, "mini-main");
6516 assert.equal(controllersPayload.data.controllers[0].is_leader, true);
6517
6518 const tasksResponse = await handleConductorHttpRequest(
6519 {
6520 method: "GET",
6521 path: "/v1/tasks?status=running&limit=5"
6522 },
6523 localApiContext
6524 );
6525 const tasksPayload = parseJsonBody(tasksResponse);
6526 assert.equal(tasksPayload.data.count, 1);
6527 assert.equal(tasksPayload.data.tasks[0].task_id, "task_demo");
6528
6529 const taskResponse = await handleConductorHttpRequest(
6530 {
6531 method: "GET",
6532 path: "/v1/tasks/task_demo"
6533 },
6534 localApiContext
6535 );
6536 assert.equal(parseJsonBody(taskResponse).data.task_id, "task_demo");
6537
6538 const taskLogsResponse = await handleConductorHttpRequest(
6539 {
6540 method: "GET",
6541 path: "/v1/tasks/task_demo/logs?limit=10"
6542 },
6543 localApiContext
6544 );
6545 const taskLogsPayload = parseJsonBody(taskLogsResponse);
6546 assert.equal(taskLogsPayload.data.task_id, "task_demo");
6547 assert.equal(taskLogsPayload.data.entries.length, 1);
6548 assert.equal(taskLogsPayload.data.entries[0].message, "hello from local api");
6549
6550 const runsResponse = await handleConductorHttpRequest(
6551 {
6552 method: "GET",
6553 path: "/v1/runs?limit=5"
6554 },
6555 localApiContext
6556 );
6557 const runsPayload = parseJsonBody(runsResponse);
6558 assert.equal(runsPayload.data.count, 1);
6559 assert.equal(runsPayload.data.runs[0].run_id, "run_demo");
6560
6561 const codexStatusResponse = await handleConductorHttpRequest(
6562 {
6563 method: "GET",
6564 path: "/v1/codex"
6565 },
6566 localApiContext
6567 );
6568 assert.equal(codexStatusResponse.status, 200);
6569 const codexStatusPayload = parseJsonBody(codexStatusResponse);
6570 assert.equal(codexStatusPayload.data.backend, "independent_codexd");
6571 assert.equal(codexStatusPayload.data.proxy.target_base_url, codexd.baseUrl);
6572 assert.equal(codexStatusPayload.data.sessions.count, 1);
6573 assert.equal(codexStatusPayload.data.sessions.active_count, 1);
6574 assert.doesNotMatch(JSON.stringify(codexStatusPayload.data.routes), /\/v1\/codex\/runs/u);
6575
6576 const codexSessionsResponse = await handleConductorHttpRequest(
6577 {
6578 method: "GET",
6579 path: "/v1/codex/sessions"
6580 },
6581 localApiContext
6582 );
6583 assert.equal(codexSessionsResponse.status, 200);
6584 const codexSessionsPayload = parseJsonBody(codexSessionsResponse);
6585 assert.equal(codexSessionsPayload.data.sessions.length, 1);
6586 assert.equal(codexSessionsPayload.data.sessions[0].sessionId, "session-demo");
6587
6588 const codexSessionReadResponse = await handleConductorHttpRequest(
6589 {
6590 method: "GET",
6591 path: "/v1/codex/sessions/session-demo"
6592 },
6593 localApiContext
6594 );
6595 assert.equal(codexSessionReadResponse.status, 200);
6596 const codexSessionReadPayload = parseJsonBody(codexSessionReadResponse);
6597 assert.equal(codexSessionReadPayload.data.session.sessionId, "session-demo");
6598
6599 const codexSessionCreateResponse = await handleConductorHttpRequest(
6600 {
6601 body: JSON.stringify({
6602 cwd: "/Users/george/code/baa-conductor",
6603 model: "gpt-5.4",
6604 purpose: "duplex"
6605 }),
6606 method: "POST",
6607 path: "/v1/codex/sessions"
6608 },
6609 localApiContext
6610 );
6611 assert.equal(codexSessionCreateResponse.status, 201);
6612 const codexSessionCreatePayload = parseJsonBody(codexSessionCreateResponse);
6613 assert.equal(codexSessionCreatePayload.data.session.sessionId, "session-2");
6614 assert.equal(codexSessionCreatePayload.data.session.purpose, "duplex");
6615
6616 const codexTurnResponse = await handleConductorHttpRequest(
6617 {
6618 body: JSON.stringify({
6619 input: "Summarize pending work.",
6620 sessionId: "session-demo"
6621 }),
6622 method: "POST",
6623 path: "/v1/codex/turn"
6624 },
6625 localApiContext
6626 );
6627 assert.equal(codexTurnResponse.status, 202);
6628 const codexTurnPayload = parseJsonBody(codexTurnResponse);
6629 assert.equal(codexTurnPayload.data.accepted, true);
6630 assert.equal(codexTurnPayload.data.turnId, "turn-created");
6631
6632 const browserSendResponse = await handleConductorHttpRequest(
6633 {
6634 body: JSON.stringify({
6635 prompt: "hello claude"
6636 }),
6637 method: "POST",
6638 path: "/v1/browser/claude/send"
6639 },
6640 localApiContext
6641 );
6642 assert.equal(browserSendResponse.status, 200);
6643 const browserSendPayload = parseJsonBody(browserSendResponse);
6644 assert.equal(browserSendPayload.data.organization.organization_id, "org-1");
6645 assert.equal(browserSendPayload.data.conversation.conversation_id, "conv-1");
6646 assert.equal(browserSendPayload.data.proxy.path, "/api/organizations/org-1/chat_conversations/conv-1/completion");
6647 assert.equal(browserSendPayload.data.response.accepted, true);
6648
6649 const browserCurrentResponse = await handleConductorHttpRequest(
6650 {
6651 method: "GET",
6652 path: "/v1/browser/claude/current"
6653 },
6654 localApiContext
6655 );
6656 assert.equal(browserCurrentResponse.status, 200);
6657 const browserCurrentPayload = parseJsonBody(browserCurrentResponse);
6658 assert.equal(browserCurrentPayload.data.organization.organization_id, "org-1");
6659 assert.equal(browserCurrentPayload.data.conversation.conversation_id, "conv-1");
6660 assert.equal(browserCurrentPayload.data.messages.length, 2);
6661 assert.equal(browserCurrentPayload.data.messages[0].role, "user");
6662 assert.equal(browserCurrentPayload.data.messages[1].role, "assistant");
6663
6664 const browserReloadResponse = await handleConductorHttpRequest(
6665 {
6666 body: JSON.stringify({
6667 reason: "integration_test"
6668 }),
6669 method: "POST",
6670 path: "/v1/browser/claude/reload"
6671 },
6672 localApiContext
6673 );
6674 assert.equal(browserReloadResponse.status, 200);
6675 assert.equal(parseJsonBody(browserReloadResponse).data.type, "reload");
6676
6677 const runResponse = await handleConductorHttpRequest(
6678 {
6679 method: "GET",
6680 path: "/v1/runs/run_demo"
6681 },
6682 localApiContext
6683 );
6684 assert.equal(parseJsonBody(runResponse).data.run_id, "run_demo");
6685
6686 const pauseResponse = await handleConductorHttpRequest(
6687 {
6688 body: JSON.stringify({
6689 reason: "human_clicked_pause",
6690 requested_by: "test"
6691 }),
6692 method: "POST",
6693 path: "/v1/system/pause"
6694 },
6695 localApiContext
6696 );
6697 assert.equal(pauseResponse.status, 200);
6698 const pausePayload = parseJsonBody(pauseResponse);
6699 assert.equal(pausePayload.data.mode, "paused");
6700 const automationState = await repository.getAutomationState();
6701 assert.equal(automationState?.mode, "paused");
6702 assert.equal(pausePayload.data.updated_at, automationState?.updatedAt);
6703 assert.equal(pausePayload.data.automation.updated_at, automationState?.updatedAt);
6704
6705 const missingTokenExecResponse = await handleConductorHttpRequest(
6706 {
6707 body: JSON.stringify({
6708 command: "printf 'host-http-ok'",
6709 cwd: hostOpsDir,
6710 timeoutMs: 2000
6711 }),
6712 method: "POST",
6713 path: "/v1/exec"
6714 },
6715 localApiContext
6716 );
6717 assert.equal(missingTokenExecResponse.status, 401);
6718 const missingTokenExecPayload = parseJsonBody(missingTokenExecResponse);
6719 assert.equal(missingTokenExecPayload.error, "unauthorized");
6720
6721 const invalidTokenReadResponse = await handleConductorHttpRequest(
6722 {
6723 body: JSON.stringify({
6724 path: "notes/demo.txt",
6725 cwd: hostOpsDir
6726 }),
6727 headers: {
6728 authorization: "Bearer wrong-token"
6729 },
6730 method: "POST",
6731 path: "/v1/files/read"
6732 },
6733 localApiContext
6734 );
6735 assert.equal(invalidTokenReadResponse.status, 401);
6736 const invalidTokenReadPayload = parseJsonBody(invalidTokenReadResponse);
6737 assert.equal(invalidTokenReadPayload.error, "unauthorized");
6738
6739 const writeResponse = await handleConductorHttpRequest(
6740 {
6741 body: JSON.stringify({
6742 path: "notes/demo.txt",
6743 cwd: hostOpsDir,
6744 content: "hello from local host ops",
6745 overwrite: false,
6746 createParents: true
6747 }),
6748 headers: authorizedHeaders,
6749 method: "POST",
6750 path: "/v1/files/write"
6751 },
6752 localApiContext
6753 );
6754 assert.equal(writeResponse.status, 200);
6755 const writePayload = parseJsonBody(writeResponse);
6756 assert.equal(writePayload.data.ok, true);
6757 assert.equal(writePayload.data.operation, "files/write");
6758 assert.equal(writePayload.data.result.created, true);
6759
6760 const readResponse = await handleConductorHttpRequest(
6761 {
6762 body: JSON.stringify({
6763 path: "notes/demo.txt",
6764 cwd: hostOpsDir
6765 }),
6766 headers: authorizedHeaders,
6767 method: "POST",
6768 path: "/v1/files/read"
6769 },
6770 localApiContext
6771 );
6772 assert.equal(readResponse.status, 200);
6773 const readPayload = parseJsonBody(readResponse);
6774 assert.equal(readPayload.data.ok, true);
6775 assert.equal(readPayload.data.result.content, "hello from local host ops");
6776
6777 const duplicateWriteResponse = await handleConductorHttpRequest(
6778 {
6779 body: JSON.stringify({
6780 path: "notes/demo.txt",
6781 cwd: hostOpsDir,
6782 content: "should not overwrite",
6783 overwrite: false
6784 }),
6785 headers: authorizedHeaders,
6786 method: "POST",
6787 path: "/v1/files/write"
6788 },
6789 localApiContext
6790 );
6791 assert.equal(duplicateWriteResponse.status, 200);
6792 const duplicateWritePayload = parseJsonBody(duplicateWriteResponse);
6793 assert.equal(duplicateWritePayload.data.ok, false);
6794 assert.equal(duplicateWritePayload.data.error.code, "FILE_ALREADY_EXISTS");
6795
6796 const execResponse = await handleConductorHttpRequest(
6797 {
6798 body: JSON.stringify({
6799 command: "printf 'host-http-ok'",
6800 cwd: hostOpsDir,
6801 timeoutMs: 2000
6802 }),
6803 headers: authorizedHeaders,
6804 method: "POST",
6805 path: "/v1/exec"
6806 },
6807 localApiContext
6808 );
6809 assert.equal(execResponse.status, 200);
6810 const execPayload = parseJsonBody(execResponse);
6811 assert.equal(execPayload.data.ok, true);
6812 assert.equal(execPayload.data.operation, "exec");
6813 assert.equal(execPayload.data.result.stdout, "host-http-ok");
6814
6815 const invalidExecResponse = await handleConductorHttpRequest(
6816 {
6817 body: JSON.stringify({
6818 command: ["echo", "hello"]
6819 }),
6820 headers: authorizedHeaders,
6821 method: "POST",
6822 path: "/v1/exec"
6823 },
6824 localApiContext
6825 );
6826 assert.equal(invalidExecResponse.status, 200);
6827 const invalidExecPayload = parseJsonBody(invalidExecResponse);
6828 assert.equal(invalidExecPayload.data.ok, false);
6829 assert.equal(invalidExecPayload.data.error.code, "INVALID_INPUT");
6830 assertEmptyExecResultShape(invalidExecPayload.data.result);
6831
6832 const systemStateResponse = await handleConductorHttpRequest(
6833 {
6834 method: "GET",
6835 path: "/v1/system/state"
6836 },
6837 localApiContext
6838 );
6839 assert.equal(parseJsonBody(systemStateResponse).data.mode, "paused");
6840
6841 const statusViewResponse = await handleConductorHttpRequest(
6842 {
6843 method: "GET",
6844 path: "/v1/status"
6845 },
6846 localApiContext
6847 );
6848 assert.equal(statusViewResponse.status, 200);
6849 const statusViewPayload = parseJsonBody(statusViewResponse);
6850 assert.deepEqual(Object.keys(statusViewPayload).sort(), ["data", "ok"]);
6851 assert.equal(statusViewPayload.data.mode, "paused");
6852 assert.equal(statusViewPayload.data.queueDepth, 0);
6853 assert.equal(statusViewPayload.data.activeRuns, 1);
6854
6855 const statusViewUiResponse = await handleConductorHttpRequest(
6856 {
6857 method: "GET",
6858 path: "/v1/status/ui"
6859 },
6860 localApiContext
6861 );
6862 assert.equal(statusViewUiResponse.status, 200);
6863 assert.equal(statusViewUiResponse.headers["content-type"], "text/html; charset=utf-8");
6864 assert.match(statusViewUiResponse.body, /JSON endpoint: <strong>\/v1\/status<\/strong>/u);
6865 assert.match(statusViewUiResponse.body, /HTML endpoint: <strong>\/v1\/status\/ui<\/strong>/u);
6866 } finally {
6867 await codexd.stop();
6868 rmSync(hostOpsDir, {
6869 force: true,
6870 recursive: true
6871 });
6872 }
6873
6874 assert.deepEqual(
6875 codexd.requests.map((request) => request.path),
6876 [
6877 "/v1/codexd/status",
6878 "/v1/codexd/sessions",
6879 "/v1/codexd/sessions/session-demo",
6880 "/v1/codexd/sessions",
6881 "/v1/codexd/turn"
6882 ]
6883 );
6884 assert.equal(codexd.requests[3].body.model, "gpt-5.4");
6885 assert.equal(codexd.requests[4].body.sessionId, "session-demo");
6886 assert.deepEqual(
6887 browser.calls.map((entry) => entry.kind === "apiRequest" ? `${entry.kind}:${entry.method}:${entry.path}` : `${entry.kind}:${entry.platform || "-"}`),
6888 [
6889 "openTab:claude",
6890 "dispatchPluginAction:-",
6891 "dispatchPluginAction:-",
6892 "apiRequest:GET:/api/organizations",
6893 "apiRequest:GET:/api/organizations/org-1/chat_conversations",
6894 "apiRequest:POST:/api/organizations/org-1/chat_conversations/conv-1/completion",
6895 "apiRequest:GET:/api/stream-buffered-smoke",
6896 "apiRequest:GET:/backend-api/conversation-buffered-smoke",
6897 "apiRequest:GET:/backend-api/models",
6898 "apiRequest:POST:/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate",
6899 "apiRequest:POST:/backend-api/conversation",
6900 "apiRequest:GET:/backend-api/conversation/conv-chatgpt-current",
6901 "apiRequest:POST:/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate",
6902 "apiRequest:GET:/api/organizations",
6903 "apiRequest:GET:/api/organizations/org-1/chat_conversations",
6904 "streamRequest:claude",
6905 "cancelApiRequest:claude",
6906 "openTab:claude",
6907 "apiRequest:GET:/api/organizations",
6908 "apiRequest:GET:/api/organizations/org-1/chat_conversations",
6909 "apiRequest:POST:/api/organizations/org-1/chat_conversations/conv-1/completion",
6910 "apiRequest:GET:/api/organizations",
6911 "apiRequest:GET:/api/organizations/org-1/chat_conversations",
6912 "apiRequest:GET:/api/organizations/org-1/chat_conversations/conv-1",
6913 "reload:claude"
6914 ]
6915 );
6916});
6917
6918test("handleConductorHttpRequest returns a codexd-specific availability error when the proxy target is down", async () => {
6919 const { repository, snapshot } = await createLocalApiFixture();
6920 snapshot.codexd.localApiBase = "http://127.0.0.1:65535";
6921
6922 const response = await handleConductorHttpRequest(
6923 {
6924 method: "GET",
6925 path: "/v1/codex"
6926 },
6927 {
6928 codexdLocalApiBase: snapshot.codexd.localApiBase,
6929 fetchImpl: async () => {
6930 throw new Error("connect ECONNREFUSED");
6931 },
6932 repository,
6933 snapshotLoader: () => snapshot
6934 }
6935 );
6936
6937 assert.equal(response.status, 503);
6938 const payload = parseJsonBody(response);
6939 assert.equal(payload.error, "codexd_unavailable");
6940 assert.match(payload.message, /Independent codexd is unavailable/u);
6941 assert.doesNotMatch(payload.message, /bridge/u);
6942});
6943
6944test("handleConductorHttpRequest returns browser_request_timeout with timeout details for stalled browser proxy calls", async () => {
6945 const { repository, snapshot } = await createLocalApiFixture();
6946 const browser = createBrowserBridgeStub();
6947 browser.context.browserBridge.apiRequest = async () => {
6948 throw new FirefoxBridgeError(
6949 "request_timeout",
6950 'Firefox client "firefox-claude" did not respond to api_request "browser-timeout-1" within 50ms.',
6951 {
6952 clientId: "firefox-claude",
6953 connectionId: "conn-firefox-claude",
6954 requestId: "browser-timeout-1",
6955 timeoutMs: 50
6956 }
6957 );
6958 };
6959
6960 const response = await handleConductorHttpRequest(
6961 {
6962 body: JSON.stringify({
6963 path: "/api/organizations",
6964 platform: "claude",
6965 timeoutMs: 50
6966 }),
6967 method: "POST",
6968 path: "/v1/browser/request"
6969 },
6970 {
6971 ...browser.context,
6972 repository,
6973 snapshotLoader: () => snapshot
6974 }
6975 );
6976
6977 assert.equal(response.status, 504);
6978 const payload = parseJsonBody(response);
6979 assert.equal(payload.error, "browser_request_timeout");
6980 assert.equal(payload.details.error_code, "request_timeout");
6981 assert.equal(payload.details.timeout_ms, 50);
6982 assert.equal(payload.details.bridge_request_id, "browser-timeout-1");
6983});
6984
6985test("handleConductorHttpRequest returns a clear 503 for Claude browser actions without an active browser client", async () => {
6986 const { repository, snapshot } = await createLocalApiFixture();
6987
6988 const response = await handleConductorHttpRequest(
6989 {
6990 method: "GET",
6991 path: "/v1/browser/claude/current"
6992 },
6993 {
6994 browserStateLoader: () => ({
6995 active_client_id: null,
6996 active_connection_id: null,
6997 client_count: 0,
6998 clients: [],
6999 ws_path: "/ws/browser",
7000 ws_url: snapshot.controlApi.browserWsUrl
7001 }),
7002 repository,
7003 snapshotLoader: () => snapshot
7004 }
7005 );
7006
7007 assert.equal(response.status, 503);
7008 const payload = parseJsonBody(response);
7009 assert.equal(payload.ok, false);
7010 assert.equal(payload.error, "browser_bridge_unavailable");
7011});
7012
7013test("BrowserRequestPolicyController times out target slot waiters and removes them from the queue", async () => {
7014 const scheduler = createManualTimerScheduler();
7015 const policy = new BrowserRequestPolicyController({
7016 clearTimeoutImpl: scheduler.clearTimeout,
7017 config: {
7018 jitter: {
7019 maxMs: 0,
7020 minMs: 0,
7021 muMs: 0,
7022 sigmaMs: 0
7023 }
7024 },
7025 now: scheduler.now,
7026 setTimeoutImpl: scheduler.setTimeout
7027 });
7028
7029 const leakedLease = await policy.beginRequest({
7030 clientId: "firefox-claude",
7031 platform: "claude"
7032 }, "request-leaked");
7033 const waitingLeasePromise = policy.beginRequest({
7034 clientId: "firefox-claude",
7035 platform: "claude"
7036 }, "request-blocked");
7037
7038 await flushAsyncWork();
7039 assert.equal(getPolicyTargetSnapshot(policy, "firefox-claude", "claude")?.waiting, 1);
7040
7041 scheduler.advanceBy(BROWSER_REQUEST_WAITER_TIMEOUT_MS);
7042
7043 await assert.rejects(waitingLeasePromise, (error) => {
7044 assert.equal(error.code, "waiter_timeout");
7045 assert.equal(error.details.wait_scope, "target_slot");
7046 assert.equal(error.details.client_id, "firefox-claude");
7047 assert.equal(error.details.platform, "claude");
7048 return true;
7049 });
7050
7051 assert.equal(getPolicyTargetSnapshot(policy, "firefox-claude", "claude")?.waiting, 0);
7052 assert.equal(getPolicyTargetSnapshot(policy, "firefox-claude", "claude")?.inFlight, 1);
7053
7054 leakedLease.complete({
7055 status: "cancelled"
7056 });
7057
7058 assert.equal(getPolicyTargetSnapshot(policy, "firefox-claude", "claude")?.inFlight, 0);
7059});
7060
7061test("BrowserRequestPolicyController sweeps stale in-flight leases and lets the next waiter proceed", async () => {
7062 const scheduler = createManualTimerScheduler();
7063 const policy = new BrowserRequestPolicyController({
7064 clearTimeoutImpl: scheduler.clearTimeout,
7065 config: {
7066 jitter: {
7067 maxMs: 0,
7068 minMs: 0,
7069 muMs: 0,
7070 sigmaMs: 0
7071 },
7072 staleLease: {
7073 idleMs: 60_000,
7074 sweepIntervalMs: 10_000
7075 }
7076 },
7077 now: scheduler.now,
7078 setTimeoutImpl: scheduler.setTimeout
7079 });
7080
7081 const leakedLease = await policy.beginRequest({
7082 clientId: "firefox-claude",
7083 platform: "claude"
7084 }, "request-leaked");
7085 const recoveredLeasePromise = policy.beginRequest({
7086 clientId: "firefox-claude",
7087 platform: "claude"
7088 }, "request-recovered");
7089
7090 await flushAsyncWork();
7091 assert.equal(getPolicyTargetSnapshot(policy, "firefox-claude", "claude")?.waiting, 1);
7092
7093 scheduler.advanceBy(60_000);
7094 await flushAsyncWork();
7095
7096 const recoveredLease = await recoveredLeasePromise;
7097 const targetSnapshot = getPolicyTargetSnapshot(policy, "firefox-claude", "claude");
7098 assert.equal(recoveredLease.admission.requestId, "request-recovered");
7099 assert.equal(targetSnapshot?.inFlight, 1);
7100 assert.equal(targetSnapshot?.waiting, 0);
7101 assert.equal(targetSnapshot?.staleSweepCount, 1);
7102 assert.equal(targetSnapshot?.lastStaleSweepAt, 60_000);
7103 assert.equal(targetSnapshot?.lastStaleSweepRequestId, "request-leaked");
7104 assert.equal(targetSnapshot?.lastStaleSweepReason, "background_interval:lease_idle_timeout");
7105
7106 leakedLease.complete({
7107 status: "cancelled"
7108 });
7109
7110 assert.equal(getPolicyTargetSnapshot(policy, "firefox-claude", "claude")?.inFlight, 1);
7111
7112 recoveredLease.complete({
7113 status: "success"
7114 });
7115
7116 assert.equal(getPolicyTargetSnapshot(policy, "firefox-claude", "claude")?.inFlight, 0);
7117});
7118
7119test("BrowserRequestPolicyController does not sweep healthy leases that keep reporting activity", async () => {
7120 const scheduler = createManualTimerScheduler();
7121 const policy = new BrowserRequestPolicyController({
7122 clearTimeoutImpl: scheduler.clearTimeout,
7123 config: {
7124 jitter: {
7125 maxMs: 0,
7126 minMs: 0,
7127 muMs: 0,
7128 sigmaMs: 0
7129 },
7130 staleLease: {
7131 idleMs: 60_000,
7132 sweepIntervalMs: 10_000
7133 }
7134 },
7135 now: scheduler.now,
7136 setTimeoutImpl: scheduler.setTimeout
7137 });
7138
7139 const healthyLease = await policy.beginRequest({
7140 clientId: "firefox-claude",
7141 platform: "claude"
7142 }, "request-healthy");
7143 const waitingLeasePromise = policy.beginRequest({
7144 clientId: "firefox-claude",
7145 platform: "claude"
7146 }, "request-waiting");
7147 let waitingSettled = false;
7148
7149 void waitingLeasePromise.then(
7150 () => {
7151 waitingSettled = true;
7152 },
7153 () => {
7154 waitingSettled = true;
7155 }
7156 );
7157
7158 await flushAsyncWork();
7159
7160 for (let index = 0; index < 4; index += 1) {
7161 scheduler.advanceBy(20_000);
7162 await flushAsyncWork();
7163 healthyLease.touch("stream_event");
7164 assert.equal(waitingSettled, false);
7165 assert.equal(getPolicyTargetSnapshot(policy, "firefox-claude", "claude")?.staleSweepCount, 0);
7166 }
7167
7168 const snapshotBeforeComplete = getPolicyTargetSnapshot(policy, "firefox-claude", "claude");
7169 assert.equal(snapshotBeforeComplete?.inFlight, 1);
7170 assert.equal(snapshotBeforeComplete?.lastActivityReason, "stream_event");
7171 assert.equal(waitingSettled, false);
7172
7173 healthyLease.complete({
7174 status: "success"
7175 });
7176 await flushAsyncWork();
7177
7178 const waitingLease = await waitingLeasePromise;
7179 assert.equal(waitingLease.admission.requestId, "request-waiting");
7180 waitingLease.complete({
7181 status: "cancelled"
7182 });
7183
7184 assert.equal(getPolicyTargetSnapshot(policy, "firefox-claude", "claude")?.inFlight, 0);
7185});
7186
7187test("BrowserRequestPolicyController times out platform admission waiters and releases their target slot", async () => {
7188 const scheduler = createManualTimerScheduler();
7189 const policy = new BrowserRequestPolicyController({
7190 clearTimeoutImpl: scheduler.clearTimeout,
7191 config: {
7192 jitter: {
7193 maxMs: 0,
7194 minMs: 0,
7195 muMs: 0,
7196 sigmaMs: 0
7197 },
7198 rateLimit: {
7199 requestsPerMinutePerPlatform: 0,
7200 windowMs: BROWSER_REQUEST_WAITER_TIMEOUT_MS + 60_000
7201 }
7202 },
7203 now: scheduler.now,
7204 setTimeoutImpl: scheduler.setTimeout
7205 });
7206
7207 const blockedAdmissionPromise = policy.beginRequest({
7208 clientId: "firefox-claude-a",
7209 platform: "claude"
7210 }, "request-admit-blocker");
7211
7212 await flushAsyncWork();
7213
7214 const waitingLeasePromise = policy.beginRequest({
7215 clientId: "firefox-claude-b",
7216 platform: "claude"
7217 }, "request-admit-blocked");
7218
7219 await flushAsyncWork();
7220 assert.equal(getPolicyPlatformSnapshot(policy, "claude")?.waiting, 1);
7221
7222 scheduler.advanceBy(BROWSER_REQUEST_WAITER_TIMEOUT_MS);
7223
7224 await assert.rejects(waitingLeasePromise, (error) => {
7225 assert.equal(error.code, "waiter_timeout");
7226 assert.equal(error.details.wait_scope, "platform_admission");
7227 assert.equal(error.details.client_id, "firefox-claude-b");
7228 assert.equal(error.details.platform, "claude");
7229 return true;
7230 });
7231
7232 assert.equal(getPolicyPlatformSnapshot(policy, "claude")?.waiting, 0);
7233 assert.equal(getPolicyTargetSnapshot(policy, "firefox-claude-b", "claude")?.inFlight, 0);
7234
7235 scheduler.advanceBy(60_000);
7236 const delayedLease = await blockedAdmissionPromise;
7237 delayedLease.complete({
7238 status: "cancelled"
7239 });
7240
7241 assert.equal(getPolicyTargetSnapshot(policy, "firefox-claude-a", "claude")?.inFlight, 0);
7242});
7243
7244test("FirefoxCommandBroker clears stream timers after the stream ends", async () => {
7245 const scheduler = createManualTimerScheduler();
7246 const sentMessages = [];
7247 const client = {
7248 clientId: "firefox-claude",
7249 connectedAt: 0,
7250 connectionId: "conn-firefox-claude",
7251 lastMessageAt: 0,
7252 sendJson(payload) {
7253 sentMessages.push(payload);
7254 return true;
7255 }
7256 };
7257 const broker = new FirefoxCommandBroker({
7258 clearTimeoutImpl: scheduler.clearTimeout,
7259 now: scheduler.now,
7260 resolveActiveClient: () => client,
7261 resolveClientById: (clientId) => (clientId === client.clientId ? client : null),
7262 setTimeoutImpl: scheduler.setTimeout
7263 });
7264
7265 const stream = broker.openApiStream({
7266 method: "GET",
7267 path: "/api/organizations",
7268 platform: "claude"
7269 }, {
7270 clientId: "firefox-claude",
7271 idleTimeoutMs: 1_000,
7272 openTimeoutMs: 1_000,
7273 requestId: "stream-finish-smoke",
7274 streamId: "stream-finish-smoke"
7275 });
7276
7277 assert.equal(sentMessages.length, 1);
7278 assert.equal(sentMessages[0].type, "api_request");
7279
7280 assert.equal(
7281 broker.handleStreamOpen("conn-firefox-claude", {
7282 id: "stream-finish-smoke",
7283 status: 200,
7284 streamId: "stream-finish-smoke"
7285 }),
7286 true
7287 );
7288 assert.equal(
7289 broker.handleStreamEnd("conn-firefox-claude", {
7290 id: "stream-finish-smoke",
7291 status: 200,
7292 streamId: "stream-finish-smoke"
7293 }),
7294 true
7295 );
7296
7297 const openEvent = await stream.next();
7298 assert.equal(openEvent.done, false);
7299 assert.equal(openEvent.value.type, "stream_open");
7300
7301 const endEvent = await stream.next();
7302 assert.equal(endEvent.done, false);
7303 assert.equal(endEvent.value.type, "stream_end");
7304
7305 const closedEvent = await stream.next();
7306 assert.equal(closedEvent.done, true);
7307 assert.equal(closedEvent.value, undefined);
7308
7309 scheduler.advanceBy(10_000);
7310
7311 assert.equal(sentMessages.length, 1);
7312});
7313
7314test("handleConductorHttpRequest returns a clear 503 when a leaked browser request lease blocks the target slot", async () => {
7315 const { repository, sharedToken, snapshot } = await createLocalApiFixture();
7316 const browser = createBrowserBridgeStub();
7317 const scheduler = createManualTimerScheduler();
7318 const policy = new BrowserRequestPolicyController({
7319 clearTimeoutImpl: scheduler.clearTimeout,
7320 config: {
7321 jitter: {
7322 maxMs: 0,
7323 minMs: 0,
7324 muMs: 0,
7325 sigmaMs: 0
7326 }
7327 },
7328 now: scheduler.now,
7329 setTimeoutImpl: scheduler.setTimeout
7330 });
7331
7332 const leakedLease = await policy.beginRequest({
7333 clientId: "firefox-claude",
7334 platform: "claude"
7335 }, "request-leaked");
7336
7337 const responsePromise = handleConductorHttpRequest(
7338 {
7339 body: JSON.stringify({
7340 platform: "claude",
7341 prompt: "blocked browser request"
7342 }),
7343 method: "POST",
7344 path: "/v1/browser/request"
7345 },
7346 {
7347 ...browser.context,
7348 browserRequestPolicy: policy,
7349 repository,
7350 sharedToken,
7351 snapshotLoader: () => snapshot
7352 }
7353 );
7354
7355 await flushAsyncWork();
7356 scheduler.advanceBy(BROWSER_REQUEST_WAITER_TIMEOUT_MS);
7357
7358 const response = await responsePromise;
7359 assert.equal(response.status, 503);
7360 const payload = parseJsonBody(response);
7361 assert.equal(payload.ok, false);
7362 assert.equal(payload.error, "browser_risk_limited");
7363 assert.match(payload.message, /could not schedule browser request/u);
7364 assert.equal(payload.details.error_code, "waiter_timeout");
7365 assert.equal(payload.details.wait_scope, "target_slot");
7366
7367 leakedLease.complete({
7368 status: "cancelled"
7369 });
7370});
7371
7372test("handleConductorHttpRequest exposes stale lease sweep diagnostics in /v1/browser", async () => {
7373 const { repository, snapshot } = await createLocalApiFixture();
7374 const browser = createBrowserBridgeStub();
7375 const scheduler = createManualTimerScheduler();
7376 const policy = new BrowserRequestPolicyController({
7377 clearTimeoutImpl: scheduler.clearTimeout,
7378 config: {
7379 jitter: {
7380 maxMs: 0,
7381 minMs: 0,
7382 muMs: 0,
7383 sigmaMs: 0
7384 },
7385 staleLease: {
7386 idleMs: 60_000,
7387 sweepIntervalMs: 10_000
7388 }
7389 },
7390 now: scheduler.now,
7391 setTimeoutImpl: scheduler.setTimeout
7392 });
7393
7394 await policy.beginRequest({
7395 clientId: "firefox-claude",
7396 platform: "claude"
7397 }, "request-leaked");
7398 const recoveredLeasePromise = policy.beginRequest({
7399 clientId: "firefox-claude",
7400 platform: "claude"
7401 }, "request-recovered");
7402
7403 await flushAsyncWork();
7404 scheduler.advanceBy(60_000);
7405 await flushAsyncWork();
7406
7407 const recoveredLease = await recoveredLeasePromise;
7408 recoveredLease.touch("stream_event");
7409
7410 const response = await handleConductorHttpRequest(
7411 {
7412 method: "GET",
7413 path: "/v1/browser"
7414 },
7415 {
7416 ...browser.context,
7417 browserRequestPolicy: policy,
7418 repository,
7419 snapshotLoader: () => snapshot
7420 }
7421 );
7422
7423 assert.equal(response.status, 200);
7424 const payload = parseJsonBody(response);
7425 assert.equal(payload.data.policy.defaults.stale_lease.idle_ms, 60_000);
7426 assert.equal(payload.data.policy.defaults.stale_lease.sweep_interval_ms, 10_000);
7427 assert.equal(payload.data.policy.targets[0].client_id, "firefox-claude");
7428 assert.equal(payload.data.policy.targets[0].platform, "claude");
7429 assert.equal(payload.data.policy.targets[0].in_flight, 1);
7430 assert.equal(payload.data.policy.targets[0].last_activity_at, 60_000);
7431 assert.equal(payload.data.policy.targets[0].last_activity_reason, "stream_event");
7432 assert.equal(payload.data.policy.targets[0].stale_sweep_count, 1);
7433 assert.equal(payload.data.policy.targets[0].last_stale_sweep_at, 60_000);
7434 assert.equal(payload.data.policy.targets[0].last_stale_sweep_idle_ms, 60_000);
7435 assert.equal(
7436 payload.data.policy.targets[0].last_stale_sweep_reason,
7437 "background_interval:lease_idle_timeout"
7438 );
7439 assert.equal(payload.data.policy.targets[0].last_stale_sweep_request_id, "request-leaked");
7440
7441 recoveredLease.complete({
7442 status: "cancelled"
7443 });
7444});
7445
7446test("BrowserRequestPolicyController restores persisted circuit state from artifact.db", async () => {
7447 const scheduler = createManualTimerScheduler();
7448
7449 await withArtifactStoreFixture(async ({ artifactStore }) => {
7450 const persistence = createArtifactStoreBrowserRequestPolicyPersistence(artifactStore, {
7451 now: scheduler.now
7452 });
7453 const firstPolicy = new BrowserRequestPolicyController({
7454 clearTimeoutImpl: scheduler.clearTimeout,
7455 config: {
7456 circuitBreaker: {
7457 failureThreshold: 1,
7458 openMs: 60_000
7459 },
7460 jitter: {
7461 maxMs: 0,
7462 minMs: 0,
7463 muMs: 0,
7464 sigmaMs: 0
7465 }
7466 },
7467 now: scheduler.now,
7468 persistence,
7469 setTimeoutImpl: scheduler.setTimeout
7470 });
7471
7472 await firstPolicy.initialize();
7473
7474 const failedLease = await firstPolicy.beginRequest({
7475 clientId: "firefox-claude",
7476 platform: "claude"
7477 }, "request-persisted-circuit");
7478
7479 failedLease.complete({
7480 code: "upstream_429",
7481 status: "failure"
7482 });
7483 await firstPolicy.flush();
7484
7485 const persistedState = await artifactStore.getBrowserRequestPolicyState("global");
7486 assert.ok(persistedState);
7487
7488 const restartedPolicy = new BrowserRequestPolicyController({
7489 clearTimeoutImpl: scheduler.clearTimeout,
7490 config: {
7491 circuitBreaker: {
7492 failureThreshold: 1,
7493 openMs: 60_000
7494 },
7495 jitter: {
7496 maxMs: 0,
7497 minMs: 0,
7498 muMs: 0,
7499 sigmaMs: 0
7500 }
7501 },
7502 now: scheduler.now,
7503 persistence,
7504 setTimeoutImpl: scheduler.setTimeout
7505 });
7506
7507 await restartedPolicy.initialize();
7508
7509 assert.equal(getPolicyPlatformSnapshot(restartedPolicy, "claude")?.recentDispatchCount, 1);
7510
7511 await assert.rejects(
7512 restartedPolicy.beginRequest({
7513 clientId: "firefox-claude",
7514 platform: "claude"
7515 }, "request-after-restart"),
7516 (error) => {
7517 assert.equal(error.code, "circuit_open");
7518 assert.equal(error.details.retry_after_ms, 60_000);
7519 return true;
7520 }
7521 );
7522 });
7523});
7524
7525test("BrowserRequestPolicyController restores persisted rate-limit windows from artifact.db", async () => {
7526 const scheduler = createManualTimerScheduler();
7527
7528 await withArtifactStoreFixture(async ({ artifactStore }) => {
7529 const persistence = createArtifactStoreBrowserRequestPolicyPersistence(artifactStore, {
7530 now: scheduler.now
7531 });
7532 const firstPolicy = new BrowserRequestPolicyController({
7533 clearTimeoutImpl: scheduler.clearTimeout,
7534 config: {
7535 jitter: {
7536 maxMs: 0,
7537 minMs: 0,
7538 muMs: 0,
7539 sigmaMs: 0
7540 },
7541 rateLimit: {
7542 requestsPerMinutePerPlatform: 1,
7543 windowMs: 60_000
7544 }
7545 },
7546 now: scheduler.now,
7547 persistence,
7548 setTimeoutImpl: scheduler.setTimeout
7549 });
7550
7551 await firstPolicy.initialize();
7552
7553 const firstLease = await firstPolicy.beginRequest({
7554 clientId: "firefox-chatgpt",
7555 platform: "chatgpt"
7556 }, "request-persisted-rate-limit");
7557
7558 firstLease.complete({
7559 status: "success"
7560 });
7561 await firstPolicy.flush();
7562
7563 const restartedPolicy = new BrowserRequestPolicyController({
7564 clearTimeoutImpl: scheduler.clearTimeout,
7565 config: {
7566 jitter: {
7567 maxMs: 0,
7568 minMs: 0,
7569 muMs: 0,
7570 sigmaMs: 0
7571 },
7572 rateLimit: {
7573 requestsPerMinutePerPlatform: 1,
7574 windowMs: 60_000
7575 }
7576 },
7577 now: scheduler.now,
7578 persistence,
7579 setTimeoutImpl: scheduler.setTimeout
7580 });
7581
7582 await restartedPolicy.initialize();
7583
7584 const delayedLeasePromise = restartedPolicy.beginRequest({
7585 clientId: "firefox-chatgpt",
7586 platform: "chatgpt"
7587 }, "request-after-rate-limit-restart");
7588 let delayedResolved = false;
7589
7590 void delayedLeasePromise.then(() => {
7591 delayedResolved = true;
7592 });
7593
7594 await flushAsyncWork();
7595 assert.equal(delayedResolved, false);
7596 assert.equal(getPolicyPlatformSnapshot(restartedPolicy, "chatgpt")?.recentDispatchCount, 1);
7597
7598 scheduler.advanceBy(60_000);
7599
7600 const delayedLease = await delayedLeasePromise;
7601 assert.equal(delayedLease.admission.rateLimitDelayMs, 60_000);
7602 delayedLease.complete({
7603 status: "cancelled"
7604 });
7605 });
7606});
7607
7608test(
7609 "handleConductorHttpRequest normalizes exec failures that are blocked by macOS TCC preflight",
7610 { concurrency: false },
7611 async () => {
7612 const { repository, sharedToken, snapshot } = await createLocalApiFixture();
7613
7614 const execResponse = await withMockedPlatform("darwin", () =>
7615 handleConductorHttpRequest(
7616 {
7617 body: JSON.stringify({
7618 command: "pwd",
7619 cwd: join(homedir(), "Downloads"),
7620 timeoutMs: 2_000
7621 }),
7622 headers: {
7623 authorization: `Bearer ${sharedToken}`
7624 },
7625 method: "POST",
7626 path: "/v1/exec"
7627 },
7628 {
7629 repository,
7630 sharedToken,
7631 snapshotLoader: () => snapshot
7632 }
7633 )
7634 );
7635
7636 assert.equal(execResponse.status, 200);
7637 const execPayload = parseJsonBody(execResponse);
7638 assert.equal(execPayload.data.ok, false);
7639 assert.equal(execPayload.data.operation, "exec");
7640 assert.equal(execPayload.data.error.code, "TCC_PERMISSION_DENIED");
7641 assert.equal(execPayload.data.result.stdout, "");
7642 assert.equal(execPayload.data.result.stderr, "");
7643 assert.equal(execPayload.data.result.exitCode, null);
7644 assert.equal(execPayload.data.result.signal, null);
7645 assert.equal(execPayload.data.result.durationMs, 0);
7646 assert.equal(execPayload.data.result.timedOut, false);
7647 assert.equal(typeof execPayload.data.result.startedAt, "string");
7648 assert.equal(typeof execPayload.data.result.finishedAt, "string");
7649 }
7650);
7651
7652test("ConductorRuntime serves health and migrated local API endpoints over HTTP", async () => {
7653 const codexd = await startCodexdStubServer();
7654 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-"));
7655 const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-host-"));
7656 const runtime = new ConductorRuntime(
7657 {
7658 nodeId: "mini-main",
7659 host: "mini",
7660 role: "primary",
7661 controlApiBase: "https://control.example.test",
7662 codexdLocalApiBase: codexd.baseUrl,
7663 localApiBase: "http://127.0.0.1:0",
7664 sharedToken: "replace-me",
7665 uiBrowserAdminPassword: "admin-secret",
7666 uiReadonlyPassword: "readonly-secret",
7667 paths: {
7668 runsDir: "/tmp/runs",
7669 stateDir
7670 }
7671 },
7672 {
7673 autoStartLoops: false,
7674 now: () => 100
7675 }
7676 );
7677
7678 try {
7679 const snapshot = await runtime.start();
7680 assert.equal(snapshot.daemon.schedulerEnabled, true);
7681 assert.equal(snapshot.codexd.localApiBase, codexd.baseUrl);
7682 assert.match(snapshot.controlApi.localApiBase, /^http:\/\/127\.0\.0\.1:\d+$/u);
7683 assert.match(snapshot.controlApi.browserWsUrl, /^ws:\/\/127\.0\.0\.1:\d+\/ws\/browser$/u);
7684 assert.match(snapshot.controlApi.firefoxWsUrl, /^ws:\/\/127\.0\.0\.1:\d+\/ws\/firefox$/u);
7685
7686 const baseUrl = snapshot.controlApi.localApiBase;
7687 const hostOpsHeaders = {
7688 authorization: "Bearer replace-me",
7689 "content-type": "application/json"
7690 };
7691
7692 const healthResponse = await fetch(`${baseUrl}/healthz`);
7693 assert.equal(healthResponse.status, 200);
7694 assert.equal(await healthResponse.text(), "ok\n");
7695
7696 const apiHealthResponse = await fetch(`${baseUrl}/health`);
7697 assert.equal(apiHealthResponse.status, 200);
7698 const apiHealthPayload = await apiHealthResponse.json();
7699 assert.equal(apiHealthPayload.ok, true);
7700 assert.equal(apiHealthPayload.data.status, "ok");
7701
7702 const readyResponse = await fetch(`${baseUrl}/readyz`);
7703 assert.equal(readyResponse.status, 200);
7704 assert.equal(await readyResponse.text(), "ready\n");
7705
7706 const roleResponse = await fetch(`${baseUrl}/rolez`);
7707 assert.equal(roleResponse.status, 200);
7708 assert.equal(await roleResponse.text(), "leader\n");
7709
7710 const runtimeResponse = await fetch(`${baseUrl}/v1/runtime`);
7711 assert.equal(runtimeResponse.status, 200);
7712 const payload = await runtimeResponse.json();
7713 assert.equal(payload.ok, true);
7714 assert.equal(payload.data.identity, "mini-main@mini(primary)");
7715 assert.equal(payload.data.controlApi.browserWsUrl, snapshot.controlApi.browserWsUrl);
7716 assert.equal(payload.data.controlApi.firefoxWsUrl, snapshot.controlApi.firefoxWsUrl);
7717 assert.equal(payload.data.controlApi.localApiBase, baseUrl);
7718 assert.equal(payload.data.runtime.started, true);
7719
7720 const systemStateResponse = await fetch(`${baseUrl}/v1/system/state`);
7721 assert.equal(systemStateResponse.status, 200);
7722 const systemStatePayload = await systemStateResponse.json();
7723 assert.equal(systemStatePayload.ok, true);
7724 assert.equal(systemStatePayload.data.holder_id, "mini-main");
7725 assert.equal(systemStatePayload.data.mode, "running");
7726
7727 const statusViewResponse = await fetch(`${baseUrl}/v1/status`);
7728 assert.equal(statusViewResponse.status, 200);
7729 const statusViewPayload = await statusViewResponse.json();
7730 assert.deepEqual(Object.keys(statusViewPayload).sort(), ["data", "ok"]);
7731 assert.equal(statusViewPayload.data.mode, "running");
7732 assert.equal(statusViewPayload.data.queueDepth, 0);
7733 assert.equal(statusViewPayload.data.activeRuns, 0);
7734
7735 const statusViewUiResponse = await fetch(`${baseUrl}/v1/status/ui`);
7736 assert.equal(statusViewUiResponse.status, 200);
7737 assert.equal(statusViewUiResponse.headers.get("content-type"), "text/html; charset=utf-8");
7738 const statusViewUiHtml = await statusViewUiResponse.text();
7739 assert.match(statusViewUiHtml, /Readable automation state for people and browser controls\./u);
7740 assert.match(statusViewUiHtml, /HTML endpoint: <strong>\/v1\/status\/ui<\/strong>/u);
7741
7742 const appResponse = await fetch(`${baseUrl}/app`);
7743 assert.equal(appResponse.status, 200);
7744 assert.equal(appResponse.headers.get("cache-control"), "no-store");
7745 assert.equal(appResponse.headers.get("content-type"), "text/html; charset=utf-8");
7746 const appHtml = await appResponse.text();
7747 assert.match(appHtml, /<div id="app"><\/div>/u);
7748 const appAssetMatch = appHtml.match(/\/app\/assets\/[^"' )]+/u);
7749 assert.ok(appAssetMatch);
7750
7751 const appControlResponse = await fetch(`${baseUrl}/app/control`);
7752 assert.equal(appControlResponse.status, 200);
7753 assert.equal(await appControlResponse.text(), appHtml);
7754
7755 const appAssetResponse = await fetch(`${baseUrl}${appAssetMatch[0]}`);
7756 assert.equal(appAssetResponse.status, 200);
7757 assert.equal(appAssetResponse.headers.get("cache-control"), "public, max-age=31536000, immutable");
7758 assert.ok(appAssetResponse.headers.get("content-type"));
7759
7760 const conductorUiLoginResponse = await fetch(`${baseUrl}/app/login`);
7761 assert.equal(conductorUiLoginResponse.status, 200);
7762 assert.equal(conductorUiLoginResponse.headers.get("content-type"), "text/html; charset=utf-8");
7763
7764 const uiSessionMeResponse = await fetch(`${baseUrl}/v1/ui/session/me`);
7765 assert.equal(uiSessionMeResponse.status, 200);
7766 const uiSessionMePayload = await uiSessionMeResponse.json();
7767 assert.equal(uiSessionMePayload.ok, true);
7768 assert.equal(uiSessionMePayload.data.authenticated, false);
7769 assert.deepEqual(uiSessionMePayload.data.available_roles, ["browser_admin", "readonly"]);
7770 assert.equal(uiSessionMePayload.data.session, null);
7771
7772 const invalidUiLoginResponse = await fetch(`${baseUrl}/v1/ui/session/login`, {
7773 method: "POST",
7774 headers: {
7775 "content-type": "application/json"
7776 },
7777 body: JSON.stringify({
7778 password: "wrong-secret",
7779 role: "readonly"
7780 })
7781 });
7782 assert.equal(invalidUiLoginResponse.status, 401);
7783 const invalidUiLoginPayload = await invalidUiLoginResponse.json();
7784 assert.equal(invalidUiLoginPayload.ok, false);
7785 assert.equal(invalidUiLoginPayload.error, "unauthorized");
7786
7787 const readonlyUiLoginResponse = await fetch(`${baseUrl}/v1/ui/session/login`, {
7788 method: "POST",
7789 headers: {
7790 "content-type": "application/json"
7791 },
7792 body: JSON.stringify({
7793 password: "readonly-secret",
7794 role: "readonly"
7795 })
7796 });
7797 assert.equal(readonlyUiLoginResponse.status, 200);
7798 const readonlyUiLoginPayload = await readonlyUiLoginResponse.json();
7799 assert.equal(readonlyUiLoginPayload.data.authenticated, true);
7800 assert.equal(readonlyUiLoginPayload.data.session.role, "readonly");
7801 const readonlySetCookie = readonlyUiLoginResponse.headers.get("set-cookie");
7802 assert.ok(readonlySetCookie);
7803 assert.match(readonlySetCookie, /baa_ui_session=/u);
7804 assert.match(readonlySetCookie, /HttpOnly/u);
7805 assert.match(readonlySetCookie, /SameSite=Lax/u);
7806 assert.match(readonlySetCookie, /Path=\//u);
7807 const readonlyCookie = readonlySetCookie.split(";", 1)[0];
7808
7809 const readonlyUiMeResponse = await fetch(`${baseUrl}/v1/ui/session/me`, {
7810 headers: {
7811 cookie: readonlyCookie
7812 }
7813 });
7814 assert.equal(readonlyUiMeResponse.status, 200);
7815 const readonlyUiMePayload = await readonlyUiMeResponse.json();
7816 assert.equal(readonlyUiMePayload.data.authenticated, true);
7817 assert.equal(readonlyUiMePayload.data.session.role, "readonly");
7818 assert.match(readonlyUiMeResponse.headers.get("set-cookie") ?? "", /baa_ui_session=/u);
7819
7820 const readonlyUiLogoutResponse = await fetch(`${baseUrl}/v1/ui/session/logout`, {
7821 method: "POST",
7822 headers: {
7823 cookie: readonlyCookie
7824 }
7825 });
7826 assert.equal(readonlyUiLogoutResponse.status, 200);
7827 assert.equal((await readonlyUiLogoutResponse.json()).data.authenticated, false);
7828 assert.match(readonlyUiLogoutResponse.headers.get("set-cookie") ?? "", /Max-Age=0/u);
7829
7830 const staleReadonlyMeResponse = await fetch(`${baseUrl}/v1/ui/session/me`, {
7831 headers: {
7832 cookie: readonlyCookie
7833 }
7834 });
7835 assert.equal(staleReadonlyMeResponse.status, 200);
7836 const staleReadonlyMePayload = await staleReadonlyMeResponse.json();
7837 assert.equal(staleReadonlyMePayload.data.authenticated, false);
7838 assert.match(staleReadonlyMeResponse.headers.get("set-cookie") ?? "", /Max-Age=0/u);
7839
7840 const browserAdminLoginResponse = await fetch(`${baseUrl}/v1/ui/session/login`, {
7841 method: "POST",
7842 headers: {
7843 "content-type": "application/json"
7844 },
7845 body: JSON.stringify({
7846 password: "admin-secret",
7847 role: "browser_admin"
7848 })
7849 });
7850 assert.equal(browserAdminLoginResponse.status, 200);
7851 const browserAdminLoginPayload = await browserAdminLoginResponse.json();
7852 assert.equal(browserAdminLoginPayload.data.session.role, "browser_admin");
7853
7854 const codexStatusResponse = await fetch(`${baseUrl}/v1/codex`);
7855 assert.equal(codexStatusResponse.status, 200);
7856 const codexStatusPayload = await codexStatusResponse.json();
7857 assert.equal(codexStatusPayload.data.proxy.target_base_url, codexd.baseUrl);
7858 assert.equal(codexStatusPayload.data.sessions.count, 1);
7859
7860 const codexSessionsResponse = await fetch(`${baseUrl}/v1/codex/sessions`);
7861 assert.equal(codexSessionsResponse.status, 200);
7862 const codexSessionsPayload = await codexSessionsResponse.json();
7863 assert.equal(codexSessionsPayload.data.sessions[0].sessionId, "session-demo");
7864
7865 const codexSessionCreateResponse = await fetch(`${baseUrl}/v1/codex/sessions`, {
7866 method: "POST",
7867 headers: {
7868 "content-type": "application/json"
7869 },
7870 body: JSON.stringify({
7871 cwd: "/Users/george/code/baa-conductor",
7872 purpose: "duplex"
7873 })
7874 });
7875 assert.equal(codexSessionCreateResponse.status, 201);
7876 const codexSessionCreatePayload = await codexSessionCreateResponse.json();
7877 assert.equal(codexSessionCreatePayload.data.session.sessionId, "session-2");
7878
7879 const codexTurnResponse = await fetch(`${baseUrl}/v1/codex/turn`, {
7880 method: "POST",
7881 headers: {
7882 "content-type": "application/json"
7883 },
7884 body: JSON.stringify({
7885 input: "Continue.",
7886 sessionId: "session-demo"
7887 })
7888 });
7889 assert.equal(codexTurnResponse.status, 202);
7890 const codexTurnPayload = await codexTurnResponse.json();
7891 assert.equal(codexTurnPayload.data.accepted, true);
7892 assert.equal(codexTurnPayload.data.turnId, "turn-created");
7893
7894 const pauseResponse = await fetch(`${baseUrl}/v1/system/pause`, {
7895 method: "POST",
7896 headers: {
7897 "content-type": "application/json"
7898 },
7899 body: JSON.stringify({
7900 requested_by: "integration_test",
7901 reason: "pause_for_verification"
7902 })
7903 });
7904 assert.equal(pauseResponse.status, 200);
7905 const pausePayload = await pauseResponse.json();
7906 assert.equal(pausePayload.data.mode, "paused");
7907
7908 const pausedStateResponse = await fetch(`${baseUrl}/v1/system/state`);
7909 const pausedStatePayload = await pausedStateResponse.json();
7910 assert.equal(pausedStatePayload.data.mode, "paused");
7911
7912 const describeResponse = await fetch(`${baseUrl}/describe`);
7913 assert.equal(describeResponse.status, 200);
7914 const describePayload = await describeResponse.json();
7915 assert.equal(describePayload.ok, true);
7916 assert.equal(describePayload.data.name, "baa-conductor-daemon");
7917 assert.equal(describePayload.data.codex.target_base_url, codexd.baseUrl);
7918
7919 const businessDescribeResponse = await fetch(`${baseUrl}/describe/business`);
7920 assert.equal(businessDescribeResponse.status, 200);
7921 const businessDescribePayload = await businessDescribeResponse.json();
7922 assert.equal(businessDescribePayload.data.surface, "business");
7923 assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/codex/u);
7924 assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/status/u);
7925
7926 const controlDescribeResponse = await fetch(`${baseUrl}/describe/control`);
7927 assert.equal(controlDescribeResponse.status, 200);
7928 const controlDescribePayload = await controlDescribeResponse.json();
7929 assert.equal(controlDescribePayload.data.surface, "control");
7930 assert.match(JSON.stringify(controlDescribePayload.data.endpoints), /\/v1\/exec/u);
7931 assert.equal(controlDescribePayload.data.host_operations.auth.header, "Authorization: Bearer <BAA_SHARED_TOKEN>");
7932
7933 const unauthorizedExecResponse = await fetch(`${baseUrl}/v1/exec`, {
7934 method: "POST",
7935 headers: {
7936 "content-type": "application/json"
7937 },
7938 body: JSON.stringify({
7939 command: "printf 'runtime-host-op'",
7940 cwd: hostOpsDir,
7941 timeoutMs: 2000
7942 })
7943 });
7944 assert.equal(unauthorizedExecResponse.status, 401);
7945 const unauthorizedExecPayload = await unauthorizedExecResponse.json();
7946 assert.equal(unauthorizedExecPayload.error, "unauthorized");
7947
7948 const execResponse = await fetch(`${baseUrl}/v1/exec`, {
7949 method: "POST",
7950 headers: hostOpsHeaders,
7951 body: JSON.stringify({
7952 command: "printf 'runtime-host-op'",
7953 cwd: hostOpsDir,
7954 timeoutMs: 2000
7955 })
7956 });
7957 assert.equal(execResponse.status, 200);
7958 const execPayload = await execResponse.json();
7959 assert.equal(execPayload.data.ok, true);
7960 assert.equal(execPayload.data.result.stdout, "runtime-host-op");
7961
7962 const writeResponse = await fetch(`${baseUrl}/v1/files/write`, {
7963 method: "POST",
7964 headers: hostOpsHeaders,
7965 body: JSON.stringify({
7966 path: "runtime/demo.txt",
7967 cwd: hostOpsDir,
7968 content: "hello from runtime host ops",
7969 overwrite: false,
7970 createParents: true
7971 })
7972 });
7973 assert.equal(writeResponse.status, 200);
7974 const writePayload = await writeResponse.json();
7975 assert.equal(writePayload.data.ok, true);
7976 assert.equal(writePayload.data.result.created, true);
7977
7978 const duplicateWriteResponse = await fetch(`${baseUrl}/v1/files/write`, {
7979 method: "POST",
7980 headers: hostOpsHeaders,
7981 body: JSON.stringify({
7982 path: "runtime/demo.txt",
7983 cwd: hostOpsDir,
7984 content: "should not overwrite",
7985 overwrite: false
7986 })
7987 });
7988 assert.equal(duplicateWriteResponse.status, 200);
7989 const duplicateWritePayload = await duplicateWriteResponse.json();
7990 assert.equal(duplicateWritePayload.data.ok, false);
7991 assert.equal(duplicateWritePayload.data.error.code, "FILE_ALREADY_EXISTS");
7992
7993 const readResponse = await fetch(`${baseUrl}/v1/files/read`, {
7994 method: "POST",
7995 headers: hostOpsHeaders,
7996 body: JSON.stringify({
7997 path: "runtime/demo.txt",
7998 cwd: hostOpsDir
7999 })
8000 });
8001 assert.equal(readResponse.status, 200);
8002 const readPayload = await readResponse.json();
8003 assert.equal(readPayload.data.ok, true);
8004 assert.equal(readPayload.data.result.content, "hello from runtime host ops");
8005
8006 const stoppedSnapshot = await runtime.stop();
8007 assert.equal(stoppedSnapshot.runtime.started, false);
8008 } finally {
8009 await codexd.stop();
8010 rmSync(stateDir, {
8011 force: true,
8012 recursive: true
8013 });
8014 rmSync(hostOpsDir, {
8015 force: true,
8016 recursive: true
8017 });
8018 }
8019});
8020
8021test("handleConductorHttpRequest serves artifact files and robots.txt", async () => {
8022 const { repository, snapshot } = await createLocalApiFixture();
8023
8024 await withArtifactStoreFixture(async ({ artifactStore }) => {
8025 await artifactStore.insertMessage({
8026 conversationId: "conv_artifact",
8027 id: "msg_artifact",
8028 observedAt: 1_743_151_740_000,
8029 platform: "claude",
8030 rawText: "artifact message body",
8031 role: "assistant"
8032 });
8033 await artifactStore.insertExecution({
8034 executedAt: 1_743_151_800_000,
8035 instructionId: "inst_artifact",
8036 messageId: "msg_artifact",
8037 params: {
8038 command: "pnpm test"
8039 },
8040 paramsKind: "body",
8041 resultData: {
8042 exit_code: 0,
8043 stdout: "ok"
8044 },
8045 resultOk: true,
8046 target: "conductor",
8047 tool: "exec"
8048 });
8049 await artifactStore.upsertSession({
8050 conversationId: "conv_artifact",
8051 executionCount: 1,
8052 id: "session_artifact",
8053 lastActivityAt: 1_743_151_800_000,
8054 messageCount: 1,
8055 platform: "claude",
8056 startedAt: 1_743_151_740_000
8057 });
8058
8059 const context = {
8060 artifactStore,
8061 repository,
8062 snapshotLoader: () => snapshot
8063 };
8064
8065 const robotsResponse = await handleConductorHttpRequest(
8066 {
8067 method: "GET",
8068 path: "/robots.txt"
8069 },
8070 context
8071 );
8072 assert.equal(robotsResponse.status, 200);
8073 assert.equal(robotsResponse.body, "User-agent: *\nAllow: /artifact/\n");
8074
8075 const messageTextResponse = await handleConductorHttpRequest(
8076 {
8077 method: "GET",
8078 path: "/artifact/msg/msg_artifact.txt"
8079 },
8080 context
8081 );
8082 assert.equal(messageTextResponse.status, 200);
8083 assert.equal(messageTextResponse.headers["content-type"], "text/plain; charset=utf-8");
8084 assert.match(Buffer.from(messageTextResponse.body).toString("utf8"), /artifact message body/u);
8085
8086 const messageJsonResponse = await handleConductorHttpRequest(
8087 {
8088 method: "GET",
8089 path: "/artifact/msg/msg_artifact.json"
8090 },
8091 context
8092 );
8093 assert.equal(messageJsonResponse.status, 200);
8094 assert.equal(messageJsonResponse.headers["content-type"], "application/json; charset=utf-8");
8095 assert.match(
8096 Buffer.from(messageJsonResponse.body).toString("utf8"),
8097 /https:\/\/conductor\.makefile\.so\/artifact\/msg\/msg_artifact\.txt/u
8098 );
8099
8100 const sessionLatestResponse = await handleConductorHttpRequest(
8101 {
8102 method: "GET",
8103 path: "/artifact/session/latest.txt"
8104 },
8105 context
8106 );
8107 assert.equal(sessionLatestResponse.status, 200);
8108 assert.match(Buffer.from(sessionLatestResponse.body).toString("utf8"), /session_artifact/u);
8109
8110 const removedRepoRootResponse = await handleConductorHttpRequest(
8111 {
8112 method: "GET",
8113 path: "/artifact/repo/demo-repo"
8114 },
8115 context
8116 );
8117 assert.equal(removedRepoRootResponse.status, 404);
8118 assert.equal(parseJsonBody(removedRepoRootResponse).error, "not_found");
8119
8120 const removedRepoFileResponse = await handleConductorHttpRequest(
8121 {
8122 method: "GET",
8123 path: "/artifact/repo/demo-repo/log.html"
8124 },
8125 context
8126 );
8127 assert.equal(removedRepoFileResponse.status, 404);
8128 assert.equal(parseJsonBody(removedRepoFileResponse).error, "not_found");
8129
8130 const missingResponse = await handleConductorHttpRequest(
8131 {
8132 method: "GET",
8133 path: "/artifact/msg/missing.txt"
8134 },
8135 context
8136 );
8137 assert.equal(missingResponse.status, 404);
8138 assert.equal(parseJsonBody(missingResponse).error, "not_found");
8139 });
8140});
8141
8142test("handleConductorHttpRequest serves conductor-ui shell assets and history fallback", async () => {
8143 const { repository, snapshot } = await createLocalApiFixture();
8144
8145 await withConductorUiDistFixture(async ({ assetFile, uiDistDir }) => {
8146 const context = {
8147 repository,
8148 snapshotLoader: () => snapshot,
8149 uiDistDir
8150 };
8151
8152 const appResponse = await handleConductorHttpRequest(
8153 {
8154 method: "GET",
8155 path: "/app"
8156 },
8157 context
8158 );
8159 assert.equal(appResponse.status, 200);
8160 assert.equal(appResponse.headers["cache-control"], "no-store");
8161 assert.equal(appResponse.headers["content-type"], "text/html; charset=utf-8");
8162 const appHtml = readBinaryBodyText(appResponse);
8163 assert.match(appHtml, /Conductor UI Shell Fixture/u);
8164
8165 const appSlashResponse = await handleConductorHttpRequest(
8166 {
8167 method: "GET",
8168 path: "/app/"
8169 },
8170 context
8171 );
8172 assert.equal(readBinaryBodyText(appSlashResponse), appHtml);
8173
8174 const historyResponse = await handleConductorHttpRequest(
8175 {
8176 method: "GET",
8177 path: "/app/control"
8178 },
8179 context
8180 );
8181 assert.equal(historyResponse.status, 200);
8182 assert.equal(readBinaryBodyText(historyResponse), appHtml);
8183
8184 const assetResponse = await handleConductorHttpRequest(
8185 {
8186 method: "GET",
8187 path: `/app/assets/${assetFile}`
8188 },
8189 context
8190 );
8191 assert.equal(assetResponse.status, 200);
8192 assert.equal(assetResponse.headers["cache-control"], "public, max-age=31536000, immutable");
8193 assert.equal(assetResponse.headers["content-type"], "text/javascript; charset=utf-8");
8194 assert.equal(readBinaryBodyText(assetResponse), "console.log('fixture ui shell');\n");
8195
8196 const missingAssetResponse = await handleConductorHttpRequest(
8197 {
8198 method: "GET",
8199 path: "/app/assets/missing.js"
8200 },
8201 context
8202 );
8203 assert.equal(missingAssetResponse.status, 404);
8204 assert.equal(parseJsonBody(missingAssetResponse).error, "not_found");
8205
8206 const assetRootResponse = await handleConductorHttpRequest(
8207 {
8208 method: "GET",
8209 path: "/app/assets"
8210 },
8211 context
8212 );
8213 assert.equal(assetRootResponse.status, 404);
8214 assert.equal(parseJsonBody(assetRootResponse).error, "not_found");
8215 });
8216});
8217
8218test("handleConductorHttpRequest serves code files and blocks unsafe paths", async () => {
8219 const { repository, snapshot } = await createLocalApiFixture();
8220 const codeRootDir = mkdtempSync(join(tmpdir(), "baa-conductor-code-route-"));
8221 const repoDir = join(codeRootDir, "demo-repo");
8222
8223 try {
8224 mkdirSync(join(repoDir, "src"), { recursive: true });
8225 mkdirSync(join(repoDir, ".git", "objects"), { recursive: true });
8226 writeFileSync(join(repoDir, "package.json"), "{\n \"name\": \"demo-repo\"\n}\n");
8227 writeFileSync(join(repoDir, ".env"), "SECRET=1\n");
8228 writeFileSync(join(repoDir, ".credentials"), "token=secret\n");
8229 writeFileSync(join(repoDir, ".git", "objects", "secret"), "hidden\n");
8230 writeFileSync(join(repoDir, "image.png"), Buffer.from([0x89, 0x50, 0x4e, 0x47]));
8231 writeFileSync(join(repoDir, "src", "index.ts"), "export const demo = true;\n");
8232
8233 const context = {
8234 codeRootDir,
8235 repository,
8236 snapshotLoader: () => snapshot
8237 };
8238
8239 const fileResponse = await handleConductorHttpRequest(
8240 {
8241 method: "GET",
8242 path: "/code/demo-repo/package.json"
8243 },
8244 context
8245 );
8246 assert.equal(fileResponse.status, 200);
8247 assert.equal(fileResponse.headers["content-type"], "text/plain; charset=utf-8");
8248 assert.equal(Buffer.from(fileResponse.body).toString("utf8"), "{\n \"name\": \"demo-repo\"\n}\n");
8249
8250 const directoryResponse = await handleConductorHttpRequest(
8251 {
8252 method: "GET",
8253 path: "/code/demo-repo"
8254 },
8255 context
8256 );
8257 assert.equal(directoryResponse.status, 200);
8258 assert.deepEqual(Buffer.from(directoryResponse.body).toString("utf8").split("\n"), [
8259 "package.json",
8260 "src"
8261 ]);
8262
8263 const hiddenEnvResponse = await handleConductorHttpRequest(
8264 {
8265 method: "GET",
8266 path: "/code/demo-repo/.env"
8267 },
8268 context
8269 );
8270 assert.equal(hiddenEnvResponse.status, 403);
8271 assert.equal(parseJsonBody(hiddenEnvResponse).error, "forbidden");
8272
8273 const hiddenCredentialsResponse = await handleConductorHttpRequest(
8274 {
8275 method: "GET",
8276 path: "/code/demo-repo/.credentials"
8277 },
8278 context
8279 );
8280 assert.equal(hiddenCredentialsResponse.status, 403);
8281 assert.equal(parseJsonBody(hiddenCredentialsResponse).error, "forbidden");
8282
8283 const hiddenGitObjectsResponse = await handleConductorHttpRequest(
8284 {
8285 method: "GET",
8286 path: "/code/demo-repo/.git/objects/secret"
8287 },
8288 context
8289 );
8290 assert.equal(hiddenGitObjectsResponse.status, 403);
8291 assert.equal(parseJsonBody(hiddenGitObjectsResponse).error, "forbidden");
8292
8293 const hiddenGitConfigResponse = await handleConductorHttpRequest(
8294 {
8295 method: "GET",
8296 path: "/code/demo-repo/.git/config"
8297 },
8298 context
8299 );
8300 assert.equal(hiddenGitConfigResponse.status, 403);
8301 assert.equal(parseJsonBody(hiddenGitConfigResponse).error, "forbidden");
8302
8303 const hiddenGitDirectoryResponse = await handleConductorHttpRequest(
8304 {
8305 method: "GET",
8306 path: "/code/demo-repo/.git"
8307 },
8308 context
8309 );
8310 assert.equal(hiddenGitDirectoryResponse.status, 403);
8311 assert.equal(parseJsonBody(hiddenGitDirectoryResponse).error, "forbidden");
8312
8313 const traversalResponse = await handleConductorHttpRequest(
8314 {
8315 method: "GET",
8316 path: "/code/demo-repo/%2e%2e/%2e%2e/%2e%2e/etc/passwd"
8317 },
8318 context
8319 );
8320 assert.ok([403, 404].includes(traversalResponse.status));
8321 assert.match(parseJsonBody(traversalResponse).error, /^(forbidden|not_found)$/u);
8322
8323 const binaryResponse = await handleConductorHttpRequest(
8324 {
8325 method: "GET",
8326 path: "/code/demo-repo/image.png"
8327 },
8328 context
8329 );
8330 assert.equal(binaryResponse.status, 403);
8331 assert.equal(parseJsonBody(binaryResponse).error, "forbidden");
8332
8333 const missingResponse = await handleConductorHttpRequest(
8334 {
8335 method: "GET",
8336 path: "/code/demo-repo/missing.ts"
8337 },
8338 context
8339 );
8340 assert.equal(missingResponse.status, 404);
8341 assert.equal(parseJsonBody(missingResponse).error, "not_found");
8342 } finally {
8343 rmSync(codeRootDir, {
8344 force: true,
8345 recursive: true
8346 });
8347 }
8348});
8349
8350test("ConductorRuntime exposes a minimal runtime snapshot for CLI and status surfaces", async () => {
8351 await withRuntimeFixture(async ({ runtime }) => {
8352 assert.equal(runtime.getRuntimeSnapshot().runtime.started, false);
8353
8354 const startedSnapshot = await runtime.start();
8355 assert.equal(startedSnapshot.runtime.started, true);
8356 assert.equal(startedSnapshot.daemon.leaseState, "leader");
8357 assert.equal(startedSnapshot.daemon.schedulerEnabled, true);
8358 assert.equal(startedSnapshot.loops.heartbeat, false);
8359 assert.equal(startedSnapshot.loops.lease, false);
8360 assert.equal(startedSnapshot.controlApi.usesPlaceholderToken, true);
8361 assert.match(startedSnapshot.warnings.join("\n"), /replace-me/);
8362
8363 const stoppedSnapshot = await runtime.stop();
8364 assert.equal(stoppedSnapshot.runtime.started, false);
8365 });
8366});
8367
8368test("ConductorRuntime initializes timed-jobs logging during startup", async () => {
8369 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-timed-jobs-state-"));
8370 const logsDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-timed-jobs-logs-"));
8371 const runtime = new ConductorRuntime(
8372 {
8373 nodeId: "mini-main",
8374 host: "mini",
8375 role: "primary",
8376 controlApiBase: "https://control.example.test",
8377 localApiBase: "http://127.0.0.1:0",
8378 sharedToken: "replace-me",
8379 paths: {
8380 logsDir,
8381 runsDir: "/tmp/runs",
8382 stateDir
8383 }
8384 },
8385 {
8386 autoStartLoops: false,
8387 now: () => 100
8388 }
8389 );
8390
8391 try {
8392 await runtime.start();
8393
8394 const timedJobsLogDir = join(logsDir, "timed-jobs");
8395 assert.equal(existsSync(timedJobsLogDir), true);
8396
8397 const entries = await waitForJsonlEntries(
8398 timedJobsLogDir,
8399 (items) => items.some((entry) => entry.runner === "timed-jobs.framework" && entry.stage === "started")
8400 );
8401 assert.ok(
8402 entries.find(
8403 (entry) => entry.runner === "timed-jobs.framework" && entry.stage === "started"
8404 )
8405 );
8406 } finally {
8407 await runtime.stop();
8408 rmSync(logsDir, {
8409 force: true,
8410 recursive: true
8411 });
8412 rmSync(stateDir, {
8413 force: true,
8414 recursive: true
8415 });
8416 }
8417});
8418
8419function createStubRuntimeDaemon(overrides = {}) {
8420 return {
8421 start: async () => createStubRuntimeDaemonSnapshot(),
8422 stop: async () => createStubRuntimeDaemonSnapshot(),
8423 getStatusSnapshot: () => createStubRuntimeDaemonSnapshot(),
8424 getStartupChecklist: () => [],
8425 describeIdentity: () => "mini-main@mini(primary)",
8426 getLoopStatus: () => ({
8427 heartbeat: false,
8428 lease: false
8429 }),
8430 ...overrides
8431 };
8432}
8433
8434function createStubRuntimeDaemonSnapshot() {
8435 return {
8436 nodeId: "mini-main",
8437 host: "mini",
8438 role: "primary",
8439 leaseState: "standby",
8440 schedulerEnabled: false,
8441 currentLeaderId: null,
8442 currentTerm: null,
8443 leaseExpiresAt: null,
8444 lastHeartbeatAt: null,
8445 lastLeaseOperation: null,
8446 nextLeaseOperation: "acquire",
8447 consecutiveRenewFailures: 0,
8448 lastError: null
8449 };
8450}
8451
8452test("ConductorRuntime awaits daemon shutdown when local API startup fails", async () => {
8453 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-start-stop-await-"));
8454 const runtime = new ConductorRuntime(
8455 {
8456 nodeId: "mini-main",
8457 host: "mini",
8458 role: "primary",
8459 controlApiBase: "https://control.example.test",
8460 localApiBase: "http://127.0.0.1:0",
8461 sharedToken: "replace-me",
8462 paths: {
8463 runsDir: "/tmp/runs",
8464 stateDir
8465 }
8466 },
8467 {
8468 autoStartLoops: false,
8469 now: () => 100
8470 }
8471 );
8472
8473 let daemonStopResolved = false;
8474
8475 runtime.localControlPlaneInitialized = true;
8476 runtime.daemon = createStubRuntimeDaemon({
8477 stop: async () => {
8478 await new Promise((resolve) => setTimeout(resolve, 0));
8479 daemonStopResolved = true;
8480 return createStubRuntimeDaemonSnapshot();
8481 }
8482 });
8483 runtime.localApiServer = {
8484 start: async () => {
8485 throw new Error("local api start failed");
8486 },
8487 stop: async () => {},
8488 getBaseUrl: () => "http://127.0.0.1:0",
8489 getBrowserWebSocketUrl: () => null,
8490 getFirefoxWebSocketUrl: () => null
8491 };
8492
8493 try {
8494 await assert.rejects(runtime.start(), /local api start failed/u);
8495 assert.equal(daemonStopResolved, true);
8496 assert.equal(runtime.getRuntimeSnapshot().runtime.started, false);
8497 } finally {
8498 rmSync(stateDir, {
8499 force: true,
8500 recursive: true
8501 });
8502 }
8503});
8504
8505test("ConductorRuntime.stop waits for daemon shutdown before closing the local API server", async () => {
8506 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-stop-await-"));
8507 const runtime = new ConductorRuntime(
8508 {
8509 nodeId: "mini-main",
8510 host: "mini",
8511 role: "primary",
8512 controlApiBase: "https://control.example.test",
8513 localApiBase: "http://127.0.0.1:0",
8514 sharedToken: "replace-me",
8515 paths: {
8516 runsDir: "/tmp/runs",
8517 stateDir
8518 }
8519 },
8520 {
8521 autoStartLoops: false,
8522 now: () => 100
8523 }
8524 );
8525
8526 let daemonStopResolved = false;
8527 let releaseDaemonStop = null;
8528 let localApiStopSawDaemonResolved = false;
8529
8530 runtime.started = true;
8531 runtime.daemon = createStubRuntimeDaemon({
8532 stop: async () => {
8533 await new Promise((resolve) => {
8534 releaseDaemonStop = () => {
8535 daemonStopResolved = true;
8536 resolve();
8537 };
8538 });
8539 return createStubRuntimeDaemonSnapshot();
8540 }
8541 });
8542 runtime.localApiServer = {
8543 start: async () => {},
8544 stop: async () => {
8545 localApiStopSawDaemonResolved = daemonStopResolved;
8546 },
8547 getBaseUrl: () => "http://127.0.0.1:0",
8548 getBrowserWebSocketUrl: () => null,
8549 getFirefoxWebSocketUrl: () => null
8550 };
8551
8552 try {
8553 const stopPromise = runtime.stop();
8554 await new Promise((resolve) => setTimeout(resolve, 0));
8555 assert.equal(localApiStopSawDaemonResolved, false);
8556
8557 releaseDaemonStop();
8558 const stoppedSnapshot = await stopPromise;
8559
8560 assert.equal(stoppedSnapshot.runtime.started, false);
8561 assert.equal(localApiStopSawDaemonResolved, true);
8562 } finally {
8563 rmSync(stateDir, {
8564 force: true,
8565 recursive: true
8566 });
8567 }
8568});
8569
8570test("ConductorRuntime.stop closes active Firefox bridge clients and releases the local API listener", async () => {
8571 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-stop-"));
8572 const runtime = new ConductorRuntime(
8573 {
8574 nodeId: "mini-main",
8575 host: "mini",
8576 role: "primary",
8577 controlApiBase: "https://control.example.test",
8578 localApiBase: "http://127.0.0.1:0",
8579 sharedToken: "replace-me",
8580 paths: {
8581 runsDir: "/tmp/runs",
8582 stateDir
8583 }
8584 },
8585 {
8586 autoStartLoops: false,
8587 now: () => 100
8588 }
8589 );
8590
8591 let client = null;
8592
8593 try {
8594 const snapshot = await runtime.start();
8595 const baseUrl = snapshot.controlApi.localApiBase;
8596 const wsUrl = snapshot.controlApi.firefoxWsUrl;
8597
8598 assert.equal(typeof baseUrl, "string");
8599 assert.equal(typeof wsUrl, "string");
8600
8601 const healthResponse = await fetch(`${baseUrl}/healthz`);
8602 assert.equal(healthResponse.status, 200);
8603
8604 client = await connectFirefoxBridgeClient(wsUrl, "firefox-stop");
8605
8606 const closePromise = waitForWebSocketClose(client.socket);
8607 const stoppedSnapshot = await runtime.stop();
8608
8609 assert.equal(stoppedSnapshot.runtime.started, false);
8610 assert.deepEqual(await closePromise, {
8611 code: 1001,
8612 reason: "server shutdown"
8613 });
8614 await assertLocalApiListenerClosed(baseUrl);
8615 } finally {
8616 client?.queue.stop();
8617 client?.socket.close(1000, "done");
8618 await runtime.stop();
8619 rmSync(stateDir, {
8620 force: true,
8621 recursive: true
8622 });
8623 }
8624});
8625
8626test("ConductorRuntime.stop remains idempotent after the local API listener is closed", async () => {
8627 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-stop-repeat-"));
8628 const runtime = new ConductorRuntime(
8629 {
8630 nodeId: "mini-main",
8631 host: "mini",
8632 role: "primary",
8633 controlApiBase: "https://control.example.test",
8634 localApiBase: "http://127.0.0.1:0",
8635 sharedToken: "replace-me",
8636 paths: {
8637 runsDir: "/tmp/runs",
8638 stateDir
8639 }
8640 },
8641 {
8642 autoStartLoops: false,
8643 now: () => 100
8644 }
8645 );
8646
8647 try {
8648 const snapshot = await runtime.start();
8649 const baseUrl = snapshot.controlApi.localApiBase;
8650
8651 assert.equal(typeof baseUrl, "string");
8652
8653 const firstStoppedSnapshot = await runtime.stop();
8654 assert.equal(firstStoppedSnapshot.runtime.started, false);
8655 await assertLocalApiListenerClosed(baseUrl);
8656
8657 const secondStoppedSnapshot = await runtime.stop();
8658 assert.equal(secondStoppedSnapshot.runtime.started, false);
8659 assert.equal(secondStoppedSnapshot.controlApi.localApiBase, baseUrl);
8660 await assertLocalApiListenerClosed(baseUrl);
8661 } finally {
8662 await runtime.stop();
8663 rmSync(stateDir, {
8664 force: true,
8665 recursive: true
8666 });
8667 }
8668});
8669
8670test("ConductorRuntime fixture closes the local API listener when a started test aborts", async () => {
8671 let baseUrl = null;
8672
8673 await assert.rejects(
8674 withRuntimeFixture(async ({ runtime }) => {
8675 const snapshot = await runtime.start();
8676 baseUrl = snapshot.controlApi.localApiBase;
8677
8678 const healthResponse = await fetch(`${baseUrl}/healthz`);
8679 assert.equal(healthResponse.status, 200);
8680
8681 throw new Error("forced runtime fixture failure");
8682 }),
8683 /forced runtime fixture failure/u
8684 );
8685
8686 assert.equal(typeof baseUrl, "string");
8687 await assertLocalApiListenerClosed(baseUrl);
8688});
8689
8690test("ConductorRuntime exposes a local Firefox websocket bridge over the local API listener", async () => {
8691 await withRuntimeFixture(async ({ runtime }) => {
8692 let socket = null;
8693 let queue = null;
8694
8695 try {
8696 const snapshot = await runtime.start();
8697 const wsUrl = snapshot.controlApi.firefoxWsUrl;
8698 const baseUrl = snapshot.controlApi.localApiBase;
8699 socket = new WebSocket(wsUrl);
8700 queue = createWebSocketMessageQueue(socket);
8701
8702 await waitForWebSocketOpen(socket);
8703
8704 socket.send(
8705 JSON.stringify({
8706 type: "hello",
8707 clientId: "firefox-test",
8708 nodeType: "browser",
8709 nodeCategory: "proxy",
8710 nodePlatform: "firefox"
8711 })
8712 );
8713
8714 const helloAck = await queue.next((message) => message.type === "hello_ack");
8715 assert.equal(helloAck.clientId, "firefox-test");
8716 assert.equal(helloAck.protocol, "baa.browser.local");
8717 assert.equal(helloAck.wsUrl, snapshot.controlApi.browserWsUrl);
8718 assert.deepEqual(helloAck.wsCompatUrls, [snapshot.controlApi.firefoxWsUrl]);
8719
8720 const initialSnapshot = await queue.next(
8721 (message) => message.type === "state_snapshot" && message.reason === "hello"
8722 );
8723 assert.equal(initialSnapshot.snapshot.system.mode, "running");
8724 assert.equal(initialSnapshot.snapshot.browser.client_count, 1);
8725
8726 const credentialRequest = await queue.next((message) => message.type === "request_credentials");
8727 assert.equal(credentialRequest.reason, "hello");
8728
8729 socket.send(
8730 JSON.stringify({
8731 type: "api_endpoints",
8732 platform: "chatgpt",
8733 account: "agent@example.com",
8734 credential_fingerprint: "fp-chatgpt-test",
8735 updated_at: 1_760_000_000_500,
8736 endpoints: ["/backend-api/conversation", "/backend-api/models"],
8737 endpoint_metadata: [
8738 {
8739 method: "POST",
8740 path: "/backend-api/conversation",
8741 first_seen_at: 1_760_000_000_100,
8742 last_seen_at: 1_760_000_000_400
8743 },
8744 {
8745 method: "GET",
8746 path: "/backend-api/models",
8747 first_seen_at: 1_760_000_000_200,
8748 last_seen_at: 1_760_000_000_500
8749 }
8750 ]
8751 })
8752 );
8753 socket.send(
8754 JSON.stringify({
8755 type: "credentials",
8756 platform: "chatgpt",
8757 account: "agent@example.com",
8758 credential_fingerprint: "fp-chatgpt-test",
8759 freshness: "fresh",
8760 captured_at: 1_760_000_000_000,
8761 last_seen_at: 1_760_000_000_500,
8762 headers: {
8763 authorization: "Bearer test-token",
8764 cookie: "session=test"
8765 },
8766 timestamp: 1_760_000_000_000
8767 })
8768 );
8769
8770 const browserSnapshot = await queue.next(
8771 (message) =>
8772 message.type === "state_snapshot"
8773 && message.snapshot.browser.clients.some((client) =>
8774 client.client_id === "firefox-test"
8775 && client.credentials.some((entry) =>
8776 entry.platform === "chatgpt"
8777 && entry.account === "agent@example.com"
8778 && entry.credential_fingerprint === "fp-chatgpt-test"
8779 && entry.header_count === 2
8780 )
8781 && client.request_hooks.some((entry) =>
8782 entry.platform === "chatgpt"
8783 && entry.account === "agent@example.com"
8784 && entry.endpoint_count === 2
8785 )
8786 )
8787 );
8788 assert.equal(browserSnapshot.snapshot.browser.client_count, 1);
8789
8790 socket.send(
8791 JSON.stringify({
8792 type: "action_request",
8793 action: "pause",
8794 requestId: "req-pause",
8795 requestedBy: "integration_test",
8796 reason: "pause_via_ws"
8797 })
8798 );
8799
8800 const actionResult = await queue.next(
8801 (message) => message.type === "action_result" && message.requestId === "req-pause"
8802 );
8803 assert.equal(actionResult.ok, true);
8804 assert.equal(actionResult.system.mode, "paused");
8805
8806 const pausedSnapshot = await queue.next(
8807 (message) => message.type === "state_snapshot" && message.snapshot.system.mode === "paused"
8808 );
8809 assert.equal(pausedSnapshot.snapshot.system.mode, "paused");
8810
8811 const systemStateResponse = await fetch(`${baseUrl}/v1/system/state`);
8812 assert.equal(systemStateResponse.status, 200);
8813 assert.equal((await systemStateResponse.json()).data.mode, "paused");
8814
8815 const stoppedSnapshot = await runtime.stop();
8816 assert.equal(stoppedSnapshot.runtime.started, false);
8817 } finally {
8818 queue?.stop();
8819
8820 if (socket) {
8821 const closePromise = waitForWebSocketClose(socket);
8822 socket.close(1000, "done");
8823 await closePromise;
8824 }
8825 }
8826 });
8827});
8828
8829test("ConductorRuntime accepts Safari bridge clients on the canonical browser websocket path", async () => {
8830 await withRuntimeFixture(async ({ runtime }) => {
8831 let client = null;
8832
8833 try {
8834 const snapshot = await runtime.start();
8835 const wsUrl = snapshot.controlApi.browserWsUrl;
8836 const baseUrl = snapshot.controlApi.localApiBase;
8837
8838 client = await connectBrowserBridgeClient(wsUrl, "safari-test", "safari");
8839
8840 assert.equal(client.helloAck.clientId, "safari-test");
8841 assert.equal(client.helloAck.protocol, "baa.browser.local");
8842 assert.equal(client.helloAck.wsUrl, snapshot.controlApi.browserWsUrl);
8843 assert.deepEqual(client.helloAck.wsCompatUrls, [snapshot.controlApi.firefoxWsUrl]);
8844 assert.equal(client.initialSnapshot.snapshot.browser.clients[0]?.node_platform, "safari");
8845 assert.equal(client.initialSnapshot.snapshot.browser.ws_path, "/ws/browser");
8846
8847 const browserStatus = await fetch(`${baseUrl}/v1/browser`);
8848 assert.equal(browserStatus.status, 200);
8849 const browserPayload = await browserStatus.json();
8850 assert.equal(browserPayload.data.bridge.transport, "local_browser_ws");
8851 assert.equal(browserPayload.data.bridge.ws_path, "/ws/browser");
8852 assert.equal(browserPayload.data.bridge.ws_url, snapshot.controlApi.browserWsUrl);
8853 assert.equal(browserPayload.data.current_client.client_id, "safari-test");
8854 assert.equal(browserPayload.data.current_client.node_platform, "safari");
8855 } finally {
8856 client?.queue.stop();
8857 client?.socket.close();
8858
8859 if (client?.socket) {
8860 await waitForWebSocketClose(client.socket);
8861 }
8862 }
8863 });
8864});
8865
8866test("ConductorRuntime exposes Firefox outbound bridge commands and api request responses", async () => {
8867 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-firefox-bridge-"));
8868 const runtime = new ConductorRuntime(
8869 {
8870 nodeId: "mini-main",
8871 host: "mini",
8872 role: "primary",
8873 controlApiBase: "https://control.example.test",
8874 localApiBase: "http://127.0.0.1:0",
8875 sharedToken: "replace-me",
8876 paths: {
8877 runsDir: "/tmp/runs",
8878 stateDir
8879 }
8880 },
8881 {
8882 autoStartLoops: false,
8883 now: () => 100
8884 }
8885 );
8886
8887 let firstClient = null;
8888 let secondClient = null;
8889
8890 try {
8891 const snapshot = await runtime.start();
8892 const bridge = runtime.getFirefoxBridgeService();
8893
8894 assert.ok(bridge);
8895
8896 firstClient = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-a");
8897 secondClient = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-b");
8898
8899 const openTabReceipt = bridge.openTab({
8900 clientId: "firefox-a",
8901 platform: "chatgpt"
8902 });
8903 assert.equal(openTabReceipt.clientId, "firefox-a");
8904
8905 const openTabMessage = await firstClient.queue.next((message) => message.type === "open_tab");
8906 assert.equal(openTabMessage.platform, "chatgpt");
8907
8908 await assert.rejects(
8909 secondClient.queue.next((message) => message.type === "open_tab", 250),
8910 /timed out waiting for websocket message/u
8911 );
8912
8913 const reloadReceipt = bridge.reload({
8914 reason: "integration_test"
8915 });
8916 assert.equal(reloadReceipt.clientId, "firefox-b");
8917
8918 const reloadMessage = await secondClient.queue.next((message) => message.type === "reload");
8919 assert.equal(reloadMessage.reason, "integration_test");
8920
8921 await assert.rejects(
8922 firstClient.queue.next((message) => message.type === "reload", 250),
8923 /timed out waiting for websocket message/u
8924 );
8925
8926 const credentialReceipt = bridge.requestCredentials({
8927 clientId: "firefox-a",
8928 platform: "chatgpt",
8929 reason: "integration_test"
8930 });
8931 assert.equal(credentialReceipt.clientId, "firefox-a");
8932
8933 const credentialMessage = await firstClient.queue.next(
8934 (message) => message.type === "request_credentials" && message.reason === "integration_test"
8935 );
8936 assert.equal(credentialMessage.platform, "chatgpt");
8937
8938 const apiRequestPromise = bridge.apiRequest({
8939 clientId: "firefox-b",
8940 platform: "chatgpt",
8941 method: "POST",
8942 path: "/backend-api/conversation",
8943 body: {
8944 prompt: "hello"
8945 },
8946 headers: {
8947 authorization: "Bearer bridge-token"
8948 }
8949 });
8950 const apiRequestMessage = await secondClient.queue.next((message) => message.type === "api_request");
8951 assert.equal(apiRequestMessage.method, "POST");
8952 assert.equal(apiRequestMessage.path, "/backend-api/conversation");
8953 assert.equal(apiRequestMessage.platform, "chatgpt");
8954 assert.equal(apiRequestMessage.headers.authorization, "Bearer bridge-token");
8955
8956 secondClient.socket.send(
8957 JSON.stringify({
8958 type: "api_response",
8959 id: apiRequestMessage.id,
8960 ok: true,
8961 status: 202,
8962 body: {
8963 accepted: true
8964 }
8965 })
8966 );
8967
8968 const apiResponse = await apiRequestPromise;
8969 assert.equal(apiResponse.clientId, "firefox-b");
8970 assert.equal(apiResponse.connectionId, reloadReceipt.connectionId);
8971 assert.equal(apiResponse.id, apiRequestMessage.id);
8972 assert.equal(apiResponse.ok, true);
8973 assert.equal(apiResponse.status, 202);
8974 assert.deepEqual(apiResponse.body, {
8975 accepted: true
8976 });
8977 } finally {
8978 firstClient?.queue.stop();
8979 secondClient?.queue.stop();
8980 firstClient?.socket.close(1000, "done");
8981 secondClient?.socket.close(1000, "done");
8982 await runtime.stop();
8983 rmSync(stateDir, {
8984 force: true,
8985 recursive: true
8986 });
8987 }
8988});
8989
8990test("ConductorRuntime routes browser.final_message into live instruction ingest and exposes the latest summary", async () => {
8991 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-final-message-ingest-"));
8992 const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-conductor-final-message-host-"));
8993 const runtime = new ConductorRuntime(
8994 {
8995 nodeId: "mini-main",
8996 host: "mini",
8997 role: "primary",
8998 controlApiBase: "https://control.example.test",
8999 localApiBase: "http://127.0.0.1:0",
9000 sharedToken: "replace-me",
9001 paths: {
9002 runsDir: "/tmp/runs",
9003 stateDir
9004 }
9005 },
9006 {
9007 autoStartLoops: false,
9008 now: () => 100
9009 }
9010 );
9011
9012 let client = null;
9013
9014 try {
9015 const snapshot = await runtime.start();
9016 const baseUrl = snapshot.controlApi.localApiBase;
9017 const messageText = [
9018 "```baa",
9019 "@conductor::describe",
9020 "```",
9021 "",
9022 "```baa",
9023 `@conductor::exec::{"command":"printf 'ws-live\\n' >> final-message-ingest.txt","cwd":${JSON.stringify(hostOpsDir)}}`,
9024 "```"
9025 ].join("\n");
9026
9027 client = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-final-message-ingest");
9028
9029 client.socket.send(
9030 JSON.stringify({
9031 type: "browser.final_message",
9032 platform: "chatgpt",
9033 assistant_message_id: "msg-final-message-ingest",
9034 raw_text: messageText,
9035 observed_at: 1_710_000_010_000
9036 })
9037 );
9038
9039 const executedSnapshot = await client.queue.next(
9040 (message) =>
9041 message.type === "state_snapshot"
9042 && message.reason === "instruction_ingest"
9043 && message.snapshot.browser.instruction_ingest.last_ingest?.assistant_message_id === "msg-final-message-ingest"
9044 && message.snapshot.browser.instruction_ingest.last_ingest?.status === "executed"
9045 );
9046
9047 assert.equal(
9048 executedSnapshot.snapshot.browser.instruction_ingest.last_ingest.conversation_id,
9049 null
9050 );
9051 assert.deepEqual(
9052 executedSnapshot.snapshot.browser.instruction_ingest.last_ingest.instruction_tools,
9053 ["conductor::describe", "conductor::exec"]
9054 );
9055 assert.equal(
9056 executedSnapshot.snapshot.browser.instruction_ingest.last_ingest.execution_ok_count,
9057 2
9058 );
9059 assert.equal(readFileSync(join(hostOpsDir, "final-message-ingest.txt"), "utf8"), "ws-live\n");
9060
9061 const persistedStore = new ArtifactStore({
9062 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
9063 databasePath: join(stateDir, ARTIFACT_DB_FILENAME),
9064 publicBaseUrl: "https://control.example.test"
9065 });
9066
9067 try {
9068 const persistedMessage = await persistedStore.getMessage("msg-final-message-ingest");
9069 const persistedExecutions = await persistedStore.listExecutions({
9070 messageId: "msg-final-message-ingest"
9071 });
9072 const persistedSessions = await persistedStore.getLatestSessions(1);
9073
9074 assert.equal(persistedMessage?.rawText, messageText);
9075 assert.equal(existsSync(join(stateDir, ARTIFACTS_DIRNAME, "msg", "msg-final-message-ingest.txt")), true);
9076 assert.equal(persistedExecutions.length, 2);
9077 assert.ok(persistedExecutions.every((execution) =>
9078 existsSync(join(stateDir, ARTIFACTS_DIRNAME, execution.staticPath))
9079 ));
9080 assert.equal(persistedSessions[0]?.messageCount, 1);
9081 assert.equal(persistedSessions[0]?.executionCount, 2);
9082 assert.equal(existsSync(join(stateDir, ARTIFACTS_DIRNAME, "session", "latest.txt")), true);
9083 } finally {
9084 persistedStore.close();
9085 }
9086
9087 const browserStatusResponse = await fetch(`${baseUrl}/v1/browser`);
9088 assert.equal(browserStatusResponse.status, 200);
9089 const browserStatusPayload = await browserStatusResponse.json();
9090 assert.equal(browserStatusPayload.data.instruction_ingest.last_ingest.status, "executed");
9091 assert.equal(
9092 browserStatusPayload.data.instruction_ingest.last_execute.assistant_message_id,
9093 "msg-final-message-ingest"
9094 );
9095
9096 client.socket.send(
9097 JSON.stringify({
9098 type: "browser.final_message",
9099 platform: "chatgpt",
9100 conversation_id: "conv-replayed",
9101 assistant_message_id: "msg-final-message-ingest",
9102 raw_text: messageText,
9103 observed_at: 1_710_000_010_500
9104 })
9105 );
9106
9107 const duplicateSnapshot = await client.queue.next(
9108 (message) =>
9109 message.type === "state_snapshot"
9110 && message.reason === "instruction_ingest"
9111 && message.snapshot.browser.instruction_ingest.last_ingest?.assistant_message_id === "msg-final-message-ingest"
9112 && message.snapshot.browser.instruction_ingest.last_ingest?.status === "duplicate_message"
9113 );
9114
9115 assert.equal(
9116 duplicateSnapshot.snapshot.browser.instruction_ingest.last_execute.status,
9117 "executed"
9118 );
9119 assert.equal(readFileSync(join(hostOpsDir, "final-message-ingest.txt"), "utf8"), "ws-live\n");
9120 } finally {
9121 client?.queue.stop();
9122 client?.socket.close(1000, "done");
9123 await runtime.stop();
9124 rmSync(stateDir, {
9125 force: true,
9126 recursive: true
9127 });
9128 rmSync(hostOpsDir, {
9129 force: true,
9130 recursive: true
9131 });
9132 }
9133});
9134
9135test("observeRenewalConversation ignores inactive remote links and creates a new local conversation", async () => {
9136 const rootDir = mkdtempSync(join(tmpdir(), "baa-conductor-renewal-observe-active-link-"));
9137 const stateDir = join(rootDir, "state");
9138 const store = new ArtifactStore({
9139 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
9140 databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
9141 });
9142
9143 try {
9144 const firstObservation = await observeRenewalConversation({
9145 assistantMessageId: "msg-renewal-observe-1",
9146 clientId: "firefox-chatgpt",
9147 observedAt: Date.UTC(2026, 2, 30, 12, 0, 0),
9148 pageTitle: "Renewal Thread",
9149 pageUrl: "https://chatgpt.com/c/conv-renewal-observe",
9150 platform: "chatgpt",
9151 remoteConversationId: "conv-renewal-observe",
9152 store
9153 });
9154
9155 assert.equal(firstObservation.created, true);
9156 assert.equal(firstObservation.resolvedBy, "new");
9157
9158 await store.upsertConversationLink({
9159 isActive: false,
9160 linkId: firstObservation.link.linkId,
9161 localConversationId: firstObservation.conversation.localConversationId,
9162 observedAt: firstObservation.link.observedAt,
9163 platform: "chatgpt",
9164 updatedAt: Date.UTC(2026, 2, 30, 12, 1, 0)
9165 });
9166
9167 assert.equal(
9168 await store.findConversationLinkByRemoteConversation("chatgpt", "conv-renewal-observe"),
9169 null
9170 );
9171
9172 const secondObservation = await observeRenewalConversation({
9173 assistantMessageId: "msg-renewal-observe-2",
9174 clientId: "firefox-chatgpt",
9175 observedAt: Date.UTC(2026, 2, 30, 12, 2, 0),
9176 pageTitle: "Renewal Thread Reopened",
9177 pageUrl: "https://chatgpt.com/c/conv-renewal-observe",
9178 platform: "chatgpt",
9179 remoteConversationId: "conv-renewal-observe",
9180 store
9181 });
9182
9183 assert.equal(secondObservation.created, true);
9184 assert.equal(secondObservation.resolvedBy, "new");
9185 assert.notEqual(
9186 secondObservation.conversation.localConversationId,
9187 firstObservation.conversation.localConversationId
9188 );
9189 assert.equal(secondObservation.link.linkId, firstObservation.link.linkId);
9190 assert.equal(secondObservation.link.localConversationId, secondObservation.conversation.localConversationId);
9191 assert.equal(secondObservation.link.isActive, true);
9192 assert.deepEqual(
9193 await store.findConversationLinkByRemoteConversation("chatgpt", "conv-renewal-observe"),
9194 secondObservation.link
9195 );
9196
9197 const originalConversation = await store.getLocalConversation(firstObservation.conversation.localConversationId);
9198 assert.ok(originalConversation);
9199 assert.equal(originalConversation.lastMessageId, "msg-renewal-observe-1");
9200
9201 const replacementConversation = await store.getLocalConversation(secondObservation.conversation.localConversationId);
9202 assert.ok(replacementConversation);
9203 assert.equal(replacementConversation.lastMessageId, "msg-renewal-observe-2");
9204 } finally {
9205 store.close();
9206 rmSync(rootDir, {
9207 force: true,
9208 recursive: true
9209 });
9210 }
9211});
9212
9213test("observeRenewalConversation reuses the same link when remote conversation id is missing", async () => {
9214 const rootDir = mkdtempSync(join(tmpdir(), "baa-conductor-renewal-observe-null-remote-"));
9215 const stateDir = join(rootDir, "state");
9216 const store = new ArtifactStore({
9217 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
9218 databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
9219 });
9220 const firstObservedAt = Date.UTC(2026, 2, 30, 12, 3, 0);
9221
9222 try {
9223 const firstObservation = await observeRenewalConversation({
9224 assistantMessageId: "msg-renewal-null-remote-1",
9225 clientId: "firefox-gemini",
9226 observedAt: firstObservedAt,
9227 pageTitle: "Gemini Thread",
9228 pageUrl: "https://gemini.google.com/app",
9229 platform: "gemini",
9230 store
9231 });
9232 const secondObservation = await observeRenewalConversation({
9233 assistantMessageId: "msg-renewal-null-remote-2",
9234 clientId: "firefox-gemini",
9235 observedAt: firstObservedAt + 60_000,
9236 pageTitle: "Gemini Thread Updated",
9237 pageUrl: "https://gemini.google.com/app",
9238 platform: "gemini",
9239 store
9240 });
9241
9242 assert.equal(firstObservation.created, true);
9243 assert.equal(firstObservation.resolvedBy, "new");
9244 assert.equal(secondObservation.created, false);
9245 assert.equal(secondObservation.resolvedBy, "active_link");
9246 assert.equal(
9247 secondObservation.conversation.localConversationId,
9248 firstObservation.conversation.localConversationId
9249 );
9250 assert.equal(secondObservation.link.linkId, firstObservation.link.linkId);
9251 assert.equal(secondObservation.link.remoteConversationId, null);
9252 assert.equal(secondObservation.link.routePath, "/app");
9253 assert.equal(secondObservation.link.pageTitle, "Gemini Thread Updated");
9254 assert.deepEqual(await store.listConversationLinks({ platform: "gemini" }), [secondObservation.link]);
9255
9256 const conversation = await store.getLocalConversation(firstObservation.conversation.localConversationId);
9257 assert.ok(conversation);
9258 assert.equal(conversation.lastMessageId, "msg-renewal-null-remote-2");
9259 } finally {
9260 store.close();
9261 rmSync(rootDir, {
9262 force: true,
9263 recursive: true
9264 });
9265 }
9266});
9267
9268test("observeRenewalConversation prefers conversation-derived business targets over stale legacy tab links", async () => {
9269 const rootDir = mkdtempSync(join(tmpdir(), "baa-conductor-renewal-target-priority-"));
9270 const stateDir = join(rootDir, "state");
9271 const store = new ArtifactStore({
9272 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
9273 databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
9274 });
9275 const observedAt = Date.UTC(2026, 2, 30, 12, 5, 0);
9276
9277 try {
9278 await store.upsertLocalConversation({
9279 localConversationId: "lc-target-priority-tab-1",
9280 platform: "chatgpt"
9281 });
9282 await store.upsertLocalConversation({
9283 localConversationId: "lc-target-priority-tab-2",
9284 platform: "chatgpt"
9285 });
9286
9287 await store.upsertConversationLink({
9288 clientId: "firefox-chatgpt",
9289 createdAt: observedAt - 10_000,
9290 isActive: true,
9291 linkId: "link-target-priority-tab-1",
9292 localConversationId: "lc-target-priority-tab-1",
9293 observedAt: observedAt - 10_000,
9294 pageTitle: "Old Tab State",
9295 pageUrl: "https://chatgpt.com/",
9296 platform: "chatgpt",
9297 routePath: "/",
9298 routePattern: "/",
9299 targetId: "tab:1",
9300 targetKind: "browser.proxy_delivery",
9301 targetPayload: {
9302 clientId: "firefox-chatgpt",
9303 tabId: 1
9304 },
9305 updatedAt: observedAt - 10_000
9306 });
9307 await store.upsertConversationLink({
9308 clientId: "firefox-chatgpt",
9309 createdAt: observedAt - 5_000,
9310 isActive: true,
9311 linkId: "link-target-priority-tab-2",
9312 localConversationId: "lc-target-priority-tab-2",
9313 observedAt: observedAt - 5_000,
9314 pageTitle: "Shared Thread",
9315 pageUrl: "https://chatgpt.com/c/conv-target-priority",
9316 platform: "chatgpt",
9317 routePath: "/c/conv-target-priority",
9318 routePattern: "/c/:conversationId",
9319 targetId: "tab:2",
9320 targetKind: "browser.proxy_delivery",
9321 targetPayload: {
9322 clientId: "firefox-chatgpt",
9323 pageUrl: "https://chatgpt.com/c/conv-target-priority",
9324 tabId: 2
9325 },
9326 updatedAt: observedAt - 5_000
9327 });
9328
9329 const observation = await observeRenewalConversation({
9330 assistantMessageId: "msg-target-priority",
9331 clientId: "firefox-chatgpt",
9332 observedAt,
9333 pageTitle: "Shared Thread",
9334 pageUrl: "https://chatgpt.com/c/conv-target-priority",
9335 platform: "chatgpt",
9336 route: {
9337 assistantMessageId: "msg-target-priority",
9338 conversationId: null,
9339 observedAt,
9340 organizationId: null,
9341 pageTitle: "Shared Thread",
9342 pageUrl: "https://chatgpt.com/c/conv-target-priority",
9343 platform: "chatgpt",
9344 shellPage: false,
9345 tabId: 1
9346 },
9347 store
9348 });
9349
9350 assert.equal(observation.created, false);
9351 assert.equal(observation.resolvedBy, "active_link");
9352 assert.equal(observation.conversation.localConversationId, "lc-target-priority-tab-2");
9353 assert.equal(observation.link.linkId, "link-target-priority-tab-2");
9354 assert.equal(observation.link.targetId, "conversation:chatgpt:conv-target-priority");
9355 assert.equal(observation.link.pageTitle, "Shared Thread");
9356 assert.equal(observation.link.pageUrl, "https://chatgpt.com/c/conv-target-priority");
9357 assert.equal(observation.link.routePath, "/c/conv-target-priority");
9358 assert.equal(observation.link.remoteConversationId, "conv-target-priority");
9359
9360 const preservedLink = await store.getConversationLink("link-target-priority-tab-2");
9361 assert.ok(preservedLink);
9362 assert.equal(preservedLink.isActive, true);
9363 assert.equal(preservedLink.localConversationId, "lc-target-priority-tab-2");
9364 assert.equal(preservedLink.remoteConversationId, "conv-target-priority");
9365 assert.equal(preservedLink.targetId, "conversation:chatgpt:conv-target-priority");
9366
9367 const legacyTabLink = await store.getConversationLink("link-target-priority-tab-1");
9368 assert.ok(legacyTabLink);
9369 assert.equal(legacyTabLink.isActive, false);
9370 assert.equal(legacyTabLink.localConversationId, "lc-target-priority-tab-1");
9371
9372 assert.deepEqual(
9373 await store.findConversationLinkByRemoteConversation("chatgpt", "conv-target-priority"),
9374 preservedLink
9375 );
9376 } finally {
9377 store.close();
9378 rmSync(rootDir, {
9379 force: true,
9380 recursive: true
9381 });
9382 }
9383});
9384
9385test("observeRenewalConversation paginates exact-match scans and reports diagnostics at the scan limit", async () => {
9386 const rootDir = mkdtempSync(join(tmpdir(), "baa-conductor-renewal-scan-limit-"));
9387 const stateDir = join(rootDir, "state");
9388 const store = new ArtifactStore({
9389 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
9390 databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
9391 });
9392 const observedAt = Date.UTC(2026, 2, 30, 12, 8, 0);
9393 const diagnostics = [];
9394
9395 try {
9396 for (let index = 0; index < 50; index += 1) {
9397 const localConversationId = `lc-scan-limit-decoy-${index}`;
9398 const linkId = `link-scan-limit-decoy-${index}`;
9399 const decoyObservedAt = observedAt + 50_000 - index;
9400
9401 await store.upsertLocalConversation({
9402 localConversationId,
9403 platform: "chatgpt"
9404 });
9405 await store.upsertConversationLink({
9406 clientId: "firefox-chatgpt",
9407 createdAt: decoyObservedAt,
9408 isActive: true,
9409 linkId,
9410 localConversationId,
9411 observedAt: decoyObservedAt,
9412 pageTitle: `Decoy Thread ${index}`,
9413 pageUrl: `https://chatgpt.com/c/conv-scan-limit-decoy-${index}`,
9414 platform: "chatgpt",
9415 remoteConversationId: `conv-scan-limit-decoy-${index}`,
9416 routePath: `/c/conv-scan-limit-decoy-${index}`,
9417 routePattern: "/c/:conversationId",
9418 targetId: "tab:1",
9419 targetKind: "browser.proxy_delivery",
9420 targetPayload: {
9421 clientId: "firefox-chatgpt",
9422 tabId: 1
9423 },
9424 updatedAt: decoyObservedAt
9425 });
9426 }
9427
9428 await store.upsertLocalConversation({
9429 localConversationId: "lc-scan-limit-target",
9430 platform: "chatgpt"
9431 });
9432 await store.upsertConversationLink({
9433 clientId: "firefox-chatgpt",
9434 createdAt: observedAt - 60_000,
9435 isActive: true,
9436 linkId: "link-scan-limit-target",
9437 localConversationId: "lc-scan-limit-target",
9438 observedAt: observedAt - 60_000,
9439 pageTitle: "Shared Thread",
9440 pageUrl: "https://chatgpt.com/c/conv-scan-limit-target",
9441 platform: "chatgpt",
9442 remoteConversationId: "conv-scan-limit-target-existing",
9443 routePath: "/c/conv-scan-limit-target",
9444 routePattern: "/c/:conversationId",
9445 targetId: "tab:1",
9446 targetKind: "browser.proxy_delivery",
9447 targetPayload: {
9448 clientId: "firefox-chatgpt",
9449 pageUrl: "https://chatgpt.com/c/conv-scan-limit-target",
9450 tabId: 1
9451 },
9452 updatedAt: observedAt - 60_000
9453 });
9454
9455 const observation = await observeRenewalConversation({
9456 assistantMessageId: "msg-scan-limit",
9457 clientId: "firefox-chatgpt",
9458 onDiagnostic: (diagnostic) => diagnostics.push(diagnostic),
9459 observedAt,
9460 pageTitle: "Shared Thread",
9461 pageUrl: "https://chatgpt.com/c/conv-scan-limit-target",
9462 platform: "chatgpt",
9463 route: {
9464 assistantMessageId: "msg-scan-limit",
9465 conversationId: null,
9466 observedAt,
9467 organizationId: null,
9468 pageTitle: "Shared Thread",
9469 pageUrl: "https://chatgpt.com/c/conv-scan-limit-target",
9470 platform: "chatgpt",
9471 shellPage: false,
9472 tabId: 1
9473 },
9474 store
9475 });
9476
9477 assert.equal(observation.created, false);
9478 assert.equal(observation.resolvedBy, "active_link");
9479 assert.equal(observation.conversation.localConversationId, "lc-scan-limit-target");
9480 assert.equal(observation.link.linkId, "link-scan-limit-target");
9481 assert.equal(observation.link.targetId, "conversation:chatgpt:conv-scan-limit-target");
9482
9483 const legacyTabLinks = await store.listConversationLinks({
9484 clientId: "firefox-chatgpt",
9485 limit: 100,
9486 platform: "chatgpt",
9487 targetId: "tab:1"
9488 });
9489 assert.equal(legacyTabLinks.length, 50);
9490 assert.equal(
9491 legacyTabLinks.filter((link) => link.isActive).length,
9492 0
9493 );
9494
9495 const conversationLinks = await store.listConversationLinks({
9496 clientId: "firefox-chatgpt",
9497 limit: 10,
9498 platform: "chatgpt",
9499 targetId: "conversation:chatgpt:conv-scan-limit-target"
9500 });
9501 assert.equal(conversationLinks.length, 1);
9502 assert.equal(conversationLinks[0]?.linkId, "link-scan-limit-target");
9503 assert.equal(
9504 conversationLinks.filter((link) => link.isActive).length,
9505 1
9506 );
9507 assert.equal(
9508 conversationLinks.find((link) => link.linkId === "link-scan-limit-target")?.isActive,
9509 true
9510 );
9511 assert.deepEqual(diagnostics, [
9512 {
9513 clientId: "firefox-chatgpt",
9514 code: "conversation_link_scan_limit_reached",
9515 limit: 50,
9516 offset: 0,
9517 operation: "resolve",
9518 platform: "chatgpt",
9519 signal: "target_id"
9520 },
9521 {
9522 clientId: "firefox-chatgpt",
9523 code: "conversation_link_scan_limit_reached",
9524 limit: 50,
9525 offset: 0,
9526 operation: "deactivate",
9527 platform: "chatgpt",
9528 signal: "target_id"
9529 }
9530 ]);
9531 } finally {
9532 store.close();
9533 rmSync(rootDir, {
9534 force: true,
9535 recursive: true
9536 });
9537 }
9538});
9539
9540test("ConductorRuntime persists renewal conversation links from browser.final_message and exposes renewal controls", async () => {
9541 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-renewal-control-"));
9542 const runtime = new ConductorRuntime(
9543 {
9544 nodeId: "mini-main",
9545 host: "mini",
9546 role: "primary",
9547 controlApiBase: "https://control.example.test",
9548 localApiBase: "http://127.0.0.1:0",
9549 sharedToken: "replace-me",
9550 paths: {
9551 runsDir: "/tmp/runs",
9552 stateDir
9553 }
9554 },
9555 {
9556 autoStartLoops: false,
9557 now: () => 200
9558 }
9559 );
9560
9561 let client = null;
9562
9563 try {
9564 const snapshot = await runtime.start();
9565 const baseUrl = snapshot.controlApi.localApiBase;
9566 client = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-renewal-control");
9567
9568 client.socket.send(
9569 JSON.stringify({
9570 type: "browser.final_message",
9571 platform: "chatgpt",
9572 conversation_id: "conv-renewal-control",
9573 assistant_message_id: "msg-renewal-control-1",
9574 raw_text: "hello renewal control",
9575 observed_at: 1_710_000_030_000,
9576 page_title: "ChatGPT Renewal",
9577 page_url: "https://chatgpt.com/c/conv-renewal-control",
9578 tab_id: 42
9579 })
9580 );
9581
9582 const linksPayload = await waitForCondition(async () => {
9583 const linksResponse = await fetch(
9584 `${baseUrl}/v1/renewal/links?platform=chatgpt&remote_conversation_id=conv-renewal-control`
9585 );
9586 assert.equal(linksResponse.status, 200);
9587 const payload = await linksResponse.json();
9588 assert.equal(payload.data.count, 1);
9589 return payload;
9590 }, 5_000, 50);
9591 assert.equal(linksPayload.data.links[0].route.pattern, "/c/:conversationId");
9592 assert.equal(linksPayload.data.links[0].target.kind, "browser.proxy_delivery");
9593 assert.equal(linksPayload.data.links[0].target.payload.tabId, 42);
9594 const localConversationId = linksPayload.data.links[0].local_conversation_id;
9595
9596 const conversationResponse = await fetch(`${baseUrl}/v1/renewal/conversations/${localConversationId}`);
9597 assert.equal(conversationResponse.status, 200);
9598 const conversationPayload = await conversationResponse.json();
9599 assert.equal(conversationPayload.data.local_conversation_id, localConversationId);
9600 assert.equal(conversationPayload.data.automation_status, "manual");
9601 assert.equal(conversationPayload.data.last_message_id, "msg-renewal-control-1");
9602 assert.equal(conversationPayload.data.links.length, 1);
9603
9604 const autoResponse = await fetch(
9605 `${baseUrl}/v1/renewal/conversations/${localConversationId}/auto`,
9606 {
9607 method: "POST"
9608 }
9609 );
9610 assert.equal(autoResponse.status, 200);
9611 const autoPayload = await autoResponse.json();
9612 assert.equal(autoPayload.data.automation_status, "auto");
9613 assert.equal(autoPayload.data.paused_at, undefined);
9614
9615 const pausedResponse = await fetch(
9616 `${baseUrl}/v1/renewal/conversations/${localConversationId}/paused`,
9617 {
9618 method: "POST"
9619 }
9620 );
9621 assert.equal(pausedResponse.status, 200);
9622 const pausedPayload = await pausedResponse.json();
9623 assert.equal(pausedPayload.data.automation_status, "paused");
9624 assert.equal(typeof pausedPayload.data.paused_at, "number");
9625
9626 client.socket.send(
9627 JSON.stringify({
9628 type: "browser.final_message",
9629 platform: "chatgpt",
9630 conversation_id: "conv-renewal-control",
9631 assistant_message_id: "msg-renewal-control-2",
9632 raw_text: "hello renewal control updated",
9633 observed_at: 1_710_000_031_000,
9634 page_title: "ChatGPT Renewal Updated",
9635 page_url: "https://chatgpt.com/c/conv-renewal-control",
9636 tab_id: 42
9637 })
9638 );
9639
9640 const updatedConversationPayload = await waitForCondition(async () => {
9641 const updatedConversationResponse = await fetch(
9642 `${baseUrl}/v1/renewal/conversations/${localConversationId}`
9643 );
9644 assert.equal(updatedConversationResponse.status, 200);
9645 const payload = await updatedConversationResponse.json();
9646 assert.equal(payload.data.last_message_id, "msg-renewal-control-2");
9647 return payload;
9648 }, 5_000, 50);
9649 assert.equal(updatedConversationPayload.data.local_conversation_id, localConversationId);
9650 assert.equal(updatedConversationPayload.data.automation_status, "paused");
9651 assert.equal(updatedConversationPayload.data.last_message_id, "msg-renewal-control-2");
9652 assert.equal(updatedConversationPayload.data.active_link.page_title, "ChatGPT Renewal Updated");
9653
9654 const listResponse = await fetch(`${baseUrl}/v1/renewal/conversations?platform=chatgpt&status=paused`);
9655 assert.equal(listResponse.status, 200);
9656 const listPayload = await listResponse.json();
9657 assert.equal(listPayload.data.count, 1);
9658 assert.equal(listPayload.data.conversations[0].local_conversation_id, localConversationId);
9659 assert.equal(listPayload.data.conversations[0].active_link.link_id, linksPayload.data.links[0].link_id);
9660
9661 const manualResponse = await fetch(
9662 `${baseUrl}/v1/renewal/conversations/${localConversationId}/manual`,
9663 {
9664 method: "POST"
9665 }
9666 );
9667 assert.equal(manualResponse.status, 200);
9668 const manualPayload = await manualResponse.json();
9669 assert.equal(manualPayload.data.automation_status, "manual");
9670 assert.equal(manualPayload.data.paused_at, undefined);
9671 } finally {
9672 client?.queue.stop();
9673 client?.socket.close(1000, "done");
9674 await runtime.stop();
9675 rmSync(stateDir, {
9676 force: true,
9677 recursive: true
9678 });
9679 }
9680});
9681
9682test("ConductorRuntime broadcasts conversation automation snapshots after renewal API mutations", async () => {
9683 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-overlay-automation-sync-"));
9684 const runtime = new ConductorRuntime(
9685 {
9686 nodeId: "mini-main",
9687 host: "mini",
9688 role: "primary",
9689 controlApiBase: "https://control.example.test",
9690 localApiBase: "http://127.0.0.1:0",
9691 sharedToken: "replace-me",
9692 paths: {
9693 runsDir: "/tmp/runs",
9694 stateDir
9695 }
9696 },
9697 {
9698 autoStartLoops: false,
9699 now: () => 250
9700 }
9701 );
9702
9703 let client = null;
9704
9705 try {
9706 const snapshot = await runtime.start();
9707 const baseUrl = snapshot.controlApi.localApiBase;
9708 client = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-overlay-automation");
9709
9710 client.socket.send(
9711 JSON.stringify({
9712 type: "browser.final_message",
9713 platform: "chatgpt",
9714 conversation_id: "conv-overlay-automation",
9715 assistant_message_id: "msg-overlay-automation-1",
9716 raw_text: "hello overlay automation",
9717 observed_at: 1_710_000_060_000,
9718 page_title: "ChatGPT Overlay Automation",
9719 page_url: "https://chatgpt.com/c/conv-overlay-automation",
9720 tab_id: 99
9721 })
9722 );
9723
9724 const localConversationId = await waitForCondition(async () => {
9725 const linksResponse = await fetch(
9726 `${baseUrl}/v1/renewal/links?platform=chatgpt&remote_conversation_id=conv-overlay-automation`
9727 );
9728 assert.equal(linksResponse.status, 200);
9729 const payload = await linksResponse.json();
9730 assert.equal(payload.data.count, 1);
9731 return payload.data.links[0].local_conversation_id;
9732 }, 5_000, 50);
9733
9734 const pauseResponse = await fetch(
9735 `${baseUrl}/v1/renewal/conversations/${localConversationId}/paused`,
9736 {
9737 method: "POST",
9738 headers: {
9739 "content-type": "application/json"
9740 },
9741 body: JSON.stringify({
9742 pause_reason: "user_pause"
9743 })
9744 }
9745 );
9746 assert.equal(pauseResponse.status, 200);
9747
9748 const pausedSnapshot = await client.queue.next(
9749 (message) =>
9750 message.type === "state_snapshot"
9751 && Array.isArray(message.snapshot?.browser?.automation_conversations)
9752 && message.snapshot.browser.automation_conversations.some((entry) =>
9753 entry.platform === "chatgpt"
9754 && entry.remote_conversation_id === "conv-overlay-automation"
9755 && entry.automation_status === "paused"
9756 && entry.pause_reason === "user_pause"
9757 ),
9758 8_000
9759 );
9760 assert.equal(
9761 pausedSnapshot.snapshot.browser.automation_conversations.find((entry) =>
9762 entry.remote_conversation_id === "conv-overlay-automation"
9763 )?.local_conversation_id,
9764 localConversationId
9765 );
9766
9767 const autoResponse = await fetch(
9768 `${baseUrl}/v1/renewal/conversations/${localConversationId}/auto`,
9769 {
9770 method: "POST"
9771 }
9772 );
9773 assert.equal(autoResponse.status, 200);
9774
9775 await client.queue.next(
9776 (message) =>
9777 message.type === "state_snapshot"
9778 && Array.isArray(message.snapshot?.browser?.automation_conversations)
9779 && message.snapshot.browser.automation_conversations.some((entry) =>
9780 entry.platform === "chatgpt"
9781 && entry.remote_conversation_id === "conv-overlay-automation"
9782 && entry.automation_status === "auto"
9783 ),
9784 8_000
9785 );
9786 } finally {
9787 client?.queue.stop();
9788 client?.socket.close(1000, "done");
9789 await runtime.stop();
9790 rmSync(stateDir, {
9791 force: true,
9792 recursive: true
9793 });
9794 }
9795});
9796
9797test("ConductorRuntime gives control instructions priority over ordinary baa instructions and persists pause_reason", async () => {
9798 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-control-priority-"));
9799 const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-conductor-control-priority-host-"));
9800 const blockedOutputPath = join(hostOpsDir, "control-should-not-write.txt");
9801 const runtime = new ConductorRuntime(
9802 {
9803 nodeId: "mini-main",
9804 host: "mini",
9805 role: "primary",
9806 controlApiBase: "https://control.example.test",
9807 localApiBase: "http://127.0.0.1:0",
9808 sharedToken: "replace-me",
9809 paths: {
9810 runsDir: "/tmp/runs",
9811 stateDir
9812 }
9813 },
9814 {
9815 autoStartLoops: false,
9816 now: () => 400
9817 }
9818 );
9819
9820 let client = null;
9821
9822 try {
9823 const snapshot = await runtime.start();
9824 const baseUrl = snapshot.controlApi.localApiBase;
9825 client = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-control-priority");
9826
9827 client.socket.send(
9828 JSON.stringify({
9829 type: "browser.final_message",
9830 platform: "chatgpt",
9831 conversation_id: "conv-control-priority",
9832 assistant_message_id: "msg-control-priority-1",
9833 raw_text: [
9834 "```baa",
9835 '@conductor::conversation/pause::{"scope":"current","reason":"rescue_wait"}',
9836 "```",
9837 "```baa",
9838 `@conductor::files/write::${JSON.stringify({
9839 path: "control-should-not-write.txt",
9840 cwd: hostOpsDir,
9841 content: "should not exist",
9842 overwrite: true
9843 })}`,
9844 "```"
9845 ].join("\n"),
9846 observed_at: 1_710_000_040_000,
9847 page_title: "ChatGPT Control Priority",
9848 page_url: "https://chatgpt.com/c/conv-control-priority",
9849 tab_id: 77
9850 })
9851 );
9852
9853 const localConversationId = await waitForCondition(async () => {
9854 const linksResponse = await fetch(
9855 `${baseUrl}/v1/renewal/links?platform=chatgpt&remote_conversation_id=conv-control-priority`
9856 );
9857 assert.equal(linksResponse.status, 200);
9858 const payload = await linksResponse.json();
9859 assert.equal(payload.data.count, 1);
9860 return payload.data.links[0].local_conversation_id;
9861 }, 5_000, 50);
9862
9863 const conversationPayload = await waitForCondition(async () => {
9864 const conversationResponse = await fetch(`${baseUrl}/v1/renewal/conversations/${localConversationId}`);
9865 assert.equal(conversationResponse.status, 200);
9866 const payload = await conversationResponse.json();
9867 assert.equal(payload.data.automation_status, "paused");
9868 assert.equal(payload.data.pause_reason, "rescue_wait");
9869 return payload;
9870 }, 5_000, 50);
9871
9872 assert.equal(existsSync(blockedOutputPath), false);
9873 } finally {
9874 client?.queue.stop();
9875 client?.socket.close(1000, "done");
9876 await runtime.stop();
9877 rmSync(stateDir, {
9878 force: true,
9879 recursive: true
9880 });
9881 rmSync(hostOpsDir, {
9882 force: true,
9883 recursive: true
9884 });
9885 }
9886});
9887
9888test("ConductorRuntime exposes renewal jobs APIs and registers the renewal dispatcher runner", async () => {
9889 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-renewal-jobs-api-"));
9890 const runtime = new ConductorRuntime(
9891 {
9892 nodeId: "mini-main",
9893 host: "mini",
9894 role: "primary",
9895 controlApiBase: "https://control.example.test",
9896 localApiBase: "http://127.0.0.1:0",
9897 sharedToken: "replace-me",
9898 paths: {
9899 runsDir: "/tmp/runs",
9900 stateDir
9901 }
9902 },
9903 {
9904 autoStartLoops: false,
9905 now: () => 300
9906 }
9907 );
9908 const nowMs = Date.UTC(2026, 2, 30, 14, 0, 0);
9909
9910 try {
9911 const snapshot = await runtime.start();
9912 const baseUrl = snapshot.controlApi.localApiBase;
9913 const artifactStore = runtime["artifactStore"];
9914 const timedJobs = runtime["timedJobs"];
9915
9916 assert.ok(timedJobs.getRegisteredRunnerNames().includes("renewal.dispatcher"));
9917
9918 await artifactStore.insertMessage({
9919 conversationId: "conv-renewal-job-api",
9920 id: "msg-renewal-job-api",
9921 observedAt: nowMs - 30_000,
9922 platform: "chatgpt",
9923 rawText: "renewal jobs api message",
9924 role: "assistant"
9925 });
9926 await artifactStore.upsertLocalConversation({
9927 automationStatus: "auto",
9928 localConversationId: "lc-renewal-job-api",
9929 platform: "chatgpt"
9930 });
9931 await artifactStore.upsertConversationLink({
9932 clientId: "firefox-chatgpt",
9933 linkId: "link-renewal-job-api",
9934 localConversationId: "lc-renewal-job-api",
9935 observedAt: nowMs - 20_000,
9936 pageTitle: "Renewal Jobs API",
9937 pageUrl: "https://chatgpt.com/c/conv-renewal-job-api",
9938 platform: "chatgpt",
9939 remoteConversationId: "conv-renewal-job-api",
9940 routeParams: {
9941 conversationId: "conv-renewal-job-api"
9942 },
9943 routePath: "/c/conv-renewal-job-api",
9944 routePattern: "/c/:conversationId",
9945 targetId: "tab:44",
9946 targetKind: "browser.proxy_delivery",
9947 targetPayload: {
9948 clientId: "firefox-chatgpt",
9949 conversationId: "conv-renewal-job-api",
9950 pageUrl: "https://chatgpt.com/c/conv-renewal-job-api",
9951 tabId: 44
9952 }
9953 });
9954 await artifactStore.insertRenewalJob({
9955 jobId: "job-renewal-job-api",
9956 localConversationId: "lc-renewal-job-api",
9957 messageId: "msg-renewal-job-api",
9958 nextAttemptAt: nowMs,
9959 payload: JSON.stringify({
9960 kind: "renewal.message",
9961 sourceMessage: {
9962 id: "msg-renewal-job-api"
9963 },
9964 template: "summary_with_link",
9965 text: "[renewal] api payload",
9966 version: 1
9967 }),
9968 payloadKind: "json",
9969 targetSnapshot: {
9970 target: {
9971 id: "tab:44",
9972 kind: "browser.proxy_delivery",
9973 payload: {
9974 clientId: "firefox-chatgpt",
9975 tabId: 44
9976 }
9977 }
9978 }
9979 });
9980
9981 const listResponse = await fetch(
9982 `${baseUrl}/v1/renewal/jobs?status=pending&local_conversation_id=lc-renewal-job-api`
9983 );
9984 assert.equal(listResponse.status, 200);
9985 const listPayload = await listResponse.json();
9986 assert.equal(listPayload.data.count, 1);
9987 assert.equal(listPayload.data.jobs[0].job_id, "job-renewal-job-api");
9988 assert.equal(listPayload.data.jobs[0].payload_text, "[renewal] api payload");
9989 assert.equal(listPayload.data.jobs[0].target_snapshot.target.kind, "browser.proxy_delivery");
9990
9991 const readResponse = await fetch(`${baseUrl}/v1/renewal/jobs/job-renewal-job-api`);
9992 assert.equal(readResponse.status, 200);
9993 const readPayload = await readResponse.json();
9994 assert.equal(readPayload.data.job_id, "job-renewal-job-api");
9995 assert.equal(readPayload.data.local_conversation_id, "lc-renewal-job-api");
9996 assert.equal(readPayload.data.status, "pending");
9997 assert.equal(readPayload.data.message_id, "msg-renewal-job-api");
9998 } finally {
9999 await runtime.stop();
10000 rmSync(stateDir, {
10001 force: true,
10002 recursive: true
10003 });
10004 }
10005});
10006
10007test("ConductorRuntime startup recovers stale automation locks and running renewal jobs", async () => {
10008 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-runtime-recovery-"));
10009 const artifactStore = new ArtifactStore({
10010 artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
10011 databasePath: join(stateDir, ARTIFACT_DB_FILENAME)
10012 });
10013 const nowMs = 100_000;
10014 let runtime = null;
10015
10016 try {
10017 await artifactStore.insertMessage({
10018 conversationId: "conv-runtime-recovery",
10019 id: "msg-runtime-recovery",
10020 observedAt: nowMs - 30_000,
10021 platform: "claude",
10022 rawText: "runtime recovery message",
10023 role: "assistant"
10024 });
10025 await artifactStore.upsertLocalConversation({
10026 automationStatus: "auto",
10027 executionState: "renewal_running",
10028 localConversationId: "lc-runtime-recovery",
10029 platform: "claude",
10030 updatedAt: nowMs - 20_000
10031 });
10032 await artifactStore.insertRenewalJob({
10033 attemptCount: 1,
10034 createdAt: nowMs - 10_000,
10035 jobId: "job-runtime-recovery",
10036 localConversationId: "lc-runtime-recovery",
10037 messageId: "msg-runtime-recovery",
10038 nextAttemptAt: null,
10039 payload: "[renew]",
10040 startedAt: nowMs - 10_000,
10041 status: "running",
10042 updatedAt: nowMs - 10_000
10043 });
10044 } finally {
10045 artifactStore.close();
10046 }
10047
10048 try {
10049 runtime = new ConductorRuntime(
10050 {
10051 nodeId: "mini-main",
10052 host: "mini",
10053 role: "primary",
10054 controlApiBase: "https://control.example.test",
10055 localApiBase: "http://127.0.0.1:0",
10056 sharedToken: "replace-me",
10057 paths: {
10058 runsDir: "/tmp/runs",
10059 stateDir
10060 }
10061 },
10062 {
10063 autoStartLoops: false,
10064 now: () => 100
10065 }
10066 );
10067
10068 await runtime.start();
10069
10070 const recoveredStore = runtime["artifactStore"];
10071 const conversation = await recoveredStore.getLocalConversation("lc-runtime-recovery");
10072 const job = await recoveredStore.getRenewalJob("job-runtime-recovery");
10073
10074 assert.equal(conversation.executionState, "idle");
10075 assert.equal(job.status, "pending");
10076 assert.equal(job.startedAt, null);
10077 assert.equal(job.finishedAt, null);
10078 assert.equal(job.nextAttemptAt, 160_000);
10079 } finally {
10080 if (runtime != null) {
10081 await runtime.stop();
10082 }
10083
10084 rmSync(stateDir, {
10085 force: true,
10086 recursive: true
10087 });
10088 }
10089});
10090
10091test("ConductorRuntime registers renewal projector, projects auto messages once, and keeps cursor across restart", async () => {
10092 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-renewal-projector-runtime-"));
10093 const logsDir = mkdtempSync(join(tmpdir(), "baa-conductor-renewal-projector-runtime-logs-"));
10094 const nowSeconds = Math.floor(Date.UTC(2026, 2, 30, 11, 0, 0) / 1000);
10095 const runtimeConfig = {
10096 nodeId: "mini-main",
10097 host: "mini",
10098 role: "primary",
10099 controlApiBase: "https://control.example.test",
10100 localApiBase: "http://127.0.0.1:0",
10101 sharedToken: "replace-me",
10102 timedJobsIntervalMs: 60_000,
10103 timedJobsSettleDelayMs: 0,
10104 paths: {
10105 logsDir,
10106 runsDir: "/tmp/runs",
10107 stateDir
10108 }
10109 };
10110 const runtimeOptions = {
10111 autoStartLoops: false,
10112 now: () => nowSeconds
10113 };
10114 const observedAt = Date.UTC(2026, 2, 30, 10, 59, 0);
10115 let runtime = new ConductorRuntime(runtimeConfig, runtimeOptions);
10116 let client = null;
10117
10118 try {
10119 const snapshot = await runtime.start();
10120 const baseUrl = snapshot.controlApi.localApiBase;
10121 client = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-renewal-projector");
10122
10123 client.socket.send(
10124 JSON.stringify({
10125 type: "browser.final_message",
10126 platform: "chatgpt",
10127 conversation_id: "conv-runtime-projector",
10128 assistant_message_id: "msg-runtime-projector-1",
10129 raw_text: "runtime renewal projector message",
10130 observed_at: observedAt,
10131 page_title: "Runtime Projector",
10132 page_url: "https://chatgpt.com/c/conv-runtime-projector",
10133 tab_id: 77
10134 })
10135 );
10136
10137 const linksPayload = await waitForCondition(async () => {
10138 const response = await fetch(
10139 `${baseUrl}/v1/renewal/links?platform=chatgpt&remote_conversation_id=conv-runtime-projector`
10140 );
10141 assert.equal(response.status, 200);
10142 const payload = await response.json();
10143 assert.equal(payload.data.count, 1);
10144 return payload;
10145 }, 5_000, 50);
10146 const localConversationId = linksPayload.data.links[0].local_conversation_id;
10147
10148 const autoResponse = await fetch(
10149 `${baseUrl}/v1/renewal/conversations/${localConversationId}/auto`,
10150 {
10151 method: "POST"
10152 }
10153 );
10154 assert.equal(autoResponse.status, 200);
10155
10156 const timedJobs = runtime["timedJobs"];
10157 const artifactStore = runtime["artifactStore"];
10158 assert.ok(timedJobs.getRegisteredRunnerNames().includes("renewal.projector"));
10159
10160 const firstTick = await timedJobs.runTick("manual");
10161 assert.equal(firstTick.decision, "scheduled");
10162
10163 const jobsAfterFirstTick = await artifactStore.listRenewalJobs({});
10164 assert.equal(jobsAfterFirstTick.length, 1);
10165 assert.equal(jobsAfterFirstTick[0].messageId, "msg-runtime-projector-1");
10166 assert.equal(jobsAfterFirstTick[0].status, "pending");
10167
10168 const secondTick = await timedJobs.runTick("manual");
10169 assert.equal(secondTick.decision, "scheduled");
10170 assert.deepEqual(await artifactStore.listRenewalJobs({}), jobsAfterFirstTick);
10171
10172 await runtime.stop();
10173 runtime = new ConductorRuntime(runtimeConfig, runtimeOptions);
10174 const restartedSnapshot = await runtime.start();
10175 const restartedTimedJobs = runtime["timedJobs"];
10176 const restartedStore = runtime["artifactStore"];
10177
10178 assert.equal(restartedSnapshot.controlApi.localApiBase.startsWith("http://127.0.0.1:"), true);
10179 const restartTick = await restartedTimedJobs.runTick("manual");
10180 assert.equal(restartTick.decision, "scheduled");
10181 const jobsAfterRestart = await restartedStore.listRenewalJobs({});
10182 assert.equal(jobsAfterRestart.length, 1);
10183 assert.equal(jobsAfterRestart[0].messageId, "msg-runtime-projector-1");
10184
10185 const timedJobsEntries = await waitForJsonlEntries(
10186 join(logsDir, "timed-jobs"),
10187 (items) => items.some((entry) => entry.runner === "renewal.projector" && entry.stage === "job_projected")
10188 );
10189 assert.ok(
10190 timedJobsEntries.find(
10191 (entry) => entry.runner === "renewal.projector" && entry.stage === "job_projected"
10192 )
10193 );
10194 assert.ok(
10195 timedJobsEntries.find(
10196 (entry) =>
10197 entry.runner === "renewal.projector"
10198 && entry.stage === "scan_completed"
10199 && entry.cursor_after === `message:${observedAt}:msg-runtime-projector-1`
10200 )
10201 );
10202 } finally {
10203 client?.queue.stop();
10204 client?.socket.close(1000, "done");
10205 await runtime.stop();
10206 rmSync(stateDir, {
10207 force: true,
10208 recursive: true
10209 });
10210 rmSync(logsDir, {
10211 force: true,
10212 recursive: true
10213 });
10214 }
10215});
10216
10217test("persistent live ingest survives restart and /v1/browser restores recent history from the journal", async () => {
10218 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-final-message-persist-"));
10219 const databasePath = join(stateDir, "control-plane.sqlite");
10220 const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-conductor-final-message-history-"));
10221 const outputPath = join(hostOpsDir, "final-message-persist.txt");
10222 const messageText = [
10223 "```baa",
10224 `@conductor::exec::{"command":"printf 'persisted-live\\n' >> final-message-persist.txt","cwd":${JSON.stringify(hostOpsDir)}}`,
10225 "```"
10226 ].join("\n");
10227 let nowMs = 1_710_000_020_000;
10228 let runtime = null;
10229
10230 const buildPersistentIngest = (repository, sharedToken, snapshot) =>
10231 new BaaLiveInstructionIngest({
10232 center: new BaaInstructionCenter({
10233 deduper: new PersistentBaaInstructionDeduper(repository, () => nowMs),
10234 localApiContext: {
10235 fetchImpl: globalThis.fetch,
10236 repository,
10237 sharedToken,
10238 snapshotLoader: () => snapshot
10239 }
10240 }),
10241 historyLimit: 10,
10242 messageDeduper: new PersistentBaaLiveInstructionMessageDeduper(repository, () => nowMs),
10243 now: () => nowMs,
10244 snapshotStore: new PersistentBaaLiveInstructionSnapshotStore(repository, 10)
10245 });
10246
10247 let firstFixture = null;
10248 let restartedControlPlane = null;
10249
10250 try {
10251 firstFixture = await createLocalApiFixture({
10252 databasePath
10253 });
10254 const firstIngest = buildPersistentIngest(
10255 firstFixture.repository,
10256 firstFixture.sharedToken,
10257 firstFixture.snapshot
10258 );
10259
10260 await firstIngest.initialize();
10261 const firstPass = await firstIngest.ingestAssistantFinalMessage({
10262 assistantMessageId: "msg-final-message-persist",
10263 conversationId: null,
10264 observedAt: 1_710_000_020_000,
10265 platform: "chatgpt",
10266 source: "browser.final_message",
10267 text: messageText
10268 });
10269
10270 assert.equal(firstPass.summary.status, "executed");
10271 assert.equal(readFileSync(outputPath, "utf8"), "persisted-live\n");
10272
10273 firstFixture.controlPlane.close();
10274 firstFixture = null;
10275
10276 restartedControlPlane = new ConductorLocalControlPlane({
10277 databasePath
10278 });
10279 await restartedControlPlane.initialize();
10280
10281 const restartedIngest = buildPersistentIngest(
10282 restartedControlPlane.repository,
10283 "local-shared-token",
10284 {
10285 claudeCoded: {
10286 localApiBase: null
10287 },
10288 codexd: {
10289 localApiBase: null
10290 },
10291 controlApi: {
10292 baseUrl: "https://control.example.test",
10293 browserWsUrl: "ws://127.0.0.1:4317/ws/browser",
10294 firefoxWsUrl: "ws://127.0.0.1:4317/ws/firefox",
10295 hasSharedToken: true,
10296 localApiBase: "http://127.0.0.1:4317",
10297 usesPlaceholderToken: false
10298 },
10299 daemon: {
10300 currentLeaderId: "mini-main",
10301 currentTerm: 1,
10302 host: "mini",
10303 lastError: null,
10304 leaseExpiresAt: 130,
10305 leaseState: "leader",
10306 nodeId: "mini-main",
10307 role: "primary",
10308 schedulerEnabled: true
10309 },
10310 identity: "mini-main@mini(primary)",
10311 runtime: {
10312 pid: 123,
10313 started: true,
10314 startedAt: 100
10315 },
10316 warnings: []
10317 }
10318 );
10319
10320 await restartedIngest.initialize();
10321 assert.equal(restartedIngest.getSnapshot().recent_ingests[0].status, "executed");
10322 assert.equal(restartedIngest.getSnapshot().recent_executes[0].status, "executed");
10323
10324 nowMs = 1_710_000_020_500;
10325
10326 const replayPass = await restartedIngest.ingestAssistantFinalMessage({
10327 assistantMessageId: "msg-final-message-persist",
10328 conversationId: "conv-replayed",
10329 observedAt: 1_710_000_020_500,
10330 platform: "chatgpt",
10331 source: "browser.final_message",
10332 text: messageText
10333 });
10334
10335 assert.equal(replayPass.summary.status, "duplicate_message");
10336 assert.equal(readFileSync(outputPath, "utf8"), "persisted-live\n");
10337 assert.equal(restartedIngest.getSnapshot().recent_ingests[0].status, "duplicate_message");
10338 assert.equal(restartedIngest.getSnapshot().recent_ingests[1].status, "executed");
10339 assert.equal(restartedIngest.getSnapshot().recent_executes[0].status, "executed");
10340
10341 restartedControlPlane.close();
10342 restartedControlPlane = null;
10343
10344 runtime = new ConductorRuntime(
10345 {
10346 nodeId: "mini-main",
10347 host: "mini",
10348 role: "primary",
10349 controlApiBase: "https://control.example.test",
10350 localApiBase: "http://127.0.0.1:0",
10351 sharedToken: "replace-me",
10352 paths: {
10353 runsDir: "/tmp/runs",
10354 stateDir
10355 }
10356 },
10357 {
10358 autoStartLoops: false,
10359 now: () => 100
10360 }
10361 );
10362
10363 const runtimeSnapshot = await runtime.start();
10364 const browserStatus = await fetchJson(`${runtimeSnapshot.controlApi.localApiBase}/v1/browser`);
10365
10366 assert.equal(browserStatus.response.status, 200);
10367 assert.equal(browserStatus.payload.data.instruction_ingest.last_ingest.status, "duplicate_message");
10368 assert.equal(browserStatus.payload.data.instruction_ingest.last_execute.status, "executed");
10369 assert.equal(browserStatus.payload.data.instruction_ingest.recent_ingests[0].status, "duplicate_message");
10370 assert.equal(browserStatus.payload.data.instruction_ingest.recent_ingests[1].status, "executed");
10371 assert.equal(browserStatus.payload.data.instruction_ingest.recent_executes[0].status, "executed");
10372 } finally {
10373 restartedControlPlane?.close();
10374 firstFixture?.controlPlane.close();
10375
10376 if (runtime != null) {
10377 await runtime.stop();
10378 }
10379
10380 rmSync(stateDir, {
10381 force: true,
10382 recursive: true
10383 });
10384 rmSync(hostOpsDir, {
10385 force: true,
10386 recursive: true
10387 });
10388 }
10389});
10390
10391test("ConductorRuntime exposes proxy-delivery browser snapshots with routed business-page targets", async () => {
10392 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-delivery-text-"));
10393 const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-conductor-delivery-text-host-"));
10394 const runtime = new ConductorRuntime(
10395 {
10396 nodeId: "mini-main",
10397 host: "mini",
10398 role: "primary",
10399 controlApiBase: "https://control.example.test",
10400 localApiBase: "http://127.0.0.1:0",
10401 sharedToken: "replace-me",
10402 paths: {
10403 runsDir: "/tmp/runs",
10404 stateDir
10405 }
10406 },
10407 {
10408 autoStartLoops: false,
10409 now: () => 100
10410 }
10411 );
10412
10413 let client = null;
10414
10415 try {
10416 const snapshot = await runtime.start();
10417 client = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-delivery-artifact");
10418 const execCommand = "i=1; while [ $i -le 260 ]; do printf 'line-%s\\n' \"$i\"; i=$((i+1)); done";
10419
10420 client.socket.send(
10421 JSON.stringify({
10422 type: "browser.final_message",
10423 platform: "chatgpt",
10424 conversation_id: "conv-delivery-artifact",
10425 assistant_message_id: "msg-delivery-artifact",
10426 page_title: "Delivery Target",
10427 page_url: "https://chatgpt.com/c/conv-delivery-artifact",
10428 raw_text: [
10429 "```baa",
10430 `@conductor::exec::${JSON.stringify({
10431 command: execCommand,
10432 cwd: hostOpsDir
10433 })}`,
10434 "```"
10435 ].join("\n"),
10436 observed_at: 1710000030000,
10437 shell_page: false
10438 })
10439 );
10440
10441 await expectQueueTimeout(
10442 client.queue,
10443 (message) => message.type === "browser.upload_artifacts",
10444 700
10445 );
10446 const proxyDelivery = await client.queue.next(
10447 (message) => message.type === "browser.proxy_delivery"
10448 );
10449 assert.match(proxyDelivery.message_text, /\[BAA 执行结果\]/u);
10450 assert.equal(
10451 proxyDelivery.message_text.includes("\"command\": \"i=1; while [ $i -le 260"),
10452 true
10453 );
10454 assert.match(
10455 proxyDelivery.message_text,
10456 /完整结果:https:\/\/control\.example\.test\/artifact\/exec\/[^ \n]+\.txt/u
10457 );
10458 assert.doesNotMatch(proxyDelivery.message_text, /line-260/u);
10459 assert.equal(proxyDelivery.page_url, "https://chatgpt.com/c/conv-delivery-artifact");
10460 assert.equal(proxyDelivery.target_tab_id, undefined);
10461
10462 sendPluginActionResult(client.socket, {
10463 action: "proxy_delivery",
10464 commandType: "browser.proxy_delivery",
10465 platform: "chatgpt",
10466 requestId: proxyDelivery.requestId,
10467 type: "browser.proxy_delivery"
10468 });
10469
10470 const browserStatus = await waitForCondition(async () => {
10471 const result = await fetchJson(`${snapshot.controlApi.localApiBase}/v1/browser`);
10472 assert.equal(result.response.status, 200);
10473 assert.equal(result.payload.data.delivery.last_session.stage, "completed");
10474 return result;
10475 });
10476
10477 assert.equal(browserStatus.payload.data.delivery.last_session.delivery_mode, "proxy");
10478 assert.equal(browserStatus.payload.data.delivery.last_session.message_truncated, true);
10479 assert.equal(browserStatus.payload.data.delivery.last_session.target_page_url, "https://chatgpt.com/c/conv-delivery-artifact");
10480 assert.equal(browserStatus.payload.data.delivery.last_session.target_tab_id, undefined);
10481 assert.equal(browserStatus.payload.data.delivery.last_route.page_url, "https://chatgpt.com/c/conv-delivery-artifact");
10482 assert.ok(
10483 browserStatus.payload.data.delivery.last_session.source_line_count
10484 > browserStatus.payload.data.delivery.last_session.message_line_count
10485 );
10486 } finally {
10487 client?.queue.stop();
10488 client?.socket.close(1000, "done");
10489 await runtime.stop();
10490 rmSync(stateDir, {
10491 force: true,
10492 recursive: true
10493 });
10494 rmSync(hostOpsDir, {
10495 force: true,
10496 recursive: true
10497 });
10498 }
10499});
10500
10501test("ConductorRuntime exposes /v1/browser Claude HTTP routes over the local Firefox bridge", async () => {
10502 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-browser-http-"));
10503 const runtime = new ConductorRuntime(
10504 {
10505 nodeId: "mini-main",
10506 host: "mini",
10507 role: "primary",
10508 controlApiBase: "https://control.example.test",
10509 localApiBase: "http://127.0.0.1:0",
10510 sharedToken: "replace-me",
10511 paths: {
10512 runsDir: "/tmp/runs",
10513 stateDir
10514 }
10515 },
10516 {
10517 autoStartLoops: false,
10518 now: () => 100
10519 }
10520 );
10521
10522 let client = null;
10523
10524 try {
10525 const snapshot = await runtime.start();
10526 client = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-claude-http");
10527 const baseUrl = snapshot.controlApi.localApiBase;
10528
10529 client.socket.send(
10530 JSON.stringify({
10531 type: "credentials",
10532 platform: "claude",
10533 account: "ops@example.com",
10534 credential_fingerprint: "fp-claude-http",
10535 freshness: "fresh",
10536 captured_at: 1710000001000,
10537 last_seen_at: 1710000001500,
10538 headers: {
10539 cookie: "session=1",
10540 "x-csrf-token": "token-1"
10541 },
10542 shell_runtime: buildShellRuntime("claude"),
10543 timestamp: 1710000001000
10544 })
10545 );
10546 await client.queue.next(
10547 (message) => message.type === "state_snapshot" && message.reason === "credentials"
10548 );
10549
10550 client.socket.send(
10551 JSON.stringify({
10552 type: "api_endpoints",
10553 platform: "claude",
10554 account: "ops@example.com",
10555 credential_fingerprint: "fp-claude-http",
10556 updated_at: 1710000002000,
10557 endpoints: [
10558 "GET /api/organizations",
10559 "GET /api/organizations/{id}/chat_conversations/{id}",
10560 "POST /api/organizations/{id}/chat_conversations/{id}/completion"
10561 ],
10562 endpoint_metadata: [
10563 {
10564 method: "GET",
10565 path: "/api/organizations",
10566 first_seen_at: 1710000001200,
10567 last_seen_at: 1710000002000
10568 }
10569 ],
10570 shell_runtime: buildShellRuntime("claude")
10571 })
10572 );
10573 await client.queue.next(
10574 (message) => message.type === "state_snapshot" && message.reason === "api_endpoints"
10575 );
10576
10577 const browserStatusResponse = await fetch(`${baseUrl}/v1/browser`);
10578 assert.equal(browserStatusResponse.status, 200);
10579 const browserStatusPayload = await browserStatusResponse.json();
10580 assert.equal(browserStatusPayload.data.bridge.client_count, 1);
10581 assert.equal(browserStatusPayload.data.claude.ready, true);
10582 assert.equal(browserStatusPayload.data.claude.shell_runtime.platform, "claude");
10583 assert.equal(browserStatusPayload.data.current_client.client_id, "firefox-claude-http");
10584 assert.equal(browserStatusPayload.data.current_client.shell_runtime[0].platform, "claude");
10585 assert.equal(browserStatusPayload.data.records[0].status, "fresh");
10586 assert.equal(browserStatusPayload.data.records[0].live.shell_runtime.platform, "claude");
10587 assert.equal(browserStatusPayload.data.records[0].persisted.credential_fingerprint, "fp-claude-http");
10588
10589 const openPromise = fetch(`${baseUrl}/v1/browser/actions`, {
10590 method: "POST",
10591 headers: {
10592 "content-type": "application/json"
10593 },
10594 body: JSON.stringify({
10595 action: "tab_open",
10596 client_id: "firefox-claude-http",
10597 platform: "claude"
10598 })
10599 });
10600 const openMessage = await client.queue.next((message) => message.type === "open_tab");
10601 assert.equal(openMessage.platform, "claude");
10602 sendPluginActionResult(client.socket, {
10603 action: "tab_open",
10604 platform: "claude",
10605 requestId: openMessage.requestId
10606 });
10607 const openResponse = await openPromise;
10608 assert.equal(openResponse.status, 200);
10609 const openPayload = await openResponse.json();
10610 assert.equal(openPayload.data.action, "tab_open");
10611 assert.equal(openPayload.data.accepted, true);
10612
10613 const sendPromise = fetch(`${baseUrl}/v1/browser/request`, {
10614 method: "POST",
10615 headers: {
10616 "content-type": "application/json"
10617 },
10618 body: JSON.stringify({
10619 platform: "claude",
10620 prompt: "hello from conductor http"
10621 })
10622 });
10623
10624 const orgRequest = await client.queue.next(
10625 (message) => message.type === "api_request" && message.path === "/api/organizations"
10626 );
10627 client.socket.send(
10628 JSON.stringify({
10629 type: "api_response",
10630 id: orgRequest.id,
10631 ok: true,
10632 status: 200,
10633 body: {
10634 organizations: [
10635 {
10636 uuid: "org-http-1",
10637 name: "HTTP Org",
10638 is_default: true
10639 }
10640 ]
10641 }
10642 })
10643 );
10644
10645 const conversationsRequest = await client.queue.next(
10646 (message) => message.type === "api_request" && message.path === "/api/organizations/org-http-1/chat_conversations"
10647 );
10648 client.socket.send(
10649 JSON.stringify({
10650 type: "api_response",
10651 id: conversationsRequest.id,
10652 ok: true,
10653 status: 200,
10654 body: {
10655 chat_conversations: [
10656 {
10657 uuid: "conv-http-1",
10658 name: "HTTP Chat",
10659 selected: true
10660 }
10661 ]
10662 }
10663 })
10664 );
10665
10666 const completionRequest = await client.queue.next(
10667 (message) =>
10668 message.type === "api_request"
10669 && message.path === "/api/organizations/org-http-1/chat_conversations/conv-http-1/completion"
10670 );
10671 assert.equal(completionRequest.body.prompt, "hello from conductor http");
10672 client.socket.send(
10673 JSON.stringify({
10674 type: "api_response",
10675 id: completionRequest.id,
10676 ok: true,
10677 status: 202,
10678 body: {
10679 accepted: true,
10680 conversation_uuid: "conv-http-1"
10681 }
10682 })
10683 );
10684
10685 const sendResponse = await sendPromise;
10686 assert.equal(sendResponse.status, 200);
10687 const sendPayload = await sendResponse.json();
10688 assert.equal(sendPayload.data.organization.organization_id, "org-http-1");
10689 assert.equal(sendPayload.data.conversation.conversation_id, "conv-http-1");
10690 assert.equal(sendPayload.data.request_mode, "claude_prompt");
10691 assert.equal(sendPayload.data.response.accepted, true);
10692
10693 const currentPromise = fetch(`${baseUrl}/v1/browser/claude/current`);
10694
10695 const currentOrgRequest = await client.queue.next(
10696 (message) => message.type === "api_request" && message.path === "/api/organizations"
10697 );
10698 client.socket.send(
10699 JSON.stringify({
10700 type: "api_response",
10701 id: currentOrgRequest.id,
10702 ok: true,
10703 status: 200,
10704 body: {
10705 organizations: [
10706 {
10707 uuid: "org-http-1",
10708 name: "HTTP Org",
10709 is_default: true
10710 }
10711 ]
10712 }
10713 })
10714 );
10715
10716 const currentConversationListRequest = await client.queue.next(
10717 (message) => message.type === "api_request" && message.path === "/api/organizations/org-http-1/chat_conversations"
10718 );
10719 client.socket.send(
10720 JSON.stringify({
10721 type: "api_response",
10722 id: currentConversationListRequest.id,
10723 ok: true,
10724 status: 200,
10725 body: {
10726 chat_conversations: [
10727 {
10728 uuid: "conv-http-1",
10729 name: "HTTP Chat",
10730 selected: true
10731 }
10732 ]
10733 }
10734 })
10735 );
10736
10737 const currentDetailRequest = await client.queue.next(
10738 (message) =>
10739 message.type === "api_request"
10740 && message.path === "/api/organizations/org-http-1/chat_conversations/conv-http-1"
10741 );
10742 client.socket.send(
10743 JSON.stringify({
10744 type: "api_response",
10745 id: currentDetailRequest.id,
10746 ok: true,
10747 status: 200,
10748 body: {
10749 conversation: {
10750 uuid: "conv-http-1",
10751 name: "HTTP Chat"
10752 },
10753 messages: [
10754 {
10755 uuid: "msg-http-user",
10756 sender: "human",
10757 text: "hello from conductor http"
10758 },
10759 {
10760 uuid: "msg-http-assistant",
10761 sender: "assistant",
10762 content: [
10763 {
10764 text: "hello from claude http"
10765 }
10766 ]
10767 }
10768 ]
10769 }
10770 })
10771 );
10772
10773 const currentResponse = await currentPromise;
10774 assert.equal(currentResponse.status, 200);
10775 const currentPayload = await currentResponse.json();
10776 assert.equal(currentPayload.data.organization.organization_id, "org-http-1");
10777 assert.equal(currentPayload.data.messages.length, 2);
10778 assert.equal(currentPayload.data.messages[0].role, "user");
10779 assert.equal(currentPayload.data.messages[1].role, "assistant");
10780
10781 const reloadPromise = fetch(`${baseUrl}/v1/browser/actions`, {
10782 method: "POST",
10783 headers: {
10784 "content-type": "application/json"
10785 },
10786 body: JSON.stringify({
10787 action: "tab_reload",
10788 platform: "claude",
10789 reason: "http_integration_test"
10790 })
10791 });
10792 const reloadMessage = await client.queue.next((message) => message.type === "reload");
10793 sendPluginActionResult(client.socket, {
10794 action: "tab_reload",
10795 platform: "claude",
10796 requestId: reloadMessage.requestId,
10797 commandType: "reload"
10798 });
10799 const reloadResponse = await reloadPromise;
10800 assert.equal(reloadResponse.status, 200);
10801 const reloadPayload = await reloadResponse.json();
10802 assert.equal(reloadPayload.data.action, "tab_reload");
10803 assert.equal(reloadMessage.reason, "http_integration_test");
10804
10805 const reconnectPromise = fetch(`${baseUrl}/v1/browser/actions`, {
10806 method: "POST",
10807 headers: {
10808 "content-type": "application/json"
10809 },
10810 body: JSON.stringify({
10811 action: "ws_reconnect",
10812 disconnect_ms: 3000,
10813 repeat_count: 3,
10814 repeat_interval_ms: 500
10815 })
10816 });
10817 const reconnectMessage = await client.queue.next((message) => message.type === "ws_reconnect");
10818 sendPluginActionResult(client.socket, {
10819 action: "ws_reconnect",
10820 requestId: reconnectMessage.requestId,
10821 commandType: "ws_reconnect",
10822 completed: false
10823 });
10824 const reconnectResponse = await reconnectPromise;
10825 assert.equal(reconnectResponse.status, 200);
10826 const reconnectPayload = await reconnectResponse.json();
10827 assert.equal(reconnectPayload.data.action, "ws_reconnect");
10828 assert.equal(reconnectPayload.data.completed, false);
10829 assert.equal(reconnectMessage.disconnect_ms, 3000);
10830 assert.equal(reconnectMessage.repeat_count, 3);
10831 assert.equal(reconnectMessage.repeat_interval_ms, 500);
10832 } finally {
10833 client?.queue.stop();
10834 client?.socket.close(1000, "done");
10835 await runtime.stop();
10836 rmSync(stateDir, {
10837 force: true,
10838 recursive: true
10839 });
10840 }
10841});
10842
10843test("ConductorRuntime writes plugin diagnostic logs received over the Firefox bridge", async () => {
10844 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-plugin-log-state-"));
10845 const logsDir = mkdtempSync(join(tmpdir(), "baa-conductor-plugin-log-output-"));
10846 const runtime = new ConductorRuntime(
10847 {
10848 nodeId: "mini-main",
10849 host: "mini",
10850 role: "primary",
10851 controlApiBase: "https://control.example.test",
10852 localApiBase: "http://127.0.0.1:0",
10853 sharedToken: "replace-me",
10854 paths: {
10855 logsDir,
10856 runsDir: "/tmp/runs",
10857 stateDir
10858 }
10859 },
10860 {
10861 autoStartLoops: false,
10862 now: () => 100
10863 }
10864 );
10865
10866 let client = null;
10867
10868 try {
10869 const snapshot = await runtime.start();
10870 client = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-plugin-log");
10871
10872 client.socket.send(
10873 JSON.stringify({
10874 type: "plugin_diagnostic_log",
10875 ts: "2026-03-29T03:30:00.000Z",
10876 level: "info",
10877 text: "[PAGE] interceptor_active platform=chatgpt tab=17 / source=page-interceptor",
10878 client_id: "firefox-plugin-log"
10879 })
10880 );
10881
10882 const logPath = join(logsDir, "baa-plugin", "2026-03-29.jsonl");
10883 const entry = await waitForCondition(async () => {
10884 assert.equal(existsSync(logPath), true);
10885 const lines = readFileSync(logPath, "utf8").trim().split("\n");
10886 assert.equal(lines.length, 1);
10887 return JSON.parse(lines[0]);
10888 });
10889
10890 assert.equal(entry.type, "plugin_diagnostic_log");
10891 assert.equal(entry.ts, "2026-03-29T03:30:00.000Z");
10892 assert.equal(entry.level, "info");
10893 assert.equal(entry.text, "[PAGE] interceptor_active platform=chatgpt tab=17 / source=page-interceptor");
10894 assert.equal(entry.client_id, "firefox-plugin-log");
10895 } finally {
10896 client?.queue.stop();
10897 client?.socket.close(1000, "done");
10898 await runtime.stop();
10899 rmSync(logsDir, {
10900 force: true,
10901 recursive: true
10902 });
10903 rmSync(stateDir, {
10904 force: true,
10905 recursive: true
10906 });
10907 }
10908});
10909
10910test("ConductorRuntime persists browser metadata across disconnect and restart without leaking raw credentials", async () => {
10911 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-browser-persistence-"));
10912 const createRuntime = () =>
10913 new ConductorRuntime(
10914 {
10915 nodeId: "mini-main",
10916 host: "mini",
10917 role: "primary",
10918 controlApiBase: "https://control.example.test",
10919 localApiBase: "http://127.0.0.1:0",
10920 sharedToken: "replace-me",
10921 paths: {
10922 runsDir: "/tmp/runs",
10923 stateDir
10924 }
10925 },
10926 {
10927 autoStartLoops: false,
10928 now: () => 100
10929 }
10930 );
10931
10932 let runtime = createRuntime();
10933 let client = null;
10934
10935 try {
10936 const snapshot = await runtime.start();
10937 const baseUrl = snapshot.controlApi.localApiBase;
10938 client = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-persist");
10939
10940 client.socket.send(
10941 JSON.stringify({
10942 type: "credentials",
10943 platform: "claude",
10944 account: "persist@example.com",
10945 credential_fingerprint: "fp-claude-persist",
10946 freshness: "fresh",
10947 captured_at: 1710000001000,
10948 last_seen_at: 1710000001500,
10949 headers: {
10950 cookie: "session=persist-secret",
10951 "x-csrf-token": "csrf-persist-secret"
10952 }
10953 })
10954 );
10955 await client.queue.next(
10956 (message) => message.type === "state_snapshot" && message.reason === "credentials"
10957 );
10958
10959 client.socket.send(
10960 JSON.stringify({
10961 type: "api_endpoints",
10962 platform: "claude",
10963 account: "persist@example.com",
10964 credential_fingerprint: "fp-claude-persist",
10965 updated_at: 1710000002000,
10966 endpoints: [
10967 "GET /api/organizations",
10968 "POST /api/organizations/{id}/chat_conversations/{id}/completion"
10969 ],
10970 endpoint_metadata: [
10971 {
10972 method: "GET",
10973 path: "/api/organizations",
10974 first_seen_at: 1710000001200,
10975 last_seen_at: 1710000002000
10976 }
10977 ]
10978 })
10979 );
10980 await client.queue.next(
10981 (message) => message.type === "state_snapshot" && message.reason === "api_endpoints"
10982 );
10983
10984 const connectedStatus = await fetchJson(
10985 `${baseUrl}/v1/browser?platform=claude&account=persist%40example.com`
10986 );
10987 assert.equal(connectedStatus.response.status, 200);
10988 assert.equal(connectedStatus.payload.data.records.length, 1);
10989 assert.equal(connectedStatus.payload.data.records[0].view, "active_and_persisted");
10990 assert.equal(connectedStatus.payload.data.records[0].status, "fresh");
10991 assert.equal(connectedStatus.payload.data.records[0].live.credentials.account, "persist@example.com");
10992 assert.equal(
10993 connectedStatus.payload.data.records[0].persisted.credential_fingerprint,
10994 "fp-claude-persist"
10995 );
10996 assert.deepEqual(
10997 connectedStatus.payload.data.records[0].persisted.endpoints,
10998 [
10999 "GET /api/organizations",
11000 "POST /api/organizations/{id}/chat_conversations/{id}/completion"
11001 ]
11002 );
11003 assert.doesNotMatch(connectedStatus.text, /persist-secret/u);
11004
11005 const closePromise = waitForWebSocketClose(client.socket);
11006 client.socket.close(1000, "disconnect-persist");
11007 await closePromise;
11008 client.queue.stop();
11009 client = null;
11010
11011 const disconnectedStatus = await waitForCondition(async () => {
11012 const result = await fetchJson(
11013 `${baseUrl}/v1/browser?platform=claude&account=persist%40example.com`
11014 );
11015 assert.equal(result.payload.data.bridge.client_count, 0);
11016 assert.equal(result.payload.data.records.length, 1);
11017 assert.equal(result.payload.data.records[0].view, "persisted_only");
11018 assert.equal(result.payload.data.records[0].status, "stale");
11019 return result;
11020 });
11021 assert.equal(disconnectedStatus.payload.data.records[0].persisted.status, "stale");
11022
11023 await runtime.stop();
11024 runtime = null;
11025
11026 runtime = createRuntime();
11027 const restartedSnapshot = await runtime.start();
11028 const restartedStatus = await fetchJson(
11029 `${restartedSnapshot.controlApi.localApiBase}/v1/browser?platform=claude&account=persist%40example.com`
11030 );
11031 assert.equal(restartedStatus.response.status, 200);
11032 assert.equal(restartedStatus.payload.data.bridge.client_count, 0);
11033 assert.equal(restartedStatus.payload.data.records.length, 1);
11034 assert.equal(restartedStatus.payload.data.records[0].view, "persisted_only");
11035 assert.equal(restartedStatus.payload.data.records[0].persisted.credential_fingerprint, "fp-claude-persist");
11036 } finally {
11037 client?.queue.stop();
11038 client?.socket.close(1000, "done");
11039
11040 if (runtime != null) {
11041 await runtime.stop();
11042 }
11043
11044 rmSync(stateDir, {
11045 force: true,
11046 recursive: true
11047 });
11048 }
11049});
11050
11051test("ConductorRuntime ages browser login state from fresh to stale to lost when Firefox WS traffic goes quiet", async () => {
11052 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-browser-aging-"));
11053 let nowSeconds = 100;
11054 const runtime = new ConductorRuntime(
11055 {
11056 nodeId: "mini-main",
11057 host: "mini",
11058 role: "primary",
11059 controlApiBase: "https://control.example.test",
11060 localApiBase: "http://127.0.0.1:0",
11061 sharedToken: "replace-me",
11062 paths: {
11063 runsDir: "/tmp/runs",
11064 stateDir
11065 }
11066 },
11067 {
11068 autoStartLoops: false,
11069 now: () => nowSeconds
11070 }
11071 );
11072
11073 let client = null;
11074
11075 try {
11076 const snapshot = await runtime.start();
11077 const baseUrl = snapshot.controlApi.localApiBase;
11078 client = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-aging");
11079
11080 client.socket.send(
11081 JSON.stringify({
11082 type: "credentials",
11083 platform: "claude",
11084 account: "aging@example.com",
11085 credential_fingerprint: "fp-aging",
11086 freshness: "fresh",
11087 captured_at: 100_000,
11088 last_seen_at: 100_000,
11089 headers: {
11090 cookie: "session=aging"
11091 }
11092 })
11093 );
11094 await client.queue.next(
11095 (message) => message.type === "state_snapshot" && message.reason === "credentials"
11096 );
11097
11098 const freshStatus = await fetchJson(
11099 `${baseUrl}/v1/browser?platform=claude&account=aging%40example.com`
11100 );
11101 assert.equal(freshStatus.payload.data.records[0].status, "fresh");
11102
11103 nowSeconds = 160;
11104 await new Promise((resolve) => setTimeout(resolve, 2_200));
11105
11106 const staleStatus = await waitForCondition(async () => {
11107 const result = await fetchJson(
11108 `${baseUrl}/v1/browser?platform=claude&account=aging%40example.com`
11109 );
11110 assert.equal(result.payload.data.records[0].status, "stale");
11111 return result;
11112 }, 3_000, 100);
11113 assert.equal(staleStatus.payload.data.records[0].view, "active_and_persisted");
11114
11115 nowSeconds = 260;
11116 await new Promise((resolve) => setTimeout(resolve, 2_200));
11117
11118 const lostStatus = await waitForCondition(async () => {
11119 const result = await fetchJson(
11120 `${baseUrl}/v1/browser?platform=claude&account=aging%40example.com`
11121 );
11122 assert.equal(result.payload.data.records[0].status, "lost");
11123 return result;
11124 }, 3_000, 100);
11125 assert.equal(lostStatus.payload.data.records[0].persisted.status, "lost");
11126 } finally {
11127 client?.queue.stop();
11128 client?.socket.close(1000, "done");
11129 await runtime.stop();
11130 rmSync(stateDir, {
11131 force: true,
11132 recursive: true
11133 });
11134 }
11135});
11136
11137test("Firefox bridge api requests reject on timeout, disconnect, and replacement", async () => {
11138 const stateDir = mkdtempSync(join(tmpdir(), "baa-conductor-firefox-bridge-errors-"));
11139 const runtime = new ConductorRuntime(
11140 {
11141 nodeId: "mini-main",
11142 host: "mini",
11143 role: "primary",
11144 controlApiBase: "https://control.example.test",
11145 localApiBase: "http://127.0.0.1:0",
11146 sharedToken: "replace-me",
11147 paths: {
11148 runsDir: "/tmp/runs",
11149 stateDir
11150 }
11151 },
11152 {
11153 autoStartLoops: false,
11154 now: () => 100
11155 }
11156 );
11157
11158 let timeoutClient = null;
11159 let replacementClient = null;
11160 let replacementClientNext = null;
11161
11162 try {
11163 const snapshot = await runtime.start();
11164 const bridge = runtime.getFirefoxBridgeService();
11165
11166 assert.ok(bridge);
11167
11168 timeoutClient = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-timeout");
11169
11170 const timedOutPromise = bridge.apiRequest({
11171 clientId: "firefox-timeout",
11172 platform: "chatgpt",
11173 path: "/backend-api/models",
11174 timeoutMs: 50
11175 });
11176 const timedOutMessage = await timeoutClient.queue.next((message) => message.type === "api_request");
11177 assert.ok(timedOutMessage.id);
11178
11179 await assert.rejects(timedOutPromise, (error) => {
11180 assert.equal(error?.code, "request_timeout");
11181 assert.equal(error?.requestId, timedOutMessage.id);
11182 assert.equal(error?.timeoutMs, 50);
11183 return true;
11184 });
11185
11186 const disconnectedPromise = bridge.apiRequest({
11187 clientId: "firefox-timeout",
11188 platform: "chatgpt",
11189 path: "/backend-api/models",
11190 timeoutMs: 1_000
11191 });
11192 const disconnectedMessage = await timeoutClient.queue.next((message) => message.type === "api_request");
11193 const disconnectClose = waitForWebSocketClose(timeoutClient.socket);
11194 timeoutClient.socket.close(1000, "disconnect-test");
11195
11196 await assert.rejects(disconnectedPromise, (error) => {
11197 assert.equal(error?.code, "client_disconnected");
11198 assert.equal(error?.requestId, disconnectedMessage.id);
11199 return true;
11200 });
11201 await disconnectClose;
11202 timeoutClient.queue.stop();
11203 timeoutClient = null;
11204
11205 replacementClient = await connectFirefoxBridgeClient(snapshot.controlApi.firefoxWsUrl, "firefox-replace");
11206
11207 const replacedPromise = bridge.apiRequest({
11208 clientId: "firefox-replace",
11209 platform: "chatgpt",
11210 path: "/backend-api/models",
11211 timeoutMs: 1_000
11212 });
11213 const replacedMessage = await replacementClient.queue.next((message) => message.type === "api_request");
11214 const replacedClose = waitForWebSocketClose(replacementClient.socket);
11215 const replacedRejection = assert.rejects(replacedPromise, (error) => {
11216 assert.equal(error?.code, "client_replaced");
11217 assert.equal(error?.requestId, replacedMessage.id);
11218 return true;
11219 });
11220
11221 replacementClientNext = await connectFirefoxBridgeClient(
11222 snapshot.controlApi.firefoxWsUrl,
11223 "firefox-replace"
11224 );
11225
11226 await replacedRejection;
11227
11228 assert.deepEqual(await replacedClose, {
11229 code: 4001,
11230 reason: "replaced by a newer connection"
11231 });
11232 } finally {
11233 timeoutClient?.queue.stop();
11234 replacementClient?.queue.stop();
11235 replacementClientNext?.queue.stop();
11236 timeoutClient?.socket.close(1000, "done");
11237 replacementClient?.socket.close(1000, "done");
11238 replacementClientNext?.socket.close(1000, "done");
11239 await runtime.stop();
11240 rmSync(stateDir, {
11241 force: true,
11242 recursive: true
11243 });
11244 }
11245});