import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js"; import { approveNodePairing, getPairedNode, listNodePairing, removePairedNode, requestNodePairing, updatePairedNodeMetadata, verifyNodeToken, } from "./node-pairing.js"; async function setupPairedNode(baseDir: string): Promise { const request = await requestNodePairing( { nodeId: "node-1", platform: "darwin", commands: ["system.run"], }, baseDir, ); await approveNodePairing( request.request.requestId, { callerScopes: ["operator.pairing", "operator.admin"] }, baseDir, ); const paired = await getPairedNode("node-1", baseDir); expect(typeof paired?.token).toBe("string"); expect(paired?.token.length).toBeGreaterThan(0); return paired!.token; } const tempDirs = createSuiteTempRootTracker({ prefix: "openclaw-node-pairing-" }); async function withNodePairingDir(run: (baseDir: string) => Promise): Promise { return await run(await tempDirs.make("case")); } function requireRecord(value: unknown): Record { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error("Expected a non-array record"); } return value as Record; } function findRecordByField>( records: T[], field: string, value: unknown, ): T { const record = records.find((entry) => entry[field] === value); if (!record) { throw new Error(`Expected record with ${field}=${String(value)}`); } return record; } describe("node pairing tokens", () => { beforeAll(async () => { await tempDirs.setup(); }); afterAll(async () => { await tempDirs.cleanup(); }); test("reuses pending requests for metadata refreshes", async () => { await withNodePairingDir(async (baseDir) => { const first = await requestNodePairing( { nodeId: "node-1", platform: "darwin", }, baseDir, ); const second = await requestNodePairing( { nodeId: "node-1", platform: "darwin", }, baseDir, ); expect(first.created).toBe(true); expect(second.created).toBe(false); expect(second.request.requestId).toBe(first.request.requestId); const commandFirst = await requestNodePairing( { nodeId: "node-2", platform: "darwin", commands: ["canvas.snapshot"], }, baseDir, ); const commandSecond = await requestNodePairing( { nodeId: "node-2", platform: "darwin", displayName: "Updated Node", commands: ["canvas.snapshot"], }, baseDir, ); expect(commandSecond.created).toBe(false); expect(commandSecond.superseded).toBeUndefined(); expect(commandSecond.request.requestId).toBe(commandFirst.request.requestId); expect(commandSecond.request.displayName).toBe("Updated Node"); expect(commandSecond.request.commands).toEqual(["canvas.snapshot"]); const reorderedFirst = await requestNodePairing( { nodeId: "node-3", platform: "darwin", caps: ["camera", "screen"], commands: ["canvas.snapshot", "system.run"], }, baseDir, ); const reorderedSecond = await requestNodePairing( { nodeId: "node-3", platform: "darwin", caps: ["screen", "camera"], commands: ["system.run", "canvas.snapshot"], }, baseDir, ); expect(reorderedSecond.created).toBe(false); expect(reorderedSecond.superseded).toBeUndefined(); expect(reorderedSecond.request.requestId).toBe(reorderedFirst.request.requestId); await requestNodePairing( { nodeId: "node-4", platform: "darwin", commands: ["canvas.present"], }, baseDir, ); const pairing = await listNodePairing(baseDir); const pendingNode = findRecordByField(pairing.pending, "nodeId", "node-4"); expect(pendingNode.commands).toEqual(["canvas.present"]); expect(pendingNode.requiredApproveScopes).toEqual(["operator.pairing", "operator.write"]); expect(pairing.paired).toEqual([]); }); }); test("generates base64url node tokens and rejects mismatches", async () => { await withNodePairingDir(async (baseDir) => { const token = await setupPairedNode(baseDir); expect(token).toMatch(/^[A-Za-z0-9_-]{43}$/); expect(Buffer.from(token, "base64url")).toHaveLength(32); const verified = await verifyNodeToken("node-1", token, baseDir); expect(verified.ok).toBe(true); expect(verified.node?.nodeId).toBe("node-1"); await expect(verifyNodeToken("node-1", "x".repeat(token.length), baseDir)).resolves.toEqual({ ok: false, }); const multibyteToken = "é".repeat(token.length); expect(Buffer.from(multibyteToken).length).not.toBe(Buffer.from(token).length); await expect(verifyNodeToken("node-1", multibyteToken, baseDir)).resolves.toEqual({ ok: false, }); }); }); test("removes paired nodes without disturbing pending requests", async () => { await withNodePairingDir(async (baseDir) => { await setupPairedNode(baseDir); const pending = await requestNodePairing( { nodeId: "node-2", platform: "darwin", }, baseDir, ); await expect(removePairedNode("node-1", baseDir)).resolves.toEqual({ nodeId: "node-1" }); await expect(removePairedNode("node-1", baseDir)).resolves.toBeNull(); await expect(getPairedNode("node-1", baseDir)).resolves.toBeNull(); const pairing = await listNodePairing(baseDir); expect(pairing.pending).toHaveLength(1); expect(pairing.pending[0]?.requestId).toBe(pending.request.requestId); expect(pairing.pending[0]?.nodeId).toBe("node-2"); expect(pairing.paired).toEqual([]); }); }); test("requires the right scopes to approve node requests", async () => { await withNodePairingDir(async (baseDir) => { const systemRunRequest = await requestNodePairing( { nodeId: "node-1", platform: "darwin", commands: ["system.run"], }, baseDir, ); await expect( approveNodePairing( systemRunRequest.request.requestId, { callerScopes: ["operator.pairing"] }, baseDir, ), ).resolves.toEqual({ status: "forbidden", missingScope: "operator.admin", }); await expect(getPairedNode("node-1", baseDir)).resolves.toBeNull(); const commandlessRequest = await requestNodePairing( { nodeId: "node-2", platform: "darwin", }, baseDir, ); await expect( approveNodePairing(commandlessRequest.request.requestId, { callerScopes: [] }, baseDir), ).resolves.toEqual({ status: "forbidden", missingScope: "operator.pairing", }); const approved = await approveNodePairing( commandlessRequest.request.requestId, { callerScopes: ["operator.pairing"] }, baseDir, ); const approvedRecord = requireRecord(approved); const approvedNode = requireRecord(approvedRecord.node); expect(approvedRecord.requestId).toBe(commandlessRequest.request.requestId); expect(approvedNode.nodeId).toBe("node-2"); expect(approvedNode.commands).toBeUndefined(); }); }); test("updates paired node last-seen metadata and reports missing nodes", async () => { await withNodePairingDir(async (baseDir) => { await setupPairedNode(baseDir); await expect( updatePairedNodeMetadata( "node-1", { lastSeenAtMs: 1234, lastSeenReason: "silent_push", }, baseDir, ), ).resolves.toBe(true); await expect(updatePairedNodeMetadata("missing", { lastSeenAtMs: 1 }, baseDir)).resolves.toBe( false, ); const pairedNode = await getPairedNode("node-1", baseDir); expect(pairedNode?.lastSeenAtMs).toBe(1234); expect(pairedNode?.lastSeenReason).toBe("silent_push"); }); }); });