Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { IPage } from './types.js';

const {
mockExploreUrl,
mockRenderExploreSummary,
mockGenerateCliFromUrl,
mockRenderGenerateSummary,
mockRecordSession,
mockRenderRecordSummary,
mockCascadeProbe,
mockRenderCascadeResult,
mockGetBrowserFactory,
mockBrowserSession,
} = vi.hoisted(() => ({
mockExploreUrl: vi.fn(),
mockRenderExploreSummary: vi.fn(),
mockGenerateCliFromUrl: vi.fn(),
mockRenderGenerateSummary: vi.fn(),
mockRecordSession: vi.fn(),
mockRenderRecordSummary: vi.fn(),
mockCascadeProbe: vi.fn(),
mockRenderCascadeResult: vi.fn(),
mockGetBrowserFactory: vi.fn(() => ({ name: 'BrowserFactory' })),
mockBrowserSession: vi.fn(),
}));

vi.mock('./explore.js', () => ({
exploreUrl: mockExploreUrl,
renderExploreSummary: mockRenderExploreSummary,
}));

vi.mock('./generate.js', () => ({
generateCliFromUrl: mockGenerateCliFromUrl,
renderGenerateSummary: mockRenderGenerateSummary,
}));

vi.mock('./record.js', () => ({
recordSession: mockRecordSession,
renderRecordSummary: mockRenderRecordSummary,
}));

vi.mock('./cascade.js', () => ({
cascadeProbe: mockCascadeProbe,
renderCascadeResult: mockRenderCascadeResult,
}));

vi.mock('./runtime.js', () => ({
getBrowserFactory: mockGetBrowserFactory,
browserSession: mockBrowserSession,
}));

import { createProgram } from './cli.js';

describe('built-in browser commands verbose wiring', () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

beforeEach(() => {
delete process.env.OPENCLI_VERBOSE;
process.exitCode = undefined;

mockExploreUrl.mockReset().mockResolvedValue({ ok: true });
mockRenderExploreSummary.mockReset().mockReturnValue('explore-summary');
mockGenerateCliFromUrl.mockReset().mockResolvedValue({ ok: true });
mockRenderGenerateSummary.mockReset().mockReturnValue('generate-summary');
mockRecordSession.mockReset().mockResolvedValue({ candidateCount: 1 });
mockRenderRecordSummary.mockReset().mockReturnValue('record-summary');
mockCascadeProbe.mockReset().mockResolvedValue({ ok: true });
mockRenderCascadeResult.mockReset().mockReturnValue('cascade-summary');
mockGetBrowserFactory.mockClear();
mockBrowserSession.mockReset().mockImplementation(async (_factory, fn) => {
const page = {
goto: vi.fn(),
wait: vi.fn(),
} as unknown as IPage;
return fn(page);
});
});

it('enables OPENCLI_VERBOSE for explore via the real CLI command', async () => {
const program = createProgram('', '');

await program.parseAsync(['node', 'opencli', 'explore', 'https://example.com', '-v']);

expect(process.env.OPENCLI_VERBOSE).toBe('1');
expect(mockExploreUrl).toHaveBeenCalledWith(
'https://example.com',
expect.objectContaining({ workspace: 'explore:example.com' }),
);
});

it('enables OPENCLI_VERBOSE for generate via the real CLI command', async () => {
const program = createProgram('', '');

await program.parseAsync(['node', 'opencli', 'generate', 'https://example.com', '-v']);

expect(process.env.OPENCLI_VERBOSE).toBe('1');
expect(mockGenerateCliFromUrl).toHaveBeenCalledWith(
expect.objectContaining({ url: 'https://example.com', workspace: 'generate:example.com' }),
);
});

it('enables OPENCLI_VERBOSE for record via the real CLI command', async () => {
const program = createProgram('', '');

await program.parseAsync(['node', 'opencli', 'record', 'https://example.com', '-v']);

expect(process.env.OPENCLI_VERBOSE).toBe('1');
expect(mockRecordSession).toHaveBeenCalledWith(
expect.objectContaining({ url: 'https://example.com' }),
);
});

it('enables OPENCLI_VERBOSE for cascade via the real CLI command', async () => {
const program = createProgram('', '');

await program.parseAsync(['node', 'opencli', 'cascade', 'https://example.com', '-v']);

expect(process.env.OPENCLI_VERBOSE).toBe('1');
expect(mockBrowserSession).toHaveBeenCalled();
expect(mockCascadeProbe).toHaveBeenCalledWith(expect.any(Object), 'https://example.com');
});

it('leaves OPENCLI_VERBOSE unset when verbose is omitted', async () => {
const program = createProgram('', '');

await program.parseAsync(['node', 'opencli', 'explore', 'https://example.com']);

expect(process.env.OPENCLI_VERBOSE).toBeUndefined();
});

consoleLogSpy.mockClear();
});
52 changes: 46 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ async function getOperatePage(): Promise<import('./types.js').IPage> {
return bridge.connect({ timeout: 30, workspace: 'operate:default' });
}

