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
5 changes: 4 additions & 1 deletion clis/douyin/draft.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,12 @@ function createPageMock(
getInterceptedRequests: vi.fn().mockResolvedValue([]),
waitForCapture: vi.fn().mockResolvedValue(undefined),
screenshot: vi.fn().mockResolvedValue(''),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
setFileInput: vi.fn().mockResolvedValue(undefined),
...overrides,
};
} as IPage;
}

describe('douyin draft registration', () => {
Expand Down
5 changes: 4 additions & 1 deletion clis/facebook/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ function createMockPage(): IPage {
tabs: vi.fn().mockResolvedValue([]),
selectTab: vi.fn(),
networkRequests: vi.fn().mockResolvedValue([]),
consoleMessages: vi.fn().mockResolvedValue(''),
consoleMessages: vi.fn().mockResolvedValue([]),
scroll: vi.fn(),
autoScroll: vi.fn(),
installInterceptor: vi.fn(),
getInterceptedRequests: vi.fn().mockResolvedValue([]),
waitForCapture: vi.fn().mockResolvedValue(undefined),
screenshot: vi.fn().mockResolvedValue(''),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
10 changes: 9 additions & 1 deletion clis/hackernews/jobs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,15 @@ pipeline:
rank: ${{ index + 1 }}
title: ${{ item.title }}
author: ${{ item.by }}
url: ${{ item.url }}
url: >-
${{
item.url
|| item.text?.match(/href="([^"]+)"/)?.[1]
?.replace(///g, '/')
?.replace(/&/g, '&')
?.replace(/'/g, "'")
|| ('https://news.ycombinator.com/item?id=' + item.id)
}}

- limit: ${{ args.limit }}

Expand Down
27 changes: 27 additions & 0 deletions clis/instagram/_shared/private-publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,33 @@ describe('instagram private publish helpers', () => {
expect(evaluateAttempts).toBe(2);
});

it('resolves private publish config when capture metadata is unavailable', async () => {
const page = {
goto: async () => undefined,
wait: async () => undefined,
getCookies: async () => [{ name: 'csrftoken', value: 'csrf-cookie', domain: 'instagram.com' }],
startNetworkCapture: async () => undefined,
readNetworkCapture: async () => [{ url: 'https://www.instagram.com/api/v1/feed/timeline/', method: 'GET' }],
evaluate: async () => ({
appId: '936619743392459',
csrfToken: 'csrf-from-html',
instagramAjax: '1036523242',
}),
} as any;

await expect(resolveInstagramPrivatePublishConfig(page)).resolves.toEqual({
apiContext: {
asbdId: '',
csrfToken: 'csrf-from-html',
igAppId: '936619743392459',
igWwwClaim: '',
instagramAjax: '1036523242',
webSessionId: '',
},
jazoest: deriveInstagramJazoest('csrf-from-html'),
});
});

it('builds the single-image configure form body', () => {
expect(buildConfigureBody({
uploadId: '1775134280303',
Expand Down
11 changes: 4 additions & 7 deletions clis/instagram/_shared/private-publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as path from 'node:path';
import { spawnSync } from 'node:child_process';

import { CommandExecutionError } from '@jackwener/opencli/errors';
import type { BrowserCookie, IPage } from '@jackwener/opencli/types';
import type { BrowserCookie, CaptureCapablePage, IPage } from '@jackwener/opencli/types';
import type { InstagramProtocolCaptureEntry } from './protocol-capture.js';
import { instagramPrivateApiFetch } from './protocol-capture.js';
import {
Expand Down Expand Up @@ -174,21 +174,18 @@ export async function resolveInstagramPrivatePublishConfig(page: IPage): Promise
apiContext: InstagramPrivateApiContext;
jazoest: string;
}> {
const capturePage = page as CaptureCapablePage;
let lastError: unknown;
for (let attempt = 0; attempt < INSTAGRAM_PRIVATE_CONFIG_RETRY_BUDGET; attempt += 1) {
try {
if (typeof page.startNetworkCapture === 'function') {
await page.startNetworkCapture(INSTAGRAM_PRIVATE_CAPTURE_PATTERN);
}
await capturePage.startNetworkCapture(INSTAGRAM_PRIVATE_CAPTURE_PATTERN);
await page.goto(`${INSTAGRAM_HOME_URL}?__opencli_private_probe=${Date.now()}`);
await page.wait({ time: 2 });

const [cookies, runtime, entries] = await Promise.all([
page.getCookies({ domain: 'instagram.com' }),
page.evaluate(buildReadInstagramRuntimeInfoJs()) as Promise<InstagramRuntimeInfo>,
typeof page.readNetworkCapture === 'function'
? page.readNetworkCapture() as Promise<unknown[]>
: Promise.resolve([]),
capturePage.readNetworkCapture() as Promise<unknown[]>,
]);

const captureEntries = (Array.isArray(entries) ? entries : []) as InstagramProtocolCaptureEntry[];
Expand Down
39 changes: 37 additions & 2 deletions clis/instagram/_shared/protocol-capture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,27 @@ describe('instagram protocol capture helpers', () => {
it('prefers native page network capture when available', async () => {
const startNetworkCapture = vi.fn().mockResolvedValue(undefined);
const evaluate = vi.fn();
const page = { startNetworkCapture, evaluate } as unknown as IPage;
const hasNativeCaptureSupport = vi.fn().mockReturnValue(true);
const page = { startNetworkCapture, evaluate, hasNativeCaptureSupport } as unknown as IPage;

await installInstagramProtocolCapture(page);

expect(startNetworkCapture).toHaveBeenCalledTimes(1);
expect(evaluate).not.toHaveBeenCalled();
});

it('falls back to the page patch when native capture is unavailable after start', async () => {
const startNetworkCapture = vi.fn().mockResolvedValue(undefined);
const hasNativeCaptureSupport = vi.fn().mockReturnValue(false);
const evaluate = vi.fn().mockResolvedValue({ ok: true });
const page = { startNetworkCapture, evaluate, hasNativeCaptureSupport } as unknown as IPage;

await installInstagramProtocolCapture(page);

expect(startNetworkCapture).toHaveBeenCalledTimes(1);
expect(evaluate).toHaveBeenCalledTimes(1);
});

it('reads and normalizes captured protocol entries', async () => {
const evaluate = vi.fn().mockResolvedValue({
data: [{ kind: 'fetch', url: 'https://www.instagram.com/api/v1/media/configure/' }],
Expand All @@ -62,7 +75,8 @@ describe('instagram protocol capture helpers', () => {
{ kind: 'cdp', url: 'https://www.instagram.com/rupload_igphoto/test', method: 'POST' },
]);
const evaluate = vi.fn();
const page = { readNetworkCapture, evaluate } as unknown as IPage;
const hasNativeCaptureSupport = vi.fn().mockReturnValue(true);
const page = { readNetworkCapture, evaluate, hasNativeCaptureSupport } as unknown as IPage;

const result = await readInstagramProtocolCapture(page);

Expand All @@ -74,6 +88,27 @@ describe('instagram protocol capture helpers', () => {
});
});

it('falls back to the page patch read when native capture is unavailable after read', async () => {
const readNetworkCapture = vi.fn().mockResolvedValue([
{ url: 'https://www.instagram.com/api/v1/web/resource/', method: 'GET' },
]);
const hasNativeCaptureSupport = vi.fn().mockReturnValue(false);
const evaluate = vi.fn().mockResolvedValue({
data: [{ kind: 'fetch', url: 'https://www.instagram.com/api/v1/media/configure/', method: 'POST' }],
errors: ['fallback-used'],
});
const page = { readNetworkCapture, evaluate, hasNativeCaptureSupport } as unknown as IPage;

const result = await readInstagramProtocolCapture(page);

expect(readNetworkCapture).toHaveBeenCalledTimes(1);
expect(evaluate).toHaveBeenCalledTimes(1);
expect(result).toEqual({
data: [{ kind: 'fetch', url: 'https://www.instagram.com/api/v1/media/configure/', method: 'POST' }],
errors: ['fallback-used'],
});
});

it('dumps protocol traces to /tmp only when capture env is enabled', async () => {
process.env.OPENCLI_INSTAGRAM_CAPTURE = '1';
const page = {
Expand Down
22 changes: 17 additions & 5 deletions clis/instagram/_shared/protocol-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ export interface InstagramProtocolCaptureEntry {
timestamp: number;
}

type CaptureAwarePage = IPage & {
hasNativeCaptureSupport?: () => boolean | undefined;
};

function hasNativeCaptureSupport(page: IPage): boolean | undefined {
return (page as CaptureAwarePage).hasNativeCaptureSupport?.();
}

export function buildInstallInstagramProtocolCaptureJs(
captureVar: string = DEFAULT_CAPTURE_VAR,
captureErrorsVar: string = DEFAULT_CAPTURE_ERRORS_VAR,
Expand Down Expand Up @@ -226,7 +234,9 @@ export async function installInstagramProtocolCapture(page: IPage): Promise<void
if (typeof page.startNetworkCapture === 'function') {
try {
await page.startNetworkCapture(INSTAGRAM_PROTOCOL_CAPTURE_PATTERN);
return;
if (hasNativeCaptureSupport(page) !== false) {
return;
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes('Unknown action') && !message.includes('network-capture')) {
Expand All @@ -244,10 +254,12 @@ export async function readInstagramProtocolCapture(page: IPage): Promise<{
if (typeof page.readNetworkCapture === 'function') {
try {
const data = await page.readNetworkCapture();
return {
data: Array.isArray(data) ? data as InstagramProtocolCaptureEntry[] : [],
errors: [],
};
if (hasNativeCaptureSupport(page) !== false) {
return {
data: Array.isArray(data) ? data as InstagramProtocolCaptureEntry[] : [],
errors: [],
};
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes('Unknown action') && !message.includes('network-capture')) {
Expand Down
3 changes: 3 additions & 0 deletions clis/instagram/note.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ function createPageMock(): IPage {
setFileInput: vi.fn().mockResolvedValue(undefined),
insertText: vi.fn().mockResolvedValue(undefined),
getCurrentUrl: vi.fn().mockResolvedValue(null),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
13 changes: 9 additions & 4 deletions clis/instagram/post.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,14 @@ function createPageMock(evaluateResults: unknown[], overrides: Partial<IPage> =
getInterceptedRequests: vi.fn().mockResolvedValue([]),
waitForCapture: vi.fn().mockResolvedValue(undefined),
screenshot: vi.fn().mockResolvedValue(''),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
setFileInput: vi.fn().mockResolvedValue(undefined),
insertText: undefined,
getCurrentUrl: vi.fn().mockResolvedValue(null),
...overrides,
};
} as IPage;
}

afterAll(() => {
Expand Down Expand Up @@ -708,7 +711,7 @@ describe('instagram post registration', () => {
]);
});

it('installs and dumps protocol capture when OPENCLI_INSTAGRAM_CAPTURE is enabled', async () => {
it('uses native protocol capture and dumps traces when OPENCLI_INSTAGRAM_CAPTURE is enabled', async () => {
process.env.OPENCLI_INSTAGRAM_CAPTURE = '1';
const imagePath = createTempImage('capture-enabled.jpg');
const evaluate = vi.fn(async (js: string) => {
Expand All @@ -735,8 +738,10 @@ describe('instagram post registration', () => {
});

const evaluateCalls = evaluate.mock.calls.map((args) => String(args[0]));
expect(evaluateCalls.some((js) => js.includes('__opencli_ig_protocol_capture') && js.includes('PATCH_GUARD'))).toBe(true);
expect(evaluateCalls.some((js) => js.includes('const data = Array.isArray(window[') && js.includes('__opencli_ig_protocol_capture'))).toBe(true);
expect((page.startNetworkCapture as ReturnType<typeof vi.fn>)).toHaveBeenCalled();
expect((page.readNetworkCapture as ReturnType<typeof vi.fn>)).toHaveBeenCalled();
expect(evaluateCalls.some((js) => js.includes('__opencli_ig_protocol_capture') && js.includes('PATCH_GUARD'))).toBe(false);
expect(evaluateCalls.some((js) => js.includes('const data = Array.isArray(window[') && js.includes('__opencli_ig_protocol_capture'))).toBe(false);
expect(result).toEqual([
{
status: '✅ Posted',
Expand Down
5 changes: 4 additions & 1 deletion clis/instagram/reel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ function createPageMock(evaluateResults: unknown[], overrides: Partial<IPage> =
setFileInput: vi.fn().mockResolvedValue(undefined),
insertText: vi.fn().mockResolvedValue(undefined),
getCurrentUrl: vi.fn().mockResolvedValue(null),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
...overrides,
};
} as IPage;
}

afterAll(() => {
Expand Down
6 changes: 2 additions & 4 deletions clis/instagram/reel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as path from 'node:path';
import { Page as BrowserPage } from '@jackwener/opencli/browser/page';
import { cli, Strategy } from '@jackwener/opencli/registry';
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import type { BrowserCookie, IPage } from '@jackwener/opencli/types';
import type { BrowserCookie, CaptureCapablePage, IPage } from '@jackwener/opencli/types';
import {
buildClickActionJs,
buildEnsureComposerOpenJs,
Expand Down Expand Up @@ -809,9 +809,7 @@ cli({
activePage: IPage,
existingMediaPaths: ReadonlySet<string> = new Set(),
): Promise<InstagramReelSuccessRow[]> => {
if (typeof activePage.startNetworkCapture === 'function') {
await activePage.startNetworkCapture('/rupload_igvideo/|/api/v1/|/reel/|/clips/|/media/|/configure|/upload');
}
await (activePage as CaptureCapablePage).startNetworkCapture('/rupload_igvideo/|/api/v1/|/reel/|/clips/|/media/|/configure|/upload');
await gotoInstagramHome(activePage, true);
await activePage.wait({ time: 2 });
await dismissResidualDialogs(activePage);
Expand Down
5 changes: 4 additions & 1 deletion clis/instagram/story.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ function createPageMock(evaluateResults: unknown[] = [], overrides: Partial<IPag
setFileInput: vi.fn().mockResolvedValue(undefined),
insertText: vi.fn().mockResolvedValue(undefined),
getCurrentUrl: vi.fn().mockResolvedValue(null),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
...overrides,
};
} as IPage;
}

afterAll(() => {
Expand Down
3 changes: 3 additions & 0 deletions clis/substack/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ function createPageMock(evaluateResult: unknown): IPage {
getCookies: vi.fn().mockResolvedValue([]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
5 changes: 4 additions & 1 deletion clis/twitter/reply.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ function createPageMock(evaluateResults: any[], overrides: Partial<IPage> = {}):
getCookies: vi.fn().mockResolvedValue([]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
...overrides,
};
} as IPage;
}

describe('twitter reply command', () => {
Expand Down
3 changes: 3 additions & 0 deletions clis/xianyu/item.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ function createPageMock(evaluateResult: unknown): IPage {
getCookies: vi.fn().mockResolvedValue([]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
} as IPage;
}

Expand Down
3 changes: 3 additions & 0 deletions clis/xiaohongshu/comments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ function createPageMock(evaluateResult: any): IPage {
getCookies: vi.fn().mockResolvedValue([]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
3 changes: 3 additions & 0 deletions clis/xiaohongshu/creator-note-detail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ function createPageMock(evaluateResult: any): IPage {
getCookies: vi.fn().mockResolvedValue([]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
3 changes: 3 additions & 0 deletions clis/xiaohongshu/creator-notes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ function createPageMock(evaluateResult: any, interceptedRequests: any[] = []): I
getCookies: vi.fn().mockResolvedValue([]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
3 changes: 3 additions & 0 deletions clis/xiaohongshu/download.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ function createPageMock(evaluateResult: any): IPage {
getCookies: vi.fn().mockResolvedValue([{ name: 'sid', value: 'secret', domain: '.xiaohongshu.com' }]),
screenshot: vi.fn().mockResolvedValue(''),
waitForCapture: vi.fn().mockResolvedValue(undefined),
startNetworkCapture: vi.fn().mockResolvedValue(undefined),
readNetworkCapture: vi.fn().mockResolvedValue([]),
stopCapture: vi.fn().mockResolvedValue(undefined),
};
}

Expand Down
Loading
Loading