import test from "node:test"; import assert from "node:assert/strict"; import { createServer } from "node:http"; import { spawn } from "node:child_process"; import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); async function startMockControlPlane() { let resolveHeartbeat; const heartbeatReceived = new Promise((resolve) => { resolveHeartbeat = resolve; }); const server = createServer(async (request, response) => { const chunks = []; for await (const chunk of request) { chunks.push(chunk); } if (request.method === "POST" && request.url === "/api/device-heartbeat") { resolveHeartbeat(JSON.parse(Buffer.concat(chunks).toString("utf8"))); } response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ ok: true })); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); if (!address || typeof address === "string") { throw new Error("failed to bind mock control plane"); } return { server, port: address.port, heartbeatReceived }; } test("local-agent heartbeat reports browser automation and computer use capabilities", async () => { const runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-local-agent-computer-capabilities-")); const skillsDir = path.join(runtimeRoot, "skills"); await mkdir(skillsDir, { recursive: true }); const mockControlPlane = await startMockControlPlane(); const exampleConfig = JSON.parse( await readFile(path.join(repoRoot, "local-agent", "config.example.json"), "utf8"), ); const configPath = path.join(runtimeRoot, "config.json"); await writeFile( configPath, JSON.stringify( { ...exampleConfig, bindHost: "127.0.0.1", port: 0, controlPlaneUrl: `http://127.0.0.1:${mockControlPlane.port}`, heartbeatIntervalMs: 60_000, masterAgentPollIntervalMs: 60_000, masterAgentEnabled: false, codexSessionDiscoveryEnabled: false, projects: [], projectCandidates: [], skillsDir, browserAutomationConnected: true, computerUseConnected: false, }, null, 2, ), "utf8", ); const child = spawn(process.execPath, ["local-agent/server.mjs", configPath], { cwd: repoRoot, stdio: ["ignore", "pipe", "pipe"], }); try { const payload = await Promise.race([ mockControlPlane.heartbeatReceived, new Promise((_, reject) => { setTimeout(() => reject(new Error("timed out waiting for heartbeat")), 8000); }), ]); assert.equal(payload.capabilities.browserAutomation.connected, true); assert.equal(payload.capabilities.computerUse.connected, true); } finally { child.kill("SIGTERM"); await new Promise((resolve) => child.once("close", resolve)).catch(() => null); await new Promise((resolve) => mockControlPlane.server.close(resolve)); await rm(runtimeRoot, { recursive: true, force: true }); } }); test("local-agent heartbeat derives browser and computer control capabilities from runtime config", async () => { const runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-local-agent-runtime-capabilities-")); const skillsDir = path.join(runtimeRoot, "skills"); await mkdir(skillsDir, { recursive: true }); const mockControlPlane = await startMockControlPlane(); const exampleConfig = JSON.parse( await readFile(path.join(repoRoot, "local-agent", "config.example.json"), "utf8"), ); const configPath = path.join(runtimeRoot, "config.json"); await writeFile( configPath, JSON.stringify( { ...exampleConfig, bindHost: "127.0.0.1", port: 0, controlPlaneUrl: `http://127.0.0.1:${mockControlPlane.port}`, heartbeatIntervalMs: 60_000, masterAgentPollIntervalMs: 60_000, masterAgentEnabled: false, codexSessionDiscoveryEnabled: false, projects: [], projectCandidates: [], skillsDir, browserAutomationConnected: false, computerUseConnected: false, browserControlEnabled: true, browserControlCommand: process.execPath, browserControlArgs: ["tests/fixtures/browser-control-runtime.mjs"], computerUseEnabled: true, computerUseCommand: process.execPath, computerUseArgs: ["tests/fixtures/computer-use-runtime.mjs"], }, null, 2, ), "utf8", ); const child = spawn(process.execPath, ["local-agent/server.mjs", configPath], { cwd: repoRoot, stdio: ["ignore", "pipe", "pipe"], }); try { const payload = await Promise.race([ mockControlPlane.heartbeatReceived, new Promise((_, reject) => { setTimeout(() => reject(new Error("timed out waiting for heartbeat")), 8000); }), ]); assert.equal(payload.capabilities.browserAutomation.connected, true); assert.equal(payload.capabilities.computerUse.connected, true); } finally { child.kill("SIGTERM"); await new Promise((resolve) => child.once("close", resolve)).catch(() => null); await new Promise((resolve) => mockControlPlane.server.close(resolve)); await rm(runtimeRoot, { recursive: true, force: true }); } });