diff --git a/src/commands/device.ts b/src/commands/device.ts index 669bef9..7a5f04e 100644 --- a/src/commands/device.ts +++ b/src/commands/device.ts @@ -456,7 +456,10 @@ const statusSub = defineCommand({ logger.log(ui.info(`A person handed it back${human.note ? `: ${human.note}` : ' (no note).'}`)); } - if (human?.human_in_control) { + // The API's own warning already says a person has control and how + // to wait; only add ours when an older API leaves it out. + const apiSaysWaiting = (session.warnings ?? []).some((w) => w.startsWith('Waiting for a human')); + if (human?.human_in_control && !apiSaysWaiting) { logger.log(ui.warn(`A person has control. Wait with \`dcd device human ${session.id} --wait\`.`)); } }, diff --git a/test/unit/device-command.test.ts b/test/unit/device-command.test.ts index 856efd0..81927b7 100644 --- a/test/unit/device-command.test.ts +++ b/test/unit/device-command.test.ts @@ -121,6 +121,31 @@ async function runJson( return { exit: code, out: JSON.parse(out) as Record }; } +/** Run a subcommand's `run` without --json, capturing what it prints. */ +async function runText(name: string, args: Record): Promise { + const sub = (deviceCommand.subCommands as Record Promise }>)[ + name + ]; + let out = ''; + const write = process.stdout.write.bind(process.stdout); + const log = console.log; + process.stdout.write = ((chunk: string) => { + out += chunk; + return true; + }) as typeof process.stdout.write; + console.log = (...parts: unknown[]) => { + out += parts.join(' ') + '\n'; + }; + try { + await sub.run({ args: { json: false, 'api-url': API, 'api-key': 'k', ...args }, rawArgs: [] }); + } finally { + process.stdout.write = write; + console.log = log; + } + + return out; +} + describe('dcd device', () => { const realFetch = globalThis.fetch; const ORIGINAL_ENV = { ...process.env }; @@ -626,6 +651,27 @@ describe('dcd device', () => { expect(out.data).to.deep.equal({ human: handedBack }); }); + it('status says a person has control once, not twice', async () => { + const inControl = human({ waiting: true, reason: 'a@b.c took control', human_in_control: true }); + const waitingWarning = + 'Waiting for a human: a@b.c took control. The session ends in 900s unless they hand it back. Poll get_human_status (MCP) or run `dcd device human --wait`; the idle timer is paused meanwhile.'; + stubFetch((call) => + call.url.endsWith('/human') + ? { body: { human: inControl, session: session('a-b-c', { state: 'needs_human' }) } } + : { body: { session: session('a-b-c', { state: 'needs_human', warnings: [waitingWarning] }) } }, + ); + const withApiWarning = await runText('status', { id: 'a-b-c' }); + expect(withApiWarning).to.include('Waiting for a human'); + expect(withApiWarning).not.to.include('A person has control'); + + stubFetch((call) => + call.url.endsWith('/human') + ? { body: { human: inControl, session: session('a-b-c', { state: 'needs_human' }) } } + : { body: { session: session('a-b-c', { state: 'needs_human' }) } }, + ); + expect(await runText('status', { id: 'a-b-c' })).to.include('A person has control'); + }); + it('status still works on an API without the human route', async () => { stubFetch((call) => call.url.endsWith('/human') ? { status: 404, body: { statusCode: 404, message: 'Not Found' } } : { body: { session: session('a-b-c') } },