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.
- package/package.json +2 -2
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/npm-unpublish-with-approval.sh +256 -0
- 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`);
|