@openclaw/plugin-inspector 0.1.1 → 0.1.2
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/CHANGELOG.md +11 -0
- package/README.md +82 -41
- package/examples/github-actions-plugin-inspector.yml +2 -2
- package/examples/plugin-inspector.config.json +3 -0
- package/package.json +1 -1
- package/src/advanced.js +9 -0
- package/src/api.js +14 -8
- package/src/capture-api.js +180 -12
- package/src/cli.js +64 -5
- package/src/config.js +16 -2
- package/src/index.js +1 -0
- package/src/init.js +150 -0
- package/src/inspector.js +89 -14
- package/src/mock-sdk-capture-runner.js +110 -17
- package/src/runtime-capture-report.js +11 -1
- package/src/sdk-mock.js +1266 -13
- package/src/synthetic-probes.js +153 -9
package/src/config.js
CHANGED
|
@@ -52,6 +52,19 @@ export function validateInspectorConfig(config) {
|
|
|
52
52
|
errors.push("config.fixtures must be a non-empty array");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
if (config.capture !== undefined) {
|
|
56
|
+
if (!config.capture || typeof config.capture !== "object" || Array.isArray(config.capture)) {
|
|
57
|
+
errors.push("config.capture must be an object when present");
|
|
58
|
+
} else {
|
|
59
|
+
if (config.capture.runtime !== undefined && typeof config.capture.runtime !== "boolean") {
|
|
60
|
+
errors.push("config.capture.runtime must be a boolean when present");
|
|
61
|
+
}
|
|
62
|
+
if (config.capture.mockSdk !== undefined && typeof config.capture.mockSdk !== "boolean") {
|
|
63
|
+
errors.push("config.capture.mockSdk must be a boolean when present");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
const ids = new Set();
|
|
56
69
|
const paths = new Set();
|
|
57
70
|
for (const fixture of config.fixtures ?? []) {
|
|
@@ -134,6 +147,7 @@ export async function normalizePluginRootConfig(config, options = {}) {
|
|
|
134
147
|
return {
|
|
135
148
|
version: 1,
|
|
136
149
|
submoduleRoot: ".",
|
|
150
|
+
capture: config.capture,
|
|
137
151
|
openclaw: config.openclaw,
|
|
138
152
|
fixtures: [fixture],
|
|
139
153
|
};
|
|
@@ -157,7 +171,7 @@ async function readJsonIfExists(filePath) {
|
|
|
157
171
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
158
172
|
}
|
|
159
173
|
|
|
160
|
-
function packageId(packageName) {
|
|
174
|
+
export function packageId(packageName) {
|
|
161
175
|
if (!packageName) {
|
|
162
176
|
return null;
|
|
163
177
|
}
|
|
@@ -170,7 +184,7 @@ function packageId(packageName) {
|
|
|
170
184
|
.toLowerCase();
|
|
171
185
|
}
|
|
172
186
|
|
|
173
|
-
function inferPluginSeams(pluginManifest, packageJson) {
|
|
187
|
+
export function inferPluginSeams(pluginManifest, packageJson) {
|
|
174
188
|
const contracts = Object.keys(pluginManifest?.contracts ?? {});
|
|
175
189
|
if (contracts.includes("tools")) {
|
|
176
190
|
return ["dynamic-tool"];
|
package/src/index.js
CHANGED
package/src/init.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { inferPluginSeams, packageId } from "./config.js";
|
|
5
|
+
|
|
6
|
+
export const defaultInitConfigPath = "plugin-inspector.config.json";
|
|
7
|
+
export const defaultInitWorkflowPath = ".github/workflows/plugin-inspector.yml";
|
|
8
|
+
|
|
9
|
+
export async function writePluginInspectorInit(options = {}) {
|
|
10
|
+
const pluginRoot = path.resolve(options.pluginRoot ?? options.cwd ?? process.cwd());
|
|
11
|
+
const configPath = path.resolve(pluginRoot, options.configPath ?? defaultInitConfigPath);
|
|
12
|
+
const written = [];
|
|
13
|
+
|
|
14
|
+
if (existsSync(configPath) && options.force !== true) {
|
|
15
|
+
throw new Error(`${path.relative(pluginRoot, configPath)} already exists; pass --force to overwrite it`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const config = await buildPluginInspectorConfig({ pluginRoot });
|
|
19
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
20
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
21
|
+
written.push(configPath);
|
|
22
|
+
|
|
23
|
+
if (options.ci === true) {
|
|
24
|
+
const workflowPath = path.resolve(pluginRoot, options.workflowPath ?? defaultInitWorkflowPath);
|
|
25
|
+
if (existsSync(workflowPath) && options.force !== true) {
|
|
26
|
+
throw new Error(`${path.relative(pluginRoot, workflowPath)} already exists; pass --force to overwrite it`);
|
|
27
|
+
}
|
|
28
|
+
await mkdir(path.dirname(workflowPath), { recursive: true });
|
|
29
|
+
await writeFile(workflowPath, renderGithubActionsWorkflow({ packageManager: options.packageManager }), "utf8");
|
|
30
|
+
written.push(workflowPath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { pluginRoot, configPath, written };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function buildPluginInspectorConfig(options = {}) {
|
|
37
|
+
const pluginRoot = path.resolve(options.pluginRoot ?? options.cwd ?? process.cwd());
|
|
38
|
+
const packageJson = await readJsonIfExists(path.join(pluginRoot, "package.json"));
|
|
39
|
+
const pluginManifest = await readJsonIfExists(path.join(pluginRoot, "openclaw.plugin.json"));
|
|
40
|
+
const sourceRoot = inferSourceRoot(packageJson);
|
|
41
|
+
|
|
42
|
+
const plugin = {
|
|
43
|
+
id: pluginManifest?.id ?? packageId(packageJson?.name) ?? "plugin",
|
|
44
|
+
priority: "high",
|
|
45
|
+
seams: inferPluginSeams(pluginManifest, packageJson),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (sourceRoot !== ".") {
|
|
49
|
+
plugin.sourceRoot = sourceRoot;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
version: 1,
|
|
54
|
+
plugin,
|
|
55
|
+
capture: {
|
|
56
|
+
mockSdk: true,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function renderGithubActionsWorkflow(options = {}) {
|
|
62
|
+
const packageManager = normalizePackageManager(options.packageManager);
|
|
63
|
+
const setup = packageManagerSetup(packageManager);
|
|
64
|
+
|
|
65
|
+
return `name: plugin-inspector
|
|
66
|
+
|
|
67
|
+
on:
|
|
68
|
+
pull_request:
|
|
69
|
+
push:
|
|
70
|
+
branches: [main]
|
|
71
|
+
|
|
72
|
+
jobs:
|
|
73
|
+
check:
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v5
|
|
77
|
+
- uses: actions/setup-node@v5
|
|
78
|
+
with:
|
|
79
|
+
node-version: 24
|
|
80
|
+
cache: ${setup.cache}
|
|
81
|
+
${setup.corepack ? " - run: corepack enable\n" : ""} - run: ${setup.install}
|
|
82
|
+
- run: ${setup.exec} @openclaw/plugin-inspector check --no-openclaw
|
|
83
|
+
- run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 ${setup.exec} @openclaw/plugin-inspector check --no-openclaw --runtime --mock-sdk
|
|
84
|
+
- uses: actions/upload-artifact@v5
|
|
85
|
+
if: always()
|
|
86
|
+
with:
|
|
87
|
+
name: plugin-inspector-reports
|
|
88
|
+
path: reports/plugin-inspector-*
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function inferSourceRoot(packageJson) {
|
|
93
|
+
const entrypoints = [
|
|
94
|
+
packageJson?.openclaw?.entrypoint,
|
|
95
|
+
...(packageJson?.openclaw?.extensions ?? []),
|
|
96
|
+
...(packageJson?.openclaw?.runtimeExtensions ?? []),
|
|
97
|
+
].filter((value) => typeof value === "string");
|
|
98
|
+
const entrypoint = entrypoints[0] ?? packageJson?.exports?.["."] ?? packageJson?.main ?? "src/index.js";
|
|
99
|
+
if (typeof entrypoint === "string" && entrypoint.startsWith("src/")) {
|
|
100
|
+
return "src";
|
|
101
|
+
}
|
|
102
|
+
return ".";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function readJsonIfExists(filePath) {
|
|
106
|
+
if (!existsSync(filePath)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizePackageManager(packageManager = "npm") {
|
|
113
|
+
if (["npm", "pnpm", "yarn", "bun"].includes(packageManager)) {
|
|
114
|
+
return packageManager;
|
|
115
|
+
}
|
|
116
|
+
throw new Error("--package-manager must be npm, pnpm, yarn, or bun");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function packageManagerSetup(packageManager) {
|
|
120
|
+
if (packageManager === "pnpm") {
|
|
121
|
+
return {
|
|
122
|
+
cache: "pnpm",
|
|
123
|
+
corepack: true,
|
|
124
|
+
install: "pnpm install --frozen-lockfile",
|
|
125
|
+
exec: "pnpm dlx",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (packageManager === "yarn") {
|
|
129
|
+
return {
|
|
130
|
+
cache: "yarn",
|
|
131
|
+
corepack: true,
|
|
132
|
+
install: "yarn install --immutable",
|
|
133
|
+
exec: "yarn dlx",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (packageManager === "bun") {
|
|
137
|
+
return {
|
|
138
|
+
cache: "npm",
|
|
139
|
+
corepack: false,
|
|
140
|
+
install: "bun install --frozen-lockfile",
|
|
141
|
+
exec: "bunx",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
cache: "npm",
|
|
146
|
+
corepack: false,
|
|
147
|
+
install: "npm ci",
|
|
148
|
+
exec: "npx",
|
|
149
|
+
};
|
|
150
|
+
}
|
package/src/inspector.js
CHANGED
|
@@ -156,7 +156,12 @@ export async function captureEntrypoint(entrypoint, options = {}) {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
const resolvedEntrypoint = path.resolve(options.cwd ?? process.cwd(), entrypoint);
|
|
159
|
-
|
|
159
|
+
let module;
|
|
160
|
+
try {
|
|
161
|
+
module = await import(pathToFileURL(resolvedEntrypoint).href);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw classifyCapturePhaseError(error, "entrypoint-import-error");
|
|
164
|
+
}
|
|
160
165
|
const register = findRegisterExport(module);
|
|
161
166
|
|
|
162
167
|
if (!register) {
|
|
@@ -168,7 +173,11 @@ export async function captureEntrypoint(entrypoint, options = {}) {
|
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
const api = createCaptureApi(options.apiOptions);
|
|
171
|
-
|
|
176
|
+
try {
|
|
177
|
+
await register(api);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw classifyCapturePhaseError(error, "registration-execution-error");
|
|
180
|
+
}
|
|
172
181
|
const result = {
|
|
173
182
|
status: "captured",
|
|
174
183
|
entrypoint: resolvedEntrypoint,
|
|
@@ -188,19 +197,85 @@ export async function captureEntrypointWithMockSdk(entrypoint, options = {}) {
|
|
|
188
197
|
pluginRoot: options.pluginRoot,
|
|
189
198
|
apiOptions: options.apiOptions,
|
|
190
199
|
};
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
200
|
+
try {
|
|
201
|
+
const { stdout } = await execFileAsync(
|
|
202
|
+
process.execPath,
|
|
203
|
+
["--no-warnings", "--preserve-symlinks", runnerPath, JSON.stringify(payload)],
|
|
204
|
+
{
|
|
205
|
+
cwd: options.cwd ?? process.cwd(),
|
|
206
|
+
env: {
|
|
207
|
+
...process.env,
|
|
208
|
+
...(options.env ?? {}),
|
|
209
|
+
},
|
|
210
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
199
211
|
},
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
203
|
-
|
|
212
|
+
);
|
|
213
|
+
return JSON.parse(stdout);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
throw classifyMockSdkCaptureError(error);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function classifyMockSdkCaptureError(error) {
|
|
220
|
+
const rawMessage = [error?.stderr, error?.stdout, error?.message].filter(Boolean).join("\n");
|
|
221
|
+
const missingExport = rawMessage.match(/does not provide an export named ['"]([^'"]+)['"]/)?.[1];
|
|
222
|
+
if (missingExport) {
|
|
223
|
+
return enrichCaptureError(error, {
|
|
224
|
+
message: `Mock SDK import failed: openclaw/plugin-sdk is missing export ${missingExport}`,
|
|
225
|
+
failureClass: "missing-sdk-export",
|
|
226
|
+
missingExport,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const missingModule =
|
|
231
|
+
rawMessage.match(/Cannot find (?:package|module) ['"]([^'"]*openclaw\/plugin-sdk[^'"]*)['"]/)?.[1] ??
|
|
232
|
+
rawMessage.match(/Package subpath ['"](\.\/plugin-sdk\/[^'"]+)['"]/)?.[1];
|
|
233
|
+
if (missingModule || rawMessage.includes("openclaw/plugin-sdk")) {
|
|
234
|
+
return enrichCaptureError(error, {
|
|
235
|
+
message: `Mock SDK import failed: ${missingModule ?? "openclaw/plugin-sdk module could not be resolved"}`,
|
|
236
|
+
failureClass: "missing-sdk-module",
|
|
237
|
+
missingModule,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const failureClass = rawMessage.match(/\[plugin-inspector:([^\]]+)\]/)?.[1];
|
|
242
|
+
if (failureClass) {
|
|
243
|
+
return enrichCaptureError(error, {
|
|
244
|
+
message: firstMeaningfulErrorLine(rawMessage.replace(/\[plugin-inspector:[^\]]+\]/, "")) ?? "Mock SDK capture failed",
|
|
245
|
+
failureClass,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return enrichCaptureError(error, {
|
|
250
|
+
message: firstMeaningfulErrorLine(rawMessage) ?? "Mock SDK capture failed",
|
|
251
|
+
failureClass: "mock-sdk-capture-error",
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function classifyCapturePhaseError(error, failureClass) {
|
|
256
|
+
return enrichCaptureError(error, {
|
|
257
|
+
message: error instanceof Error ? error.message : String(error),
|
|
258
|
+
failureClass,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function enrichCaptureError(error, details) {
|
|
263
|
+
const wrapped = new Error(details.message, { cause: error });
|
|
264
|
+
wrapped.failureClass = details.failureClass;
|
|
265
|
+
if (details.missingExport) {
|
|
266
|
+
wrapped.missingExport = details.missingExport;
|
|
267
|
+
}
|
|
268
|
+
if (details.missingModule) {
|
|
269
|
+
wrapped.missingModule = details.missingModule;
|
|
270
|
+
}
|
|
271
|
+
return wrapped;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function firstMeaningfulErrorLine(message) {
|
|
275
|
+
return String(message)
|
|
276
|
+
.split("\n")
|
|
277
|
+
.map((line) => line.trim())
|
|
278
|
+
.find((line) => line && !line.startsWith("Command failed:"));
|
|
204
279
|
}
|
|
205
280
|
|
|
206
281
|
function findRegisterExport(module) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdtemp, rm
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { register } from "node:module";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { pathToFileURL } from "node:url";
|
|
@@ -7,12 +8,16 @@ import { createCaptureApi } from "./capture-api.js";
|
|
|
7
8
|
import { createMockSdkPackage } from "./sdk-mock.js";
|
|
8
9
|
|
|
9
10
|
const options = JSON.parse(process.argv[2] ?? "{}");
|
|
11
|
+
let activeOutputCapture = null;
|
|
10
12
|
|
|
11
13
|
try {
|
|
12
14
|
const result = await run(options);
|
|
13
|
-
|
|
15
|
+
writeRunnerStdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
14
16
|
} catch (error) {
|
|
15
|
-
|
|
17
|
+
if (error.failureClass) {
|
|
18
|
+
writeRunnerStderr(`[plugin-inspector:${error.failureClass}]\n`);
|
|
19
|
+
}
|
|
20
|
+
writeRunnerStderr(`${error.stack ?? error.message}\n`);
|
|
16
21
|
process.exitCode = 1;
|
|
17
22
|
}
|
|
18
23
|
|
|
@@ -22,37 +27,79 @@ async function run(options) {
|
|
|
22
27
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-mock-sdk-"));
|
|
23
28
|
|
|
24
29
|
try {
|
|
25
|
-
await createMockSdkPackage(workspace);
|
|
26
|
-
|
|
27
|
-
await
|
|
28
|
-
const linkedEntrypoint = path.join(linkedPluginRoot, path.relative(pluginRoot, entrypoint));
|
|
29
|
-
return await captureLinkedEntrypoint(linkedEntrypoint, options);
|
|
30
|
+
const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
|
|
31
|
+
register(pathToFileURL(loaderPath));
|
|
32
|
+
return await captureLinkedEntrypoint(entrypoint, options);
|
|
30
33
|
} finally {
|
|
31
34
|
await rm(workspace, { force: true, recursive: true });
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
async function captureLinkedEntrypoint(entrypoint, options) {
|
|
36
|
-
const
|
|
39
|
+
const outputCapture = installProcessOutputCapture();
|
|
40
|
+
activeOutputCapture = outputCapture;
|
|
41
|
+
|
|
42
|
+
let module;
|
|
43
|
+
try {
|
|
44
|
+
module = await import(pathToFileURL(entrypoint).href);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
await drainAsyncOutput();
|
|
47
|
+
throw capturePhaseError(error, "entrypoint-import-error");
|
|
48
|
+
}
|
|
37
49
|
const register = findRegisterExport(module);
|
|
38
50
|
|
|
39
51
|
if (!register) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
await drainAsyncOutput();
|
|
53
|
+
return withProcessOutput(
|
|
54
|
+
{
|
|
55
|
+
status: "no-register-export",
|
|
56
|
+
entrypoint: options.entrypoint,
|
|
57
|
+
mockSdk: true,
|
|
58
|
+
captured: [],
|
|
59
|
+
},
|
|
60
|
+
outputCapture,
|
|
61
|
+
);
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
const api = createCaptureApi(options.apiOptions);
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
try {
|
|
66
|
+
await register(api);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
await drainAsyncOutput();
|
|
69
|
+
throw capturePhaseError(error, "registration-execution-error");
|
|
70
|
+
}
|
|
71
|
+
await drainAsyncOutput();
|
|
72
|
+
|
|
73
|
+
const result = {
|
|
51
74
|
status: "captured",
|
|
52
75
|
entrypoint: options.entrypoint,
|
|
53
76
|
mockSdk: true,
|
|
54
77
|
captured: api.getCapturedContracts(),
|
|
55
78
|
};
|
|
79
|
+
if (options.apiOptions?.retainHandlers === true) {
|
|
80
|
+
result.retained = api.getRetainedContracts();
|
|
81
|
+
}
|
|
82
|
+
return withProcessOutput(result, outputCapture);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function withProcessOutput(result, outputCapture) {
|
|
86
|
+
const stdout = outputCapture.stdout();
|
|
87
|
+
const stderr = outputCapture.stderr();
|
|
88
|
+
if (stdout.length === 0 && stderr.length === 0) {
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
...result,
|
|
93
|
+
processOutput: {
|
|
94
|
+
stdout,
|
|
95
|
+
stderr,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function capturePhaseError(error, failureClass) {
|
|
101
|
+
error.failureClass = failureClass;
|
|
102
|
+
return error;
|
|
56
103
|
}
|
|
57
104
|
|
|
58
105
|
function findRegisterExport(module) {
|
|
@@ -67,3 +114,49 @@ function findRegisterExport(module) {
|
|
|
67
114
|
}
|
|
68
115
|
return null;
|
|
69
116
|
}
|
|
117
|
+
|
|
118
|
+
function installProcessOutputCapture() {
|
|
119
|
+
const stdoutChunks = [];
|
|
120
|
+
const stderrChunks = [];
|
|
121
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
122
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
123
|
+
|
|
124
|
+
process.stdout.write = (chunk, encoding, callback) => {
|
|
125
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk));
|
|
126
|
+
invokeWriteCallback(encoding, callback);
|
|
127
|
+
return true;
|
|
128
|
+
};
|
|
129
|
+
process.stderr.write = (chunk, encoding, callback) => {
|
|
130
|
+
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk));
|
|
131
|
+
invokeWriteCallback(encoding, callback);
|
|
132
|
+
return true;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
originalStdoutWrite,
|
|
137
|
+
originalStderrWrite,
|
|
138
|
+
stdout: () => stdoutChunks.join(""),
|
|
139
|
+
stderr: () => stderrChunks.join(""),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function invokeWriteCallback(encoding, callback) {
|
|
144
|
+
if (typeof encoding === "function") {
|
|
145
|
+
encoding();
|
|
146
|
+
} else if (typeof callback === "function") {
|
|
147
|
+
callback();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function drainAsyncOutput() {
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
153
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function writeRunnerStdout(text) {
|
|
157
|
+
(activeOutputCapture?.originalStdoutWrite ?? process.stdout.write.bind(process.stdout))(text);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeRunnerStderr(text) {
|
|
161
|
+
(activeOutputCapture?.originalStderrWrite ?? process.stderr.write.bind(process.stderr))(text);
|
|
162
|
+
}
|
|
@@ -71,13 +71,20 @@ export function renderRuntimeCaptureMarkdown(captureReport, options = {}) {
|
|
|
71
71
|
result.fixture,
|
|
72
72
|
result.status,
|
|
73
73
|
result.entrypoint,
|
|
74
|
-
(result.captured ?? []).map((item) => `${item.kind}:${item.name}`).join(", ") || result
|
|
74
|
+
(result.captured ?? []).map((item) => `${item.kind}:${item.name}`).join(", ") || formatCaptureError(result),
|
|
75
75
|
]),
|
|
76
76
|
["Fixture", "Status", "Entrypoint", "Captured"],
|
|
77
77
|
),
|
|
78
78
|
].join("\n");
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function formatCaptureError(result) {
|
|
82
|
+
if (!result.error) {
|
|
83
|
+
return "-";
|
|
84
|
+
}
|
|
85
|
+
return result.failureClass ? `${result.failureClass}: ${result.error}` : result.error;
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
function captureTargets(fixture, rootDir) {
|
|
82
89
|
return fixture.packages.flatMap((packageSummary) => {
|
|
83
90
|
const packageRoot = path.dirname(path.resolve(rootDir, packageSummary.path));
|
|
@@ -124,6 +131,9 @@ async function captureTarget(target, options) {
|
|
|
124
131
|
packagePath: target.packagePath,
|
|
125
132
|
entrypoint: target.entrypoint.relativePath,
|
|
126
133
|
error: error.message,
|
|
134
|
+
...(error.failureClass ? { failureClass: error.failureClass } : {}),
|
|
135
|
+
...(error.missingExport ? { missingExport: error.missingExport } : {}),
|
|
136
|
+
...(error.missingModule ? { missingModule: error.missingModule } : {}),
|
|
127
137
|
captured: [],
|
|
128
138
|
};
|
|
129
139
|
}
|