@openclaw/plugin-inspector 0.3.24 → 0.3.26
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 +31 -0
- package/README.md +141 -0
- package/package.json +4 -2
- package/src/api.js +7 -0
- package/src/artifacts.js +22 -1
- package/src/batch.js +7 -12
- package/src/capture-api.js +15 -0
- package/src/capture-cli.js +1 -1
- package/src/capture-config.js +10 -7
- package/src/cli.js +6 -2
- package/src/cold-import-readiness.js +3 -3
- package/src/fixture-summary.js +43 -6
- package/src/import-loop-profile.js +56 -11
- package/src/inspector.js +120 -99
- package/src/mock-sdk-capture-runner.js +61 -22
- package/src/openclaw-target.js +110 -5
- package/src/openclaw-version.js +121 -9
- package/src/process-profile.js +255 -107
- package/src/runtime-capture-report.js +6 -0
- package/src/runtime-imports.js +191 -0
- package/src/runtime-profile.js +4 -0
- package/src/runtime-reconciliation.js +0 -6
- package/src/sdk-mock.js +128 -13
- package/src/synthetic-entrypoint.js +19 -19
- package/src/synthetic-probes-cli.js +79 -2
- package/src/synthetic-probes.js +212 -34
package/src/sdk-mock.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
2
|
+
import * as nodeModule from "node:module";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { collectRuntimeModuleImports } from "./runtime-imports.js";
|
|
3
6
|
|
|
4
7
|
const SOURCE_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"]);
|
|
5
8
|
const SKIP_DIRS = new Set([".git", "coverage", "node_modules", "reports"]);
|
|
@@ -162,6 +165,14 @@ export const mockSdkSubpathExports = {
|
|
|
162
165
|
"normalizeSecretInputString",
|
|
163
166
|
],
|
|
164
167
|
"plugin-runtime": ["createLoggerBackedRuntime", "createSubsystemLogger"],
|
|
168
|
+
"lazy-runtime": [
|
|
169
|
+
"createLazyRuntimeModule",
|
|
170
|
+
"createLazyRuntimeMethod",
|
|
171
|
+
"createLazyRuntimeMethodBinder",
|
|
172
|
+
"createLazyRuntimeNamedExport",
|
|
173
|
+
"createLazyRuntimeSurface",
|
|
174
|
+
],
|
|
175
|
+
"error-runtime": ["formatErrorMessage"],
|
|
165
176
|
"secret-input": [
|
|
166
177
|
"buildOptionalSecretInputSchema",
|
|
167
178
|
"buildSecretInputArraySchema",
|
|
@@ -305,18 +316,48 @@ export async function createMockSdkPackage(rootDir, options = {}) {
|
|
|
305
316
|
|
|
306
317
|
const fallbackExternalPath = path.join(externalDir, "__fallback__.js");
|
|
307
318
|
await writeFile(fallbackExternalPath, externalMockModuleSource("__fallback__", new Set()), "utf8");
|
|
319
|
+
const roots = new Set();
|
|
320
|
+
for (const root of [options.pluginRoot, rootDir].filter(Boolean)) {
|
|
321
|
+
roots.add(path.resolve(root));
|
|
322
|
+
roots.add(await realpath(root));
|
|
323
|
+
}
|
|
308
324
|
const loaderPath = path.join(rootDir, "mock-loader.mjs");
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
mockLoaderSource({
|
|
325
|
+
const syncLoaderPath = path.join(rootDir, "mock-loader-sync.mjs");
|
|
326
|
+
for (const [filePath, synchronous] of [[loaderPath, false], [syncLoaderPath, true]]) {
|
|
327
|
+
await writeFile(filePath, mockLoaderSource({
|
|
312
328
|
externalMap,
|
|
313
329
|
fallbackExternalPath,
|
|
314
330
|
pluginSdkDir,
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
331
|
+
roots: [...roots],
|
|
332
|
+
requireFiles: [...imports.requireFiles],
|
|
333
|
+
synchronous,
|
|
334
|
+
}), "utf8");
|
|
335
|
+
}
|
|
318
336
|
|
|
319
|
-
return { packageDir, loaderPath, pluginSdkDir };
|
|
337
|
+
return { packageDir, loaderPath, syncLoaderPath, pluginSdkDir };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export async function installMockSdkLoader(mockPackage) {
|
|
341
|
+
const supportsCommonJs = typeof nodeModule.registerHooks === "function";
|
|
342
|
+
// Async hooks cannot deregister. Shared state makes them inert after this capture.
|
|
343
|
+
const active = new Int32Array(new SharedArrayBuffer(4));
|
|
344
|
+
Atomics.store(active, 0, 1);
|
|
345
|
+
nodeModule.register(pathToFileURL(mockPackage.loaderPath), { data: { active, supportsCommonJs } });
|
|
346
|
+
let hooks;
|
|
347
|
+
try {
|
|
348
|
+
if (supportsCommonJs) {
|
|
349
|
+
const { resolve } = await import(pathToFileURL(mockPackage.syncLoaderPath).href);
|
|
350
|
+
// Keep CommonJS loading native; a sync load hook changes its import path on Node 22.15.
|
|
351
|
+
hooks = nodeModule.registerHooks({ resolve });
|
|
352
|
+
}
|
|
353
|
+
} catch (error) {
|
|
354
|
+
Atomics.store(active, 0, 0);
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
return () => {
|
|
358
|
+
hooks?.deregister();
|
|
359
|
+
Atomics.store(active, 0, 0);
|
|
360
|
+
};
|
|
320
361
|
}
|
|
321
362
|
|
|
322
363
|
function emptyRuntimeImports() {
|
|
@@ -324,6 +365,7 @@ function emptyRuntimeImports() {
|
|
|
324
365
|
bySpecifier: new Map(),
|
|
325
366
|
openclawSdkSpecifiers: new Set(["openclaw/plugin-sdk"]),
|
|
326
367
|
bareSpecifiers: new Set(),
|
|
368
|
+
requireFiles: new Set(),
|
|
327
369
|
};
|
|
328
370
|
}
|
|
329
371
|
|
|
@@ -331,6 +373,7 @@ async function collectRuntimeImports(pluginRoot) {
|
|
|
331
373
|
const bySpecifier = new Map();
|
|
332
374
|
const openclawSdkSpecifiers = new Set(["openclaw/plugin-sdk"]);
|
|
333
375
|
const bareSpecifiers = new Set();
|
|
376
|
+
const requireFiles = new Set();
|
|
334
377
|
for (const filePath of await listSourceFiles(pluginRoot)) {
|
|
335
378
|
const text = await readFile(filePath, "utf8");
|
|
336
379
|
for (const entry of parseModuleImports(text)) {
|
|
@@ -341,6 +384,10 @@ async function collectRuntimeImports(pluginRoot) {
|
|
|
341
384
|
} else {
|
|
342
385
|
continue;
|
|
343
386
|
}
|
|
387
|
+
if (entry.require && !requireFiles.has(path.resolve(filePath))) {
|
|
388
|
+
requireFiles.add(path.resolve(filePath));
|
|
389
|
+
requireFiles.add(await realpath(filePath));
|
|
390
|
+
}
|
|
344
391
|
const names = bySpecifier.get(entry.specifier) ?? new Set();
|
|
345
392
|
for (const name of entry.names) {
|
|
346
393
|
names.add(name);
|
|
@@ -348,7 +395,7 @@ async function collectRuntimeImports(pluginRoot) {
|
|
|
348
395
|
bySpecifier.set(entry.specifier, names);
|
|
349
396
|
}
|
|
350
397
|
}
|
|
351
|
-
return { bySpecifier, openclawSdkSpecifiers, bareSpecifiers };
|
|
398
|
+
return { bySpecifier, openclawSdkSpecifiers, bareSpecifiers, requireFiles };
|
|
352
399
|
}
|
|
353
400
|
|
|
354
401
|
async function listSourceFiles(dir) {
|
|
@@ -392,6 +439,9 @@ function parseModuleImports(text) {
|
|
|
392
439
|
for (const match of text.matchAll(/\bimport\s+["']([^"']+)["']/g)) {
|
|
393
440
|
entries.push({ specifier: match[1], names: new Set() });
|
|
394
441
|
}
|
|
442
|
+
for (const { specifier, names, kind } of collectRuntimeModuleImports(text)) {
|
|
443
|
+
entries.push({ specifier, names, require: kind === "require" });
|
|
444
|
+
}
|
|
395
445
|
return entries;
|
|
396
446
|
}
|
|
397
447
|
|
|
@@ -417,6 +467,7 @@ function parseNamedImports(clause) {
|
|
|
417
467
|
|
|
418
468
|
function isMockableBareSpecifier(specifier) {
|
|
419
469
|
return (
|
|
470
|
+
!nodeModule.isBuiltin(specifier) &&
|
|
420
471
|
!specifier.startsWith(".") &&
|
|
421
472
|
!specifier.startsWith("/") &&
|
|
422
473
|
!specifier.startsWith("node:") &&
|
|
@@ -429,7 +480,9 @@ function safeModuleFileName(specifier) {
|
|
|
429
480
|
return specifier.replace(/[^A-Za-z0-9._-]+/gu, "__");
|
|
430
481
|
}
|
|
431
482
|
|
|
432
|
-
function mockLoaderSource({ externalMap, fallbackExternalPath, pluginSdkDir }) {
|
|
483
|
+
function mockLoaderSource({ externalMap, fallbackExternalPath, pluginSdkDir, roots, requireFiles, synchronous }) {
|
|
484
|
+
const asyncKeyword = synchronous ? "" : "async ";
|
|
485
|
+
const awaitKeyword = synchronous ? "" : "await ";
|
|
433
486
|
return `import { existsSync } from "node:fs";
|
|
434
487
|
import { readFile } from "node:fs/promises";
|
|
435
488
|
import { builtinModules, stripTypeScriptTypes } from "node:module";
|
|
@@ -439,9 +492,30 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
439
492
|
const externalMap = new Map(Object.entries(${JSON.stringify(externalMap)}));
|
|
440
493
|
const fallbackExternalPath = ${JSON.stringify(fallbackExternalPath)};
|
|
441
494
|
const pluginSdkDir = ${JSON.stringify(pluginSdkDir)};
|
|
495
|
+
const roots = ${JSON.stringify(roots)};
|
|
496
|
+
const requireFiles = new Set(${JSON.stringify(requireFiles)});
|
|
442
497
|
const builtins = new Set([...builtinModules, ...builtinModules.map((name) => \`node:\${name}\`)]);
|
|
498
|
+
let active;
|
|
499
|
+
let supportsCommonJs = false;
|
|
500
|
+
|
|
501
|
+
export function initialize(data) {
|
|
502
|
+
active = data?.active;
|
|
503
|
+
supportsCommonJs = data?.supportsCommonJs === true;
|
|
504
|
+
}
|
|
443
505
|
|
|
444
|
-
|
|
506
|
+
function owns(url) {
|
|
507
|
+
if ((active && Atomics.load(active, 0) === 0) || !url?.startsWith("file:")) return false;
|
|
508
|
+
const filePath = fileURLToPath(url);
|
|
509
|
+
return roots.some((root) => {
|
|
510
|
+
const relative = path.relative(root, filePath);
|
|
511
|
+
return relative === "" || (!relative.startsWith(\`..\${path.sep}\`) && relative !== ".." && !path.isAbsolute(relative));
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export ${asyncKeyword}function resolve(specifier, context, nextResolve) {
|
|
516
|
+
if (!owns(context.parentURL) || builtins.has(specifier) || specifier.startsWith("node:")) {
|
|
517
|
+
return nextResolve(specifier, context);
|
|
518
|
+
}
|
|
445
519
|
if (specifier === "openclaw/plugin-sdk") {
|
|
446
520
|
return moduleUrl(path.join(pluginSdkDir, "index.js"));
|
|
447
521
|
}
|
|
@@ -458,7 +532,7 @@ export async function resolve(specifier, context, nextResolve) {
|
|
|
458
532
|
return moduleUrl(externalMap.get(specifier));
|
|
459
533
|
}
|
|
460
534
|
try {
|
|
461
|
-
return
|
|
535
|
+
return ${awaitKeyword}nextResolve(specifier, context);
|
|
462
536
|
} catch (error) {
|
|
463
537
|
const resolved = resolveExtensionless(specifier, context.parentURL);
|
|
464
538
|
if (resolved) {
|
|
@@ -472,11 +546,16 @@ export async function resolve(specifier, context, nextResolve) {
|
|
|
472
546
|
}
|
|
473
547
|
|
|
474
548
|
export async function load(url, context, nextLoad) {
|
|
549
|
+
if (!owns(url)) return nextLoad(url, context);
|
|
475
550
|
if (url.startsWith("file:") && /\\.[cm]?ts$/u.test(fileURLToPath(url))) {
|
|
476
551
|
const rawSource = await readFile(fileURLToPath(url), "utf8");
|
|
477
552
|
return { format: "module", source: stripPluginTypeScript(rawSource), shortCircuit: true };
|
|
478
553
|
}
|
|
479
|
-
|
|
554
|
+
const result = await nextLoad(url, context);
|
|
555
|
+
${synchronous ? "" : `if (!supportsCommonJs && result.format === "commonjs" && requireFiles.has(fileURLToPath(url))) {
|
|
556
|
+
throw new Error("CommonJS SDK mocking requires Node.js 22.15 or newer with module.registerHooks(); upgrade Node.js or use an ESM/TypeScript entrypoint.");
|
|
557
|
+
}`}
|
|
558
|
+
return result;
|
|
480
559
|
}
|
|
481
560
|
|
|
482
561
|
function stripPluginTypeScript(source) {
|
|
@@ -621,6 +700,11 @@ function isValidExportName(name) {
|
|
|
621
700
|
}
|
|
622
701
|
|
|
623
702
|
function genericExportStatement(name) {
|
|
703
|
+
if (name === "normalizeOptionalString") {
|
|
704
|
+
// Optional values stay absent; an empty string invents explicit input in
|
|
705
|
+
// callers that distinguish undefined from a configured policy value.
|
|
706
|
+
return 'export function normalizeOptionalString(value) { return typeof value === "string" ? value.trim() || undefined : undefined; }';
|
|
707
|
+
}
|
|
624
708
|
if (name === "isRecord") {
|
|
625
709
|
return "export function isRecord(value) { return isPlainObject(value); }";
|
|
626
710
|
}
|
|
@@ -1638,6 +1722,37 @@ export function resolveRuntimeEnv(env = {}) {
|
|
|
1638
1722
|
return createRuntimeEnv(env);
|
|
1639
1723
|
}
|
|
1640
1724
|
|
|
1725
|
+
export function createLazyRuntimeSurface(importer, select) {
|
|
1726
|
+
let promise;
|
|
1727
|
+
const load = () => {
|
|
1728
|
+
// SDK runtime imports retain the same promise, including rejection, until clear().
|
|
1729
|
+
promise ??= Promise.resolve().then(() => importer().then(select));
|
|
1730
|
+
return promise;
|
|
1731
|
+
};
|
|
1732
|
+
load.peek = () => promise;
|
|
1733
|
+
load.clear = () => { promise = undefined; };
|
|
1734
|
+
return load;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
export function createLazyRuntimeModule(importer) {
|
|
1738
|
+
return createLazyRuntimeSurface(importer, (module) => module);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
export function createLazyRuntimeNamedExport(importer, key) {
|
|
1742
|
+
return createLazyRuntimeSurface(importer, (module) => module[key]);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
export function createLazyRuntimeMethod(load, select) {
|
|
1746
|
+
return async (...args) => {
|
|
1747
|
+
const method = select(await load());
|
|
1748
|
+
return await method(...args);
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
export function createLazyRuntimeMethodBinder(load) {
|
|
1753
|
+
return (select) => createLazyRuntimeMethod(load, select);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1641
1756
|
export function createLoggerBackedRuntime(logger = console) {
|
|
1642
1757
|
return { logger };
|
|
1643
1758
|
}
|
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import { rmSync } from "node:fs";
|
|
2
2
|
import { mkdtemp } from "node:fs/promises";
|
|
3
|
-
import { register } from "node:module";
|
|
4
3
|
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
|
-
import { pathToFileURL } from "node:url";
|
|
7
5
|
import { captureEntrypoint } from "./inspector.js";
|
|
8
|
-
import { createMockSdkPackage } from "./sdk-mock.js";
|
|
6
|
+
import { createMockSdkPackage, installMockSdkLoader } from "./sdk-mock.js";
|
|
9
7
|
import { runCapturedSyntheticProbes } from "./synthetic-probes.js";
|
|
10
8
|
|
|
11
9
|
export async function runEntrypointSyntheticProbes(entrypoint, options = {}) {
|
|
12
|
-
const
|
|
10
|
+
const captureOptions = {
|
|
13
11
|
...options,
|
|
14
12
|
apiOptions: {
|
|
15
13
|
...(options.apiOptions ?? {}),
|
|
16
14
|
retainHandlers: true,
|
|
17
15
|
},
|
|
18
|
-
}
|
|
19
|
-
return runCapturedSyntheticProbes(capture, options);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function captureEntrypointForSyntheticProbes(entrypoint, options) {
|
|
16
|
+
};
|
|
23
17
|
if (options.mockSdk !== true) {
|
|
24
|
-
|
|
18
|
+
const capture = await captureEntrypoint(entrypoint, captureOptions);
|
|
19
|
+
return runCapturedSyntheticProbes(capture, options);
|
|
25
20
|
}
|
|
26
21
|
|
|
27
22
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -29,15 +24,20 @@ async function captureEntrypointForSyntheticProbes(entrypoint, options) {
|
|
|
29
24
|
const pluginRoot = path.resolve(cwd, options.pluginRoot ?? path.dirname(resolvedEntrypoint));
|
|
30
25
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-synthetic-mock-sdk-"));
|
|
31
26
|
cleanupTempDirOnExit(workspace);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
const mockPackage = await createMockSdkPackage(workspace, { pluginRoot });
|
|
28
|
+
const stopLoader = await installMockSdkLoader(mockPackage);
|
|
29
|
+
try {
|
|
30
|
+
const capture = await captureEntrypoint(entrypoint, {
|
|
31
|
+
...captureOptions,
|
|
32
|
+
cwd,
|
|
33
|
+
mockSdk: false,
|
|
34
|
+
pluginRoot,
|
|
35
|
+
});
|
|
36
|
+
// Retained handlers can require SDK modules lazily during invocation.
|
|
37
|
+
return await runCapturedSyntheticProbes(capture, options);
|
|
38
|
+
} finally {
|
|
39
|
+
stopLoader();
|
|
40
|
+
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function cleanupTempDirOnExit(dir) {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { readBoundedJsonArtifact, writeArtifacts } from "./artifacts.js";
|
|
7
|
+
import { resolveProcessLimits, startOwnedProcess } from "./process-profile.js";
|
|
3
8
|
|
|
4
9
|
const args = process.argv.slice(2);
|
|
5
10
|
|
|
@@ -26,7 +31,7 @@ async function run(commandArgs) {
|
|
|
26
31
|
throw new Error("synthetic probes import plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 in an isolated workspace");
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
const results = await
|
|
34
|
+
const results = await runInChild(entrypoint, {
|
|
30
35
|
mockSdk,
|
|
31
36
|
pluginRoot,
|
|
32
37
|
apiOptions: { retainHandlers: true },
|
|
@@ -43,6 +48,78 @@ async function run(commandArgs) {
|
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
async function runInChild(entrypoint, options) {
|
|
52
|
+
const limits = resolveProcessLimits({}, "PROBE");
|
|
53
|
+
const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-synthetic-cli-"));
|
|
54
|
+
const outputPath = path.join(workspace, "result.json");
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const cancel = () => controller.abort(new Error("Synthetic probes cancelled"));
|
|
57
|
+
process.once("SIGINT", cancel);
|
|
58
|
+
process.once("SIGTERM", cancel);
|
|
59
|
+
try {
|
|
60
|
+
const runnerPath = fileURLToPath(new URL("./mock-sdk-capture-runner.js", import.meta.url));
|
|
61
|
+
const { result } = startOwnedProcess({
|
|
62
|
+
command: process.execPath,
|
|
63
|
+
args: [
|
|
64
|
+
"--no-warnings",
|
|
65
|
+
...(options.mockSdk ? ["--preserve-symlinks"] : []),
|
|
66
|
+
runnerPath,
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
...options, ...limits, entrypoint, outputPath,
|
|
69
|
+
cwd: process.cwd(), syntheticProbes: true,
|
|
70
|
+
}),
|
|
71
|
+
],
|
|
72
|
+
...limits,
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
}, "PROBE");
|
|
75
|
+
const outcome = await result;
|
|
76
|
+
if (outcome.exitCode !== 0 || outcome.outputTruncated) {
|
|
77
|
+
const message = outcome.cancelled ? "Synthetic probes cancelled"
|
|
78
|
+
: outcome.timedOut ? `Synthetic probes timed out after ${limits.timeoutMs}ms`
|
|
79
|
+
: outcome.outputTruncated ? "Synthetic probe child output exceeded its byte limit"
|
|
80
|
+
: outcome.stderr.trim() || outcome.error?.message || "Synthetic probe child failed";
|
|
81
|
+
throw new Error(message);
|
|
82
|
+
}
|
|
83
|
+
controller.signal.throwIfAborted();
|
|
84
|
+
// The report is separate from plugin stdout, including direct fd writes.
|
|
85
|
+
// Only accept a fresh complete artifact after successful child cleanup.
|
|
86
|
+
const results = await readBoundedJsonArtifact(outputPath, limits.maxOutputBytes);
|
|
87
|
+
validateSyntheticReport(results);
|
|
88
|
+
controller.signal.throwIfAborted();
|
|
89
|
+
return results;
|
|
90
|
+
} finally {
|
|
91
|
+
process.removeListener("SIGINT", cancel);
|
|
92
|
+
process.removeListener("SIGTERM", cancel);
|
|
93
|
+
await rm(workspace, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validateSyntheticReport(report) {
|
|
98
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
99
|
+
if (!isObject(report) || typeof report.entrypoint !== "string" ||
|
|
100
|
+
!["captured", "no-register-export"].includes(report.status) ||
|
|
101
|
+
!isObject(report.summary) || !Array.isArray(report.results)) {
|
|
102
|
+
throw new Error("Invalid synthetic probe report: expected entrypoint, status, summary, and results");
|
|
103
|
+
}
|
|
104
|
+
const counts = { probeCount: report.results.length, passCount: 0, failCount: 0, blockedCount: 0 };
|
|
105
|
+
for (const row of report.results) {
|
|
106
|
+
if (!isObject(row) || !Number.isSafeInteger(row.captureIndex) || row.captureIndex < 0 ||
|
|
107
|
+
!["kind", "seam", "label"].every((key) => typeof row[key] === "string") ||
|
|
108
|
+
!["pass", "fail", "blocked"].includes(row.status) ||
|
|
109
|
+
(row.status === "fail" && typeof row.error !== "string") ||
|
|
110
|
+
(row.status === "blocked" && typeof row.reason !== "string")) {
|
|
111
|
+
throw new Error("Invalid synthetic probe report: malformed result row");
|
|
112
|
+
}
|
|
113
|
+
counts[`${row.status}Count`] += 1;
|
|
114
|
+
}
|
|
115
|
+
for (const [key, expected] of Object.entries(counts)) {
|
|
116
|
+
if (!Number.isSafeInteger(report.summary[key]) || report.summary[key] < 0 ||
|
|
117
|
+
report.summary[key] !== expected) {
|
|
118
|
+
throw new Error(`Invalid synthetic probe report: invalid or inconsistent ${key}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
46
123
|
function readFlag(commandArgs, name) {
|
|
47
124
|
const index = commandArgs.indexOf(name);
|
|
48
125
|
if (index === -1) {
|