From d352beed78690bfac4eadb22960bb4d602f6111d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 1 Aug 2026 12:53:05 -0700 Subject: [PATCH] fix(nodes): preserve operator name on reapproval --- src/infra/node-pairing.test.ts | 42 ++++++++++++++++++++++++++++++---- src/infra/node-pairing.ts | 3 ++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/infra/node-pairing.test.ts b/src/infra/node-pairing.test.ts index 90a8137a434d..50957776d4d2 100644 --- a/src/infra/node-pairing.test.ts +++ b/src/infra/node-pairing.test.ts @@ -36,11 +36,12 @@ async function seedNodeDevice(baseDir: string, nodeId: string): Promise { await approveDevicePairing(request.request.requestId, { callerScopes: [] }, baseDir); } -async function setupPairedNode(baseDir: string): Promise { +async function setupPairedNode(baseDir: string, displayName?: string): Promise { await seedNodeDevice(baseDir, "node-1"); const request = await requestNodePairing( { nodeId: "node-1", + displayName, platform: "darwin", commands: ["system.run"], }, @@ -783,17 +784,50 @@ describe("node surface approvals", () => { }); }); - test("renames the operator-facing node name without touching approval state", async () => { + test("keeps the operator-facing node name through capability reapproval", async () => { await withNodePairingDir(async (baseDir) => { - await setupPairedNode(baseDir); + await setupPairedNode(baseDir, "Reported iPad"); + const initialGeneration = resolveNodePairingGeneration( + await getPairedDevice("node-1", baseDir), + ); + if (!initialGeneration) { + throw new Error("expected initial node pairing generation"); + } + const upgrade = await requestNodePairing( + { + nodeId: "node-1", + displayName: "Reported iPad (updated)", + platform: "darwin", + commands: ["system.run", "canvas.snapshot"], + }, + baseDir, + ); + expect(upgrade.request.displayName).toBe("Reported iPad (updated)"); const renamed = await renamePairedNode("node-1", "Living Room iPad", baseDir); expect(renamed?.displayName).toBe("Living Room iPad"); await expect(renamePairedNode("missing", "Nope", baseDir)).resolves.toBeNull(); + await expect( + approveNodePairing( + upgrade.request.requestId, + { callerScopes: ["operator.pairing", "operator.admin", "operator.write"] }, + baseDir, + ), + ).resolves.toMatchObject({ + node: { + displayName: "Living Room iPad", + commands: ["system.run", "canvas.snapshot"], + }, + }); + const pairedNode = await findPairedNode("node-1", baseDir); expect(pairedNode?.displayName).toBe("Living Room iPad"); - expect(pairedNode?.commands).toEqual(["system.run"]); + expect(pairedNode?.commands).toEqual(["system.run", "canvas.snapshot"]); + expect((await listNodePairing(baseDir)).pending).toEqual([]); + expect(resolveNodePairingGeneration(await getPairedDevice("node-1", baseDir))?.key).not.toBe( + initialGeneration.key, + ); }); }); }); diff --git a/src/infra/node-pairing.ts b/src/infra/node-pairing.ts index 0e6e9fe28acc..ecebd2c6b589 100644 --- a/src/infra/node-pairing.ts +++ b/src/infra/node-pairing.ts @@ -569,7 +569,8 @@ export async function approveNodePairing( const previousPairingGeneration = resolveNodePairingGeneration(device); const now = Math.max(Date.now(), (device.nodeSurface?.approvedAtMs ?? -1) + 1); device.nodeSurface = { - displayName: pending.displayName, + // Reapproval refreshes the node-declared surface without replacing the operator-owned name. + displayName: device.nodeSurface?.displayName ?? pending.displayName, version: pending.version, coreVersion: pending.coreVersion, uiVersion: pending.uiVersion,