@azure-id/orc 0.56.0 → 1.0.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/CHANGELOG.md +228 -0
- package/README-id.md +143 -53
- package/README.md +124 -103
- package/bin/build-agents.js +18 -11
- package/bin/cli.js +4139 -315
- package/bin/onboarding-content.js +0 -3
- package/bin/test-run.js +491 -0
- package/bin/verify-contracts.js +4256 -3277
- package/bin/verify-package.js +563 -498
- package/bin/webui/api.js +1186 -1163
- package/bin/webui/app.html +9 -0
- package/bin/webui/css/06-responsive.css +120 -108
- package/bin/webui/css/panels/extra.css +29 -0
- package/bin/webui/css/panels/lanes.css +134 -0
- package/bin/webui/css/panels/settings.css +79 -3
- package/bin/webui/fixtures/extra.js +2036 -1934
- package/bin/webui/fixtures/flow.js +2 -2
- package/bin/webui/fixtures/index.js +503 -486
- package/bin/webui/fixtures/lanes.js +218 -0
- package/bin/webui/fixtures/settings.js +261 -34
- package/bin/webui/fixtures/stats.js +3 -3
- package/bin/webui/i18n/TERMS.md +135 -134
- package/bin/webui/i18n/en/extra.json +345 -313
- package/bin/webui/i18n/en/lanes.json +29 -0
- package/bin/webui/i18n/en/nav.json +1 -0
- package/bin/webui/i18n/en/overview.json +2 -0
- package/bin/webui/i18n/en/settings.json +16 -5
- package/bin/webui/i18n/id/extra.json +345 -313
- package/bin/webui/i18n/id/lanes.json +29 -0
- package/bin/webui/i18n/id/nav.json +1 -0
- package/bin/webui/i18n/id/overview.json +2 -0
- package/bin/webui/i18n/id/settings.json +15 -4
- package/bin/webui/js/01-i18n.js +1 -0
- package/bin/webui/js/panels/extra.js +3199 -3011
- package/bin/webui/js/panels/lanes.js +259 -0
- package/bin/webui/js/panels/overview.js +10 -0
- package/bin/webui/js/panels/settings.js +149 -3
- package/package.json +3 -3
- package/templates/agents/MODEL-MAPPING.md +23 -35
- package/templates/agents/orc-executor-opus-4-7-high.md +1 -1
- package/templates/agents/orc-executor-opus-4-7-med.md +1 -1
- package/templates/agents/orc-executor-opus-4-8-high.md +1 -1
- package/templates/agents/orc-executor-opus-5-high.md +1 -1
- package/templates/agents/orc-executor-opus-5-low.md +1 -1
- package/templates/agents/orc-executor-opus-5-med.md +1 -1
- package/templates/agents/orc-retro-opus-5-med.md +73 -73
- package/templates/agents/orc-retro-sonnet-5-high.md +72 -72
- package/templates/agents/orc-trace-writer-haiku-4-5.md +107 -107
- package/templates/commands/orc-route.md +1 -1
- package/templates/commands/orc.md +1 -1
- package/templates/skills/_shared/README.md +7 -3
- package/templates/skills/_shared/config-precedence.md +198 -0
- package/templates/skills/_shared/drift-recovery.md +1 -1
- package/templates/skills/_shared/extra-dispatch.md +292 -6
- package/templates/skills/_shared/interview.md +1 -1
- package/templates/skills/_shared/opus5-only.md +9 -7
- package/templates/skills/_shared/phases/README.md +82 -0
- package/templates/skills/{orc/references → _shared/phases}/analyst-gates.md +15 -3
- package/templates/skills/_shared/phases/execution.md +143 -0
- package/templates/skills/{orc/references → _shared/phases}/house-rules.md +13 -1
- package/templates/skills/{orc/references → _shared/phases}/intake.md +15 -2
- package/templates/skills/_shared/phases/mock-example.md +56 -0
- package/templates/skills/{orc/references → _shared/phases}/plan-handoff.md +16 -3
- package/templates/skills/_shared/phases/planning.md +135 -0
- package/templates/skills/_shared/phases/preflight.md +76 -0
- package/templates/skills/_shared/phases/review.md +53 -0
- package/templates/skills/_shared/phases/scoring.md +65 -0
- package/templates/skills/{orc/references → _shared/phases}/security-checklist.md +12 -1
- package/templates/skills/_shared/phases/security.md +55 -0
- package/templates/skills/_shared/phases/ship.md +88 -0
- package/templates/skills/{orc/references/stop-and-resume.md → _shared/phases/stop-resume.md} +169 -160
- package/templates/skills/_shared/phases/summary.md +48 -0
- package/templates/skills/_shared/phases/testgen.md +51 -0
- package/templates/skills/{orc/references/trace-protocol.md → _shared/phases/trace.md} +49 -6
- package/templates/skills/_shared/phases/verify.md +70 -0
- package/templates/skills/{orc/references → _shared/phases}/wave-grouping.md +14 -2
- package/templates/skills/{orc/references → _shared/phases}/wiki-consult.md +13 -1
- package/templates/skills/_shared/read-ladder.md +48 -0
- package/templates/skills/context-combiner/SKILL.md +214 -187
- package/templates/skills/orc/README.md +148 -150
- package/templates/skills/orc/SKILL.md +70 -358
- package/templates/skills/orc/config.md +137 -389
- package/templates/skills/orc/examples/full-run-mock.md +1 -1
- package/templates/skills/orc/references/effort-and-mode.md +16 -18
- package/templates/skills/orc/references/phases/intake.md +47 -0
- package/templates/skills/orc/references/phases/integration.md +19 -0
- package/templates/skills/orc/references/preflight-report.md +4 -4
- package/templates/skills/orc/references/ultra-mode.md +117 -123
- package/templates/skills/orc/schemas/planning-output.md +279 -279
- package/templates/skills/orc/subskills/orc-planner/SKILL.md +260 -264
- package/templates/skills/orc/subskills/orc-pr/stack-gate.md +4 -1
- package/templates/skills/orc-aftermath/SKILL.md +153 -136
- package/templates/skills/orc-analyze/SKILL.md +247 -220
- package/templates/skills/orc-analyze/references/branching.md +51 -51
- package/templates/skills/orc-analyze/references/thin-input.md +1 -1
- package/templates/skills/orc-analyze-mini/SKILL.md +128 -112
- package/templates/skills/orc-boundary/SKILL.md +244 -208
- package/templates/skills/orc-brainstorm/SKILL.md +372 -339
- package/templates/skills/orc-budget/SKILL.md +243 -225
- package/templates/skills/orc-challenge/README.md +1 -1
- package/templates/skills/orc-challenge/SKILL.md +293 -255
- package/templates/skills/orc-challenge/examples/council-full-roster.md +1 -1
- package/templates/skills/orc-claude/SKILL.md +216 -199
- package/templates/skills/orc-diy/README.md +2 -1
- package/templates/skills/orc-diy/SKILL.md +129 -70
- package/templates/skills/orc-diy/references/blocks/analyze.md +1 -1
- package/templates/skills/orc-diy/references/compile.md +21 -7
- package/templates/skills/orc-diy/references/flow-schema.md +2 -2
- package/templates/skills/orc-doc/SKILL.md +44 -7
- package/templates/skills/orc-explain/SKILL.md +19 -0
- package/templates/skills/orc-export/SKILL.md +26 -5
- package/templates/skills/orc-fast/SKILL.md +215 -187
- package/templates/skills/orc-grill/SKILL.md +233 -207
- package/templates/skills/orc-handoff/SKILL.md +223 -204
- package/templates/skills/orc-learn/SKILL.md +176 -156
- package/templates/skills/orc-mini/SKILL.md +41 -23
- package/templates/skills/orc-pact/SKILL.md +250 -218
- package/templates/skills/orc-pattern/SKILL.md +149 -124
- package/templates/skills/orc-poly/SKILL.md +237 -216
- package/templates/skills/orc-poly/references/gather.md +1 -1
- package/templates/skills/orc-pr-driver/SKILL.md +196 -167
- package/templates/skills/orc-pr-driver/references/orc-run-split.md +1 -1
- package/templates/skills/orc-pr-setup/SKILL.md +212 -184
- package/templates/skills/orc-quick/README.md +2 -2
- package/templates/skills/orc-quick/SKILL.md +346 -313
- package/templates/skills/orc-quick/references/dispatch-gate.md +12 -4
- package/templates/skills/orc-retro/SKILL.md +244 -222
- package/templates/skills/orc-retro/examples/retro-mock.md +171 -171
- package/templates/skills/orc-route/SKILL.md +183 -165
- package/templates/skills/orc-verify/SKILL.md +100 -86
- package/templates/skills/orc-wiki/SKILL.md +55 -206
- package/templates/skills/orc-wiki/references/phases/phase-0.md +71 -0
- package/templates/skills/orc-wiki/references/phases/phase-1.md +35 -0
- package/templates/skills/orc-wiki/references/phases/phase-2.md +52 -0
- package/templates/skills/orc-wiki/references/phases/phase-3.md +57 -0
- package/templates/skills/orc-wiki/references/phases/phase-3c.md +36 -0
- package/templates/agents/orc-advisor-fable-5.md +0 -50
- package/templates/agents/orc-analyst-fable-5.md +0 -115
- package/templates/agents/orc-judge-fable-5.md +0 -79
- package/templates/agents/orc-planner-fable-5.md +0 -152
- package/templates/agents/orc-reviewer-fable-5.md +0 -57
- package/templates/skills/_shared/fable5-override.md +0 -56
- package/templates/skills/orc-diy/references/blocks/execution.md +0 -42
- package/templates/skills/orc-diy/references/blocks/mock-example.md +0 -22
- package/templates/skills/orc-diy/references/blocks/planning.md +0 -34
- package/templates/skills/orc-diy/references/blocks/review.md +0 -18
- package/templates/skills/orc-diy/references/blocks/scoring.md +0 -16
- package/templates/skills/orc-diy/references/blocks/security.md +0 -25
- package/templates/skills/orc-diy/references/blocks/ship.md +0 -25
- package/templates/skills/orc-diy/references/blocks/summary.md +0 -18
- package/templates/skills/orc-diy/references/blocks/testgen.md +0 -17
- package/templates/skills/orc-diy/references/blocks/trace.md +0 -28
- package/templates/skills/orc-diy/references/blocks/verify.md +0 -25
|
@@ -116,9 +116,6 @@ const SECTIONS = [
|
|
|
116
116
|
"Groups:",
|
|
117
117
|
" Common waves, pauses, scoring granularity, analysis depth, tests,",
|
|
118
118
|
" pattern gate, security pass, stacked PRs (stacked_pr + thresholds)",
|
|
119
|
-
" Fable 5 fable5_enabled / fable5_effort / fable5_roles — HARD-GATED role",
|
|
120
|
-
" override: route analyze/plan/advisor/judge/review to Fable 5 agents",
|
|
121
|
-
" (nothing happens unless fable5_enabled: true AND roles are selected)",
|
|
122
119
|
" Advanced dirs, crosslink day-tiers, orchestrator model, log dir",
|
|
123
120
|
],
|
|
124
121
|
},
|
package/bin/test-run.js
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// ORC's test runner — POOLS, not shards. (v1.0.0 W0; design-07.)
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS. `npm test` used to be `node --test --test-concurrency=6
|
|
6
|
+
// test/`: one global concurrency for 41 files with four completely different
|
|
7
|
+
// resource appetites. 250 of 597 tests cost 36 s between them and were being
|
|
8
|
+
// throttled to protect ~12 files that spawn real child processes, stand up a
|
|
9
|
+
// loopback server, or derive scrypt at N=2^17. One number cannot serve both,
|
|
10
|
+
// and the number that keeps the badly-behaved files honest is the number the
|
|
11
|
+
// whole suite ran at. `test/_helpers.js` carries the full scar: the peak came
|
|
12
|
+
// 8 -> 6 after a full run failed TWENTY-NINE tests that all passed on their own
|
|
13
|
+
// file. That comment is still the REASON these numbers exist; it now points
|
|
14
|
+
// here for the numbers themselves.
|
|
15
|
+
//
|
|
16
|
+
// So: every file DECLARES its resource class in a first-lines pragma
|
|
17
|
+
//
|
|
18
|
+
// // @test-pool net — stands up the fake provider on loopback
|
|
19
|
+
//
|
|
20
|
+
// and files run pool by pool, each pool at its own concurrency. A class is
|
|
21
|
+
// never sniffed from the source: a grep would be wrong the first time somebody
|
|
22
|
+
// puts a spawnSync inside a helper, which is a debugging round this repo has
|
|
23
|
+
// already paid for once (test/cli/_fake-provider.js).
|
|
24
|
+
//
|
|
25
|
+
// This script schedules; `node --test` still does the testing. One child per
|
|
26
|
+
// file — which is what `node --test` does internally anyway — so the pass count
|
|
27
|
+
// is identical to the old command, per-file durations are exact, and a failure
|
|
28
|
+
// prints ONLY the failing file's output instead of 3,000 lines of TAP.
|
|
29
|
+
//
|
|
30
|
+
// THE GUARD THAT MATTERS: a selective run is NEVER the gate. `--file`,
|
|
31
|
+
// `--pool` and `--since` all set `is_gate: false`, name every file they skipped
|
|
32
|
+
// in the JSON, and print a loud last line. A green partial run reported as a
|
|
33
|
+
// green suite is the worst possible outcome of making a suite faster.
|
|
34
|
+
// There is no retry-until-green here, at any level: a flake is RECORDED.
|
|
35
|
+
const fs = require("fs");
|
|
36
|
+
const os = require("os");
|
|
37
|
+
const path = require("path");
|
|
38
|
+
const { spawn, spawnSync } = require("child_process");
|
|
39
|
+
|
|
40
|
+
// Run start, so every file reports WHEN it ran and not only for how long.
|
|
41
|
+
let T0 = Date.now();
|
|
42
|
+
const REPO = path.join(__dirname, "..");
|
|
43
|
+
const TEST_DIR = path.join(REPO, "test");
|
|
44
|
+
const TIMINGS = path.join(TEST_DIR, ".timings.json");
|
|
45
|
+
|
|
46
|
+
// ── the pools ───────────────────────────────────────────────────────────────
|
|
47
|
+
// Concurrency numbers here are EMPIRICAL, not chosen. Raising one requires
|
|
48
|
+
// three clean runs of that pool (design-07 §5.3) — the v0.55.0 scar again: 8
|
|
49
|
+
// was "already marginal" and only became visible when two files were added.
|
|
50
|
+
const CPUS = os.cpus().length || 4;
|
|
51
|
+
const POOLS = {
|
|
52
|
+
// Pure computation: reads files, greps the payload, evaluates pure functions
|
|
53
|
+
// out of cli.js. Over-subscription cannot hurt these.
|
|
54
|
+
pure: { concurrency: Math.max(2, CPUS - 2), why: "no child process, no server, no crypto, no real clock" },
|
|
55
|
+
// A child process is memory and a scheduler slot, not a socket.
|
|
56
|
+
spawn: { concurrency: 4, why: "shells `node bin/cli.js` or an installed hook" },
|
|
57
|
+
// THE STARVATION CLASS. A real timeout on a loopback socket is what breaks
|
|
58
|
+
// when these are crowded, and it breaks looking exactly like a regression.
|
|
59
|
+
net: { concurrency: 2, why: "stands up a fake provider, a fake CLI or the ui server on loopback" },
|
|
60
|
+
// scrypt at N=2^17 is 128 MB and a beat of wall clock, ON PURPOSE — that cost
|
|
61
|
+
// IS the vault's defence and must never be tuned down. It gets a lane to
|
|
62
|
+
// itself instead.
|
|
63
|
+
heavy: { concurrency: 1, why: "derives scrypt at N=2^17" },
|
|
64
|
+
};
|
|
65
|
+
const POOL_ORDER = ["pure", "spawn", "net", "heavy"];
|
|
66
|
+
// THE POOLS OVERLAP, and the correction is worth writing down (v1.0.0 W0).
|
|
67
|
+
//
|
|
68
|
+
// design-07 §3 had the pools run sequentially, one after another. Built that
|
|
69
|
+
// way and MEASURED, the full gate took 504 s against the old command's 328 s —
|
|
70
|
+
// slower, because a pool boundary serialises the whole box: `net` at 2 left 14
|
|
71
|
+
// cores idle for three minutes. The design's arithmetic assumed the cost was
|
|
72
|
+
// where the TEST durations were (250 free tests, 36 s), and the cost is
|
|
73
|
+
// actually per-FILE wall clock — 1,037 s of it, most of it node startup and
|
|
74
|
+
// fixture setup inside files that spawn.
|
|
75
|
+
//
|
|
76
|
+
// So the pools run TOGETHER, under one global cap, each still holding its own
|
|
77
|
+
// per-class limit. That keeps the thesis exactly — `net` is never more than 2
|
|
78
|
+
// and `heavy` is never more than 1 — and it is strictly SAFER than the old
|
|
79
|
+
// single number, under which all 6 concurrent processes could be `net` files.
|
|
80
|
+
// The global cap is what stops 14 + 4 + 2 + 1 becoming 21 processes.
|
|
81
|
+
// v1.0.0 W8 — a MEASUREMENT SEAM, not a behaviour change. Unset, this is 8 and
|
|
82
|
+
// the runner is byte-identical to what shipped (the `ORC_TEST_BUDGET_FLOOR_MS`
|
|
83
|
+
// precedent from W0/D22).
|
|
84
|
+
//
|
|
85
|
+
// It exists because the gate itself became unreliable. The full suite flaked on
|
|
86
|
+
// UNCHANGED code in three consecutive waves — W6 (12 failures, detail lost),
|
|
87
|
+
// W7 (4), W8 (3, then 13) — always in the `spawn`/`net` pools, always with
|
|
88
|
+
// ~24 s durations that are a budget elapsing rather than an assertion failing,
|
|
89
|
+
// and always on a run 35-90% slower than the ~188 s norm. Every affected file
|
|
90
|
+
// passes in isolation.
|
|
91
|
+
//
|
|
92
|
+
// Measured on the same tree, same commit: at 8 the suite failed 13 tests in
|
|
93
|
+
// 358 s; at 4 it passed 653/653 in 302 s. SLOWER at higher concurrency is the
|
|
94
|
+
// contention signature — the box is oversubscribed, so real timeouts elapse.
|
|
95
|
+
//
|
|
96
|
+
// ANSWERED at v1.0.0 W11: the default is 4, on that measurement. A cap that is
|
|
97
|
+
// both slower and red is not buying anything, and a green run at 8 on an idle
|
|
98
|
+
// box (W9, W10) is evidence about the box rather than about the cap. The seam
|
|
99
|
+
// stays — `ORC_TEST_GLOBAL_CONCURRENCY=8` reproduces the old behaviour exactly.
|
|
100
|
+
const GLOBAL_CONCURRENCY = Number(process.env.ORC_TEST_GLOBAL_CONCURRENCY) || 4;
|
|
101
|
+
// A file with no pragma lands in the safe middle AND is reported by name.
|
|
102
|
+
const DEFAULT_POOL = "spawn";
|
|
103
|
+
|
|
104
|
+
// ── which tests a source change can break ───────────────────────────────────
|
|
105
|
+
// A DECLARED table, never an inference. A changed path that matches no rule
|
|
106
|
+
// runs EVERYTHING — the fail-safe direction. An engine that quietly picks three
|
|
107
|
+
// files is how a suite stops testing things.
|
|
108
|
+
const TEST_TOUCHES = [
|
|
109
|
+
[/^bin\/cli\.js$/, ["test/cli/", "test/lanes/", "test/webui/api.test.js", "test/webui/serve.test.js"]],
|
|
110
|
+
[/^bin\/webui\//, ["test/webui/"]],
|
|
111
|
+
[/^bin\/pricing\.json$/, ["test/cli/extra-stats.test.js"]],
|
|
112
|
+
[/^bin\/providers\.json$/, ["test/cli/extra-tools.test.js", "test/cli/extra-routing.test.js"]],
|
|
113
|
+
[/^bin\/mockrun-catalog\.js$/, ["test/cli/knowledge.test.js", "test/docs.test.js"]],
|
|
114
|
+
[/^bin\/verify-/, ["test/payload.test.js"]],
|
|
115
|
+
[/^templates\/hooks\//, ["test/hooks.test.js"]],
|
|
116
|
+
[/^templates\/agents\//, ["test/payload.test.js"]],
|
|
117
|
+
[/^templates\/(skills|commands)\//, ["test/payload.test.js", "test/docs.test.js"]],
|
|
118
|
+
// The lane suites grep the payload of the lane they are named after, so a
|
|
119
|
+
// skill edit inside one of these directories widens to its own suite. This is
|
|
120
|
+
// the ONLY inference in the table and it is a name match, not a guess.
|
|
121
|
+
[/^templates\/(skills|commands)\/orc-doc/, ["test/lanes/doc.test.js"]],
|
|
122
|
+
[/^templates\/(skills|commands)\/orc-challenge/, ["test/lanes/challenge.test.js", "test/lanes/council.test.js"]],
|
|
123
|
+
[/^templates\/skills\/_shared\//, ["test/lanes/"]],
|
|
124
|
+
[/^test\//, ["SELF"]],
|
|
125
|
+
[/^(README|README-id|CHANGELOG)\.md$/, ["test/docs.test.js"]],
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
function walk(dir, out) {
|
|
129
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
130
|
+
const p = path.join(dir, e.name);
|
|
131
|
+
if (e.isDirectory()) walk(p, out);
|
|
132
|
+
else if (e.name.endsWith(".js")) out.push(path.relative(REPO, p).split(path.sep).join("/"));
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The pragma is read from the first lines only: a `@test-pool` mentioned in
|
|
138
|
+
// prose 400 lines down is documentation, not a declaration.
|
|
139
|
+
function poolOf(rel) {
|
|
140
|
+
const head = fs.readFileSync(path.join(REPO, rel), "utf8").split(/\r?\n/).slice(0, 8).join("\n");
|
|
141
|
+
const m = /@test-pool\s+(\w+)/.exec(head);
|
|
142
|
+
if (!m) return { pool: DEFAULT_POOL, declared: false };
|
|
143
|
+
if (!POOLS[m[1]]) return { pool: DEFAULT_POOL, declared: false, bad: m[1] };
|
|
144
|
+
return { pool: m[1], declared: true };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function readTimings() {
|
|
148
|
+
try {
|
|
149
|
+
return JSON.parse(fs.readFileSync(TIMINGS, "utf8"));
|
|
150
|
+
} catch (_) {
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── TAP parsing ─────────────────────────────────────────────────────────────
|
|
156
|
+
// Only the trailing counters are read. The per-test lines are kept as raw
|
|
157
|
+
// output and printed only when the file fails.
|
|
158
|
+
function counters(tap) {
|
|
159
|
+
const num = (k) => {
|
|
160
|
+
const m = new RegExp("^# " + k + " (\\d+)$", "m").exec(tap);
|
|
161
|
+
return m ? +m[1] : 0;
|
|
162
|
+
};
|
|
163
|
+
return { tests: num("tests"), pass: num("pass"), fail: num("fail"), skipped: num("skipped"), todo: num("todo") };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// THE FAILING TESTS, BY NAME AND BY DURATION (v1.0.0 W15).
|
|
167
|
+
//
|
|
168
|
+
// `--json is not a summary` (plan sec5 rule 10, which says in terms that it
|
|
169
|
+
// binds this file). Until W15 the human branch printed a failing file's WHOLE
|
|
170
|
+
// TAP - every test name, its duration and the assertion - and `--json` carried
|
|
171
|
+
// `{file, status, fail}`. A field the human path prints and the JSON omits is
|
|
172
|
+
// drift no lint can see, because both halves live in one function.
|
|
173
|
+
//
|
|
174
|
+
// It was not a theoretical gap. W14 recorded four full runs with four different
|
|
175
|
+
// failure sets and could not say WHICH tests failed or how long they took -
|
|
176
|
+
// which is precisely the evidence that separates a budget elapsing from an
|
|
177
|
+
// assertion failing, the two hypotheses that were open. Four runs, no durations
|
|
178
|
+
// kept, and the wave had to hand the question on.
|
|
179
|
+
function failedTests(tap) {
|
|
180
|
+
const out = [];
|
|
181
|
+
const re = /^not ok \d+ - (.+)$((?:\n[ ]+.*)*)/gm;
|
|
182
|
+
let m;
|
|
183
|
+
while ((m = re.exec(tap))) {
|
|
184
|
+
const body = m[2] || "";
|
|
185
|
+
const dur = /duration_ms:\s*(\d+(?:\.[0-9]+)?)/.exec(body);
|
|
186
|
+
const loc = /location:\s*'([^']*)'/.exec(body);
|
|
187
|
+
// The FIRST line of the error is the assertion. The YAML diff under it is
|
|
188
|
+
// for the human branch; a report field that carried it would be a wall of
|
|
189
|
+
// text in every consumer.
|
|
190
|
+
const err = /error:\s*[|-]*\s*'?([^\n]*)/.exec(body);
|
|
191
|
+
out.push({
|
|
192
|
+
name: m[1].trim(),
|
|
193
|
+
duration_s: dur ? +(Number(dur[1]) / 1000).toFixed(1) : null,
|
|
194
|
+
location: loc ? loc[1] : null,
|
|
195
|
+
error: err ? err[1].replace(/'$/, "").trim().slice(0, 300) : null,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function runFile(rel) {
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
const started = Date.now();
|
|
204
|
+
const child = spawn(process.execPath, ["--test", "--test-reporter=tap", rel], {
|
|
205
|
+
cwd: REPO,
|
|
206
|
+
env: { ...process.env, ORC_NO_UPDATE_CHECK: "1" },
|
|
207
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
208
|
+
});
|
|
209
|
+
let out = "";
|
|
210
|
+
let err = "";
|
|
211
|
+
child.stdout.on("data", (d) => (out += d));
|
|
212
|
+
child.stderr.on("data", (d) => (err += d));
|
|
213
|
+
child.on("close", (code) => {
|
|
214
|
+
const c = counters(out);
|
|
215
|
+
resolve({
|
|
216
|
+
file: rel,
|
|
217
|
+
ms: Date.now() - started,
|
|
218
|
+
// WHEN a file ran, not only how long it took. Two loopback files being
|
|
219
|
+
// in flight together IS the contention question, and a report with no
|
|
220
|
+
// start offset cannot answer it however many times it is re-run.
|
|
221
|
+
started_at: started,
|
|
222
|
+
failed_tests: failedTests(out),
|
|
223
|
+
status: code,
|
|
224
|
+
ok: code === 0,
|
|
225
|
+
...c,
|
|
226
|
+
output: out + (err ? "\n--- stderr ---\n" + err : ""),
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ONE scheduler over every pool at once. Longest-job-first from the last run's
|
|
233
|
+
// timings — a file with no recorded time sorts FIRST (it is unknown, and
|
|
234
|
+
// unknown is not fast); ties fall back to the sorted path, so a failure
|
|
235
|
+
// reproduces. A file starts only when its pool has a free slot AND the global
|
|
236
|
+
// cap has one, so the box stays busy without any class exceeding its limit.
|
|
237
|
+
async function runAll(byPool, timings, opts) {
|
|
238
|
+
const cost = (f) => (timings[f] === undefined ? Infinity : timings[f]);
|
|
239
|
+
const queue = [];
|
|
240
|
+
for (const name of POOL_ORDER) for (const f of byPool[name] || []) queue.push({ file: f, pool: name });
|
|
241
|
+
queue.sort((a, b) => cost(b.file) - cost(a.file) || (a.file < b.file ? -1 : 1));
|
|
242
|
+
|
|
243
|
+
const inPool = {};
|
|
244
|
+
const results = [];
|
|
245
|
+
const poolMs = {};
|
|
246
|
+
const poolStart = {};
|
|
247
|
+
let running = 0;
|
|
248
|
+
let idx = 0;
|
|
249
|
+
|
|
250
|
+
await new Promise((done) => {
|
|
251
|
+
const pump = () => {
|
|
252
|
+
while (running < GLOBAL_CONCURRENCY) {
|
|
253
|
+
// Scan for the most expensive job whose pool has room. Skipping a
|
|
254
|
+
// blocked job rather than stalling on it is what lets `pure` drain
|
|
255
|
+
// while `net`'s two slots are busy.
|
|
256
|
+
let pick = -1;
|
|
257
|
+
for (let i = idx; i < queue.length; i++) {
|
|
258
|
+
const j = queue[i];
|
|
259
|
+
if (j.done) continue;
|
|
260
|
+
if ((inPool[j.pool] || 0) < POOLS[j.pool].concurrency) {
|
|
261
|
+
pick = i;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (pick < 0) break;
|
|
266
|
+
const job = queue[pick];
|
|
267
|
+
job.done = true;
|
|
268
|
+
while (idx < queue.length && queue[idx].done) idx++;
|
|
269
|
+
inPool[job.pool] = (inPool[job.pool] || 0) + 1;
|
|
270
|
+
if (poolStart[job.pool] === undefined) poolStart[job.pool] = Date.now();
|
|
271
|
+
running++;
|
|
272
|
+
runFile(job.file).then((r) => {
|
|
273
|
+
results.push({ ...r, pool: job.pool });
|
|
274
|
+
poolMs[job.pool] = Date.now() - poolStart[job.pool];
|
|
275
|
+
inPool[job.pool]--;
|
|
276
|
+
running--;
|
|
277
|
+
if (!opts.json) process.stdout.write(r.ok ? "." : "\n FAIL " + r.file + "\n");
|
|
278
|
+
if (results.length === queue.length) return done();
|
|
279
|
+
pump();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (!queue.length) done();
|
|
283
|
+
};
|
|
284
|
+
pump();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return POOL_ORDER.filter((n) => (byPool[n] || []).length).map((n) => ({
|
|
288
|
+
pool: n,
|
|
289
|
+
concurrency: POOLS[n].concurrency,
|
|
290
|
+
files: results.filter((r) => r.pool === n).sort((a, b) => (a.file < b.file ? -1 : 1)),
|
|
291
|
+
ms: poolMs[n] || 0,
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── selection ───────────────────────────────────────────────────────────────
|
|
296
|
+
function changedPaths(ref) {
|
|
297
|
+
const args = ["diff", "--name-only", ref === true ? "HEAD" : ref];
|
|
298
|
+
const a = spawnSync("git", args, { cwd: REPO, encoding: "utf8" });
|
|
299
|
+
const b = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd: REPO, encoding: "utf8" });
|
|
300
|
+
if (a.status !== 0) return null;
|
|
301
|
+
return (a.stdout + (b.stdout || "")).split(/\r?\n/).filter(Boolean);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function selectSince(all, paths) {
|
|
305
|
+
const picked = new Set();
|
|
306
|
+
for (const p of paths) {
|
|
307
|
+
let matched = false;
|
|
308
|
+
for (const [re, targets] of TEST_TOUCHES) {
|
|
309
|
+
if (!re.test(p)) continue;
|
|
310
|
+
matched = true;
|
|
311
|
+
for (const t of targets) {
|
|
312
|
+
if (t === "SELF") {
|
|
313
|
+
if (all.includes(p)) picked.add(p);
|
|
314
|
+
} else for (const f of all) if (f === t || f.startsWith(t)) picked.add(f);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// The fail-safe direction: a path no rule claims runs the whole suite.
|
|
318
|
+
if (!matched) return { files: all, widened_by: p };
|
|
319
|
+
}
|
|
320
|
+
return { files: all.filter((f) => picked.has(f)), widened_by: null };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function usage(msg) {
|
|
324
|
+
if (msg) console.error("test-run: " + msg);
|
|
325
|
+
console.error(
|
|
326
|
+
[
|
|
327
|
+
"",
|
|
328
|
+
"usage: node bin/test-run.js [--pool <name>] [--file <substr>] [--since [ref]] [--json]",
|
|
329
|
+
"",
|
|
330
|
+
" (no flags) run everything, pool by pool. THIS is the gate.",
|
|
331
|
+
" --pool <name> one pool only: " + POOL_ORDER.join(" | "),
|
|
332
|
+
" --file <substr> files whose path contains <substr>",
|
|
333
|
+
" --since [ref] files a working-tree change against <ref> (default HEAD) can break",
|
|
334
|
+
" --json the whole computed object on stdout, nothing else",
|
|
335
|
+
"",
|
|
336
|
+
"exit: 0 all green · 1 a test failed · 2 a bad argument",
|
|
337
|
+
"",
|
|
338
|
+
].join("\n")
|
|
339
|
+
);
|
|
340
|
+
return 2;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function main(argv) {
|
|
344
|
+
const opts = { json: false, pool: null, file: null, since: null };
|
|
345
|
+
for (let i = 0; i < argv.length; i++) {
|
|
346
|
+
const a = argv[i];
|
|
347
|
+
if (a === "--json") opts.json = true;
|
|
348
|
+
else if (a === "--pool") {
|
|
349
|
+
opts.pool = argv[++i];
|
|
350
|
+
if (!POOLS[opts.pool]) return usage("unknown pool: " + opts.pool);
|
|
351
|
+
} else if (a === "--file") {
|
|
352
|
+
opts.file = argv[++i];
|
|
353
|
+
if (!opts.file) return usage("--file needs a substring");
|
|
354
|
+
} else if (a === "--since") {
|
|
355
|
+
opts.since = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : true;
|
|
356
|
+
} else if (a === "-h" || a === "--help") return usage();
|
|
357
|
+
else return usage("unknown argument: " + a);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const all = walk(TEST_DIR, []);
|
|
361
|
+
const classified = {};
|
|
362
|
+
const unclassified = [];
|
|
363
|
+
const misdeclared = [];
|
|
364
|
+
for (const f of all) {
|
|
365
|
+
const c = poolOf(f);
|
|
366
|
+
classified[f] = c.pool;
|
|
367
|
+
if (!c.declared) (c.bad ? misdeclared : unclassified).push(c.bad ? f + " (@test-pool " + c.bad + ")" : f);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let selected = all;
|
|
371
|
+
let widened_by = null;
|
|
372
|
+
const is_gate = !opts.pool && !opts.file && !opts.since;
|
|
373
|
+
if (opts.file) selected = all.filter((f) => f.includes(opts.file));
|
|
374
|
+
if (opts.since) {
|
|
375
|
+
const paths = changedPaths(opts.since);
|
|
376
|
+
if (!paths) return usage("--since needs a git repository");
|
|
377
|
+
const s = selectSince(selected, paths);
|
|
378
|
+
selected = s.files;
|
|
379
|
+
widened_by = s.widened_by;
|
|
380
|
+
}
|
|
381
|
+
if (opts.pool) selected = selected.filter((f) => classified[f] === opts.pool);
|
|
382
|
+
|
|
383
|
+
const timings = readTimings();
|
|
384
|
+
const byPool = {};
|
|
385
|
+
for (const name of POOL_ORDER) {
|
|
386
|
+
const files = selected.filter((f) => classified[f] === name);
|
|
387
|
+
if (files.length) byPool[name] = files;
|
|
388
|
+
}
|
|
389
|
+
if (!opts.json && selected.length)
|
|
390
|
+
console.log(
|
|
391
|
+
"running " +
|
|
392
|
+
selected.length +
|
|
393
|
+
" files, global x" +
|
|
394
|
+
GLOBAL_CONCURRENCY +
|
|
395
|
+
" — " +
|
|
396
|
+
POOL_ORDER.filter((n) => byPool[n])
|
|
397
|
+
.map((n) => n + " x" + POOLS[n].concurrency + " (" + byPool[n].length + ")")
|
|
398
|
+
.join(" · ")
|
|
399
|
+
);
|
|
400
|
+
const t0 = Date.now();
|
|
401
|
+
T0 = t0;
|
|
402
|
+
const pools = await runAll(byPool, timings, opts);
|
|
403
|
+
const wall_ms = Date.now() - t0;
|
|
404
|
+
if (!opts.json && selected.length) process.stdout.write("\n");
|
|
405
|
+
|
|
406
|
+
const files = pools.flatMap((p) => p.files);
|
|
407
|
+
const totals = files.reduce(
|
|
408
|
+
(t, f) => ({ tests: t.tests + f.tests, pass: t.pass + f.pass, fail: t.fail + f.fail, skipped: t.skipped + f.skipped, todo: t.todo + f.todo }),
|
|
409
|
+
{ tests: 0, pass: 0, fail: 0, skipped: 0, todo: 0 }
|
|
410
|
+
);
|
|
411
|
+
const failed = files.filter((f) => !f.ok);
|
|
412
|
+
const skippedFiles = all.filter((f) => !selected.includes(f));
|
|
413
|
+
|
|
414
|
+
// Longest-job-first needs a record. Written on every run, gate or not;
|
|
415
|
+
// a selective run only refreshes the files it actually ran.
|
|
416
|
+
const nextTimings = { ...timings };
|
|
417
|
+
for (const f of files) nextTimings[f.file] = f.ms;
|
|
418
|
+
try {
|
|
419
|
+
fs.writeFileSync(TIMINGS, JSON.stringify(nextTimings, null, 1) + "\n");
|
|
420
|
+
} catch (_) {}
|
|
421
|
+
|
|
422
|
+
const report = {
|
|
423
|
+
ok: failed.length === 0,
|
|
424
|
+
// A selective run is NEVER the gate. This field is the machine-readable
|
|
425
|
+
// half of that rule and the last line below is the human half.
|
|
426
|
+
is_gate,
|
|
427
|
+
wall_clock_s: +(wall_ms / 1000).toFixed(1),
|
|
428
|
+
global_concurrency: GLOBAL_CONCURRENCY,
|
|
429
|
+
totals,
|
|
430
|
+
files_run: selected.length,
|
|
431
|
+
files_total: all.length,
|
|
432
|
+
// A file not run is NAMED, always.
|
|
433
|
+
skipped_files: skippedFiles,
|
|
434
|
+
widened_by,
|
|
435
|
+
unclassified,
|
|
436
|
+
misdeclared,
|
|
437
|
+
// A flake is RECORDED, never retried away: nothing in this runner re-runs a
|
|
438
|
+
// failure, so this array is filled only by a human recording one.
|
|
439
|
+
flaky: [],
|
|
440
|
+
pools: pools.map((p) => ({
|
|
441
|
+
pool: p.pool,
|
|
442
|
+
concurrency: p.concurrency,
|
|
443
|
+
wall_clock_s: +(p.ms / 1000).toFixed(1),
|
|
444
|
+
why: POOLS[p.pool].why,
|
|
445
|
+
files: p.files.map((f) => ({ file: f.file, ms: f.ms, started_s: +((f.started_at - T0) / 1000).toFixed(1), tests: f.tests, pass: f.pass, fail: f.fail, ok: f.ok })),
|
|
446
|
+
})),
|
|
447
|
+
failures: failed.map((f) => ({ file: f.file, status: f.status, fail: f.fail, pool: f.pool, ms: f.ms, started_s: +((f.started_at - T0) / 1000).toFixed(1), tests: f.failed_tests })),
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
if (opts.json) {
|
|
451
|
+
process.stdout.write(JSON.stringify(report, null, 1) + "\n");
|
|
452
|
+
return failed.length ? 1 : 0;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Failure-only reporting. 3,000 lines of TAP for one ECONNRESET is why
|
|
456
|
+
// nobody reads the current output.
|
|
457
|
+
for (const f of failed) {
|
|
458
|
+
console.log("\n" + "=".repeat(72) + "\n" + f.file + " — exit " + f.status + "\n" + "=".repeat(72));
|
|
459
|
+
console.log(f.output.trim());
|
|
460
|
+
}
|
|
461
|
+
if (unclassified.length) console.log("\nunclassified (ran in `" + DEFAULT_POOL + "`): " + unclassified.join(", "));
|
|
462
|
+
if (misdeclared.length) console.log("\nBAD @test-pool pragma (ran in `" + DEFAULT_POOL + "`): " + misdeclared.join(", "));
|
|
463
|
+
console.log(
|
|
464
|
+
"\n" +
|
|
465
|
+
(failed.length ? "FAIL" : "ok") +
|
|
466
|
+
" — " +
|
|
467
|
+
totals.pass +
|
|
468
|
+
" passed, " +
|
|
469
|
+
totals.fail +
|
|
470
|
+
" failed, " +
|
|
471
|
+
totals.tests +
|
|
472
|
+
" tests in " +
|
|
473
|
+
(wall_ms / 1000).toFixed(1) +
|
|
474
|
+
"s across " +
|
|
475
|
+
selected.length +
|
|
476
|
+
" files"
|
|
477
|
+
);
|
|
478
|
+
if (!is_gate)
|
|
479
|
+
console.log(
|
|
480
|
+
"ran " + selected.length + " of " + all.length + " files — this is NOT the gate" + (widened_by ? " (widened to everything by " + widened_by + ")" : "")
|
|
481
|
+
);
|
|
482
|
+
return failed.length ? 1 : 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
main(process.argv.slice(2)).then(
|
|
486
|
+
(code) => process.exit(code),
|
|
487
|
+
(e) => {
|
|
488
|
+
console.error(e && e.stack ? e.stack : String(e));
|
|
489
|
+
process.exit(2);
|
|
490
|
+
}
|
|
491
|
+
);
|