Skip to content
Open
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
77 changes: 77 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2228,6 +2228,83 @@ describe('browser console command', () => {
});
});

describe('browser cookies command', () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
Comment on lines +2231 to +2232

beforeEach(() => {
process.exitCode = undefined;
consoleLogSpy.mockClear();
mockBrowserConnect.mockClear();
mockBrowserClose.mockReset().mockResolvedValue(undefined);
browserState.page = {
session: 'test',
setActivePage: vi.fn(),
getActivePage: vi.fn().mockReturnValue('tab-1'),
tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
getCookies: vi.fn().mockResolvedValue([
{ name: 'session', value: 'abc', domain: 'example.com', path: '/', secure: true, httpOnly: true },
{ name: 'theme', value: 'dark', domain: 'example.com', path: '/', secure: false, httpOnly: false },
]),
} as unknown as IPage;
});

function lastJsonLog(): any {
const calls = consoleLogSpy.mock.calls;
if (calls.length === 0) throw new Error('Expected at least one console.log call');
const last = calls[calls.length - 1][0];
if (typeof last !== 'string') throw new Error(`Expected string arg to console.log, got ${typeof last}`);
return JSON.parse(last);
}

it('returns cookies including HttpOnly entries for a scoped domain read', async () => {
const program = createProgram('', '');

await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'cookies', '--domain', 'example.com']);

expect(browserState.page!.getCookies).toHaveBeenCalledWith({ domain: 'example.com' });
const out = lastJsonLog();
expect(out).toMatchObject({
session: 'test',
count: 2,
captured_at: expect.any(String),
});
expect(out.cookies).toEqual(expect.arrayContaining([
expect.objectContaining({ name: 'session', httpOnly: true }),
expect.objectContaining({ name: 'theme', httpOnly: false }),
]));
});

it('passes --url through to getCookies without sending an unset --domain', async () => {
const program = createProgram('', '');

await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'cookies', '--url', 'https://example.com/login']);

expect(browserState.page!.getCookies).toHaveBeenCalledWith({ url: 'https://example.com/login' });
});

it('emits an empty cookies envelope with count 0 when the scope matches nothing', async () => {
(browserState.page!.getCookies as ReturnType<typeof vi.fn>).mockResolvedValueOnce([]);
const program = createProgram('', '');

await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'cookies', '--domain', 'nothing.example']);

const out = lastJsonLog();
expect(out.count).toBe(0);
expect(out.cookies).toEqual([]);
});

it('errors with missing_scope when neither --domain nor --url is provided', async () => {
const program = createProgram('', '');

await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'cookies']);

expect(browserState.page!.getCookies).not.toHaveBeenCalled();
expect(process.exitCode).toBeDefined();
const out = lastJsonLog();
expect(out.error.code).toBe('missing_scope');
Comment on lines +2301 to +2304
});
});

describe('browser get html command', () => {
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

Expand Down
22 changes: 22 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,28 @@ Examples:
}, null, 2));
}));

addBrowserTabOption(browser.command('cookies'))
.option('--domain <domain>', 'Cookie domain scope (e.g. example.com)')
.option('--url <url>', 'URL scope (may be combined with --domain to narrow)')
.description('Read scoped cookies (includes HttpOnly)')
.action(browserAction(async (page, opts) => {
if (!opts.domain && !opts.url) {
console.log(JSON.stringify({ error: { code: 'missing_scope', message: 'Provide --domain or --url to scope the cookie read' } }, null, 2));
process.exitCode = EXIT_CODES.USAGE_ERROR;
return;
}
const cookies = await page.getCookies({
...(opts.domain ? { domain: opts.domain } : {}),
...(opts.url ? { url: opts.url } : {}),
});
console.log(JSON.stringify({
session: getPageSession(page),
captured_at: new Date().toISOString(),
count: cookies.length,
cookies,
}, null, 2));
Comment on lines +1288 to +1293
}));

// ── Analyze (site recon, agent-native) ──
//
// Mechanizes the `site-recon.md` decision tree into one CLI call. The agent
Expand Down
Loading