diff --git a/src/cli/commands/dev/__tests__/harness-logs.test.ts b/src/cli/commands/dev/__tests__/harness-logs.test.ts new file mode 100644 index 000000000..974c7af64 --- /dev/null +++ b/src/cli/commands/dev/__tests__/harness-logs.test.ts @@ -0,0 +1,73 @@ +// Regression test for #1406: `dev --logs` on a harness-only project must exit 1 +// (ValidationError), not print guidance and exit 0. +import { registerDev } from '../command.js'; +import * as devOps from '../../../operations/dev'; +import { Command } from '@commander-js/extra-typings'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('../../../tui/guards', () => ({ + requireProject: vi.fn(), + requireTTY: vi.fn(), +})); + +vi.mock('../../../telemetry/cli-command-run.js', () => ({ + withCommandRunTelemetry: vi.fn((_key: string, _attrs: unknown, fn: (recorder: { set: () => void }) => unknown) => + fn({ set: vi.fn() }) + ), +})); + +vi.mock('../../../operations/dev', async importOriginal => ({ + ...(await importOriginal()), + loadProjectConfig: vi.fn(), + getDevSupportedAgents: vi.fn(), +})); + +describe('dev --logs on a harness-only project (#1406)', () => { + let exitCodes: (number | undefined)[]; + let errors: string[]; + + beforeEach(() => { + vi.clearAllMocks(); + exitCodes = []; + errors = []; + // Harness-only project: no runtimes, one harness, zero dev-supported agents. + vi.mocked(devOps.loadProjectConfig).mockResolvedValue({ + runtimes: [], + harnesses: [{ name: 'my-harness' }], + } as never); + vi.mocked(devOps.getDevSupportedAgents).mockReturnValue([]); + vi.spyOn(console, 'log').mockImplementation(() => undefined); + vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => { + errors.push(args.join(' ')); + }); + vi.spyOn(process, 'exit').mockImplementation((code?: string | number | null) => { + exitCodes.push(typeof code === 'number' ? code : undefined); + return undefined as never; + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('exits 1 (ValidationError) instead of returning success and exiting 0', async () => { + const program = new Command(); + program.exitOverride(); + registerDev(program); + + await program.parseAsync(['dev', '--logs', '--skip-deploy', '--no-traces'], { from: 'user' }).catch(() => undefined); + + expect(exitCodes).toContain(1); + expect(exitCodes).not.toContain(0); + }); + + it('errors directing the user to `agentcore invoke --harness `', async () => { + const program = new Command(); + program.exitOverride(); + registerDev(program); + + await program.parseAsync(['dev', '--logs', '--skip-deploy', '--no-traces'], { from: 'user' }).catch(() => undefined); + + expect(errors.join('\n')).toContain('agentcore invoke --harness my-harness'); + }); +}); diff --git a/src/cli/commands/dev/command.tsx b/src/cli/commands/dev/command.tsx index e680899d7..422f86e65 100644 --- a/src/cli/commands/dev/command.tsx +++ b/src/cli/commands/dev/command.tsx @@ -359,21 +359,13 @@ export const registerDev = (program: Command) => { // --logs: non-interactive server mode if (opts.logs) { - // Harness-only projects need deploy then print invoke instructions + // Harness-only projects have no local dev server to tail logs from. if (supportedAgents.length === 0 && hasHarnesses) { recorder.set({ agent_environment: 'harness' as const }); - if (!opts.skipDeploy) { - await runCliDeploy(); - } - const harnessNames = (project.harnesses ?? []).map(h => h.name); - console.log('Harness dev runs against the deployed service (no local server).'); - console.log(`If you changed the harness config, redeploy to pick up changes: agentcore deploy`); - console.log(`\nInvoke your harness:`); - for (const name of harnessNames) { - console.log(` agentcore invoke --harness ${name} "your prompt"`); - } - console.log(`\nOr use the interactive TUI: agentcore dev`); - return { success: true as const, blockingPromise: Promise.resolve() }; + const firstHarness = project.harnesses?.[0]?.name ?? ''; + throw new ValidationError( + `Harness projects do not support local dev. Use \`agentcore invoke --harness ${firstHarness}\` instead.` + ); } if (project.runtimes.length > 1 && !opts.runtime) {