From c47bfc454971ce0aa429a03cb8263477e6da784a Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 28 Sep 2026 17:21:34 -0400 Subject: [PATCH 1/7] feat(doctor): report each build machine's state in doctor --json Refs #1806 --- .../src/__tests__/build-machines.test.ts | 40 ++- packages/stim-cli/src/commands/doctor.ts | 12 +- packages/stim-cli/src/guide/facts.ts | 5 + .../stim-cli/src/offload/build-machines.ts | 231 +++++++++++------- website/docs/commands.md | 5 +- 5 files changed, 199 insertions(+), 94 deletions(-) diff --git a/packages/stim-cli/src/__tests__/build-machines.test.ts b/packages/stim-cli/src/__tests__/build-machines.test.ts index e2d659cc8..353826a76 100644 --- a/packages/stim-cli/src/__tests__/build-machines.test.ts +++ b/packages/stim-cli/src/__tests__/build-machines.test.ts @@ -70,7 +70,7 @@ describe('findPeer', () => { describe('inspectBuildMachines', () => { it('requests access with --fix and pins the node it asked', async () => { const { io, calls } = fakeIo('nMini', [pending]); - const findings = await inspectBuildMachines({ fix: true }, io, ['mini']); + const { findings, machines } = await inspectBuildMachines({ fix: true }, io, ['mini']); expect(calls).toEqual([ { endpoint: { url: 'wss://100.64.0.7:7443', servername: 'mini.tail1.ts.net', host: 'mini.tail1.ts.net:7443' }, @@ -78,18 +78,25 @@ describe('inspectBuildMachines', () => { }, ]); expect(findings[0]!.fix).toContain('stim-server devices grant ab12 --build'); + expect(machines).toEqual([ + expect.objectContaining({ machine: 'mini', state: 'pending', dnsName: 'mini.tail1.ts.net', deviceId: 'ab12' }), + ]); expect(readBuildMachines()).toEqual([ expect.objectContaining({ machine: 'mini', nodeId: 'nMini', deviceToken: 'secret', state: 'pending' }), ]); - expect(await inspectBuildMachines({ fix: false }, fakeIo('nMini', []).io, [])).toEqual([]); + expect(await inspectBuildMachines({ fix: false }, fakeIo('nMini', []).io, [])).toEqual({ + findings: [], + machines: [], + }); }); it('never sends the token to a node other than the pinned one', async () => { await inspectBuildMachines({ fix: true }, fakeIo('nMini', [pending]).io, ['mini']); const { io, calls } = fakeIo('nImpostor', []); - const findings = await inspectBuildMachines({ fix: true }, io, ['mini']); + const { findings, machines } = await inspectBuildMachines({ fix: true }, io, ['mini']); expect(calls).toEqual([]); expect(findings).toEqual([expect.objectContaining({ title: 'Build machine mini is a different tailnet node' })]); + expect(machines).toEqual([expect.objectContaining({ machine: 'mini', state: 'node-changed' })]); expect(readBuildMachines()[0]!.nodeId).toBe('nMini'); }); @@ -97,14 +104,33 @@ describe('inspectBuildMachines', () => { await inspectBuildMachines({ fix: true }, fakeIo('nMini', [pending]).io, ['mini']); const approved: HelloReply = { result: { capabilities: ['build'], device: { id: 'ab12', name: 'laptop' } } }; const { io, calls } = fakeIo('nMini', [{ error: { code: 'approval-pending', message: 'wait' } }, approved]); - expect(await inspectBuildMachines({ fix: false }, io, ['mini'])).toEqual([ - expect.objectContaining({ title: 'Build machine mini has not approved this Mac yet' }), - ]); - expect(await inspectBuildMachines({ fix: false }, io, ['mini'])).toEqual([]); + expect(await inspectBuildMachines({ fix: false }, io, ['mini'])).toEqual({ + findings: [expect.objectContaining({ title: 'Build machine mini has not approved this Mac yet' })], + machines: [expect.objectContaining({ state: 'pending', deviceId: 'ab12' })], + }); + expect(await inspectBuildMachines({ fix: false }, io, ['mini'])).toEqual({ + findings: [], + machines: [expect.objectContaining({ machine: 'mini', state: 'approved', deviceId: 'ab12' })], + }); expect(calls.map((call) => call.auth)).toEqual([{ deviceToken: 'secret' }, { deviceToken: 'secret' }]); expect(readBuildMachines()[0]!.state).toBe('approved'); }); + it('reports a revoked machine and one never asked without asking either', async () => { + await inspectBuildMachines({ fix: true }, fakeIo('nMini', [pending]).io, ['mini']); + const { io, calls } = fakeIo('nMini', [{ error: { code: 'unauthorized', message: 'Unknown device.' } }]); + const { machines } = await inspectBuildMachines({ fix: false }, io, ['mini', 'minimal', 'nope', 'bad;name']); + expect(machines).toEqual([ + expect.objectContaining({ machine: 'mini', state: 'revoked', deviceId: 'ab12' }), + { machine: 'minimal', state: 'not-asked', dnsName: 'minimal.tail1.ts.net' }, + { machine: 'nope', state: 'not-on-tailnet' }, + { machine: 'bad;name', state: 'invalid' }, + ]); + expect(calls.map((call) => call.auth)).toEqual([{ deviceToken: 'secret' }]); + const off = await inspectBuildMachines({ fix: false }, { status: () => null, hello: io.hello }, ['mini']); + expect(off.machines).toEqual([{ machine: 'mini', state: 'tailscale-off' }]); + }); + it('forgets the pairing of a machine no longer named, with --fix only', async () => { await inspectBuildMachines({ fix: true }, fakeIo('nMini', [pending]).io, ['mini']); await inspectBuildMachines({ fix: false }, fakeIo('nMini', []).io, []); diff --git a/packages/stim-cli/src/commands/doctor.ts b/packages/stim-cli/src/commands/doctor.ts index 721d89273..98eff8451 100644 --- a/packages/stim-cli/src/commands/doctor.ts +++ b/packages/stim-cli/src/commands/doctor.ts @@ -237,11 +237,19 @@ export default function doctorCommand( const budget = await inspectBudget(root); findings.push(...budget.findings); - findings.push(...(await inspectBuildMachines({ fix: opts.fix === true }))); + const buildMachines = await inspectBuildMachines({ fix: opts.fix === true }); + findings.push(...buildMachines.findings); if (opts.json) { console.log( - JSON.stringify({ project: root, platform: opts.platform ?? null, stim, budget: budget.report, findings }), + JSON.stringify({ + project: root, + platform: opts.platform ?? null, + stim, + budget: budget.report, + buildMachines: buildMachines.machines, + findings, + }), ); recordDoctorRun(root, opts.platform, version); return; diff --git a/packages/stim-cli/src/guide/facts.ts b/packages/stim-cli/src/guide/facts.ts index 8af1f0603..910bba047 100644 --- a/packages/stim-cli/src/guide/facts.ts +++ b/packages/stim-cli/src/guide/facts.ts @@ -451,6 +451,11 @@ leased until