dsh-vibe-math 2.7.2 → 2.8.0
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/LICENSE +21 -21
- package/README.en.md +682 -681
- package/README.md +683 -682
- package/cordis.patch.yml +112 -16
- package/docs/AUDIT-CHECKLIST.md +755 -518
- package/docs/COMPAT-AUDIT-ROUND2.md +354 -328
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/audit-methodology.md +93 -0
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +613 -588
- package/docs/generate_framework_diagram_v2.py +114 -114
- package/docs/generate_framework_diagram_v3.py +127 -127
- package/docs/math-computation.md +195 -187
- package/docs/parameter-schema.md +37 -0
- package/docs/release-notes/README.en.md +2 -0
- package/docs/release-notes/README.md +2 -0
- package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
- package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
- package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
- package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
- package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
- package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
- package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
- package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
- package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
- package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
- package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
- package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
- package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
- package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
- package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
- package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
- package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
- package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
- package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
- package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
- package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
- package/docs/release-notes/TEMPLATE.md +6 -0
- package/docs/release-playbook.md +103 -0
- package/docs/slv-playbook.md +117 -0
- package/docs/status-report-fields.md +167 -0
- package/docs/test-timing.md +158 -154
- package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
- package/installer.js +793 -783
- package/package.json +260 -203
- package/prompt-corpus-persona/persona-corpus.json +9 -9
- package/prompt-corpus-persona/persona-corpus.md +225 -33
- package/prompt-corpus-v2/formal-verify-v2.json +295 -115
- package/prompt-corpus-v2/formal-verify-v2.md +2290 -282
- package/prompt-corpus-v3/formal-verify-v3.json +112 -112
- package/prompt-corpus-v3/formal-verify-v3.md +227 -143
- package/prompt-corpus-v4/formal-verify-v4.json +18 -18
- package/prompt-corpus-v4/formal-verify-v4.md +49 -44
- package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
- package/prompt-corpus-v5/prompt-corpus-v5.md +573 -207
- package/scripts/clean-temp.mjs +234 -0
- package/scripts/update-doc-counts.mjs +71 -0
- package/tests/audit-artifact-docs.mjs +72 -0
- package/tests/audit-artifact-docs.mutants.mjs +58 -0
- package/tests/audit-engine-faces.mjs +298 -0
- package/tests/audit-formal-sensitivity.mjs +345 -342
- package/tests/audit-installer-assertions.mutants.mjs +63 -0
- package/tests/audit-installer-compat.test.mjs +256 -208
- package/tests/audit-installer-policy.test.mjs +41 -8
- package/tests/audit-math-computation-contract.mjs +64 -8
- package/tests/audit-math-computation-contract.mutants.mjs +106 -0
- package/tests/audit-math-computation-parity.mjs +164 -3
- package/tests/audit-math-computation-sensitivity.mjs +5 -3
- package/tests/audit-package-membership.mjs +57 -0
- package/tests/audit-package-membership.mutants.mjs +28 -0
- package/tests/audit-participant-set-parity.mjs +160 -0
- package/tests/audit-path-discipline.mjs +83 -0
- package/tests/audit-path-discipline.mutants.mjs +51 -0
- package/tests/audit-persona-sensitivity.mjs +323 -309
- package/tests/audit-persona-surface.mutants.mjs +69 -0
- package/tests/audit-persona-surface.test.mjs +431 -390
- package/tests/audit-prompt-duplication.mjs +143 -0
- package/tests/audit-prompt-duplication.mutants.mjs +51 -0
- package/tests/audit-prompt-invariants.mjs +589 -508
- package/tests/audit-readme-bilingual.test.mjs +152 -0
- package/tests/audit-readme-counts.mjs +95 -0
- package/tests/audit-readme-counts.mutants.mjs +52 -0
- package/tests/audit-registration.mjs +78 -0
- package/tests/audit-spec-traceability.mjs +193 -193
- package/tests/audit-status-report-fields.mjs +172 -0
- package/tests/audit-status-report-fields.mutants.mjs +64 -0
- package/tests/audit-v3-registration-parity.mjs +156 -0
- package/tests/audit-v5-integrity.mjs +795 -508
- package/tests/audit-v5-lean-abstention.mjs +392 -0
- package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
- package/tests/audit-v5-prompt-duplication.mjs +88 -0
- package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
- package/tests/audit-v5-sensitivity.mjs +433 -427
- package/tests/e2e-identity-a6.mutants.mjs +65 -0
- package/tests/e2e-v5-round2.test.mjs +2453 -1981
- package/tests/formal-verify-v2.test.mjs +1778 -1724
- package/tests/formal-verify-v3.mutants.mjs +117 -0
- package/tests/formal-verify-v3.test.mjs +1724 -1671
- package/tests/formal-verify-v4.mutants.mjs +152 -0
- package/tests/formal-verify-v4.test.mjs +2114 -1574
- package/tests/formal-verify-v5.test.mjs +790 -724
- package/tests/helpers/doc-counts-format.mjs +41 -0
- package/tests/helpers/math-computation-fake-seam.mjs +4 -0
- package/tests/math-computation-a1.mutants.mjs +72 -0
- package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
- package/tests/math-computation-discovery.mutants.mjs +76 -0
- package/tests/math-computation-shared.test.mjs +392 -6
- package/tests/math-computation-v2.test.mjs +779 -0
- package/tests/math-computation-v3.test.mjs +771 -0
- package/tests/math-computation-v4.mutants.mjs +133 -0
- package/tests/math-computation-v4.test.mjs +739 -0
- package/tests/prompt-v5-integrity.test.mjs +1291 -1280
- package/tests/run-tests.mjs +393 -202
- package/tests/run-tests.mutants.mjs +70 -0
- package/tests/selfdrive-v5.mjs +560 -508
- package/tests/selfdrive-v5.mutants.mjs +132 -0
- package/tests/temp-hygiene.mutants.mjs +80 -0
- package/tests/v2-fix-probes.mutants.mjs +104 -0
- package/tests/v2-fix-probes.test.mjs +1040 -0
- package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
- package/tests/v2v3-interrupt.mutants.mjs +111 -0
- package/tests/v3-fix-probes.mutants.mjs +99 -0
- package/tests/v3-fix-probes.test.mjs +892 -0
- package/tests/v4-final-paper.mutants.mjs +108 -0
- package/tests/v5-institute-fixes.mutants.mjs +218 -0
- package/vibe-math-v2/agent.cordis.yml +378 -322
- package/vibe-math-v2/math-computation.js +144 -53
- package/vibe-math-v2/math-engines.js +12 -12
- package/vibe-math-v2/preset.yml +2 -2
- package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
- package/vibe-math-v2/vibe-math-v2.js +5097 -4755
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +491 -487
- package/vibe-math-v3/agent.cordis.yml +422 -356
- package/vibe-math-v3/math-computation.js +144 -53
- package/vibe-math-v3/math-engines.js +12 -12
- package/vibe-math-v3/preset.yml +2 -2
- package/vibe-math-v3/vibe-math-v3.js +6235 -5873
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
- package/vibe-math-v4/agent.cordis.yml +438 -402
- package/vibe-math-v4/math-computation.js +144 -53
- package/vibe-math-v4/math-engines.js +12 -12
- package/vibe-math-v4/preset.yml +2 -2
- package/vibe-math-v4/vibe-math-v4.js +4490 -4269
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1013
- package/vibe-math-v5/agent.cordis.yml +46 -12
- package/vibe-math-v5/math-computation.js +144 -53
- package/vibe-math-v5/math-engines.js +12 -12
- package/vibe-math-v5/vibe-math-v5.js +1266 -229
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1660
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
package/tests/run-tests.mjs
CHANGED
|
@@ -1,202 +1,393 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* PARALLEL TEST RUNNER — run every shipped suite AND every probe (or a filtered subset)
|
|
4
|
-
* concurrently and report per-suite timings, so the strategy for the next run is chosen
|
|
5
|
-
* from DATA instead of guesswork.
|
|
6
|
-
*
|
|
7
|
-
* Why this exists: the suites are wildly uneven (one suite is ~160 s, most are under 2 s), so a
|
|
8
|
-
* sequential sweep spends almost all of its wall time waiting for the slowest one. Running them
|
|
9
|
-
* with a worker pool makes the sweep bounded by the slowest SUITE rather than by their SUM.
|
|
10
|
-
* On this machine (4 cores) the sweep went from ~5.5 min to ~2 min; see ../docs/test-timing.md.
|
|
11
|
-
*
|
|
12
|
-
* WHAT IS COLLECTED (Round B): `tests/` holds two families of `.mjs`:
|
|
13
|
-
* · `*.test.mjs` — the suites proper;
|
|
14
|
-
* · every other `.mjs` beside this runner — the PROBES that prove those suites are not
|
|
15
|
-
* vacuous (sensitivity mutations, the registration surface, the self-driving guards).
|
|
16
|
-
* Collecting only the first family was a false green: two probe suites sat at exit 1 while
|
|
17
|
-
* every report said "30/30 green". Both families are therefore collected, and a script can
|
|
18
|
-
* only stay out through the explicit, documented `NEEDS_ARGS` list below — a stale entry in
|
|
19
|
-
* that list is a hard error in a development checkout (`.git` present), so no script can be
|
|
20
|
-
* dropped silently there. Scripts whose bare run under-covers get argument `VARIANTS` (e.g.
|
|
21
|
-
* `audit-registration.mjs` defaults to v3 alone). A PUBLISHED tarball ships only a subset of
|
|
22
|
-
* `tests/`; there the missing entries are reported instead of aborting the run.
|
|
23
|
-
*
|
|
24
|
-
* Every suite already isolates itself (each creates its own mkdtemp workspace), so parallelism is
|
|
25
|
-
* safe. Suites that WRITE a corpus take a per-run corpus dir from an env var; this runner points
|
|
26
|
-
* them at a scratch dir when it runs a suite more than once, which it never does — but the probe
|
|
27
|
-
* runner (audit-formal-sensitivity.mjs) does, and it passes its own dirs.
|
|
28
|
-
*
|
|
29
|
-
* Usage:
|
|
30
|
-
* node tests/run-tests.mjs # every suite + probe, concurrency = min(
|
|
31
|
-
* node tests/run-tests.mjs --concurrency=6
|
|
32
|
-
* node tests/run-tests.mjs --only formal # substring match on the file name (repeatable, OR)
|
|
33
|
-
* node tests/run-tests.mjs --exclude e2e-v4 # substring to skip (repeatable)
|
|
34
|
-
* node tests/run-tests.mjs --json # machine-readable summary on stdout
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
for (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PARALLEL TEST RUNNER — run every shipped suite AND every probe (or a filtered subset)
|
|
4
|
+
* concurrently and report per-suite timings, so the strategy for the next run is chosen
|
|
5
|
+
* from DATA instead of guesswork.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists: the suites are wildly uneven (one suite is ~160 s, most are under 2 s), so a
|
|
8
|
+
* sequential sweep spends almost all of its wall time waiting for the slowest one. Running them
|
|
9
|
+
* with a worker pool makes the sweep bounded by the slowest SUITE rather than by their SUM.
|
|
10
|
+
* On this machine (4 cores) the sweep went from ~5.5 min to ~2 min; see ../docs/test-timing.md.
|
|
11
|
+
*
|
|
12
|
+
* WHAT IS COLLECTED (Round B): `tests/` holds two families of `.mjs`:
|
|
13
|
+
* · `*.test.mjs` — the suites proper;
|
|
14
|
+
* · every other `.mjs` beside this runner — the PROBES that prove those suites are not
|
|
15
|
+
* vacuous (sensitivity mutations, the registration surface, the self-driving guards).
|
|
16
|
+
* Collecting only the first family was a false green: two probe suites sat at exit 1 while
|
|
17
|
+
* every report said "30/30 green". Both families are therefore collected, and a script can
|
|
18
|
+
* only stay out through the explicit, documented `NEEDS_ARGS` list below — a stale entry in
|
|
19
|
+
* that list is a hard error in a development checkout (`.git` present), so no script can be
|
|
20
|
+
* dropped silently there. Scripts whose bare run under-covers get argument `VARIANTS` (e.g.
|
|
21
|
+
* `audit-registration.mjs` defaults to v3 alone). A PUBLISHED tarball ships only a subset of
|
|
22
|
+
* `tests/`; there the missing entries are reported instead of aborting the run.
|
|
23
|
+
*
|
|
24
|
+
* Every suite already isolates itself (each creates its own mkdtemp workspace), so parallelism is
|
|
25
|
+
* safe. Suites that WRITE a corpus take a per-run corpus dir from an env var; this runner points
|
|
26
|
+
* them at a scratch dir when it runs a suite more than once, which it never does — but the probe
|
|
27
|
+
* runner (audit-formal-sensitivity.mjs) does, and it passes its own dirs.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* node tests/run-tests.mjs # every suite + probe, concurrency = min(2, cpus) (measured: 4 oversubscribes)
|
|
31
|
+
* node tests/run-tests.mjs --concurrency=6
|
|
32
|
+
* node tests/run-tests.mjs --only formal # substring match on the file name (repeatable, OR)
|
|
33
|
+
* node tests/run-tests.mjs --exclude e2e-v4 # substring to skip (repeatable)
|
|
34
|
+
* node tests/run-tests.mjs --json # machine-readable summary on stdout
|
|
35
|
+
* node tests/run-tests.mjs --temp-age-hours=12 # temp hygiene: stale-scratch threshold (default 6 h)
|
|
36
|
+
* node tests/run-tests.mjs --temp-dry-run # temp hygiene: print the plan, delete nothing
|
|
37
|
+
* node tests/run-tests.mjs --no-temp-hygiene # skip the sweep and the roomier-drive temp root
|
|
38
|
+
*/
|
|
39
|
+
import { spawn } from 'node:child_process'
|
|
40
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
41
|
+
import { cpus } from 'node:os'
|
|
42
|
+
import { join } from 'node:path'
|
|
43
|
+
import { fileURLToPath } from 'node:url'
|
|
44
|
+
|
|
45
|
+
const HERE = fileURLToPath(new URL('./', import.meta.url))
|
|
46
|
+
// Suites live beside this runner, but they resolve the presets and the corpora from the
|
|
47
|
+
// repository root, so they must keep running with that as their working directory.
|
|
48
|
+
const REPO = fileURLToPath(new URL('../', import.meta.url))
|
|
49
|
+
const argv = process.argv.slice(2)
|
|
50
|
+
// Accept BOTH `--only=x` and `--only x` (the help text used the space form, which a value-taking
|
|
51
|
+
// flag() did not understand — the filter silently did nothing and every suite still ran).
|
|
52
|
+
const flag = (name) => {
|
|
53
|
+
const out = []
|
|
54
|
+
for (let i = 0; i < argv.length; i++) {
|
|
55
|
+
const a = argv[i]
|
|
56
|
+
if (a === '--' + name && i + 1 < argv.length && !argv[i + 1].startsWith('--')) out.push(argv[++i])
|
|
57
|
+
else if (a.startsWith('--' + name + '=')) out.push(a.split('=').slice(1).join('='))
|
|
58
|
+
}
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
const has = (name) => argv.includes('--' + name)
|
|
62
|
+
const only = flag('only')
|
|
63
|
+
const exclude = flag('exclude')
|
|
64
|
+
// TEMP HYGIENE (task-12): a suite killed by the per-suite timeout cannot clean up, and some suites
|
|
65
|
+
// create many workspace dirs per run — measured here: %TEMP% held 160,923 top-level entries, 144,404 of
|
|
66
|
+
// them stale suite dirs (oldest ~2 weeks). Sweep the stale ones (top level, EXTRACTED suite prefixes,
|
|
67
|
+
// older than --temp-age-hours) and prefer the roomier drive's temp root; `os.tmpdir()` follows TEMP/TMP,
|
|
68
|
+
// so the suites need NO change. Hygiene must never break the sweep itself, and it reports on **stderr**
|
|
69
|
+
// because stdout is a MACHINE-READABLE channel here (`--counts`/`--json` are JSON.parsed by callers).
|
|
70
|
+
// Machine modes (`--self-check`, `--counts`) must not DELETE anything: the first is a guard the mutant
|
|
71
|
+
// family runs repeatedly, the second is called by scripts/update-doc-counts.mjs on every check - a sweep
|
|
72
|
+
// there is a surprising side effect (and a slow one). Only a genuine test sweep cleans up.
|
|
73
|
+
if (!has('no-temp-hygiene') && !has('self-check') && !has('counts')) {
|
|
74
|
+
try {
|
|
75
|
+
const { suitePrefixes, sweep, preferredTempRoot, useTempRoot, listProcesses, suspectProcesses } = await import('../scripts/clean-temp.mjs')
|
|
76
|
+
if (useTempRoot(preferredTempRoot())) console.error('run-tests: temp root -> ' + process.env.TEMP + ' (roomier drive preferred; D:\\_tmp when present)')
|
|
77
|
+
sweep({
|
|
78
|
+
prefixes: suitePrefixes(),
|
|
79
|
+
ageHours: Number(flag('temp-age-hours')[0] || 6),
|
|
80
|
+
dryRun: has('temp-dry-run'),
|
|
81
|
+
log: (m) => console.error(m),
|
|
82
|
+
})
|
|
83
|
+
// task-15 (MEASURED): a LEFTOVER process can steal a whole core and make honest suites look flaky - a
|
|
84
|
+
// leftover DSH host with 197,410 s CPU turned a 2822 s gate into 7875 s and amplified single suites
|
|
85
|
+
// 1.3x-4.5x. REPORT ONLY: the gate never kills anything it did not start (that decision belongs to a
|
|
86
|
+
// human, via `node scripts/clean-temp.mjs --ps [--kill]`).
|
|
87
|
+
const susp = suspectProcesses(listProcesses(), { selfPids: [process.pid, process.ppid] })
|
|
88
|
+
if (susp.length) {
|
|
89
|
+
console.error('run-tests: WARNING - ' + susp.length + ' suspected LEFTOVER process(es) (orphaned, project/team):')
|
|
90
|
+
for (const s of susp) console.error(' pid=' + s.pid + ' ' + String(s.cmd || '').slice(0, 110))
|
|
91
|
+
console.error('run-tests: leftover CPU load makes honest suites flaky; inspect with `node scripts/clean-temp.mjs --ps` (add --kill to stop them).')
|
|
92
|
+
}
|
|
93
|
+
} catch (e) { console.error('run-tests: temp hygiene skipped (' + ((e && e.message) || e) + ')') }
|
|
94
|
+
}
|
|
95
|
+
const asJson = has('json')
|
|
96
|
+
// task-13 (MEASURED): the default used to be min(4, cpus) = 4 on this 4-core box, and the heavy jobs then
|
|
97
|
+
// ran 1.3x-4.5x SLOWER than standalone - the gate's own "slowest" lines recorded v2-fix-probes.mutants
|
|
98
|
+
// 430 s standalone -> 568-651 s under the gate, v5-institute-fixes.mutants 202 s -> 419-900 s (the last
|
|
99
|
+
// one TIMING OUT at its own 900 s override). That oversubscription is what made timing-sensitive suites
|
|
100
|
+
// report failures that never reproduce standalone (e2e-v4-fixes T1/T19/T22/T23/T26/T31, e2e-v5-round2
|
|
101
|
+
// paper phase, host-failure-paths stall watchdog). Two concurrent jobs keep the machine honest; pass
|
|
102
|
+
// --concurrency=N explicitly when a faster (noisier) sweep is wanted.
|
|
103
|
+
const concurrency = Math.max(1, Number(flag('concurrency')[0] || Math.min(2, cpus().length)))
|
|
104
|
+
|
|
105
|
+
const SELF = 'run-tests.mjs'
|
|
106
|
+
// Per-suite HARD timeout (GATE_SUITE_TIMEOUT_MS overrides, ms). It bounds an intermittent hang so the
|
|
107
|
+
// gate always terminates and a hang is reported - never mistaken for "still running". 180 s comes from
|
|
108
|
+
// measurements: whole-gate wall ~200 s, slowest honest suite 135-144 s (audit-math-computation-sensitivity),
|
|
109
|
+
// so 180 s bounds a hang without killing an honest slow suite. Do NOT raise it silently to "fix" reds.
|
|
110
|
+
const SUITE_TIMEOUT_MS = Math.max(1000, Number(process.env.GATE_SUITE_TIMEOUT_MS || 180000))
|
|
111
|
+
|
|
112
|
+
// NAMED per-suite overrides for suites that are HONEST but slow (measured, not guessed). Raising a
|
|
113
|
+
// limit is a documented act: update the numbers here AND say so in the commit message - never silently.
|
|
114
|
+
// v2-fix-probes.mutants.mjs: ~43.1 s per family x 10 families ~= 430 s measured (inner child cap 120 s)
|
|
115
|
+
// v3-fix-probes.mutants.mjs: ~23.5 s per family x 11 families ~= 260 s measured (inner child cap 120 s)
|
|
116
|
+
// Both report hangs=[] with their own 120 s child cap, i.e. they are slow, not hung - so the 180 s
|
|
117
|
+
// default would misreport them. Everything else keeps the 180 s default.
|
|
118
|
+
const TIMEOUT_OVERRIDES = {
|
|
119
|
+
'v2-fix-probes.mutants.mjs': 900000,
|
|
120
|
+
'v3-fix-probes.mutants.mjs': 900000,
|
|
121
|
+
// v5-institute-fixes.mutants.mjs: MEASURED 201.9 s wall (hangs=[], skipped=[], ALL MUTANTS RED
|
|
122
|
+
// AS REQUIRED) - i.e. OVER the 180 s default, so it gets the same 900 s as the v2/v3 families.
|
|
123
|
+
// History worth keeping: a writer CLAIMED this family fitted its override when it did not, and the
|
|
124
|
+
// gate caught it as "FAILED: v5-institute-fixes.mutants.mjs [probe] (TIMEOUT after 180s)" - the
|
|
125
|
+
// named-timeout mechanism doing its job. (The older note here claimed formal-verify-v4.mutants.mjs
|
|
126
|
+
// MEASURED 70.9 s with ample headroom - that became STALE as the family grew; see its own entry below.)
|
|
127
|
+
'v5-institute-fixes.mutants.mjs': 900000,
|
|
128
|
+
// e2e-v4-fixes.test.mjs: MEASURED, not guessed (task-13). Its cases script an institute and pump
|
|
129
|
+
// member followups; the pump loops used FIXED iteration caps (i<300 etc.) which, under a loaded gate,
|
|
130
|
+
// ran out BEFORE the plugin's next scheduling tick produced the verification/debate wakes.
|
|
131
|
+
// Instrumented proof under load: `T1 DEBUG: {"fi":300,"fu":302,"running":true,"phase":"active",
|
|
132
|
+
// "autoDone":false}` - the run was ALIVE (not concluded, so NOT a product race), the loop had simply
|
|
133
|
+
// exhausted its cap. The loops now also stop on a wall-clock deadline (LOOP_CAP_MS, default 45 s per
|
|
134
|
+
// loop, reached only in that pathological case, so healthy runs are unchanged), and the suite honestly
|
|
135
|
+
// needs more than the 180 s default: it MEASURED 160.2 s wall under the gate before this change.
|
|
136
|
+
// 420 s = that measurement + headroom for the bounded deadlines. Never raise this silently.
|
|
137
|
+
'e2e-v4-fixes.test.mjs': 420000,
|
|
138
|
+
// formal-verify-v4.mutants.mjs: the family grew after the 70.9 s note above was written. Re-measured
|
|
139
|
+
// for task-13: 146.2 s standalone and 212.7 s with three heavy peers in parallel (ALL MUTANTS RED AS
|
|
140
|
+
// REQUIRED in both) - i.e. it legitimately exceeds the 180 s default under gate load, where a killed
|
|
141
|
+
// family then reports its children's F-5/N15/N18 assertions as failures (which reads like a regression
|
|
142
|
+
// but is only the timeout). 900 s matches the sibling families' convention.
|
|
143
|
+
'formal-verify-v4.mutants.mjs': 900000,
|
|
144
|
+
}
|
|
145
|
+
/** One place decides a job limit: explicit job value, then the named override, then the default. */
|
|
146
|
+
function jobLimit(job) { return job.timeoutMs || TIMEOUT_OVERRIDES[job.file] || SUITE_TIMEOUT_MS }
|
|
147
|
+
/**
|
|
148
|
+
* DIAGNOSABILITY (protocol: every red must name an assertion): a failing suite's assertion NAMES are
|
|
149
|
+
* what a reader needs, and they must appear under the FAILED line - not only in the suite's own last
|
|
150
|
+
* stdout line. Extract the failing-assertion lines (this repo prints ` - <name>` or ` FAIL - <name>`,
|
|
151
|
+
* and suites that abort print `FAILURES:`/`Error:` markers), and fall back to the raw tail when a suite
|
|
152
|
+
* names nothing.
|
|
153
|
+
*/
|
|
154
|
+
/**
|
|
155
|
+
* A reporting line's OWN prefix decides whether it names a failure. Anchored at the start (after the
|
|
156
|
+
* reporter's indent) so a token appearing INSIDE a passing line (e.g. ` ok - FAIL - ★★★ message`) can
|
|
157
|
+
* never be extracted as a failure — that false positive made a green run look red.
|
|
158
|
+
* own prefix: `FAIL`, `FAIL -`/`FAIL:`, `FAILURES:`; assertion bullets `- <name>` / `* <name>`; `✗`/`✘`.
|
|
159
|
+
* line-anchored abort markers: `SyntaxError:`, `TypeError:`, `ReferenceError:`, `Error:`.
|
|
160
|
+
* `ok`-prefixed lines are excluded explicitly, so a passing bullet is never counted.
|
|
161
|
+
*/
|
|
162
|
+
function namedFailureLines(text) {
|
|
163
|
+
const out = []
|
|
164
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
165
|
+
const l = raw.replace(/\s+$/, '')
|
|
166
|
+
if (!l.trim()) continue
|
|
167
|
+
if (/^\s*ok\b/.test(l)) continue // a PASSING line, whatever it contains
|
|
168
|
+
if (/^\s*(?:FAIL\b|FAILURES:|✗|✘)/.test(l)) { out.push(l); continue }
|
|
169
|
+
if (/^\s*(?:-|\*)\s+\S/.test(l)) { out.push(l); continue }
|
|
170
|
+
if (/^\s*(?:SyntaxError|TypeError|ReferenceError|Error):/.test(l)) { out.push(l); continue }
|
|
171
|
+
}
|
|
172
|
+
return out
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function failureDetail(r) {
|
|
176
|
+
const lines = (String(r.out || '') + '\n' + String(r.err || '')).split('\n').map((l) => l.replace(/\s+$/, '')).filter(Boolean)
|
|
177
|
+
const named = namedFailureLines(String(r.out || '') + '\n' + String(r.err || ''))
|
|
178
|
+
const chosen = (named.length ? named : lines).slice(-40)
|
|
179
|
+
return chosen.length ? chosen : ['(no captured output)']
|
|
180
|
+
}
|
|
181
|
+
// `--self-check`: prove the diagnostics above actually surface a NAME. Runs one synthetic failing child
|
|
182
|
+
// through the same extractor and asserts the extracted detail contains its assertion name; the mutant
|
|
183
|
+
// that strips `failureDetail` makes this red.
|
|
184
|
+
/** ONE place builds the failure line, so the reporter and the self-check cannot diverge. */
|
|
185
|
+
function failedLine(b) {
|
|
186
|
+
const kind = '[' + b.job.kind + ']'
|
|
187
|
+
const why = b.timedOut ? 'TIMEOUT after ' + Math.round(jobLimit(b.job) / 1000) + 's'
|
|
188
|
+
: 'exit ' + b.code + (b.job.expectExit ? ', required exit ' + b.job.expectExit : '')
|
|
189
|
+
return ' FAILED: ' + label(b.job) + ' ' + kind + ' (' + why + ')'
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (process.argv.includes('--self-check')) {
|
|
193
|
+
const { spawnSync } = await import('node:child_process')
|
|
194
|
+
const synth = spawnSync(process.execPath, ['-e', "console.error(' - synthetic assertion name XYZ'); process.exit(1)"], { encoding: 'utf8' })
|
|
195
|
+
const detail = failureDetail({ out: synth.stdout, err: synth.stderr }).join('\n')
|
|
196
|
+
const okSelf = detail.indexOf('synthetic assertion name XYZ') !== -1
|
|
197
|
+
console.log((okSelf ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': a failing child yields a NAMED assertion line (' + detail.trim().slice(0, 80) + ')')
|
|
198
|
+
// FALSE-POSITIVE direction: a PASSING line whose message literally contains `FAIL - ` must extract
|
|
199
|
+
// NOTHING (the v2/v3 owner saw a green run look red this way).
|
|
200
|
+
const leaked = namedFailureLines(' ok - FAIL - ★★★ [F2cap] the assertion message mentions FAIL - inside it\n')
|
|
201
|
+
const okPassing = leaked.length === 0
|
|
202
|
+
console.log((okPassing ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': a passing line containing "FAIL - " is NOT extracted as a failure (' + JSON.stringify(leaked) + ')')
|
|
203
|
+
// FALSE-NEGATIVE direction stays closed: a genuine FAIL line and a line-anchored abort marker extract.
|
|
204
|
+
const okReal = namedFailureLines(' FAIL - ★★★ real failure\n').length === 1
|
|
205
|
+
const okAbort = namedFailureLines('TypeError: boom\n').length === 1
|
|
206
|
+
console.log((okReal && okAbort ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': a genuine FAIL line and an abort marker are still extracted')
|
|
207
|
+
// TIMEOUT case on the REAL path: a synthetic job that sleeps past a 1 s per-job limit, run through
|
|
208
|
+
// the SAME runSuite/reporting path the gate uses. A string-only check could pass with a half-armed
|
|
209
|
+
// timer; this one cannot, because it reads the run own timedOut/exit and the line the reporter builds.
|
|
210
|
+
const slow = await runSuite({ file: '(synthetic-sleeper)', args: [], expectExit: 0, kind: 'probe', eval: 'setTimeout(() => {}, 5000)', timeoutMs: 1000 })
|
|
211
|
+
const killedByRunner = slow.timedOut === true && slow.code !== 0
|
|
212
|
+
const line = failedLine(slow)
|
|
213
|
+
const namedTimeout = /FAILED: .*\[probe\] \(TIMEOUT after 1s\)/.test(line)
|
|
214
|
+
console.log((killedByRunner ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': the RUNNER kills a sleeping job at its per-job limit (timedOut=true, exit=' + slow.code + ')')
|
|
215
|
+
console.log((namedTimeout ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': the failure line names the timeout: ' + line.trim())
|
|
216
|
+
// OVERRIDE case (real path): the named override must actually EXTEND the limit. A job that sleeps
|
|
217
|
+
// 1.5 s would be killed by 1 s, so it must survive under the override and report its own seconds.
|
|
218
|
+
// task-13: the list is DERIVED from the table. A hard-coded example went stale the moment
|
|
219
|
+
// `formal-verify-v4.mutants.mjs` earned an override - and this self-check caught exactly that, by name
|
|
220
|
+
// (which is why it is worth deriving: the invariant is "every entry resolves to its own limit, and an
|
|
221
|
+
// unlisted file gets the default", not "these three files are the special ones").
|
|
222
|
+
const NAMED_OVERRIDES = Object.keys(TIMEOUT_OVERRIDES)
|
|
223
|
+
const overrideOk = NAMED_OVERRIDES.length > 0
|
|
224
|
+
&& NAMED_OVERRIDES.every((f) => jobLimit({ file: f }) === TIMEOUT_OVERRIDES[f] && jobLimit({ file: f }) > SUITE_TIMEOUT_MS)
|
|
225
|
+
&& jobLimit({ file: 'anything-else.mjs' }) === SUITE_TIMEOUT_MS
|
|
226
|
+
const survived = await runSuite({ file: '(synthetic-ok)', args: [], expectExit: 0, kind: 'probe', eval: 'setTimeout(() => {}, 1500)', timeoutMs: 900000 })
|
|
227
|
+
const extended = survived.timedOut === false && survived.code === 0
|
|
228
|
+
const overrideLine = failedLine({ job: { file: 'v2-fix-probes.mutants.mjs', args: [], kind: 'probe', expectExit: 0 }, code: null, timedOut: true })
|
|
229
|
+
const namesOverride = /TIMEOUT after 900s/.test(overrideLine)
|
|
230
|
+
console.log((overrideOk ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': a named override is resolved for its suite and nothing else')
|
|
231
|
+
console.log((extended ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': a job that would die at 1 s SURVIVES under the override (real runSuite)')
|
|
232
|
+
console.log((namesOverride ? 'SELF-CHECK PASS' : 'SELF-CHECK FAIL') + ': a timed-out override reports its own limit (' + overrideLine.trim() + ')')
|
|
233
|
+
process.exit(okSelf && okPassing && okReal && okAbort && killedByRunner && namedTimeout && overrideOk && extended && namesOverride ? 0 : 1)
|
|
234
|
+
}
|
|
235
|
+
// The scripts that genuinely cannot run without arguments. They are named here (with the exact
|
|
236
|
+
// command a human must run) instead of being omitted quietly: an entry that no longer exists
|
|
237
|
+
// aborts the run, so a rename cannot turn into a silent gap.
|
|
238
|
+
const NEEDS_ARGS = {
|
|
239
|
+
'audit-tool-exec.mjs': 'node tests/audit-tool-exec.mjs <preset-js> <tool-name> (calls one real tool through the host path)',
|
|
240
|
+
}
|
|
241
|
+
// Argument variants, run IN ADDITION to the bare run unless `replaceBare` says the bare run is a
|
|
242
|
+
// strict subset of them. `expectExit` documents a probe whose SUCCESS is a non-zero exit (it
|
|
243
|
+
// mutates the source and requires the guarded check to fail): anything else is a FAIL here.
|
|
244
|
+
const VARIANTS = [
|
|
245
|
+
// A bare `audit-registration.mjs` audits ONLY v3 (its `process.argv[2] || v3` default), so the
|
|
246
|
+
// four presets are run explicitly and the redundant bare run is replaced.
|
|
247
|
+
{ file: 'audit-registration.mjs', args: ['vibe-math-v2/vibe-math-v2.js'], replaceBare: true },
|
|
248
|
+
{ file: 'audit-registration.mjs', args: ['vibe-math-v3/vibe-math-v3.js'] },
|
|
249
|
+
{ file: 'audit-registration.mjs', args: ['vibe-math-v4/vibe-math-v4.js'] },
|
|
250
|
+
{ file: 'audit-registration.mjs', args: ['vibe-math-v5/vibe-math-v5.js'] },
|
|
251
|
+
// The invariant scanner's own falsifiability check: it exits 0 only when every injected
|
|
252
|
+
// mutation really made a run go RED (and the unmutated control stayed green).
|
|
253
|
+
{ file: 'audit-prompt-invariants.mjs', args: ['--self-probe'] },
|
|
254
|
+
// The participant-set proof, shipped: the guard's own predicates against broken strings.
|
|
255
|
+
{ file: 'audit-participant-set-parity.mjs', args: ['--self-probe'] },
|
|
256
|
+
// §7/§8 predicates, shipped: same functions fed broken strings.
|
|
257
|
+
{ file: 'audit-math-computation-parity.mjs', args: ['--self-probe'] },
|
|
258
|
+
// §30 verify-provenance predicates (case 3), shipped.
|
|
259
|
+
{ file: 'math-computation-shared.test.mjs', args: ['--self-probe'] },
|
|
260
|
+
// The two-registration-paths probe, inverted: it applies a real description mutation and
|
|
261
|
+
// REQUIRES the parity check to exit 1 (exit 2 means the mutation no longer applies = drift).
|
|
262
|
+
{
|
|
263
|
+
file: 'audit-v3-registration-parity.mjs',
|
|
264
|
+
args: ['--self-probe', '["TOOL_DESC.vibe_math_start","TOOL_DESC.vibe_math_startX"]'],
|
|
265
|
+
expectExit: 1,
|
|
266
|
+
},
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
const present = readdirSync(HERE).filter((f) => f.endsWith('.mjs')).sort()
|
|
270
|
+
const presentSet = new Set(present)
|
|
271
|
+
// A published tarball ships only a SUBSET of `tests/` (see docs/test-timing.md §1), so the
|
|
272
|
+
// scripts that stay behind are simply ABSENT there. The skip/variant lists are therefore
|
|
273
|
+
// enforced strictly in a development checkout (a `.git` entry marks one) and merely REPORTED —
|
|
274
|
+
// never silently dropped, they still appear in every run's output — in a partial tree.
|
|
275
|
+
const DEV_CHECKOUT = existsSync(join(REPO, '.git'))
|
|
276
|
+
const stale = Object.keys(NEEDS_ARGS).filter((f) => !presentSet.has(f))
|
|
277
|
+
if (stale.length && DEV_CHECKOUT) {
|
|
278
|
+
console.error('NEEDS_ARGS names a script that no longer exists: ' + stale.join(', ') + ' — fix the skip list, do not delete the entry blindly')
|
|
279
|
+
process.exit(2)
|
|
280
|
+
}
|
|
281
|
+
function label(j) { return j.file + (j.args.length ? ' ' + j.args.join(' ') : '') }
|
|
282
|
+
const replacedBare = new Set(VARIANTS.filter((v) => v.replaceBare).map((v) => v.file))
|
|
283
|
+
let suites = []
|
|
284
|
+
for (const file of present) {
|
|
285
|
+
if (file === SELF || NEEDS_ARGS[file] || replacedBare.has(file)) continue
|
|
286
|
+
suites.push({ file, args: [], expectExit: 0, kind: file.endsWith('.test.mjs') ? 'suite' : 'probe' })
|
|
287
|
+
}
|
|
288
|
+
for (const v of VARIANTS) {
|
|
289
|
+
if (!presentSet.has(v.file)) {
|
|
290
|
+
if (DEV_CHECKOUT) { console.error('a VARIANTS entry names a missing script: ' + v.file); process.exit(2) }
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
// A VARIANT is an EXTRA instrumented run of the SAME script (argument variants), never an additional
|
|
294
|
+
// suite: the counting unit for `suites` is "the suites proper", one per script. Classifying by file name
|
|
295
|
+
// here would inflate `suites` whenever a suite gains a `--self-probe` job (R18: it did, 44 -> 45).
|
|
296
|
+
suites.push({ file: v.file, args: v.args || [], expectExit: v.expectExit || 0, kind: 'probe' })
|
|
297
|
+
}
|
|
298
|
+
suites.sort((a, b) => (label(a) < label(b) ? -1 : 1))
|
|
299
|
+
if (only.length) suites = suites.filter((j) => only.some((o) => label(j).includes(o)))
|
|
300
|
+
if (exclude.length) suites = suites.filter((j) => !exclude.some((o) => label(j).includes(o)))
|
|
301
|
+
if (!suites.length) { console.error('no suites matched'); process.exit(2) }
|
|
302
|
+
|
|
303
|
+
// --counts: print the DERIVED suite/probe totals (the same job list the gate runs) as JSON, then
|
|
304
|
+
// exit. Docs quote these numbers, so they must be derived and checked rather than typed by hand.
|
|
305
|
+
if (process.argv.includes('--counts')) {
|
|
306
|
+
const suiteN = suites.filter((j) => j.kind === 'suite').length
|
|
307
|
+
console.log(JSON.stringify({ total: suites.length, suites: suiteN, probes: suites.length - suiteN }))
|
|
308
|
+
process.exit(0)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function runSuite(job) {
|
|
312
|
+
return new Promise((resolve) => {
|
|
313
|
+
const t0 = Date.now()
|
|
314
|
+
const argv = job.eval ? ['-e', job.eval] : [join(HERE, job.file), ...job.args]
|
|
315
|
+
const limitMs = jobLimit(job)
|
|
316
|
+
const child = spawn(process.execPath, argv, { cwd: REPO, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
317
|
+
let timedOut = false
|
|
318
|
+
const killer = setTimeout(() => { timedOut = true; try { child.kill('SIGKILL') } catch (e) { /* already gone */ } }, limitMs)
|
|
319
|
+
const clearKiller = () => clearTimeout(killer)
|
|
320
|
+
let out = '', err = ''
|
|
321
|
+
child.stdout.on('data', (d) => { out += d.toString() })
|
|
322
|
+
child.stderr.on('data', (d) => { err += d.toString() })
|
|
323
|
+
child.on('error', (e) => { clearKiller(); resolve({ job, code: -1, ms: Date.now() - t0, out, err: err + '\n' + String(e), timedOut }) })
|
|
324
|
+
child.on('close', (code) => { clearKiller(); resolve({ job, code, ms: Date.now() - t0, out, err, timedOut }) })
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const results = []
|
|
329
|
+
let cursor = 0
|
|
330
|
+
const started = Date.now()
|
|
331
|
+
async function worker(id) {
|
|
332
|
+
for (;;) {
|
|
333
|
+
const i = cursor++
|
|
334
|
+
if (i >= suites.length) return
|
|
335
|
+
const r = await runSuite(suites[i])
|
|
336
|
+
const tail = String(r.out).trim().split('\n').filter(Boolean).slice(-1)[0] || ''
|
|
337
|
+
r.ok = r.code === r.job.expectExit
|
|
338
|
+
results[i] = r
|
|
339
|
+
if (!asJson) {
|
|
340
|
+
const mark = r.ok ? 'PASS' : 'FAIL'
|
|
341
|
+
console.log(
|
|
342
|
+
mark + ' ' + r.job.file.padEnd(34) +
|
|
343
|
+
' exit=' + String(r.code).padStart(3) + (r.job.expectExit ? '(want ' + r.job.expectExit + ')' : ' ') +
|
|
344
|
+
' ' + (r.ms / 1000).toFixed(1).padStart(6) + 's' +
|
|
345
|
+
(r.job.args.length ? ' [' + r.job.args.join(' ').slice(0, 40) + ']' : '') +
|
|
346
|
+
(tail ? ' ' + tail.slice(0, 60) : '')
|
|
347
|
+
)
|
|
348
|
+
if (!r.ok) {
|
|
349
|
+
for (const l of failureDetail(r)) console.log(' ' + l)
|
|
350
|
+
}
|
|
351
|
+
} else if (!r.ok) {
|
|
352
|
+
// In --json mode keep stdout machine-readable: the failure detail travels in the JSON.
|
|
353
|
+
r.tailDetail = failureDetail(r).join('\n')
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, suites.length) }, (_, i) => worker(i)))
|
|
358
|
+
|
|
359
|
+
const wall = (Date.now() - started) / 1000
|
|
360
|
+
const sum = results.reduce((a, r) => a + r.ms, 0) / 1000
|
|
361
|
+
const bad = results.filter((r) => !r.ok)
|
|
362
|
+
const slowest = results.slice().sort((a, b) => b.ms - a.ms).slice(0, 5)
|
|
363
|
+
const suiteCount = results.filter((r) => r.job.kind === 'suite').length
|
|
364
|
+
|
|
365
|
+
if (asJson) {
|
|
366
|
+
console.log(JSON.stringify({
|
|
367
|
+
concurrency, wallSeconds: Number(wall.toFixed(1)), sumSeconds: Number(sum.toFixed(1)),
|
|
368
|
+
pass: results.length - bad.length, fail: bad.length,
|
|
369
|
+
suites: suiteCount, probes: results.length - suiteCount,
|
|
370
|
+
devCheckout: DEV_CHECKOUT,
|
|
371
|
+
skippedNeedsArgs: NEEDS_ARGS,
|
|
372
|
+
runs: results.map((r) => ({
|
|
373
|
+
file: r.job.file, args: r.job.args, expectExit: r.job.expectExit, exit: r.code,
|
|
374
|
+
seconds: Number((r.ms / 1000).toFixed(1)),
|
|
375
|
+
...(r.tailDetail ? { detail: r.tailDetail } : {}),
|
|
376
|
+
})),
|
|
377
|
+
}, null, 2))
|
|
378
|
+
} else {
|
|
379
|
+
console.log('')
|
|
380
|
+
console.log('concurrency ' + concurrency + ' · wall ' + wall.toFixed(1) + 's · sum of suite times ' + sum.toFixed(1) + 's'
|
|
381
|
+
+ ' · speed-up x' + (sum / Math.max(wall, 0.001)).toFixed(2))
|
|
382
|
+
console.log('slowest: ' + slowest.map((r) => r.job.file.replace('.test.mjs', '') + ' ' + (r.ms / 1000).toFixed(1) + 's').join(' · '))
|
|
383
|
+
console.log('TOTAL ' + results.length + ' PASS ' + (results.length - bad.length) + ' FAIL ' + bad.length
|
|
384
|
+
+ ' (suites ' + suiteCount + ' · probes ' + (results.length - suiteCount) + ')')
|
|
385
|
+
for (const b of bad) {
|
|
386
|
+
console.log(failedLine(b))
|
|
387
|
+
for (const l of failureDetail(b)) console.log(' ' + l)
|
|
388
|
+
}
|
|
389
|
+
for (const [f, why] of Object.entries(NEEDS_ARGS)) {
|
|
390
|
+
console.log(' SKIPPED (needs CLI args): ' + f + ' — ' + why + (presentSet.has(f) ? '' : ' [not present in this checkout]'))
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
process.exit(bad.length === 0 ? 0 : 1)
|