export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
function applyVerbose(opts: { verbose?: boolean }): void {
if (opts.verbose) process.env.OPENCLI_VERBOSE = '1';
}

export function createProgram(BUILTIN_CLIS: string, USER_CLIS: string): Command {
const program = new Command();
// enablePositionalOptions: prevents parent from consuming flags meant for subcommands;
// prerequisite for passThroughOptions to forward --help/--version to external binaries
Expand Down Expand Up @@ -145,7 +149,16 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
.option('--wait <s>', '', '3')
.option('--auto', 'Enable interactive fuzzing')
.option('--click <labels>', 'Comma-separated labels to click before fuzzing')
.action(async (url, opts) => {
.option('-v, --verbose', 'Debug output')
.action(async (url: string, opts: {
site?: string;
goal?: string;
wait: string;
auto?: boolean;
click?: string;
verbose?: boolean;
}) => {
applyVerbose(opts);
const { exploreUrl, renderExploreSummary } = await import('./explore.js');
const clickLabels = opts.click
? opts.click.split(',').map((s: string) => s.trim())
Expand All @@ -168,7 +181,9 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
.description('Synthesize CLIs from explore')
.argument('<target>')
.option('--top <n>', '', '3')
.option('-v, --verbose', 'Debug output')
.action(async (target, opts) => {
applyVerbose(opts);
const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js');
console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) })));
});
Expand All @@ -179,7 +194,13 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
.argument('<url>')
.option('--goal <text>')
.option('--site <name>')
.action(async (url, opts) => {
.option('-v, --verbose', 'Debug output')
.action(async (url: string, opts: {
goal?: string;
site?: string;
verbose?: boolean;
}) => {
applyVerbose(opts);
const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js');
const workspace = `generate:${inferHost(url, opts.site)}`;
const r = await generateCliFromUrl({
Expand All @@ -203,7 +224,15 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
.option('--out <dir>', 'Output directory for candidates')
.option('--poll <ms>', 'Poll interval in milliseconds', '2000')
.option('--timeout <ms>', 'Auto-stop after N milliseconds (default: 60000)', '60000')
.action(async (url, opts) => {
.option('-v, --verbose', 'Debug output')
.action(async (url: string, opts: {
site?: string;
out?: string;
poll: string;
timeout: string;
verbose?: boolean;
}) => {
applyVerbose(opts);
const { recordSession, renderRecordSummary } = await import('./record.js');
const result = await recordSession({
BrowserFactory: getBrowserFactory(),
Expand All @@ -222,7 +251,12 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
.description('Strategy cascade: find simplest working strategy')
.argument('<url>')
.option('--site <name>')
.action(async (url, opts) => {
.option('-v, --verbose', 'Debug output')
.action(async (url: string, opts: {
site?: string;
verbose?: boolean;
}) => {
applyVerbose(opts);
const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
const workspace = `cascade:${inferHost(url, opts.site)}`;
const result = await browserSession(getBrowserFactory(), async (page) => {
Expand Down Expand Up @@ -644,7 +678,9 @@ cli({
.description('Diagnose opencli browser bridge connectivity')
.option('--no-live', 'Skip live browser connectivity test')
.option('--sessions', 'Show active automation sessions', false)
.option('-v, --verbose', 'Debug output')
.action(async (opts) => {
applyVerbose(opts);
const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
const report = await runBrowserDoctor({ live: opts.live, sessions: opts.sessions, cliVersion: PKG_VERSION });
console.log(renderBrowserDoctorReport(report));
Expand Down Expand Up @@ -954,7 +990,11 @@ cli({
process.exitCode = EXIT_CODES.USAGE_ERROR;
});

program.parse();
return program;
}

export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
createProgram(BUILTIN_CLIS, USER_CLIS).parse();
}

// ── Helpers ─────────────────────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion src/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ export async function exploreUrl(
const clicks = await page.evaluate(INTERACT_FUZZ_JS);
await page.wait(2); // wait for XHRs to settle
} catch (e) {
log.debug(`Interactive fuzzing skipped: ${e instanceof Error ? e.message : String(e)}`);
log.verbose(`Interactive fuzzing skipped: ${e instanceof Error ? e.message : String(e)}`);
}
}

Expand Down