pi-harness-runtime 1.1.431 → 1.1.433

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.
Files changed (112) hide show
  1. package/package.json +2 -2
  2. package/packages/a2a-adapter/package.json +1 -1
  3. package/packages/agent-policy/package.json +1 -1
  4. package/packages/architecture-generator/package.json +1 -1
  5. package/packages/auth/package.json +1 -1
  6. package/packages/autonomous-refactor/package.json +1 -1
  7. package/packages/autonomous-runtime/package.json +1 -1
  8. package/packages/cache-strategy/package.json +1 -1
  9. package/packages/capability-registry/package.json +1 -1
  10. package/packages/checkpoint/package.json +1 -1
  11. package/packages/cli-plugin-sdk/package.json +1 -1
  12. package/packages/clipboard/package.json +1 -1
  13. package/packages/clipboard-plugin/package.json +1 -1
  14. package/packages/code-generation/package.json +1 -1
  15. package/packages/code-review/package.json +1 -1
  16. package/packages/codex-adapter/package.json +1 -1
  17. package/packages/codex-watcher/package.json +1 -1
  18. package/packages/config-capture/package.json +1 -1
  19. package/packages/connection-runtime/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/contracts/package.json +1 -1
  24. package/packages/cookie-sanitizer/package.json +1 -1
  25. package/packages/cost-optimizer/package.json +1 -1
  26. package/packages/dependency-analyzer/package.json +1 -1
  27. package/packages/django-plugin/package.json +1 -1
  28. package/packages/doc-generator/package.json +1 -1
  29. package/packages/durable-review/package.json +1 -1
  30. package/packages/e2e-tester/package.json +1 -1
  31. package/packages/evaluation-engine/package.json +1 -1
  32. package/packages/evaluation-runner/package.json +1 -1
  33. package/packages/event-bus/package.json +1 -1
  34. package/packages/event-sink/package.json +1 -1
  35. package/packages/event-store/package.json +1 -1
  36. package/packages/experience-replay/package.json +1 -1
  37. package/packages/feedback-collector/package.json +1 -1
  38. package/packages/file-copy-helper/package.json +1 -1
  39. package/packages/framework-detector/package.json +1 -1
  40. package/packages/framework-plugin-sdk/package.json +1 -1
  41. package/packages/frappe-plugin/package.json +1 -1
  42. package/packages/generic-web-plugin/package.json +1 -1
  43. package/packages/health-monitor/package.json +1 -1
  44. package/packages/intent-analyzer/package.json +1 -1
  45. package/packages/jev-judge/package.json +1 -1
  46. package/packages/knowledge-graph/package.json +1 -1
  47. package/packages/knowledge-retrieval/package.json +1 -1
  48. package/packages/laravel-plugin/package.json +1 -1
  49. package/packages/learning-engine/package.json +1 -1
  50. package/packages/logging/package.json +1 -1
  51. package/packages/mcp-adapter/package.json +1 -1
  52. package/packages/memory-engine/package.json +1 -1
  53. package/packages/milestone-manager/package.json +1 -1
  54. package/packages/model-registry/package.json +1 -1
  55. package/packages/nextjs-plugin/package.json +1 -1
  56. package/packages/notification/package.json +1 -1
  57. package/packages/observability/package.json +1 -1
  58. package/packages/okf-indexer/package.json +1 -1
  59. package/packages/paperclip-integration/package.json +1 -1
  60. package/packages/performance-optimizer/package.json +1 -1
  61. package/packages/permission-mode/package.json +1 -1
  62. package/packages/privilege-broker/package.json +1 -1
  63. package/packages/project-analyzer/package.json +1 -1
  64. package/packages/project-bootstrap/package.json +1 -1
  65. package/packages/projection-engine/package.json +1 -1
  66. package/packages/prompt-compiler/package.json +1 -1
  67. package/packages/prompt-versioning/package.json +1 -1
  68. package/packages/provider-adapter-sdk/package.json +1 -1
  69. package/packages/provider-router/package.json +1 -1
  70. package/packages/provider-selector/package.json +1 -1
  71. package/packages/providers/package.json +1 -1
  72. package/packages/qdrant-skills/package.json +1 -1
  73. package/packages/quota-manager/package.json +1 -1
  74. package/packages/rate-limiter/package.json +1 -1
  75. package/packages/react-vite-plugin/package.json +1 -1
  76. package/packages/relay/package.json +1 -1
  77. package/packages/release-manager/package.json +1 -1
  78. package/packages/requirement-compiler/package.json +1 -1
  79. package/packages/runtime/package.json +1 -1
  80. package/packages/scheduler/package.json +1 -1
  81. package/packages/scheduler-adapter/package.json +1 -1
  82. package/packages/session/package.json +1 -1
  83. package/packages/session-api/package.json +1 -1
  84. package/packages/session-export/package.json +1 -1
  85. package/packages/shared-context/package.json +1 -1
  86. package/packages/skill-mcp-client/package.json +1 -1
  87. package/packages/skill-registry/package.json +1 -1
  88. package/packages/skills/package.json +1 -1
  89. package/packages/sprint-planner/package.json +1 -1
  90. package/packages/subscription-engine/package.json +1 -1
  91. package/packages/task-compiler/package.json +1 -1
  92. package/packages/tencentdb-memory/package.json +1 -1
  93. package/packages/tencentdb-sync/package.json +1 -1
  94. package/packages/test-data-generator/package.json +1 -1
  95. package/packages/test-generator/package.json +1 -1
  96. package/packages/todo-bd-sync/package.json +1 -1
  97. package/packages/token-estimation/package.json +1 -1
  98. package/packages/token-optimizer/package.json +1 -1
  99. package/packages/tooling-preference/package.json +1 -1
  100. package/packages/trajectory/package.json +1 -1
  101. package/packages/tui/package.json +1 -1
  102. package/packages/two-phase-review/package.json +1 -1
  103. package/packages/types/package.json +1 -1
  104. package/packages/voice-api/package.json +1 -1
  105. package/packages/webmcp-tester/package.json +1 -1
  106. package/packages/wiki/package.json +1 -1
  107. package/packages/workflow-events/package.json +1 -1
  108. package/packages/workspace-scanner/package.json +1 -1
  109. package/packages/worktree/package.json +1 -1
  110. package/packages/write-review/package.json +1 -1
  111. package/scripts/npm-unpublish-with-approval.sh +256 -0
  112. package/scripts/run-node-tests.mjs +196 -0
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Remove published versions, completing a web-auth approval when the npm
4
+ # registry asks for one.
5
+ #
6
+ # The registry can refuse a write with an EOTP challenge that carries a
7
+ # browser approval URL and a polling URL. The approval URL embeds
8
+ # credential material, so it must never reach a log - a log line is
9
+ # masked, which would leave the operator with nothing to open, and a
10
+ # public artifact would publish the token. The approval therefore happens
11
+ # where the URL can be seen safely: a terminal.
12
+ #
13
+ # Modes:
14
+ #
15
+ # request <package> <version>
16
+ # Attempts the removal. On a challenge it prints the approval link
17
+ # and exits 42.
18
+ #
19
+ # wait <package> <version> <seconds>
20
+ # Polls the registry until the approval lands, writes the approved
21
+ # token into the npmrc, and exits 0. The caller retries "request".
22
+ #
23
+ # batch <package> <version> [version ...]
24
+ # Runs the whole list, handling an approval inline: it prints the
25
+ # link, waits for the approval, refreshes the credential and carries
26
+ # on.
27
+ #
28
+ # Exit codes: 0 removed, 1 failed, 42 a browser approval is needed.
29
+ #
30
+ # The credential comes from the npmrc, or from the key file
31
+ # ~/.pi-harness-runtime/keys/npm-token.txt when one exists. It is never
32
+ # printed.
33
+
34
+ set -uo pipefail
35
+
36
+ APPROVAL_DIR=/tmp/npm-approval
37
+ NPMRC="${NPM_CONFIG_USERCONFIG:-${HOME:-/tmp}/.npmrc}"
38
+ ERR_FILE=/tmp/npm-approval-error.txt
39
+ TOKEN_FILE="${NPM_TOKEN_FILE:-$HOME/.pi-harness-runtime/keys/npm-token.txt}"
40
+
41
+ # The registry hands the approved token back in the body of the polling
42
+ # response. That body is a credential, so it is written inside the
43
+ # approval directory, readable by its owner only, and removed as soon as
44
+ # the token has been read. A file in /tmp that survives the run leaves a
45
+ # usable npm credential behind on a shared machine.
46
+ APPROVAL_BODY="${APPROVAL_DIR}/done-body.json"
47
+
48
+ # The credential must exist somewhere before npm is called.
49
+ ensure_credential() {
50
+ if [ -s "$NPMRC" ] && grep -q "_authToken=" "$NPMRC"; then
51
+ echo "credential: the npmrc"
52
+ return 0
53
+ fi
54
+ if [ -s "$TOKEN_FILE" ]; then
55
+ umask 077
56
+ mkdir -p "$(dirname "$NPMRC")"
57
+ touch "$NPMRC"
58
+ grep -v '_authToken=' "$NPMRC" > "$NPMRC.tmp" || true
59
+ printf '//registry.npmjs.org/:_authToken=%s\n' \
60
+ "$(tr -d '\n' < "$TOKEN_FILE")" >> "$NPMRC.tmp"
61
+ mv "$NPMRC.tmp" "$NPMRC"
62
+ echo "credential: the key file"
63
+ return 0
64
+ fi
65
+ echo "no credential: sign in with npm, or set the key file" >&2
66
+ return 1
67
+ }
68
+
69
+ auth_url_from_error() {
70
+ grep -oE 'https://www[.]npmjs[.]com/auth/cli/[A-Za-z0-9._~-]+' "$ERR_FILE" | head -1
71
+ }
72
+
73
+ # The polling URL is the registry's, or a loopback stand-in in a test.
74
+ # The host stays pinned so a forged URL cannot redirect the poll.
75
+ done_url_from_error() {
76
+ grep -oE '(https://registry[.]npmjs[.]org|http://127[.]0[.]0[.]1(:[0-9]+)?)/-/v1/done[?]authId=[A-Za-z0-9._~-]+' "$ERR_FILE" | head -1
77
+ }
78
+
79
+ write_token_to_npmrc() {
80
+ local token="$1"
81
+ umask 077
82
+ mkdir -p "$(dirname "$NPMRC")"
83
+ touch "$NPMRC"
84
+ # Keep every other setting: only the token line is replaced.
85
+ grep -v '_authToken=' "$NPMRC" > "$NPMRC.tmp" || true
86
+ printf '//registry.npmjs.org/:_authToken=%s\n' "$token" >> "$NPMRC.tmp"
87
+ mv "$NPMRC.tmp" "$NPMRC"
88
+ }
89
+
90
+ # Attempt one removal. Sets APPROVAL_URL and DONE_URL when the registry
91
+ # asks for a browser approval.
92
+ attempt_removal() {
93
+ local package="$1" version="$2"
94
+ APPROVAL_URL=""
95
+ DONE_URL=""
96
+ if [ -n "${NPM_OTP:-}" ]; then
97
+ npm unpublish --otp="${NPM_OTP}" "${package}@${version}" \
98
+ >/dev/null 2>"$ERR_FILE"
99
+ else
100
+ npm unpublish "${package}@${version}" >/dev/null 2>"$ERR_FILE"
101
+ fi
102
+ local rc=$?
103
+ if [ $rc -eq 0 ]; then
104
+ return 0
105
+ fi
106
+ APPROVAL_URL="$(auth_url_from_error)"
107
+ DONE_URL="$(done_url_from_error)"
108
+ if [ -n "$APPROVAL_URL" ] && [ -n "$DONE_URL" ]; then
109
+ mkdir -p "$APPROVAL_DIR"
110
+ printf '%s' "$APPROVAL_URL" > "$APPROVAL_DIR/url.txt"
111
+ printf '%s' "$DONE_URL" > "$APPROVAL_DIR/done-url.txt"
112
+ return 42
113
+ fi
114
+ # The registry may also ask for a typed one-time code. The prompt
115
+ # was captured, so the retry runs with the terminal attached.
116
+ if grep -qiE 'one-time password|enter otp' "$ERR_FILE"; then
117
+ echo "the registry asked for a one-time code"
118
+ if [ -n "${NPM_OTP:-}" ]; then
119
+ npm unpublish --otp="${NPM_OTP}" "${package}@${version}"
120
+ else
121
+ npm unpublish "${package}@${version}"
122
+ fi
123
+ rc=$?
124
+ if [ $rc -eq 0 ]; then
125
+ return 0
126
+ fi
127
+ fi
128
+ return 1
129
+ }
130
+
131
+ # Wait for the approval to land and refresh the credential.
132
+ wait_for_approval() {
133
+ local done_url="$1" wait_seconds="$2"
134
+ local deadline=$(( $(date +%s) + wait_seconds ))
135
+ while [ "$(date +%s)" -lt "$deadline" ]; do
136
+ local code token
137
+ umask 077
138
+ mkdir -p "$APPROVAL_DIR"
139
+ code="$(curl -sS -o "$APPROVAL_BODY" -w '%{http_code}' \
140
+ -H "Authorization: Bearer ${NODE_AUTH_TOKEN:-}" "$done_url")"
141
+ if [ "$code" = "200" ]; then
142
+ token="$(node -e '
143
+ const fs = require("fs");
144
+ let c = {};
145
+ try { c = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); }
146
+ catch {}
147
+ process.stdout.write(c.token || "");
148
+ ' "$APPROVAL_BODY")"
149
+ # The body is a credential: it does not outlive the read.
150
+ rm -f "$APPROVAL_BODY"
151
+ if [ -n "$token" ]; then
152
+ write_token_to_npmrc "$token"
153
+ echo "the approval landed; the credential was refreshed"
154
+ return 0
155
+ fi
156
+ fi
157
+ sleep 5
158
+ done
159
+ rm -f "$APPROVAL_BODY"
160
+ echo "no approval arrived within ${wait_seconds}s"
161
+ return 1
162
+ }
163
+
164
+ print_approval_link() {
165
+ echo ""
166
+ echo "The npm registry wants a browser approval before it removes"
167
+ echo "a version. Open this link, sign in to npm if needed, and approve:"
168
+ echo ""
169
+ echo " $1"
170
+ echo ""
171
+ echo "The removal continues on its own once the approval lands."
172
+ echo ""
173
+ }
174
+
175
+ MODE="${1:?mode required}"
176
+ shift
177
+
178
+ case "$MODE" in
179
+ request)
180
+ PACKAGE="${1:?package name required}"
181
+ VERSION="${2:?version required}"
182
+ ensure_credential || exit 1
183
+ attempt_removal "$PACKAGE" "$VERSION"
184
+ rc=$?
185
+ case $rc in
186
+ 0)
187
+ echo "OK: unpublished ${PACKAGE}@${VERSION}"
188
+ exit 0
189
+ ;;
190
+ 42)
191
+ print_approval_link "$APPROVAL_URL"
192
+ exit 42
193
+ ;;
194
+ *)
195
+ echo "FAILED: ${PACKAGE}@${VERSION}"
196
+ grep -v -E 'auth/cli|v1/done' "$ERR_FILE" | tail -5
197
+ exit 1
198
+ ;;
199
+ esac
200
+ ;;
201
+ wait)
202
+ PACKAGE="${1:?package name required}"
203
+ VERSION="${2:?version required}"
204
+ WAIT_SECONDS="${3:-900}"
205
+ DONE_URL="$(cat "$APPROVAL_DIR/done-url.txt" 2>/dev/null || true)"
206
+ if [ -z "$DONE_URL" ]; then
207
+ echo "no pending approval to wait for"
208
+ exit 1
209
+ fi
210
+ wait_for_approval "$DONE_URL" "$WAIT_SECONDS"
211
+ exit $?
212
+ ;;
213
+ batch)
214
+ PACKAGE="${1:?package name required}"
215
+ shift
216
+ ensure_credential || exit 1
217
+ failed=""
218
+ for version in "$@"; do
219
+ attempt_removal "$PACKAGE" "$version"
220
+ rc=$?
221
+ if [ $rc -eq 0 ]; then
222
+ echo "OK: unpublished ${PACKAGE}@${version}"
223
+ continue
224
+ fi
225
+ if [ $rc -ne 42 ]; then
226
+ echo "FAILED: ${PACKAGE}@${version}"
227
+ grep -v -E 'auth/cli|v1/done' "$ERR_FILE" | tail -5
228
+ failed="$failed $version"
229
+ continue
230
+ fi
231
+ print_approval_link "$APPROVAL_URL"
232
+ if ! wait_for_approval "$DONE_URL" "${APPROVAL_WAIT:-900}"; then
233
+ echo "FAILED: ${PACKAGE}@${version}"
234
+ failed="$failed $version"
235
+ continue
236
+ fi
237
+ attempt_removal "$PACKAGE" "$version"
238
+ rc=$?
239
+ if [ $rc -eq 0 ]; then
240
+ echo "OK: unpublished ${PACKAGE}@${version}"
241
+ else
242
+ echo "FAILED: ${PACKAGE}@${version}"
243
+ failed="$failed $version"
244
+ fi
245
+ done
246
+ if [ -n "$failed" ]; then
247
+ echo "Versions that could not be unpublished:$failed"
248
+ exit 1
249
+ fi
250
+ exit 0
251
+ ;;
252
+ *)
253
+ echo "unknown mode: $MODE" >&2
254
+ exit 2
255
+ ;;
256
+ esac
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Run the test files that can run under node.
4
+ *
5
+ * The root suite (`npm test`) runs every test file under bun, one file per
6
+ * process. That is the authoritative suite: the harness sources are
7
+ * TypeScript and import each other with the ".js" specifier convention, and
8
+ * bun maps such a specifier to the TypeScript source. Node resolves the
9
+ * specifier to a real file, so it needs the compiled artifacts.
10
+ *
11
+ * This suite is the other half of the promise: it proves that the code a
12
+ * consumer receives still runs under node, the runtime that pi itself uses.
13
+ * Only a part of the root suite can do that, because some files cannot be
14
+ * loaded by node at all:
15
+ *
16
+ * - one imports `bun:test`, which node's ESM loader rejects outright,
17
+ * - one reads a source that uses a TypeScript parameter property, which
18
+ * node's strip-only mode cannot transform,
19
+ * - the rest reach a root-level or harness module whose internal
20
+ * specifiers use the compiled ".js" convention. Bun maps such a
21
+ * specifier to the TypeScript source; node resolves it to a real file.
22
+ * The published package ships the root sources as TypeScript, so those
23
+ * files are meant to run under bun.
24
+ *
25
+ * The npm script used to run `node --test` over every test file, which
26
+ * failed on exactly those files. A script that always fails is worse than
27
+ * no script: it looks like a failing test and hides the files that really
28
+ * do run under node.
29
+ *
30
+ * The excluded files are listed by name and reason on every run, so nobody
31
+ * can mistake "not run here" for "passed here". The root suite still runs
32
+ * all of them.
33
+ */
34
+ import { spawnSync } from "node:child_process";
35
+ import { existsSync, readdirSync } from "node:fs";
36
+ import { join, sep } from "node:path";
37
+
38
+ /**
39
+ * Files that node cannot load, and why. Each entry names a real reason; a
40
+ * file that starts working under node has to be taken out of this list, not
41
+ * left in it.
42
+ */
43
+ const NOT_NODE_COMPATIBLE = new Map([
44
+ [
45
+ "test/console-guard.test.mjs",
46
+ "imports bun:test, which node's ESM loader rejects",
47
+ ],
48
+ [
49
+ "test/skill-remote-source.test.mjs",
50
+ "the source uses a TypeScript parameter property, which node's strip-only mode cannot transform",
51
+ ],
52
+ [
53
+ "test/loop-runtime.test.mjs",
54
+ "reaches a harness module that imports siblings with the compiled '.js' convention, and the repository does not compile the harness",
55
+ ],
56
+ [
57
+ "test/quota-status.test.mjs",
58
+ "reaches a harness module that imports siblings with the compiled '.js' convention, and the repository does not compile the harness",
59
+ ],
60
+ [
61
+ "test/mirror.test.mjs",
62
+ "reaches a root-level module that imports siblings with the compiled '.js' convention, and the published package ships the root sources as TypeScript",
63
+ ],
64
+ [
65
+ "test/renderer.test.mjs",
66
+ "reaches a root-level module that imports siblings with the compiled '.js' convention, and the published package ships the root sources as TypeScript",
67
+ ],
68
+ [
69
+ "test/repair-engine.test.mjs",
70
+ "reaches a root-level module that imports siblings with the compiled '.js' convention, and the published package ships the root sources as TypeScript",
71
+ ],
72
+ [
73
+ "test/tracker.test.mjs",
74
+ "reaches a root-level module that imports siblings with the compiled '.js' convention, and the published package ships the root sources as TypeScript",
75
+ ],
76
+ ]);
77
+
78
+ /** The directories that hold test files. */
79
+ const SUITE_DIRS = ["test", join("packages", "clipboard", "test")];
80
+
81
+ /** Directories below a suite root that hold helpers, not suites. */
82
+ const NON_SUITE_DIRS = new Set(["helpers", "fixtures"]);
83
+
84
+ /**
85
+ * The per-test timeout. A few of these files pack the whole application or
86
+ * start a shell script, and node's default of five seconds is enough on a
87
+ * fast machine and not enough on a slow one.
88
+ */
89
+ const TEST_TIMEOUT_MS = 120_000;
90
+
91
+ const root = join(import.meta.dirname, "..");
92
+
93
+ /** The same path with POSIX separators, so the list compares across systems. */
94
+ function toPosixPath(file) {
95
+ return file.split(sep).join("/");
96
+ }
97
+
98
+ /** Every test file below a suite root. */
99
+ function findTestFiles(dir) {
100
+ const abs = join(root, dir);
101
+ if (!existsSync(abs)) return [];
102
+ const found = [];
103
+ for (const name of readdirSync(abs, { recursive: true })) {
104
+ if (!name.endsWith(".test.mjs")) continue;
105
+ const relativePath = toPosixPath(join(dir, name));
106
+ if (relativePath.split("/").some((segment) => NON_SUITE_DIRS.has(segment))) continue;
107
+ found.push(relativePath);
108
+ }
109
+ return found.sort();
110
+ }
111
+
112
+ const allFiles = SUITE_DIRS.flatMap(findTestFiles).sort();
113
+
114
+ if (allFiles.length === 0) {
115
+ // The published package ships the scripts but not the test files, so an
116
+ // installed copy has nothing to run. A missing suite in the repository is
117
+ // a regression, though - never let it pass silently.
118
+ const isInstalledCopy =
119
+ process.env.PI_HARNESS_INSTALLED === "1" || root.split(sep).includes("node_modules");
120
+ if (isInstalledCopy) {
121
+ console.log("node tests: no test files in this installation - nothing to run");
122
+ process.exit(0);
123
+ }
124
+ console.error("node tests: the test suite is missing - no *.test.mjs files found");
125
+ process.exit(1);
126
+ }
127
+
128
+ const excluded = [];
129
+ const selected = [];
130
+ for (const file of allFiles) {
131
+ const reason = NOT_NODE_COMPATIBLE.get(file);
132
+ if (reason) {
133
+ excluded.push({ file, reason });
134
+ } else {
135
+ selected.push(file);
136
+ }
137
+ }
138
+
139
+ // An exclusion that names a file that does not exist is a stale list, and a
140
+ // stale list hides a file that could run.
141
+ for (const { file } of excluded) {
142
+ if (!allFiles.includes(file)) {
143
+ console.error(`node tests: the exclusion list names "${file}", which does not exist`);
144
+ process.exit(1);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * An excluded file has to be excluded for a reason that still holds: the
150
+ * suite runs a file with `node --test`, so a file that passes under node
151
+ * belongs in the suite. If it passes, the exclusion is stale and it is
152
+ * quietly keeping a file out of the suite that would have run.
153
+ */
154
+ for (const { file, reason } of excluded) {
155
+ const probe = spawnSync(
156
+ process.execPath,
157
+ ["--test", `--test-timeout=${TEST_TIMEOUT_MS}`, file],
158
+ { cwd: root, encoding: "utf8", timeout: 5 * 60_000, maxBuffer: 16 * 1024 * 1024 },
159
+ );
160
+ if (probe.status === 0) {
161
+ console.error(
162
+ `node tests: "${file}" is excluded but passes under node in this tree - either the exclusion is stale, or this tree has compiled artifacts that a clean checkout does not have`,
163
+ );
164
+ process.exit(1);
165
+ }
166
+ console.log(`node tests: confirmed ${file} does not run under node (${reason})`);
167
+ }
168
+
169
+ console.log(`node tests: ${selected.length} file(s) selected, ${excluded.length} skipped`);
170
+ for (const { file, reason } of excluded) {
171
+ console.log(`node tests: skipped ${file} - ${reason}`);
172
+ }
173
+ if (selected.length === 0) {
174
+ console.error("node tests: no file can run under node");
175
+ process.exit(1);
176
+ }
177
+
178
+ const result = spawnSync(
179
+ process.execPath,
180
+ ["--test", `--test-timeout=${TEST_TIMEOUT_MS}`, ...selected],
181
+ { cwd: root, encoding: "utf8", timeout: 30 * 60_000, maxBuffer: 64 * 1024 * 1024 },
182
+ );
183
+
184
+ // node writes the report to stdout; the exit code carries the verdict.
185
+ process.stdout.write(result.stdout ?? "");
186
+ if (result.stderr) process.stderr.write(result.stderr);
187
+
188
+ if (result.error) {
189
+ console.error(`node tests: ${result.error.message}`);
190
+ process.exit(1);
191
+ }
192
+ if (result.status !== 0) {
193
+ console.error(`node tests: FAILED (exit ${result.status})`);
194
+ process.exit(result.status ?? 1);
195
+ }
196
+ console.log(`node tests: ${selected.length} file(s) passed under node`);