@jackwener/opencli 1.7.8 → 1.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -14
- package/README.zh-CN.md +30 -10
- package/cli-manifest.json +646 -30
- package/clis/36kr/news.js +1 -1
- package/clis/apple-podcasts/commands.test.js +4 -4
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/apple-podcasts/search.js +1 -1
- package/clis/apple-podcasts/top.js +1 -1
- package/clis/arxiv/paper.js +1 -1
- package/clis/arxiv/search.js +1 -1
- package/clis/band/mentions.js +3 -3
- package/clis/bbc/news.js +1 -1
- package/clis/bilibili/subtitle.js +2 -2
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/economics.js +1 -1
- package/clis/bloomberg/industries.js +1 -1
- package/clis/bloomberg/main.js +1 -1
- package/clis/bloomberg/markets.js +1 -1
- package/clis/bloomberg/opinions.js +1 -1
- package/clis/bloomberg/politics.js +1 -1
- package/clis/bloomberg/tech.js +1 -1
- package/clis/boss/search.js +49 -8
- package/clis/boss/search.test.js +78 -0
- package/clis/boss/send.js +3 -3
- package/clis/chatgpt/image.js +37 -8
- package/clis/chatgpt/image.test.js +92 -0
- package/clis/chatgpt/utils.js +39 -6
- package/clis/chatgpt/utils.test.js +63 -0
- package/clis/chatgpt-app/ask.js +1 -1
- package/clis/chatgpt-app/ax.js +4 -2
- package/clis/chatgpt-app/ax.test.js +12 -0
- package/clis/chatgpt-app/model.js +1 -1
- package/clis/chatgpt-app/new.js +1 -1
- package/clis/chatgpt-app/read.js +1 -1
- package/clis/chatgpt-app/send.js +1 -1
- package/clis/chatgpt-app/status.js +1 -1
- package/clis/chatwise/ask.js +2 -2
- package/clis/chatwise/model.js +2 -2
- package/clis/chatwise/send.js +2 -2
- package/clis/claude/ask.js +128 -0
- package/clis/claude/ask.test.js +338 -0
- package/clis/claude/commands.test.js +118 -0
- package/clis/claude/detail.js +29 -0
- package/clis/claude/history.js +31 -0
- package/clis/claude/new.js +21 -0
- package/clis/claude/read.js +24 -0
- package/clis/claude/send.js +41 -0
- package/clis/claude/status.js +24 -0
- package/clis/claude/utils.js +440 -0
- package/clis/claude/utils.test.js +148 -0
- package/clis/codex/ask.js +2 -2
- package/clis/codex/send.js +2 -2
- package/clis/ctrip/search.js +1 -1
- package/clis/ctrip/search.test.js +4 -4
- package/clis/cursor/ask.js +2 -2
- package/clis/cursor/composer.js +2 -2
- package/clis/cursor/send.js +2 -2
- package/clis/deepseek/ask.js +17 -4
- package/clis/deepseek/ask.test.js +46 -0
- package/clis/deepseek/utils.js +55 -16
- package/clis/deepseek/utils.test.js +124 -5
- package/clis/doubao/utils.js +53 -11
- package/clis/doubao/utils.test.js +22 -2
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/convertible.js +1 -1
- package/clis/eastmoney/etf.js +1 -1
- package/clis/eastmoney/holders.js +1 -1
- package/clis/eastmoney/index-board.js +1 -1
- package/clis/eastmoney/kline.js +1 -1
- package/clis/eastmoney/kuaixun.js +1 -1
- package/clis/eastmoney/longhu.js +1 -1
- package/clis/eastmoney/money-flow.js +1 -1
- package/clis/eastmoney/northbound.js +1 -1
- package/clis/eastmoney/quote.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/facebook/marketplace-inbox.js +83 -0
- package/clis/facebook/marketplace-listings.js +83 -0
- package/clis/facebook/marketplace.test.js +91 -0
- package/clis/google/news.js +1 -1
- package/clis/google/suggest.js +1 -1
- package/clis/google/trends.js +1 -1
- package/clis/google-scholar/cite.js +74 -0
- package/clis/google-scholar/cite.test.js +47 -0
- package/clis/google-scholar/profile.js +92 -0
- package/clis/google-scholar/profile.test.js +49 -0
- package/clis/google-scholar/search.js +1 -1
- package/clis/google-scholar/search.test.js +15 -0
- package/clis/hf/top.js +1 -1
- package/clis/instagram/collection-create.js +57 -0
- package/clis/instagram/saved.js +21 -7
- package/clis/jd/item.js +679 -47
- package/clis/jd/item.test.js +318 -7
- package/clis/jd/item.test.ts +517 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/tags.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/lesswrong/user-posts.js +1 -1
- package/clis/lesswrong/user.js +1 -1
- package/clis/paperreview/commands.test.js +6 -6
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/producthunt/posts.js +1 -1
- package/clis/producthunt/today.js +1 -1
- package/clis/sinablog/search.js +1 -1
- package/clis/sinafinance/news.js +1 -1
- package/clis/sinafinance/stock.js +1 -1
- package/clis/sinafinance/stock.test.js +2 -2
- package/clis/spotify/spotify.js +6 -6
- package/clis/substack/search.js +1 -1
- package/clis/toutiao/articles.js +5 -6
- package/clis/toutiao/articles.test.js +22 -15
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +224 -73
- package/clis/twitter/following.test.js +277 -0
- package/clis/twitter/post.js +184 -47
- package/clis/twitter/post.test.js +114 -34
- package/clis/uiverse/_shared.js +63 -4
- package/clis/uiverse/_shared.test.js +7 -0
- package/clis/uiverse/code.js +1 -0
- package/clis/uiverse/navigation.test.js +12 -0
- package/clis/uiverse/preview.js +1 -0
- package/clis/web/read.js +319 -81
- package/clis/web/read.test.js +221 -5
- package/clis/weibo/favorites.js +169 -0
- package/clis/weibo/favorites.test.js +114 -0
- package/clis/weibo/publish.js +282 -0
- package/clis/weibo/publish.test.js +183 -0
- package/clis/weread/ranking.js +1 -1
- package/clis/weread/search-regression.test.js +8 -8
- package/clis/weread/search.js +1 -1
- package/clis/wikipedia/random.js +1 -1
- package/clis/wikipedia/search.js +1 -1
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/trending.js +1 -1
- package/clis/xianyu/chat.js +3 -3
- package/clis/xianyu/item.js +2 -2
- package/clis/xianyu/item.test.js +3 -3
- package/clis/xiaohongshu/search.js +17 -2
- package/clis/xiaohongshu/search.test.js +37 -1
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/download.test.js +3 -3
- package/clis/xiaoyuzhou/episode.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
- package/clis/xiaoyuzhou/podcast.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/xiaoyuzhou/transcript.test.js +5 -5
- package/clis/yollomi/models.js +1 -1
- package/clis/youtube/channel.js +24 -1
- package/clis/youtube/channel.test.js +59 -0
- package/clis/zhihu/answer.js +21 -162
- package/clis/zhihu/answer.test.js +26 -53
- package/clis/zhihu/collection.js +197 -0
- package/clis/zhihu/collection.test.js +290 -0
- package/clis/zhihu/collections.js +127 -0
- package/clis/zhihu/collections.test.js +182 -0
- package/clis/zhihu/comment.js +24 -305
- package/clis/zhihu/comment.test.js +31 -35
- package/clis/zhihu/favorite.js +44 -182
- package/clis/zhihu/favorite.test.js +30 -167
- package/clis/zhihu/follow.js +25 -56
- package/clis/zhihu/follow.test.js +20 -23
- package/clis/zhihu/like.js +22 -67
- package/clis/zhihu/like.test.js +19 -42
- package/clis/zhihu/search.js +3 -2
- package/clis/zhihu/write-shared.js +8 -1
- package/clis/zhihu/write-shared.test.js +1 -0
- package/clis/zlibrary/commands.test.js +75 -0
- package/clis/zlibrary/info.js +47 -0
- package/clis/zlibrary/search.js +46 -0
- package/clis/zlibrary/utils.js +136 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/browser/base-page.d.ts +3 -1
- package/dist/src/browser/base-page.js +76 -1
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +74 -0
- package/dist/src/browser/bridge.d.ts +1 -2
- package/dist/src/browser/bridge.js +40 -41
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +3 -3
- package/dist/src/browser/daemon-client.d.ts +38 -4
- package/dist/src/browser/daemon-client.js +24 -7
- package/dist/src/browser/daemon-client.test.js +49 -0
- package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
- package/dist/src/browser/daemon-lifecycle.js +67 -0
- package/dist/src/browser/daemon-version.d.ts +4 -0
- package/dist/src/browser/daemon-version.js +12 -0
- package/dist/src/browser/errors.js +3 -0
- package/dist/src/browser/errors.test.js +3 -0
- package/dist/src/browser/network-cache.d.ts +1 -0
- package/dist/src/browser/page.d.ts +3 -1
- package/dist/src/browser/page.js +10 -2
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +85 -0
- package/dist/src/build-manifest.d.ts +2 -0
- package/dist/src/build-manifest.js +13 -3
- package/dist/src/build-manifest.test.js +20 -2
- package/dist/src/cli.d.ts +6 -0
- package/dist/src/cli.js +477 -35
- package/dist/src/cli.test.js +303 -2
- package/dist/src/commanderAdapter.js +17 -9
- package/dist/src/commanderAdapter.test.js +67 -2
- package/dist/src/commands/daemon.d.ts +2 -0
- package/dist/src/commands/daemon.js +42 -1
- package/dist/src/commands/daemon.test.js +103 -2
- package/dist/src/completion-shared.js +1 -2
- package/dist/src/completion.test.js +3 -2
- package/dist/src/daemon.js +125 -41
- package/dist/src/doctor.d.ts +5 -6
- package/dist/src/doctor.js +77 -19
- package/dist/src/doctor.test.js +117 -0
- package/dist/src/engine.test.js +6 -5
- package/dist/src/errors.d.ts +14 -8
- package/dist/src/errors.js +36 -30
- package/dist/src/errors.test.js +5 -5
- package/dist/src/execution.d.ts +4 -0
- package/dist/src/execution.js +173 -25
- package/dist/src/execution.test.js +171 -1
- package/dist/src/main.js +10 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/pipeline/steps/fetch.js +1 -21
- package/dist/src/pipeline/steps/fetch.test.js +6 -12
- package/dist/src/plugin-scaffold.js +1 -1
- package/dist/src/plugin-scaffold.test.js +1 -1
- package/dist/src/registry.d.ts +40 -9
- package/dist/src/registry.js +3 -1
- package/dist/src/runtime-detect.d.ts +10 -0
- package/dist/src/runtime-detect.js +19 -0
- package/dist/src/runtime-detect.test.js +12 -1
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/update-check.d.ts +31 -1
- package/dist/src/update-check.js +62 -16
- package/dist/src/update-check.test.js +86 -1
- package/package.json +1 -1
- package/dist/src/diagnostic.d.ts +0 -63
- package/dist/src/diagnostic.js +0 -292
- package/dist/src/diagnostic.test.js +0 -302
- /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { redactHeaders, redactUrl, redactValue } from './redaction.js';
|
|
3
|
+
describe('observation redaction', () => {
|
|
4
|
+
it('redacts sensitive headers by default', () => {
|
|
5
|
+
expect(redactHeaders({
|
|
6
|
+
authorization: 'Bearer secret-token',
|
|
7
|
+
cookie: 'sid=abc',
|
|
8
|
+
'set-cookie': 'sid=abc',
|
|
9
|
+
accept: 'application/json',
|
|
10
|
+
})).toEqual({
|
|
11
|
+
authorization: '[REDACTED]',
|
|
12
|
+
cookie: '[REDACTED]',
|
|
13
|
+
'set-cookie': '[REDACTED]',
|
|
14
|
+
accept: 'application/json',
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
it('redacts sensitive url query params', () => {
|
|
18
|
+
expect(redactUrl('https://x.test/api?token=abc&ok=1&password=secret'))
|
|
19
|
+
.toBe('https://x.test/api?token=[REDACTED]&ok=1&password=[REDACTED]');
|
|
20
|
+
});
|
|
21
|
+
it('redacts password and token fields recursively', () => {
|
|
22
|
+
expect(redactValue({
|
|
23
|
+
user: 'alice',
|
|
24
|
+
password: 'secret',
|
|
25
|
+
nested: { access_token: 'abc123456789', value: 'safe' },
|
|
26
|
+
})).toEqual({
|
|
27
|
+
user: 'alice',
|
|
28
|
+
password: '[REDACTED]',
|
|
29
|
+
nested: { access_token: '[REDACTED]', value: 'safe' },
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface TraceRetentionPolicyInput {
|
|
2
|
+
maxAgeDays?: number;
|
|
3
|
+
maxCountPerProfile?: number;
|
|
4
|
+
maxBytesPerProfile?: string | number;
|
|
5
|
+
}
|
|
6
|
+
export interface ResolvedTraceRetentionPolicy {
|
|
7
|
+
maxAgeDays: number;
|
|
8
|
+
maxAgeMs: number;
|
|
9
|
+
maxCountPerProfile: number;
|
|
10
|
+
maxBytesPerProfile: number;
|
|
11
|
+
}
|
|
12
|
+
export interface TraceRetentionPruneResult {
|
|
13
|
+
scanned: number;
|
|
14
|
+
deleted: string[];
|
|
15
|
+
kept: string[];
|
|
16
|
+
totalBytesBefore: number;
|
|
17
|
+
totalBytesAfter: number;
|
|
18
|
+
}
|
|
19
|
+
export declare const DEFAULT_TRACE_RETENTION_POLICY: {
|
|
20
|
+
maxAgeDays: number;
|
|
21
|
+
maxCountPerProfile: number;
|
|
22
|
+
maxBytesPerProfile: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function parseByteSize(value: string | number): number;
|
|
25
|
+
export declare function resolveTraceRetentionPolicy(input?: TraceRetentionPolicyInput): ResolvedTraceRetentionPolicy;
|
|
26
|
+
export declare function traceExpiresAt(createdAt: string, policyInput?: TraceRetentionPolicyInput): string;
|
|
27
|
+
export declare function pruneTraceArtifacts(tracesDir: string, opts?: {
|
|
28
|
+
policy?: TraceRetentionPolicyInput;
|
|
29
|
+
protectedTraceDirs?: string[];
|
|
30
|
+
now?: () => number;
|
|
31
|
+
warn?: (message: string) => void;
|
|
32
|
+
}): TraceRetentionPruneResult;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { log } from '../logger.js';
|
|
4
|
+
export const DEFAULT_TRACE_RETENTION_POLICY = {
|
|
5
|
+
maxAgeDays: 7,
|
|
6
|
+
maxCountPerProfile: 20,
|
|
7
|
+
maxBytesPerProfile: '500MB',
|
|
8
|
+
};
|
|
9
|
+
const BYTES_UNITS = {
|
|
10
|
+
B: 1,
|
|
11
|
+
KB: 1024,
|
|
12
|
+
MB: 1024 ** 2,
|
|
13
|
+
GB: 1024 ** 3,
|
|
14
|
+
};
|
|
15
|
+
export function parseByteSize(value) {
|
|
16
|
+
if (typeof value === 'number') {
|
|
17
|
+
if (!Number.isFinite(value) || value < 0)
|
|
18
|
+
throw new Error(`Invalid byte size: ${value}`);
|
|
19
|
+
return Math.floor(value);
|
|
20
|
+
}
|
|
21
|
+
const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)?$/i);
|
|
22
|
+
if (!match)
|
|
23
|
+
throw new Error(`Invalid byte size: ${value}`);
|
|
24
|
+
const amount = Number(match[1]);
|
|
25
|
+
const unit = (match[2] ?? 'B').toUpperCase();
|
|
26
|
+
return Math.floor(amount * BYTES_UNITS[unit]);
|
|
27
|
+
}
|
|
28
|
+
export function resolveTraceRetentionPolicy(input = {}) {
|
|
29
|
+
const maxAgeDays = input.maxAgeDays ?? DEFAULT_TRACE_RETENTION_POLICY.maxAgeDays;
|
|
30
|
+
const maxCountPerProfile = input.maxCountPerProfile ?? DEFAULT_TRACE_RETENTION_POLICY.maxCountPerProfile;
|
|
31
|
+
if (!Number.isFinite(maxAgeDays) || maxAgeDays < 0)
|
|
32
|
+
throw new Error(`Invalid trace maxAgeDays: ${maxAgeDays}`);
|
|
33
|
+
if (!Number.isInteger(maxCountPerProfile) || maxCountPerProfile < 0) {
|
|
34
|
+
throw new Error(`Invalid trace maxCountPerProfile: ${maxCountPerProfile}`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
maxAgeDays,
|
|
38
|
+
maxAgeMs: maxAgeDays * 24 * 60 * 60 * 1000,
|
|
39
|
+
maxCountPerProfile,
|
|
40
|
+
maxBytesPerProfile: parseByteSize(input.maxBytesPerProfile ?? DEFAULT_TRACE_RETENTION_POLICY.maxBytesPerProfile),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function traceExpiresAt(createdAt, policyInput = {}) {
|
|
44
|
+
const policy = resolveTraceRetentionPolicy(policyInput);
|
|
45
|
+
const createdAtMs = Date.parse(createdAt);
|
|
46
|
+
const base = Number.isFinite(createdAtMs) ? createdAtMs : Date.now();
|
|
47
|
+
return new Date(base + policy.maxAgeMs).toISOString();
|
|
48
|
+
}
|
|
49
|
+
export function pruneTraceArtifacts(tracesDir, opts = {}) {
|
|
50
|
+
const warn = opts.warn ?? ((message) => log.warn(`[trace] ${message}`));
|
|
51
|
+
const policy = resolveTraceRetentionPolicy(opts.policy);
|
|
52
|
+
const now = opts.now ?? Date.now;
|
|
53
|
+
const protectedDirs = new Set((opts.protectedTraceDirs ?? []).map((dir) => path.resolve(dir)));
|
|
54
|
+
const entries = readTraceEntries(tracesDir, protectedDirs, warn);
|
|
55
|
+
const totalBytesBefore = entries.reduce((sum, entry) => sum + entry.sizeBytes, 0);
|
|
56
|
+
const deleted = new Set();
|
|
57
|
+
const sorted = [...entries].sort((a, b) => a.createdAtMs - b.createdAtMs || a.dir.localeCompare(b.dir));
|
|
58
|
+
const cutoff = now() - policy.maxAgeMs;
|
|
59
|
+
for (const entry of sorted) {
|
|
60
|
+
if (!entry.protected && entry.createdAtMs < cutoff)
|
|
61
|
+
deleted.add(entry.dir);
|
|
62
|
+
}
|
|
63
|
+
let remaining = sorted.filter((entry) => !deleted.has(entry.dir));
|
|
64
|
+
while (remaining.length > policy.maxCountPerProfile) {
|
|
65
|
+
const victim = remaining.find((entry) => !entry.protected);
|
|
66
|
+
if (!victim)
|
|
67
|
+
break;
|
|
68
|
+
deleted.add(victim.dir);
|
|
69
|
+
remaining = remaining.filter((entry) => entry.dir !== victim.dir);
|
|
70
|
+
}
|
|
71
|
+
let remainingBytes = remaining.reduce((sum, entry) => sum + entry.sizeBytes, 0);
|
|
72
|
+
while (remainingBytes > policy.maxBytesPerProfile) {
|
|
73
|
+
const victim = remaining.find((entry) => !entry.protected);
|
|
74
|
+
if (!victim)
|
|
75
|
+
break;
|
|
76
|
+
deleted.add(victim.dir);
|
|
77
|
+
remaining = remaining.filter((entry) => entry.dir !== victim.dir);
|
|
78
|
+
remainingBytes -= victim.sizeBytes;
|
|
79
|
+
}
|
|
80
|
+
const deletedDirs = [];
|
|
81
|
+
for (const dir of sorted.map((entry) => entry.dir).filter((dir) => deleted.has(dir))) {
|
|
82
|
+
try {
|
|
83
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
84
|
+
deletedDirs.push(dir);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
warn(`Failed to prune trace artifact ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const keptEntries = entries.filter((entry) => !deletedDirs.includes(entry.dir));
|
|
91
|
+
return {
|
|
92
|
+
scanned: entries.length,
|
|
93
|
+
deleted: deletedDirs,
|
|
94
|
+
kept: keptEntries.map((entry) => entry.dir),
|
|
95
|
+
totalBytesBefore,
|
|
96
|
+
totalBytesAfter: keptEntries.reduce((sum, entry) => sum + entry.sizeBytes, 0),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function readTraceEntries(tracesDir, protectedDirs, warn) {
|
|
100
|
+
let names;
|
|
101
|
+
try {
|
|
102
|
+
names = fs.readdirSync(tracesDir);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (isEnoent(err))
|
|
106
|
+
return [];
|
|
107
|
+
warn(`Failed to list trace artifacts in ${tracesDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
const entries = [];
|
|
111
|
+
for (const name of names) {
|
|
112
|
+
const dir = path.join(tracesDir, name);
|
|
113
|
+
try {
|
|
114
|
+
const stat = fs.statSync(dir);
|
|
115
|
+
if (!stat.isDirectory())
|
|
116
|
+
continue;
|
|
117
|
+
entries.push({
|
|
118
|
+
dir,
|
|
119
|
+
createdAtMs: readCreatedAtMs(dir, stat.mtimeMs),
|
|
120
|
+
sizeBytes: directorySize(dir),
|
|
121
|
+
protected: protectedDirs.has(path.resolve(dir)),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (!isEnoent(err)) {
|
|
126
|
+
warn(`Failed to inspect trace artifact ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return entries;
|
|
131
|
+
}
|
|
132
|
+
function readCreatedAtMs(dir, fallbackMs) {
|
|
133
|
+
try {
|
|
134
|
+
const receipt = JSON.parse(fs.readFileSync(path.join(dir, 'receipt.json'), 'utf-8'));
|
|
135
|
+
if (typeof receipt.createdAt === 'string') {
|
|
136
|
+
const parsed = Date.parse(receipt.createdAt);
|
|
137
|
+
if (Number.isFinite(parsed))
|
|
138
|
+
return parsed;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Older or hand-edited trace directories may not have a receipt.
|
|
143
|
+
}
|
|
144
|
+
return fallbackMs;
|
|
145
|
+
}
|
|
146
|
+
function directorySize(dir) {
|
|
147
|
+
let total = 0;
|
|
148
|
+
for (const name of fs.readdirSync(dir)) {
|
|
149
|
+
const item = path.join(dir, name);
|
|
150
|
+
const stat = fs.lstatSync(item);
|
|
151
|
+
if (stat.isDirectory())
|
|
152
|
+
total += directorySize(item);
|
|
153
|
+
else
|
|
154
|
+
total += stat.size;
|
|
155
|
+
}
|
|
156
|
+
return total;
|
|
157
|
+
}
|
|
158
|
+
function isEnoent(err) {
|
|
159
|
+
return typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT';
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { parseByteSize, pruneTraceArtifacts } from './retention.js';
|
|
6
|
+
describe('trace artifact retention', () => {
|
|
7
|
+
let tmpDir;
|
|
8
|
+
let tracesDir;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-trace-retention-'));
|
|
11
|
+
tracesDir = path.join(tmpDir, 'profiles', 'default', 'traces');
|
|
12
|
+
fs.mkdirSync(tracesDir, { recursive: true });
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
it('parses byte budgets with units', () => {
|
|
18
|
+
expect(parseByteSize(128)).toBe(128);
|
|
19
|
+
expect(parseByteSize('128')).toBe(128);
|
|
20
|
+
expect(parseByteSize('2KB')).toBe(2 * 1024);
|
|
21
|
+
expect(parseByteSize('1.5MB')).toBe(Math.floor(1.5 * 1024 * 1024));
|
|
22
|
+
expect(parseByteSize('1 GB')).toBe(1024 ** 3);
|
|
23
|
+
expect(() => parseByteSize('many')).toThrow('Invalid byte size');
|
|
24
|
+
});
|
|
25
|
+
it('prunes traces older than the age budget', () => {
|
|
26
|
+
const now = Date.parse('2026-05-03T00:00:00.000Z');
|
|
27
|
+
const old = createTraceDir('old', '2026-04-24T00:00:00.000Z');
|
|
28
|
+
const recent = createTraceDir('recent', '2026-05-02T00:00:00.000Z');
|
|
29
|
+
const result = pruneTraceArtifacts(tracesDir, {
|
|
30
|
+
now: () => now,
|
|
31
|
+
policy: { maxAgeDays: 7, maxCountPerProfile: 20, maxBytesPerProfile: '500MB' },
|
|
32
|
+
warn: vi.fn(),
|
|
33
|
+
});
|
|
34
|
+
expect(result.deleted).toEqual([old]);
|
|
35
|
+
expect(fs.existsSync(old)).toBe(false);
|
|
36
|
+
expect(fs.existsSync(recent)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it('prunes oldest traces over the count budget', () => {
|
|
39
|
+
const oldest = createTraceDir('oldest', '2026-05-01T00:00:00.000Z');
|
|
40
|
+
const middle = createTraceDir('middle', '2026-05-02T00:00:00.000Z');
|
|
41
|
+
const newest = createTraceDir('newest', '2026-05-03T00:00:00.000Z');
|
|
42
|
+
const result = pruneTraceArtifacts(tracesDir, {
|
|
43
|
+
now: () => Date.parse('2026-05-03T12:00:00.000Z'),
|
|
44
|
+
policy: { maxAgeDays: 30, maxCountPerProfile: 2, maxBytesPerProfile: '500MB' },
|
|
45
|
+
warn: vi.fn(),
|
|
46
|
+
});
|
|
47
|
+
expect(result.deleted).toEqual([oldest]);
|
|
48
|
+
expect(fs.existsSync(oldest)).toBe(false);
|
|
49
|
+
expect(fs.existsSync(middle)).toBe(true);
|
|
50
|
+
expect(fs.existsSync(newest)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it('prunes oldest traces over the byte budget', () => {
|
|
53
|
+
const oldest = createTraceDir('oldest', '2026-05-01T00:00:00.000Z', 512);
|
|
54
|
+
const middle = createTraceDir('middle', '2026-05-02T00:00:00.000Z', 64);
|
|
55
|
+
const newest = createTraceDir('newest', '2026-05-03T00:00:00.000Z', 64);
|
|
56
|
+
const budget = directorySize(middle) + directorySize(newest) + 1;
|
|
57
|
+
const result = pruneTraceArtifacts(tracesDir, {
|
|
58
|
+
now: () => Date.parse('2026-05-03T12:00:00.000Z'),
|
|
59
|
+
policy: { maxAgeDays: 30, maxCountPerProfile: 20, maxBytesPerProfile: budget },
|
|
60
|
+
warn: vi.fn(),
|
|
61
|
+
});
|
|
62
|
+
expect(result.deleted).toEqual([oldest]);
|
|
63
|
+
expect(fs.existsSync(oldest)).toBe(false);
|
|
64
|
+
expect(fs.existsSync(middle)).toBe(true);
|
|
65
|
+
expect(fs.existsSync(newest)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
it('falls back to directory mtime when receipt is missing', () => {
|
|
68
|
+
const now = Date.parse('2026-05-03T00:00:00.000Z');
|
|
69
|
+
const old = createTraceDir('old-no-receipt', undefined);
|
|
70
|
+
const recent = createTraceDir('recent', '2026-05-02T00:00:00.000Z');
|
|
71
|
+
const oldDate = new Date('2026-04-20T00:00:00.000Z');
|
|
72
|
+
fs.utimesSync(old, oldDate, oldDate);
|
|
73
|
+
const result = pruneTraceArtifacts(tracesDir, {
|
|
74
|
+
now: () => now,
|
|
75
|
+
policy: { maxAgeDays: 7, maxCountPerProfile: 20, maxBytesPerProfile: '500MB' },
|
|
76
|
+
warn: vi.fn(),
|
|
77
|
+
});
|
|
78
|
+
expect(result.deleted).toEqual([old]);
|
|
79
|
+
expect(fs.existsSync(old)).toBe(false);
|
|
80
|
+
expect(fs.existsSync(recent)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it('does not delete the protected trace exported by the current run', () => {
|
|
83
|
+
const old = createTraceDir('oldest', '2026-05-01T00:00:00.000Z');
|
|
84
|
+
const current = createTraceDir('current', '2026-04-01T00:00:00.000Z', 1024);
|
|
85
|
+
const result = pruneTraceArtifacts(tracesDir, {
|
|
86
|
+
now: () => Date.parse('2026-05-03T12:00:00.000Z'),
|
|
87
|
+
policy: { maxAgeDays: 0, maxCountPerProfile: 0, maxBytesPerProfile: 1 },
|
|
88
|
+
protectedTraceDirs: [current],
|
|
89
|
+
warn: vi.fn(),
|
|
90
|
+
});
|
|
91
|
+
expect(result.deleted).toEqual([old]);
|
|
92
|
+
expect(fs.existsSync(old)).toBe(false);
|
|
93
|
+
expect(fs.existsSync(current)).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
function createTraceDir(name, createdAt, payloadBytes = 8) {
|
|
96
|
+
const dir = path.join(tracesDir, name);
|
|
97
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
98
|
+
fs.writeFileSync(path.join(dir, 'trace.jsonl'), 'x'.repeat(payloadBytes), 'utf-8');
|
|
99
|
+
if (createdAt) {
|
|
100
|
+
fs.writeFileSync(path.join(dir, 'receipt.json'), JSON.stringify({ createdAt }, null, 2), 'utf-8');
|
|
101
|
+
const date = new Date(createdAt);
|
|
102
|
+
fs.utimesSync(dir, date, date);
|
|
103
|
+
}
|
|
104
|
+
return dir;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
function directorySize(dir) {
|
|
108
|
+
let total = 0;
|
|
109
|
+
for (const name of fs.readdirSync(dir)) {
|
|
110
|
+
const item = path.join(dir, name);
|
|
111
|
+
const stat = fs.lstatSync(item);
|
|
112
|
+
if (stat.isDirectory())
|
|
113
|
+
total += directorySize(item);
|
|
114
|
+
else
|
|
115
|
+
total += stat.size;
|
|
116
|
+
}
|
|
117
|
+
return total;
|
|
118
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface RingBufferOptions {
|
|
2
|
+
maxAgeMs?: number;
|
|
3
|
+
maxItems?: number;
|
|
4
|
+
now?: () => number;
|
|
5
|
+
}
|
|
6
|
+
export declare class RingBuffer<T extends {
|
|
7
|
+
ts: number;
|
|
8
|
+
}> {
|
|
9
|
+
private readonly maxAgeMs;
|
|
10
|
+
private readonly maxItems;
|
|
11
|
+
private readonly now;
|
|
12
|
+
private items;
|
|
13
|
+
constructor(opts?: RingBufferOptions);
|
|
14
|
+
push(item: T): void;
|
|
15
|
+
values(opts?: {
|
|
16
|
+
since?: number;
|
|
17
|
+
until?: number;
|
|
18
|
+
}): T[];
|
|
19
|
+
clear(): void;
|
|
20
|
+
get size(): number;
|
|
21
|
+
private prune;
|
|
22
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export class RingBuffer {
|
|
2
|
+
maxAgeMs;
|
|
3
|
+
maxItems;
|
|
4
|
+
now;
|
|
5
|
+
items = [];
|
|
6
|
+
constructor(opts = {}) {
|
|
7
|
+
this.maxAgeMs = opts.maxAgeMs ?? 120_000;
|
|
8
|
+
this.maxItems = opts.maxItems ?? 1_000;
|
|
9
|
+
this.now = opts.now ?? Date.now;
|
|
10
|
+
}
|
|
11
|
+
push(item) {
|
|
12
|
+
this.items.push(item);
|
|
13
|
+
this.prune();
|
|
14
|
+
}
|
|
15
|
+
values(opts = {}) {
|
|
16
|
+
this.prune();
|
|
17
|
+
return this.items.filter((item) => {
|
|
18
|
+
if (opts.since !== undefined && item.ts < opts.since)
|
|
19
|
+
return false;
|
|
20
|
+
if (opts.until !== undefined && item.ts > opts.until)
|
|
21
|
+
return false;
|
|
22
|
+
return true;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
clear() {
|
|
26
|
+
this.items = [];
|
|
27
|
+
}
|
|
28
|
+
get size() {
|
|
29
|
+
this.prune();
|
|
30
|
+
return this.items.length;
|
|
31
|
+
}
|
|
32
|
+
prune() {
|
|
33
|
+
const minTs = this.now() - this.maxAgeMs;
|
|
34
|
+
if (this.items.length > this.maxItems) {
|
|
35
|
+
this.items = this.items.slice(this.items.length - this.maxItems);
|
|
36
|
+
}
|
|
37
|
+
if (this.maxAgeMs > 0) {
|
|
38
|
+
const firstKept = this.items.findIndex((item) => item.ts >= minTs);
|
|
39
|
+
if (firstKept > 0)
|
|
40
|
+
this.items = this.items.slice(firstKept);
|
|
41
|
+
else if (firstKept === -1)
|
|
42
|
+
this.items = [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { RingBuffer } from './ring-buffer.js';
|
|
3
|
+
describe('RingBuffer', () => {
|
|
4
|
+
it('keeps items in event order and filters by time window', () => {
|
|
5
|
+
let now = 10_000;
|
|
6
|
+
const buffer = new RingBuffer({ maxAgeMs: 5_000, now: () => now });
|
|
7
|
+
buffer.push({ ts: 4_000, value: 'old' });
|
|
8
|
+
buffer.push({ ts: 7_000, value: 'kept' });
|
|
9
|
+
buffer.push({ ts: 9_000, value: 'new' });
|
|
10
|
+
expect(buffer.values().map((item) => item.value)).toEqual(['kept', 'new']);
|
|
11
|
+
expect(buffer.values({ since: 8_000 }).map((item) => item.value)).toEqual(['new']);
|
|
12
|
+
now = 13_000;
|
|
13
|
+
expect(buffer.values().map((item) => item.value)).toEqual(['new']);
|
|
14
|
+
});
|
|
15
|
+
it('caps by item count', () => {
|
|
16
|
+
const buffer = new RingBuffer({ maxItems: 2, maxAgeMs: 100_000, now: () => 10 });
|
|
17
|
+
buffer.push({ ts: 1, value: 1 });
|
|
18
|
+
buffer.push({ ts: 2, value: 2 });
|
|
19
|
+
buffer.push({ ts: 3, value: 3 });
|
|
20
|
+
expect(buffer.values().map((item) => item.value)).toEqual([2, 3]);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ObservationEvent, ObservationEventInput, ObservationScope, ObservationStream } from './events.js';
|
|
2
|
+
export declare const DEFAULT_OBSERVATION_WINDOW_MS = 120000;
|
|
3
|
+
export interface ObservationSessionOptions {
|
|
4
|
+
id?: string;
|
|
5
|
+
scope: ObservationScope;
|
|
6
|
+
windowMs?: number;
|
|
7
|
+
maxEventsPerStream?: number;
|
|
8
|
+
now?: () => number;
|
|
9
|
+
}
|
|
10
|
+
export declare class ObservationSession {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly scope: ObservationScope;
|
|
13
|
+
readonly startedAt: number;
|
|
14
|
+
private readonly now;
|
|
15
|
+
private counter;
|
|
16
|
+
private readonly buffers;
|
|
17
|
+
constructor(opts: ObservationSessionOptions);
|
|
18
|
+
record(input: ObservationEventInput): ObservationEvent;
|
|
19
|
+
events(opts?: {
|
|
20
|
+
stream?: ObservationStream;
|
|
21
|
+
since?: number;
|
|
22
|
+
until?: number;
|
|
23
|
+
}): ObservationEvent[];
|
|
24
|
+
}
|
|
25
|
+
export declare function createTraceId(now?: number | (() => number)): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { RingBuffer } from './ring-buffer.js';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
export const DEFAULT_OBSERVATION_WINDOW_MS = 120_000;
|
|
4
|
+
export class ObservationSession {
|
|
5
|
+
id;
|
|
6
|
+
scope;
|
|
7
|
+
startedAt;
|
|
8
|
+
now;
|
|
9
|
+
counter = 0;
|
|
10
|
+
buffers;
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
this.id = opts.id ?? createTraceId(opts.now?.() ?? Date.now());
|
|
13
|
+
this.scope = opts.scope;
|
|
14
|
+
this.now = opts.now ?? Date.now;
|
|
15
|
+
this.startedAt = this.now();
|
|
16
|
+
const bufferOpts = {
|
|
17
|
+
maxAgeMs: opts.windowMs ?? DEFAULT_OBSERVATION_WINDOW_MS,
|
|
18
|
+
maxItems: opts.maxEventsPerStream ?? 1_000,
|
|
19
|
+
now: this.now,
|
|
20
|
+
};
|
|
21
|
+
this.buffers = {
|
|
22
|
+
action: new RingBuffer(bufferOpts),
|
|
23
|
+
network: new RingBuffer(bufferOpts),
|
|
24
|
+
console: new RingBuffer(bufferOpts),
|
|
25
|
+
screenshot: new RingBuffer({ ...bufferOpts, maxItems: Math.min(opts.maxEventsPerStream ?? 50, 50) }),
|
|
26
|
+
state: new RingBuffer({ ...bufferOpts, maxItems: Math.min(opts.maxEventsPerStream ?? 50, 50) }),
|
|
27
|
+
error: new RingBuffer(bufferOpts),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
record(input) {
|
|
31
|
+
const event = {
|
|
32
|
+
...input,
|
|
33
|
+
id: input.id ?? `${this.id}-${++this.counter}`,
|
|
34
|
+
ts: input.ts ?? this.now(),
|
|
35
|
+
};
|
|
36
|
+
this.buffers[event.stream].push(event);
|
|
37
|
+
return event;
|
|
38
|
+
}
|
|
39
|
+
events(opts = {}) {
|
|
40
|
+
const streams = opts.stream ? [opts.stream] : Object.keys(this.buffers);
|
|
41
|
+
return streams
|
|
42
|
+
.flatMap((stream) => this.buffers[stream].values({ since: opts.since, until: opts.until }))
|
|
43
|
+
.sort((a, b) => a.ts - b.ts || a.id.localeCompare(b.id));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function createTraceId(now = Date.now) {
|
|
47
|
+
const ts = typeof now === 'function' ? now() : now;
|
|
48
|
+
const rand = randomBytes(4).toString('hex');
|
|
49
|
+
return `${new Date(ts).toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}-${rand}`;
|
|
50
|
+
}
|
|
@@ -9,6 +9,7 @@ function createMockPage(overrides = {}) {
|
|
|
9
9
|
return {
|
|
10
10
|
goto: vi.fn(),
|
|
11
11
|
evaluate: vi.fn().mockResolvedValue(null),
|
|
12
|
+
fetchJson: vi.fn().mockResolvedValue(null),
|
|
12
13
|
getCookies: vi.fn().mockResolvedValue([]),
|
|
13
14
|
snapshot: vi.fn().mockResolvedValue(''),
|
|
14
15
|
click: vi.fn(),
|
|
@@ -25,27 +25,7 @@ async function fetchSingle(page, url, method, queryParams, headers, args, data)
|
|
|
25
25
|
}
|
|
26
26
|
return resp.json();
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
const urlJs = JSON.stringify(finalUrl);
|
|
30
|
-
const methodJs = JSON.stringify(method.toUpperCase());
|
|
31
|
-
// Return error status instead of throwing inside evaluate to avoid CDP wrapper
|
|
32
|
-
// rewriting the message (CDP prepends "Evaluate error: " to thrown errors).
|
|
33
|
-
const result = await page.evaluate(`
|
|
34
|
-
async () => {
|
|
35
|
-
const resp = await fetch(${urlJs}, {
|
|
36
|
-
method: ${methodJs}, headers: ${headersJs}, credentials: "include"
|
|
37
|
-
});
|
|
38
|
-
if (!resp.ok) {
|
|
39
|
-
return { __httpError: resp.status, statusText: resp.statusText };
|
|
40
|
-
}
|
|
41
|
-
return await resp.json();
|
|
42
|
-
}
|
|
43
|
-
`);
|
|
44
|
-
if (result && typeof result === 'object' && '__httpError' in result) {
|
|
45
|
-
const { __httpError: status, statusText } = result;
|
|
46
|
-
throw new CliError('FETCH_ERROR', `HTTP ${status} ${statusText} from ${finalUrl}`);
|
|
47
|
-
}
|
|
48
|
-
return result;
|
|
28
|
+
return page.fetchJson(finalUrl, { method: method.toUpperCase(), headers: renderedHeaders });
|
|
49
29
|
}
|
|
50
30
|
/**
|
|
51
31
|
* Batch fetch: send all URLs into the browser as a single evaluate() call.
|
|
@@ -22,25 +22,19 @@ describe('stepFetch', () => {
|
|
|
22
22
|
expect(err.message).toBe('HTTP 429 Too Many Requests from https://api.example.com/items');
|
|
23
23
|
expect(jsonMock).not.toHaveBeenCalled();
|
|
24
24
|
});
|
|
25
|
-
// W1 + W3: browser single fetch
|
|
25
|
+
// W1 + W3: browser single fetch delegates to page.fetchJson, which owns browser-context fetch errors
|
|
26
26
|
it('throws CliError with FETCH_ERROR code on non-ok responses inside the browser session', async () => {
|
|
27
|
-
const jsonMock = vi.fn().mockResolvedValue({ error: 'auth required' });
|
|
28
|
-
const fetchMock = vi.fn().mockResolvedValue({
|
|
29
|
-
ok: false,
|
|
30
|
-
status: 401,
|
|
31
|
-
statusText: 'Unauthorized',
|
|
32
|
-
json: jsonMock,
|
|
33
|
-
});
|
|
34
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
35
|
-
// Simulate real CDP behavior: evaluate returns a value, errors are thrown outside
|
|
36
27
|
const page = {
|
|
37
|
-
|
|
28
|
+
fetchJson: vi.fn().mockRejectedValue(new CliError('FETCH_ERROR', 'HTTP 401 Unauthorized from https://api.example.com/items')),
|
|
38
29
|
};
|
|
39
30
|
const err = await stepFetch(page, { url: 'https://api.example.com/items' }, null, {}).catch((e) => e);
|
|
40
31
|
expect(err).toBeInstanceOf(CliError);
|
|
41
32
|
expect(err.code).toBe('FETCH_ERROR');
|
|
42
33
|
expect(err.message).toBe('HTTP 401 Unauthorized from https://api.example.com/items');
|
|
43
|
-
expect(
|
|
34
|
+
expect(page.fetchJson).toHaveBeenCalledWith('https://api.example.com/items', {
|
|
35
|
+
method: 'GET',
|
|
36
|
+
headers: {},
|
|
37
|
+
});
|
|
44
38
|
});
|
|
45
39
|
it('returns per-item HTTP errors for batch fetches without a browser session', async () => {
|
|
46
40
|
const jsonMock = vi.fn().mockResolvedValue({ error: 'upstream unavailable' });
|
|
@@ -93,7 +93,7 @@ cli({
|
|
|
93
93
|
{ name: 'name', positional: true, required: true, help: 'Name to greet' },
|
|
94
94
|
],
|
|
95
95
|
columns: ['greeting'],
|
|
96
|
-
func: async (
|
|
96
|
+
func: async (kwargs) => [{ greeting: \`Hello, \${String(kwargs.name ?? 'World')}!\` }],
|
|
97
97
|
});
|
|
98
98
|
`;
|
|
99
99
|
writeFile(targetDir, 'greet.ts', tsContent);
|
|
@@ -59,7 +59,7 @@ describe('createPluginScaffold', () => {
|
|
|
59
59
|
expect(tsSample).toContain(`import { cli, Strategy } from '@jackwener/opencli/registry';`);
|
|
60
60
|
expect(tsSample).toContain(`strategy: Strategy.PUBLIC`);
|
|
61
61
|
expect(tsSample).toContain(`help: 'Name to greet'`);
|
|
62
|
-
expect(tsSample).toContain(`func: async (
|
|
62
|
+
expect(tsSample).toContain(`func: async (kwargs)`);
|
|
63
63
|
expect(tsSample).not.toContain('async run(');
|
|
64
64
|
});
|
|
65
65
|
it('documents a supported local install flow', () => {
|