From 2e07ae44a32cfbaad9f16187f8247ea3dbfa8cf0 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 16 Feb 2026 18:32:00 -0800 Subject: [PATCH] test(cli): cover onboard flow and start onboarding guidance --- docs/plans/state.json | 17 +++++++++-- src/cli/onboard.test.ts | 49 +++++++++++++++++++++++++++++++ src/cli/start.test.ts | 65 +++++++++++++++++++++++++++++++++++++++++ src/cli/start.ts | 2 +- 4 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 src/cli/onboard.test.ts create mode 100644 src/cli/start.test.ts diff --git a/docs/plans/state.json b/docs/plans/state.json index 49b2ee1..2cc21d2 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -166,14 +166,25 @@ "updated": "2026-02-17", "summary": "Added a first-class `flynn onboard` CLI entrypoint as an explicit guided-onboarding alias to the setup wizard (`runSetup`), improving onboarding discoverability and OpenClaw-style command-surface parity without changing setup behavior.", "files_modified": [ - "src/cli/onboard.ts", "src/cli/index.ts", "src/cli/index.test.ts", - "README.md", - "docs/plans/state.json" + "README.md" ], "test_status": "pnpm test:run src/cli/index.test.ts + pnpm typecheck passing" }, + "onboard-command-tests-and-start-guidance": { + "status": "completed", + "date": "2026-02-17", + "updated": "2026-02-17", + "summary": "Added dedicated onboarding command behavior tests (default + explicit config path) and updated `flynn start` missing-config guidance to suggest `flynn onboard` as the primary guided entrypoint while retaining `flynn setup` compatibility.", + "files_modified": [ + "src/cli/onboard.test.ts", + "src/cli/start.ts", + "src/cli/start.test.ts", + "docs/plans/state.json" + ], + "test_status": "pnpm test:run src/cli/onboard.test.ts src/cli/start.test.ts src/cli/index.test.ts + pnpm typecheck passing" + }, "browser-tools-activation-clarity": { "status": "completed", "date": "2026-02-17", diff --git a/src/cli/onboard.test.ts b/src/cli/onboard.test.ts new file mode 100644 index 0000000..19ce60d --- /dev/null +++ b/src/cli/onboard.test.ts @@ -0,0 +1,49 @@ +import { Command } from 'commander'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const { mockRunSetup } = vi.hoisted(() => ({ + mockRunSetup: vi.fn(async () => undefined), +})); + +const { mockGetConfigPath } = vi.hoisted(() => ({ + mockGetConfigPath: vi.fn(() => '/tmp/default-config.yaml'), +})); + +vi.mock('./setup.js', () => ({ + runSetup: mockRunSetup, +})); + +vi.mock('./shared.js', () => ({ + getConfigPath: mockGetConfigPath, +})); + +describe('onboard command', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockRunSetup.mockReset(); + mockGetConfigPath.mockReset(); + mockGetConfigPath.mockReturnValue('/tmp/default-config.yaml'); + }); + + it('runs setup using default config path when --config is omitted', async () => { + const program = new Command(); + const { registerOnboardCommand } = await import('./onboard.js'); + registerOnboardCommand(program); + + await program.parseAsync(['node', 'test', 'onboard']); + + expect(mockGetConfigPath).toHaveBeenCalledOnce(); + expect(mockRunSetup).toHaveBeenCalledWith('/tmp/default-config.yaml'); + }); + + it('runs setup using explicit --config path when provided', async () => { + const program = new Command(); + const { registerOnboardCommand } = await import('./onboard.js'); + registerOnboardCommand(program); + + await program.parseAsync(['node', 'test', 'onboard', '--config', '/tmp/custom.yaml']); + + expect(mockGetConfigPath).not.toHaveBeenCalled(); + expect(mockRunSetup).toHaveBeenCalledWith('/tmp/custom.yaml'); + }); +}); diff --git a/src/cli/start.test.ts b/src/cli/start.test.ts new file mode 100644 index 0000000..7399318 --- /dev/null +++ b/src/cli/start.test.ts @@ -0,0 +1,65 @@ +import { Command } from 'commander'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const { mockExistsSync } = vi.hoisted(() => ({ + mockExistsSync: vi.fn(() => false), +})); + +const { mockCreateInterface } = vi.hoisted(() => ({ + mockCreateInterface: vi.fn(), +})); + +const { mockCreatePrompter, mockConfirm } = vi.hoisted(() => ({ + mockCreatePrompter: vi.fn(), + mockConfirm: vi.fn(async () => false), +})); + +vi.mock('fs', () => ({ + existsSync: mockExistsSync, +})); + +vi.mock('readline/promises', () => ({ + createInterface: mockCreateInterface, +})); + +vi.mock('./setup/prompts.js', () => ({ + createPrompter: mockCreatePrompter, +})); + +describe('start command', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockExistsSync.mockReset(); + mockExistsSync.mockReturnValue(false); + mockConfirm.mockReset(); + mockConfirm.mockResolvedValue(false); + mockCreateInterface.mockReset(); + mockCreateInterface.mockReturnValue({ close: vi.fn() }); + mockCreatePrompter.mockReset(); + mockCreatePrompter.mockReturnValue({ + confirm: mockConfirm, + }); + }); + + it('suggests onboard/setup guidance when config is missing and wizard is declined', async () => { + const program = new Command(); + const { registerStartCommand } = await import('./start.js'); + registerStartCommand(program); + + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { + throw new Error(`EXIT:${code ?? 0}`); + }) as never); + + await expect( + program.parseAsync(['node', 'test', 'start', '--config', '/tmp/missing.yaml']), + ).rejects.toThrow('EXIT:1'); + + expect(consoleError).toHaveBeenCalledWith( + 'Run "flynn onboard" (or "flynn setup") to create one, or "flynn doctor" to diagnose.', + ); + + exitSpy.mockRestore(); + consoleError.mockRestore(); + }); +}); diff --git a/src/cli/start.ts b/src/cli/start.ts index 9fb69c7..a26cf16 100644 --- a/src/cli/start.ts +++ b/src/cli/start.ts @@ -29,7 +29,7 @@ export function registerStartCommand(program: Command): void { } console.error(`Config file not found: ${configPath}`); - console.error('Run "flynn setup" to create one, or "flynn doctor" to diagnose.'); + console.error('Run "flynn onboard" (or "flynn setup") to create one, or "flynn doctor" to diagnose.'); process.exit(1); }