@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
package/dist/src/diagnostic.js
DELETED
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Structured diagnostic output for AI-driven adapter repair.
|
|
3
|
-
*
|
|
4
|
-
* When OPENCLI_DIAGNOSTIC=1, failed commands emit a JSON RepairContext to stderr
|
|
5
|
-
* containing the error, adapter source, and browser state (DOM snapshot, network
|
|
6
|
-
* requests, console errors). AI Agents consume this to diagnose and fix adapters.
|
|
7
|
-
*
|
|
8
|
-
* Safety boundaries:
|
|
9
|
-
* - Sensitive headers/cookies are redacted before emission
|
|
10
|
-
* - Individual fields are capped to prevent unbounded output
|
|
11
|
-
* - Network response bodies from authenticated requests are stripped
|
|
12
|
-
* - Total output is capped to MAX_DIAGNOSTIC_BYTES
|
|
13
|
-
*/
|
|
14
|
-
import * as fs from 'node:fs';
|
|
15
|
-
import { CliError, getErrorMessage } from './errors.js';
|
|
16
|
-
import { fullName } from './registry.js';
|
|
17
|
-
// ── Size budgets ─────────────────────────────────────────────────────────────
|
|
18
|
-
/** Maximum bytes for the entire diagnostic JSON output. */
|
|
19
|
-
export const MAX_DIAGNOSTIC_BYTES = 256 * 1024; // 256 KB
|
|
20
|
-
/** Maximum characters for DOM snapshot. */
|
|
21
|
-
const MAX_SNAPSHOT_CHARS = 100_000;
|
|
22
|
-
/** Maximum characters for adapter source. */
|
|
23
|
-
const MAX_SOURCE_CHARS = 50_000;
|
|
24
|
-
/** Maximum number of network requests to include. */
|
|
25
|
-
const MAX_NETWORK_REQUESTS = 50;
|
|
26
|
-
/** Maximum number of captured interceptor payloads to include. */
|
|
27
|
-
const MAX_CAPTURED_PAYLOADS = 20;
|
|
28
|
-
/** Maximum characters for a single network request body. */
|
|
29
|
-
const MAX_REQUEST_BODY_CHARS = 4_000;
|
|
30
|
-
/** Maximum characters for error stack trace. */
|
|
31
|
-
const MAX_STACK_CHARS = 5_000;
|
|
32
|
-
/** Maximum nesting depth for arbitrary captured payloads. */
|
|
33
|
-
const MAX_CAPTURED_DEPTH = 4;
|
|
34
|
-
/** Maximum object keys or array items to keep per nesting level. */
|
|
35
|
-
const MAX_CAPTURED_CHILDREN = 20;
|
|
36
|
-
// ── Sensitive data patterns ──────────────────────────────────────────────────
|
|
37
|
-
const SENSITIVE_HEADERS = new Set([
|
|
38
|
-
'authorization',
|
|
39
|
-
'cookie',
|
|
40
|
-
'set-cookie',
|
|
41
|
-
'x-csrf-token',
|
|
42
|
-
'x-xsrf-token',
|
|
43
|
-
'proxy-authorization',
|
|
44
|
-
'x-api-key',
|
|
45
|
-
'x-auth-token',
|
|
46
|
-
]);
|
|
47
|
-
const SENSITIVE_URL_PARAMS = /([?&])(token|key|secret|password|auth|access_token|api_key|session_id|csrf)=[^&]*/gi;
|
|
48
|
-
/** Patterns that match inline secrets in free-text strings (error messages, stack traces, console output, DOM). */
|
|
49
|
-
const SENSITIVE_TEXT_PATTERNS = [
|
|
50
|
-
// Bearer tokens
|
|
51
|
-
{ pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, replacement: 'Bearer [REDACTED]' },
|
|
52
|
-
// Generic "token=...", "key=...", etc. in non-URL text
|
|
53
|
-
{ pattern: /(token|secret|password|api_key|apikey|access_token|session_id)[=:]\s*['"]?[A-Za-z0-9\-._~+/]{8,}['"]?/gi, replacement: '$1=[REDACTED]' },
|
|
54
|
-
// Cookie header values (key=value pairs)
|
|
55
|
-
{ pattern: /(cookie[=:]\s*)[^\n;]{10,}/gi, replacement: '$1[REDACTED]' },
|
|
56
|
-
// JWT-like tokens (three base64 segments separated by dots)
|
|
57
|
-
{ pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, replacement: '[REDACTED_JWT]' },
|
|
58
|
-
];
|
|
59
|
-
// ── Redaction helpers ────────────────────────────────────────────────────────
|
|
60
|
-
/** Truncate a string to maxLen, appending a truncation marker. */
|
|
61
|
-
export function truncate(str, maxLen) {
|
|
62
|
-
if (str.length <= maxLen)
|
|
63
|
-
return str;
|
|
64
|
-
return str.slice(0, maxLen) + `\n...[truncated, ${str.length - maxLen} chars omitted]`;
|
|
65
|
-
}
|
|
66
|
-
/** Redact sensitive query parameters from a URL. */
|
|
67
|
-
export function redactUrl(url) {
|
|
68
|
-
return url.replace(SENSITIVE_URL_PARAMS, '$1$2=[REDACTED]');
|
|
69
|
-
}
|
|
70
|
-
/** Redact inline secrets from free-text strings (error messages, stack traces, console output, DOM). */
|
|
71
|
-
export function redactText(text) {
|
|
72
|
-
let result = text;
|
|
73
|
-
for (const { pattern, replacement } of SENSITIVE_TEXT_PATTERNS) {
|
|
74
|
-
// Reset lastIndex for global regexps
|
|
75
|
-
pattern.lastIndex = 0;
|
|
76
|
-
result = result.replace(pattern, replacement);
|
|
77
|
-
}
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
/** Redact sensitive headers from a headers object. */
|
|
81
|
-
function redactHeaders(headers) {
|
|
82
|
-
if (!headers || typeof headers !== 'object')
|
|
83
|
-
return headers;
|
|
84
|
-
const result = {};
|
|
85
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
86
|
-
result[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? '[REDACTED]' : value;
|
|
87
|
-
}
|
|
88
|
-
return result;
|
|
89
|
-
}
|
|
90
|
-
/** Recursively sanitize arbitrary captured response content for diagnostic output. */
|
|
91
|
-
function sanitizeCapturedValue(value, depth = 0) {
|
|
92
|
-
if (typeof value === 'string') {
|
|
93
|
-
return redactText(truncate(value, MAX_REQUEST_BODY_CHARS));
|
|
94
|
-
}
|
|
95
|
-
if (value === null || typeof value === 'number' || typeof value === 'boolean') {
|
|
96
|
-
return value;
|
|
97
|
-
}
|
|
98
|
-
if (depth >= MAX_CAPTURED_DEPTH) {
|
|
99
|
-
return '[truncated: max depth reached]';
|
|
100
|
-
}
|
|
101
|
-
if (Array.isArray(value)) {
|
|
102
|
-
const items = value
|
|
103
|
-
.slice(0, MAX_CAPTURED_CHILDREN)
|
|
104
|
-
.map(item => sanitizeCapturedValue(item, depth + 1));
|
|
105
|
-
if (value.length > MAX_CAPTURED_CHILDREN) {
|
|
106
|
-
items.push(`[truncated, ${value.length - MAX_CAPTURED_CHILDREN} items omitted]`);
|
|
107
|
-
}
|
|
108
|
-
return items;
|
|
109
|
-
}
|
|
110
|
-
if (!value || typeof value !== 'object') {
|
|
111
|
-
return value;
|
|
112
|
-
}
|
|
113
|
-
const entries = Object.entries(value);
|
|
114
|
-
const result = {};
|
|
115
|
-
for (const [key, child] of entries.slice(0, MAX_CAPTURED_CHILDREN)) {
|
|
116
|
-
result[key] = sanitizeCapturedValue(child, depth + 1);
|
|
117
|
-
}
|
|
118
|
-
if (entries.length > MAX_CAPTURED_CHILDREN) {
|
|
119
|
-
result.__truncated__ = `[${entries.length - MAX_CAPTURED_CHILDREN} fields omitted]`;
|
|
120
|
-
}
|
|
121
|
-
return result;
|
|
122
|
-
}
|
|
123
|
-
/** Redact sensitive data from a single network request entry. */
|
|
124
|
-
function redactNetworkRequest(req) {
|
|
125
|
-
if (!req || typeof req !== 'object')
|
|
126
|
-
return req;
|
|
127
|
-
const r = req;
|
|
128
|
-
const redacted = { ...r };
|
|
129
|
-
// Redact URL
|
|
130
|
-
if (typeof redacted.url === 'string') {
|
|
131
|
-
redacted.url = redactUrl(redacted.url);
|
|
132
|
-
}
|
|
133
|
-
// Redact headers
|
|
134
|
-
if (redacted.headers && typeof redacted.headers === 'object') {
|
|
135
|
-
redacted.headers = redactHeaders(redacted.headers);
|
|
136
|
-
}
|
|
137
|
-
if (redacted.requestHeaders && typeof redacted.requestHeaders === 'object') {
|
|
138
|
-
redacted.requestHeaders = redactHeaders(redacted.requestHeaders);
|
|
139
|
-
}
|
|
140
|
-
if (redacted.responseHeaders && typeof redacted.responseHeaders === 'object') {
|
|
141
|
-
redacted.responseHeaders = redactHeaders(redacted.responseHeaders);
|
|
142
|
-
}
|
|
143
|
-
// Redact and truncate response body
|
|
144
|
-
if (typeof redacted.body === 'string') {
|
|
145
|
-
redacted.body = redactText(truncate(redacted.body, MAX_REQUEST_BODY_CHARS));
|
|
146
|
-
}
|
|
147
|
-
if ('responseBody' in redacted) {
|
|
148
|
-
redacted.responseBody = sanitizeCapturedValue(redacted.responseBody);
|
|
149
|
-
}
|
|
150
|
-
if ('responsePreview' in redacted) {
|
|
151
|
-
redacted.responsePreview = sanitizeCapturedValue(redacted.responsePreview);
|
|
152
|
-
}
|
|
153
|
-
return redacted;
|
|
154
|
-
}
|
|
155
|
-
// ── Timeout helper ───────────────────────────────────────────────────────────
|
|
156
|
-
/** Timeout for page state collection (prevents hang when CDP connection is stuck). */
|
|
157
|
-
const PAGE_STATE_TIMEOUT_MS = 5_000;
|
|
158
|
-
function withTimeout(promise, ms, fallback) {
|
|
159
|
-
return Promise.race([
|
|
160
|
-
promise,
|
|
161
|
-
new Promise(resolve => setTimeout(() => resolve(fallback), ms)),
|
|
162
|
-
]);
|
|
163
|
-
}
|
|
164
|
-
// ── Source path resolution ───────────────────────────────────────────────────
|
|
165
|
-
/**
|
|
166
|
-
* Resolve the editable source file path for an adapter.
|
|
167
|
-
*
|
|
168
|
-
* Priority:
|
|
169
|
-
* 1. cmd.source (set for FS-scanned JS and manifest lazy-loaded JS)
|
|
170
|
-
* 2. cmd._modulePath (set for manifest lazy-loaded JS)
|
|
171
|
-
*
|
|
172
|
-
* Skip manifest: prefixed pseudo-paths (YAML commands inlined in manifest).
|
|
173
|
-
*/
|
|
174
|
-
export function resolveAdapterSourcePath(cmd) {
|
|
175
|
-
const candidates = [];
|
|
176
|
-
// cmd.source may be a real file path or 'manifest:site/name'
|
|
177
|
-
if (cmd.source && !cmd.source.startsWith('manifest:')) {
|
|
178
|
-
candidates.push(cmd.source);
|
|
179
|
-
}
|
|
180
|
-
if (cmd._modulePath) {
|
|
181
|
-
candidates.push(cmd._modulePath);
|
|
182
|
-
}
|
|
183
|
-
for (const candidate of candidates) {
|
|
184
|
-
if (fs.existsSync(candidate))
|
|
185
|
-
return candidate;
|
|
186
|
-
}
|
|
187
|
-
return candidates[0]; // Return best guess even if file doesn't exist
|
|
188
|
-
}
|
|
189
|
-
// ── Diagnostic collection ────────────────────────────────────────────────────
|
|
190
|
-
/** Whether diagnostic mode is enabled. */
|
|
191
|
-
export function isDiagnosticEnabled() {
|
|
192
|
-
return process.env.OPENCLI_DIAGNOSTIC === '1';
|
|
193
|
-
}
|
|
194
|
-
function normalizeInterceptedRequests(interceptedRequests) {
|
|
195
|
-
return interceptedRequests.slice(0, MAX_CAPTURED_PAYLOADS).map(responseBody => ({
|
|
196
|
-
source: 'interceptor',
|
|
197
|
-
responseBody: sanitizeCapturedValue(responseBody),
|
|
198
|
-
}));
|
|
199
|
-
}
|
|
200
|
-
/** Safely collect page diagnostic state with redaction, size caps, and timeout. */
|
|
201
|
-
async function collectPageState(page) {
|
|
202
|
-
const collect = async () => {
|
|
203
|
-
try {
|
|
204
|
-
const [url, snapshot, networkRequests, interceptedRequests, consoleErrors] = await Promise.all([
|
|
205
|
-
page.getCurrentUrl?.().catch(() => null) ?? Promise.resolve(null),
|
|
206
|
-
page.snapshot().catch(() => '(snapshot unavailable)'),
|
|
207
|
-
page.networkRequests().catch(() => []),
|
|
208
|
-
page.getInterceptedRequests().catch(() => []),
|
|
209
|
-
page.consoleMessages('error').catch(() => []),
|
|
210
|
-
]);
|
|
211
|
-
const rawUrl = url ?? 'unknown';
|
|
212
|
-
const capturedResponses = normalizeInterceptedRequests(interceptedRequests);
|
|
213
|
-
return {
|
|
214
|
-
url: redactUrl(rawUrl),
|
|
215
|
-
snapshot: redactText(truncate(snapshot, MAX_SNAPSHOT_CHARS)),
|
|
216
|
-
networkRequests: networkRequests
|
|
217
|
-
.slice(0, MAX_NETWORK_REQUESTS)
|
|
218
|
-
.map(redactNetworkRequest),
|
|
219
|
-
capturedPayloads: capturedResponses,
|
|
220
|
-
consoleErrors: consoleErrors
|
|
221
|
-
.slice(0, 50)
|
|
222
|
-
.map(e => typeof e === 'string' ? redactText(e) : e),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
return undefined;
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
return withTimeout(collect(), PAGE_STATE_TIMEOUT_MS, undefined);
|
|
230
|
-
}
|
|
231
|
-
/** Read adapter source file content with size cap. */
|
|
232
|
-
function readAdapterSource(sourcePath) {
|
|
233
|
-
if (!sourcePath)
|
|
234
|
-
return undefined;
|
|
235
|
-
try {
|
|
236
|
-
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
237
|
-
return truncate(content, MAX_SOURCE_CHARS);
|
|
238
|
-
}
|
|
239
|
-
catch {
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
/** Build a RepairContext from an error, command metadata, and optional page state. */
|
|
244
|
-
export function buildRepairContext(err, cmd, pageState) {
|
|
245
|
-
const isCliError = err instanceof CliError;
|
|
246
|
-
const sourcePath = resolveAdapterSourcePath(cmd);
|
|
247
|
-
return {
|
|
248
|
-
error: {
|
|
249
|
-
code: isCliError ? err.code : 'UNKNOWN',
|
|
250
|
-
message: redactText(getErrorMessage(err)),
|
|
251
|
-
hint: isCliError && err.hint ? redactText(err.hint) : undefined,
|
|
252
|
-
stack: err instanceof Error ? redactText(truncate(err.stack ?? '', MAX_STACK_CHARS)) : undefined,
|
|
253
|
-
},
|
|
254
|
-
adapter: {
|
|
255
|
-
site: cmd.site,
|
|
256
|
-
command: fullName(cmd),
|
|
257
|
-
sourcePath,
|
|
258
|
-
source: readAdapterSource(sourcePath),
|
|
259
|
-
},
|
|
260
|
-
page: pageState,
|
|
261
|
-
timestamp: new Date().toISOString(),
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
/** Collect full diagnostic context including page state (with timeout). */
|
|
265
|
-
export async function collectDiagnostic(err, cmd, page) {
|
|
266
|
-
const pageState = page ? await collectPageState(page) : undefined;
|
|
267
|
-
return buildRepairContext(err, cmd, pageState);
|
|
268
|
-
}
|
|
269
|
-
/** Emit diagnostic JSON to stderr, enforcing total size cap. */
|
|
270
|
-
export function emitDiagnostic(ctx) {
|
|
271
|
-
const marker = '___OPENCLI_DIAGNOSTIC___';
|
|
272
|
-
let json = JSON.stringify(ctx);
|
|
273
|
-
// Enforce total output budget — drop page state (largest section) first if over budget
|
|
274
|
-
if (json.length > MAX_DIAGNOSTIC_BYTES && ctx.page) {
|
|
275
|
-
const trimmed = {
|
|
276
|
-
...ctx,
|
|
277
|
-
page: {
|
|
278
|
-
...ctx.page,
|
|
279
|
-
snapshot: '[omitted: over size budget]',
|
|
280
|
-
networkRequests: [],
|
|
281
|
-
capturedPayloads: [],
|
|
282
|
-
},
|
|
283
|
-
};
|
|
284
|
-
json = JSON.stringify(trimmed);
|
|
285
|
-
}
|
|
286
|
-
// If still over budget, drop page entirely
|
|
287
|
-
if (json.length > MAX_DIAGNOSTIC_BYTES) {
|
|
288
|
-
const minimal = { ...ctx, page: undefined };
|
|
289
|
-
json = JSON.stringify(minimal);
|
|
290
|
-
}
|
|
291
|
-
process.stderr.write(`\n${marker}\n${json}\n${marker}\n`);
|
|
292
|
-
}
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
-
import { buildRepairContext, collectDiagnostic, isDiagnosticEnabled, emitDiagnostic, truncate, redactUrl, redactText, resolveAdapterSourcePath, MAX_DIAGNOSTIC_BYTES, } from './diagnostic.js';
|
|
3
|
-
import { SelectorError, CommandExecutionError } from './errors.js';
|
|
4
|
-
function makeCmd(overrides = {}) {
|
|
5
|
-
return {
|
|
6
|
-
site: 'test-site',
|
|
7
|
-
name: 'test-cmd',
|
|
8
|
-
description: 'test',
|
|
9
|
-
args: [],
|
|
10
|
-
...overrides,
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
describe('isDiagnosticEnabled', () => {
|
|
14
|
-
const origEnv = process.env.OPENCLI_DIAGNOSTIC;
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
if (origEnv === undefined)
|
|
17
|
-
delete process.env.OPENCLI_DIAGNOSTIC;
|
|
18
|
-
else
|
|
19
|
-
process.env.OPENCLI_DIAGNOSTIC = origEnv;
|
|
20
|
-
});
|
|
21
|
-
it('returns false when env not set', () => {
|
|
22
|
-
delete process.env.OPENCLI_DIAGNOSTIC;
|
|
23
|
-
expect(isDiagnosticEnabled()).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
it('returns true when env is "1"', () => {
|
|
26
|
-
process.env.OPENCLI_DIAGNOSTIC = '1';
|
|
27
|
-
expect(isDiagnosticEnabled()).toBe(true);
|
|
28
|
-
});
|
|
29
|
-
it('returns false for other values', () => {
|
|
30
|
-
process.env.OPENCLI_DIAGNOSTIC = 'true';
|
|
31
|
-
expect(isDiagnosticEnabled()).toBe(false);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe('truncate', () => {
|
|
35
|
-
it('returns short strings unchanged', () => {
|
|
36
|
-
expect(truncate('hello', 100)).toBe('hello');
|
|
37
|
-
});
|
|
38
|
-
it('truncates long strings with marker', () => {
|
|
39
|
-
const long = 'a'.repeat(200);
|
|
40
|
-
const result = truncate(long, 50);
|
|
41
|
-
expect(result.length).toBeLessThan(200);
|
|
42
|
-
expect(result).toContain('...[truncated,');
|
|
43
|
-
expect(result).toContain('150 chars omitted]');
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
describe('redactUrl', () => {
|
|
47
|
-
it('redacts sensitive query parameters', () => {
|
|
48
|
-
expect(redactUrl('https://api.com/v1?token=abc123&q=test'))
|
|
49
|
-
.toBe('https://api.com/v1?token=[REDACTED]&q=test');
|
|
50
|
-
});
|
|
51
|
-
it('redacts multiple sensitive params', () => {
|
|
52
|
-
const url = 'https://api.com?api_key=xxx&secret=yyy&page=1';
|
|
53
|
-
const result = redactUrl(url);
|
|
54
|
-
expect(result).toContain('api_key=[REDACTED]');
|
|
55
|
-
expect(result).toContain('secret=[REDACTED]');
|
|
56
|
-
expect(result).toContain('page=1');
|
|
57
|
-
});
|
|
58
|
-
it('leaves clean URLs unchanged', () => {
|
|
59
|
-
expect(redactUrl('https://example.com/page?q=test')).toBe('https://example.com/page?q=test');
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
describe('redactText', () => {
|
|
63
|
-
it('redacts Bearer tokens', () => {
|
|
64
|
-
expect(redactText('Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.test'))
|
|
65
|
-
.toContain('Bearer [REDACTED]');
|
|
66
|
-
});
|
|
67
|
-
it('redacts JWT tokens', () => {
|
|
68
|
-
const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U';
|
|
69
|
-
expect(redactText(`token is ${jwt}`)).toContain('[REDACTED_JWT]');
|
|
70
|
-
expect(redactText(`token is ${jwt}`)).not.toContain('eyJhbGci');
|
|
71
|
-
});
|
|
72
|
-
it('redacts inline token=value patterns', () => {
|
|
73
|
-
expect(redactText('failed with token=abc123def456')).toContain('token=[REDACTED]');
|
|
74
|
-
});
|
|
75
|
-
it('redacts cookie values', () => {
|
|
76
|
-
const result = redactText('cookie: session=abc123; user=xyz789; path=/');
|
|
77
|
-
expect(result).toContain('[REDACTED]');
|
|
78
|
-
expect(result).not.toContain('session=abc123');
|
|
79
|
-
});
|
|
80
|
-
it('leaves normal text unchanged', () => {
|
|
81
|
-
expect(redactText('Error: element not found')).toBe('Error: element not found');
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
describe('resolveAdapterSourcePath', () => {
|
|
85
|
-
it('returns source when it is a real file path (not manifest:)', () => {
|
|
86
|
-
const cmd = makeCmd({ source: '/home/user/.opencli/clis/arxiv/search.js' });
|
|
87
|
-
expect(resolveAdapterSourcePath(cmd)).toBe('/home/user/.opencli/clis/arxiv/search.js');
|
|
88
|
-
});
|
|
89
|
-
it('skips manifest: pseudo-paths and falls back to _modulePath', () => {
|
|
90
|
-
const cmd = makeCmd({ source: 'manifest:arxiv/search', _modulePath: '/pkg/clis/arxiv/search.js' });
|
|
91
|
-
// Should try to map to source, but since files don't exist on disk, returns _modulePath
|
|
92
|
-
const result = resolveAdapterSourcePath(cmd);
|
|
93
|
-
expect(result).toBeDefined();
|
|
94
|
-
expect(result).not.toContain('manifest:');
|
|
95
|
-
});
|
|
96
|
-
it('returns undefined when only manifest: pseudo-path and no _modulePath', () => {
|
|
97
|
-
const cmd = makeCmd({ source: 'manifest:test/cmd' });
|
|
98
|
-
expect(resolveAdapterSourcePath(cmd)).toBeUndefined();
|
|
99
|
-
});
|
|
100
|
-
it('returns _modulePath when it is the only path available', () => {
|
|
101
|
-
const cmd = makeCmd({ _modulePath: '/project/clis/site/cmd.js' });
|
|
102
|
-
const result = resolveAdapterSourcePath(cmd);
|
|
103
|
-
// Since file doesn't exist, returns _modulePath as best guess
|
|
104
|
-
expect(result).toBe('/project/clis/site/cmd.js');
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
describe('buildRepairContext', () => {
|
|
108
|
-
it('captures CliError fields', () => {
|
|
109
|
-
const err = new SelectorError('.missing-element', 'Element removed');
|
|
110
|
-
const ctx = buildRepairContext(err, makeCmd());
|
|
111
|
-
expect(ctx.error.code).toBe('SELECTOR');
|
|
112
|
-
expect(ctx.error.message).toContain('.missing-element');
|
|
113
|
-
expect(ctx.error.hint).toBe('Element removed');
|
|
114
|
-
expect(ctx.error.stack).toBeDefined();
|
|
115
|
-
expect(ctx.adapter.site).toBe('test-site');
|
|
116
|
-
expect(ctx.adapter.command).toBe('test-site/test-cmd');
|
|
117
|
-
expect(ctx.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
118
|
-
});
|
|
119
|
-
it('handles non-CliError errors', () => {
|
|
120
|
-
const err = new TypeError('Cannot read property "x" of undefined');
|
|
121
|
-
const ctx = buildRepairContext(err, makeCmd());
|
|
122
|
-
expect(ctx.error.code).toBe('UNKNOWN');
|
|
123
|
-
expect(ctx.error.message).toContain('Cannot read property');
|
|
124
|
-
expect(ctx.error.hint).toBeUndefined();
|
|
125
|
-
});
|
|
126
|
-
it('includes page state when provided', () => {
|
|
127
|
-
const pageState = {
|
|
128
|
-
url: 'https://example.com/page',
|
|
129
|
-
snapshot: '<div>...</div>',
|
|
130
|
-
networkRequests: [{ url: '/api/data', status: 200 }],
|
|
131
|
-
consoleErrors: ['Uncaught TypeError'],
|
|
132
|
-
};
|
|
133
|
-
const ctx = buildRepairContext(new CommandExecutionError('boom'), makeCmd(), pageState);
|
|
134
|
-
expect(ctx.page).toEqual(pageState);
|
|
135
|
-
});
|
|
136
|
-
it('omits page when not provided', () => {
|
|
137
|
-
const ctx = buildRepairContext(new Error('boom'), makeCmd());
|
|
138
|
-
expect(ctx.page).toBeUndefined();
|
|
139
|
-
});
|
|
140
|
-
it('truncates long stack traces', () => {
|
|
141
|
-
const err = new Error('boom');
|
|
142
|
-
err.stack = 'x'.repeat(10_000);
|
|
143
|
-
const ctx = buildRepairContext(err, makeCmd());
|
|
144
|
-
expect(ctx.error.stack.length).toBeLessThan(10_000);
|
|
145
|
-
expect(ctx.error.stack).toContain('truncated');
|
|
146
|
-
});
|
|
147
|
-
it('redacts sensitive data in error message and stack', () => {
|
|
148
|
-
const err = new Error('Request failed with Bearer eyJhbGciOiJIUzI1NiJ9.test.sig');
|
|
149
|
-
const ctx = buildRepairContext(err, makeCmd());
|
|
150
|
-
expect(ctx.error.message).toContain('Bearer [REDACTED]');
|
|
151
|
-
expect(ctx.error.message).not.toContain('eyJhbGci');
|
|
152
|
-
// Stack also gets redacted
|
|
153
|
-
expect(ctx.error.stack).toContain('Bearer [REDACTED]');
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
describe('emitDiagnostic', () => {
|
|
157
|
-
it('writes delimited JSON to stderr', () => {
|
|
158
|
-
const writeSpy = vi.spyOn(process.stderr, 'write').mockReturnValue(true);
|
|
159
|
-
const ctx = buildRepairContext(new CommandExecutionError('test error'), makeCmd());
|
|
160
|
-
emitDiagnostic(ctx);
|
|
161
|
-
const output = writeSpy.mock.calls.map(c => c[0]).join('');
|
|
162
|
-
expect(output).toContain('___OPENCLI_DIAGNOSTIC___');
|
|
163
|
-
expect(output).toContain('"code":"COMMAND_EXEC"');
|
|
164
|
-
expect(output).toContain('"message":"test error"');
|
|
165
|
-
// Verify JSON is parseable between markers
|
|
166
|
-
const match = output.match(/___OPENCLI_DIAGNOSTIC___\n(.*)\n___OPENCLI_DIAGNOSTIC___/);
|
|
167
|
-
expect(match).toBeTruthy();
|
|
168
|
-
const parsed = JSON.parse(match[1]);
|
|
169
|
-
expect(parsed.error.code).toBe('COMMAND_EXEC');
|
|
170
|
-
writeSpy.mockRestore();
|
|
171
|
-
});
|
|
172
|
-
it('drops page snapshot when over size budget', () => {
|
|
173
|
-
const writeSpy = vi.spyOn(process.stderr, 'write').mockReturnValue(true);
|
|
174
|
-
const ctx = {
|
|
175
|
-
error: { code: 'COMMAND_EXEC', message: 'boom' },
|
|
176
|
-
adapter: { site: 'test', command: 'test/cmd' },
|
|
177
|
-
page: {
|
|
178
|
-
url: 'https://example.com',
|
|
179
|
-
snapshot: 'x'.repeat(MAX_DIAGNOSTIC_BYTES + 1000),
|
|
180
|
-
networkRequests: [],
|
|
181
|
-
consoleErrors: [],
|
|
182
|
-
},
|
|
183
|
-
timestamp: new Date().toISOString(),
|
|
184
|
-
};
|
|
185
|
-
emitDiagnostic(ctx);
|
|
186
|
-
const output = writeSpy.mock.calls.map(c => c[0]).join('');
|
|
187
|
-
const match = output.match(/___OPENCLI_DIAGNOSTIC___\n(.*)\n___OPENCLI_DIAGNOSTIC___/);
|
|
188
|
-
expect(match).toBeTruthy();
|
|
189
|
-
const parsed = JSON.parse(match[1]);
|
|
190
|
-
// Page snapshot should be replaced or page dropped entirely
|
|
191
|
-
expect(parsed.page?.snapshot !== ctx.page.snapshot || parsed.page === undefined).toBe(true);
|
|
192
|
-
expect(match[1].length).toBeLessThanOrEqual(MAX_DIAGNOSTIC_BYTES);
|
|
193
|
-
writeSpy.mockRestore();
|
|
194
|
-
});
|
|
195
|
-
it('redacts sensitive headers in network requests', () => {
|
|
196
|
-
const pageState = {
|
|
197
|
-
url: 'https://example.com',
|
|
198
|
-
snapshot: '<div/>',
|
|
199
|
-
networkRequests: [{
|
|
200
|
-
url: 'https://api.com/data?token=secret123',
|
|
201
|
-
headers: { authorization: 'Bearer xyz', 'content-type': 'application/json' },
|
|
202
|
-
body: '{"data": "ok"}',
|
|
203
|
-
}],
|
|
204
|
-
consoleErrors: [],
|
|
205
|
-
};
|
|
206
|
-
// Build context manually to test redaction via collectPageState
|
|
207
|
-
// Since collectPageState is private, test the output of buildRepairContext
|
|
208
|
-
// with already-collected page state — redaction happens in collectPageState.
|
|
209
|
-
// For unit test, verify redactUrl directly (tested above) and trust integration.
|
|
210
|
-
expect(redactUrl('https://api.com/data?token=secret123')).toContain('[REDACTED]');
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
function makePage(overrides = {}) {
|
|
214
|
-
return {
|
|
215
|
-
goto: vi.fn(),
|
|
216
|
-
evaluate: vi.fn(),
|
|
217
|
-
getCookies: vi.fn(),
|
|
218
|
-
snapshot: vi.fn().mockResolvedValue('<div>...</div>'),
|
|
219
|
-
click: vi.fn(),
|
|
220
|
-
typeText: vi.fn(),
|
|
221
|
-
pressKey: vi.fn(),
|
|
222
|
-
scrollTo: vi.fn(),
|
|
223
|
-
getFormState: vi.fn(),
|
|
224
|
-
wait: vi.fn(),
|
|
225
|
-
tabs: vi.fn(),
|
|
226
|
-
selectTab: vi.fn(),
|
|
227
|
-
networkRequests: vi.fn().mockResolvedValue([]),
|
|
228
|
-
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
229
|
-
scroll: vi.fn(),
|
|
230
|
-
autoScroll: vi.fn(),
|
|
231
|
-
installInterceptor: vi.fn(),
|
|
232
|
-
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
233
|
-
waitForCapture: vi.fn(),
|
|
234
|
-
screenshot: vi.fn(),
|
|
235
|
-
getCurrentUrl: vi.fn().mockResolvedValue('https://example.com/page'),
|
|
236
|
-
...overrides,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
describe('collectDiagnostic', () => {
|
|
240
|
-
it('keeps intercepted payloads in a dedicated capturedPayloads field', async () => {
|
|
241
|
-
const page = makePage({
|
|
242
|
-
networkRequests: vi.fn().mockResolvedValue([{ url: '/api/data', status: 200 }]),
|
|
243
|
-
getInterceptedRequests: vi.fn().mockResolvedValue([{ items: [{ id: 1 }] }]),
|
|
244
|
-
});
|
|
245
|
-
const ctx = await collectDiagnostic(new Error('boom'), makeCmd(), page);
|
|
246
|
-
expect(ctx.page?.networkRequests).toEqual([
|
|
247
|
-
{ url: '/api/data', status: 200 },
|
|
248
|
-
]);
|
|
249
|
-
expect(ctx.page?.capturedPayloads).toEqual([
|
|
250
|
-
{ source: 'interceptor', responseBody: { items: [{ id: 1 }] } },
|
|
251
|
-
]);
|
|
252
|
-
});
|
|
253
|
-
it('preserves the previous network request output when interception is empty', async () => {
|
|
254
|
-
const page = makePage({
|
|
255
|
-
networkRequests: vi.fn().mockResolvedValue([{ url: '/api/data', status: 200 }]),
|
|
256
|
-
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
257
|
-
});
|
|
258
|
-
const ctx = await collectDiagnostic(new Error('boom'), makeCmd(), page);
|
|
259
|
-
expect(ctx.page?.networkRequests).toEqual([{ url: '/api/data', status: 200 }]);
|
|
260
|
-
expect(ctx.page?.capturedPayloads).toEqual([]);
|
|
261
|
-
});
|
|
262
|
-
it('swallows intercepted request failures and still returns page state', async () => {
|
|
263
|
-
const page = makePage({
|
|
264
|
-
networkRequests: vi.fn().mockResolvedValue([{ url: '/api/data', status: 200 }]),
|
|
265
|
-
getInterceptedRequests: vi.fn().mockRejectedValue(new Error('interceptor unavailable')),
|
|
266
|
-
});
|
|
267
|
-
const ctx = await collectDiagnostic(new Error('boom'), makeCmd(), page);
|
|
268
|
-
expect(ctx.page).toEqual({
|
|
269
|
-
url: 'https://example.com/page',
|
|
270
|
-
snapshot: '<div>...</div>',
|
|
271
|
-
networkRequests: [{ url: '/api/data', status: 200 }],
|
|
272
|
-
capturedPayloads: [],
|
|
273
|
-
consoleErrors: [],
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
it('redacts and truncates intercepted payloads recursively', async () => {
|
|
277
|
-
const page = makePage({
|
|
278
|
-
getInterceptedRequests: vi.fn().mockResolvedValue([{
|
|
279
|
-
token: 'token=abc123def456ghi789',
|
|
280
|
-
nested: {
|
|
281
|
-
cookie: 'cookie: session=super-secret-cookie-value',
|
|
282
|
-
body: 'x'.repeat(5000),
|
|
283
|
-
},
|
|
284
|
-
}]),
|
|
285
|
-
});
|
|
286
|
-
const ctx = await collectDiagnostic(new Error('boom'), makeCmd(), page);
|
|
287
|
-
const payload = ctx.page?.capturedPayloads?.[0];
|
|
288
|
-
const body = payload.responseBody.nested.body;
|
|
289
|
-
expect(payload).toEqual({
|
|
290
|
-
source: 'interceptor',
|
|
291
|
-
responseBody: {
|
|
292
|
-
token: 'token=[REDACTED]',
|
|
293
|
-
nested: {
|
|
294
|
-
cookie: 'cookie: [REDACTED]',
|
|
295
|
-
body,
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
|
-
expect(body).toContain('[truncated,');
|
|
300
|
-
expect(body.length).toBeLessThan(5000);
|
|
301
|
-
});
|
|
302
|
-
});
|
|
File without changes
|