From 9dc5c5655f1c123507b16765edfcef15de4e1659 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 25 Jun 2026 06:08:51 +0000 Subject: [PATCH 1/2] fix(status): exit non-zero on invalid --type/--state filter The status command's --type and --state validation branches rendered an error then bare-returned without setting a non-zero exit code, so scripts and CI under set -e could not detect a rejected filter (silent success on failure). Set process.exitCode = 1 on both branches before returning, which still lets cli.ts's finally cleanup run. Adds command.test.ts asserting exit code 1 for invalid --type and --state, and unset for a valid filter. Fixes #984 --- .../commands/status/__tests__/command.test.ts | 77 +++++++++++++++++++ src/cli/commands/status/command.tsx | 2 + 2 files changed, 79 insertions(+) create mode 100644 src/cli/commands/status/__tests__/command.test.ts diff --git a/src/cli/commands/status/__tests__/command.test.ts b/src/cli/commands/status/__tests__/command.test.ts new file mode 100644 index 000000000..bc198a3d0 --- /dev/null +++ b/src/cli/commands/status/__tests__/command.test.ts @@ -0,0 +1,77 @@ +import { registerStatus } from '../command.js'; +import { handleProjectStatus, loadStatusConfig } from '../action.js'; +import { Command } from '@commander-js/extra-typings'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { mockRender } = vi.hoisted(() => ({ + mockRender: vi.fn(), +})); + +vi.mock('../../../tui/guards', () => ({ + requireProject: vi.fn(), +})); + +vi.mock('../../../telemetry/cli-command-run.js', () => ({ + withCommandRunTelemetry: vi.fn((_command, _attrs, run) => run()), +})); + +vi.mock('../../../operations/dataset', () => ({ + getDatasetStatus: vi.fn(), +})); + +vi.mock('../action.js', () => ({ + handleProjectStatus: vi.fn(), + handleRuntimeLookup: vi.fn(), + loadStatusConfig: vi.fn(), +})); + +vi.mock('../../../feature-flags', () => ({ + isPreviewEnabled: () => false, +})); + +vi.mock('ink', () => ({ + Box: ({ children }: { children?: unknown }) => children, + Text: ({ children }: { children?: unknown }) => children, + render: mockRender, +})); + +describe('status command validation', () => { + let program: Command; + let originalExitCode: typeof process.exitCode; + + beforeEach(() => { + originalExitCode = process.exitCode; + process.exitCode = undefined; + program = new Command(); + program.exitOverride(); + registerStatus(program); + }); + + afterEach(() => { + process.exitCode = originalExitCode; + vi.clearAllMocks(); + }); + + it('sets a non-zero exit code for invalid resource type', async () => { + await program.parseAsync(['status', '--type', 'bogus'], { from: 'user' }); + + expect(process.exitCode).toBe(1); + expect(mockRender).toHaveBeenCalled(); + }); + + it('sets a non-zero exit code for invalid state', async () => { + await program.parseAsync(['status', '--state', 'bogus'], { from: 'user' }); + + expect(process.exitCode).toBe(1); + expect(mockRender).toHaveBeenCalled(); + }); + + it('leaves exit code unset for a valid filter', async () => { + vi.mocked(loadStatusConfig).mockResolvedValue({} as never); + vi.mocked(handleProjectStatus).mockResolvedValue({ success: true, resources: [] } as never); + + await program.parseAsync(['status', '--type', 'agent'], { from: 'user' }); + + expect(process.exitCode).toBeUndefined(); + }); +}); diff --git a/src/cli/commands/status/command.tsx b/src/cli/commands/status/command.tsx index dda4e226f..bafccb023 100644 --- a/src/cli/commands/status/command.tsx +++ b/src/cli/commands/status/command.tsx @@ -96,6 +96,7 @@ export const registerStatus = (program: Command) => { error: new ValidationError(msg), })); render({msg}); + process.exitCode = 1; return; } @@ -107,6 +108,7 @@ export const registerStatus = (program: Command) => { error: new ValidationError(msg), })); render({msg}); + process.exitCode = 1; return; } From 83a709f3fabeb079a297e31b87b294848ed2f58c Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Fri, 26 Jun 2026 19:40:40 +0000 Subject: [PATCH 2/2] test(status): fix formatting and align integ exit-code expectations --- integ-tests/status.test.ts | 4 ++-- src/cli/commands/status/__tests__/command.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/integ-tests/status.test.ts b/integ-tests/status.test.ts index 55890847f..5381ce5cd 100644 --- a/integ-tests/status.test.ts +++ b/integ-tests/status.test.ts @@ -69,7 +69,7 @@ describe('status command', () => { it('emits failure telemetry for invalid --type', async () => { const result = await runCLI(['status', '--type', 'bogus'], projectDir, { env: telemetry.env }); - expect(result.exitCode).toBe(0); + expect(result.exitCode).toBe(1); telemetry.assertMetricEmitted({ command: 'status', exit_reason: 'failure', @@ -80,7 +80,7 @@ describe('status command', () => { it('emits failure telemetry for invalid --state', async () => { const result = await runCLI(['status', '--state', 'bogus'], projectDir, { env: telemetry.env }); - expect(result.exitCode).toBe(0); + expect(result.exitCode).toBe(1); telemetry.assertMetricEmitted({ command: 'status', exit_reason: 'failure', diff --git a/src/cli/commands/status/__tests__/command.test.ts b/src/cli/commands/status/__tests__/command.test.ts index bc198a3d0..55521cc56 100644 --- a/src/cli/commands/status/__tests__/command.test.ts +++ b/src/cli/commands/status/__tests__/command.test.ts @@ -1,5 +1,5 @@ -import { registerStatus } from '../command.js'; import { handleProjectStatus, loadStatusConfig } from '../action.js'; +import { registerStatus } from '../command.js'; import { Command } from '@commander-js/extra-typings'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';