@fairfox/polly 0.77.3 → 0.79.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/dist/cli/polly.js +46 -3
- package/dist/cli/polly.js.map +3 -3
- package/dist/src/background/index.js.map +3 -3
- package/dist/src/background/message-router.js.map +3 -3
- package/dist/src/client/index.js +137 -32
- package/dist/src/client/index.js.map +6 -5
- package/dist/src/client/wrapper.d.ts +39 -2
- package/dist/src/elysia/index.js +22 -3
- package/dist/src/elysia/index.js.map +5 -5
- package/dist/src/elysia/route-match.d.ts +13 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +12 -2
- package/dist/src/index.js.map +7 -7
- package/dist/src/mesh.js +87 -46
- package/dist/src/mesh.js.map +12 -11
- package/dist/src/peer.js +7 -3
- package/dist/src/peer.js.map +6 -6
- package/dist/src/polly-ui/Badge.d.ts +5 -0
- package/dist/src/polly-ui/Button.d.ts +31 -6
- package/dist/src/polly-ui/Dropdown.d.ts +6 -0
- package/dist/src/polly-ui/Select.d.ts +11 -1
- package/dist/src/polly-ui/TextInput.d.ts +30 -0
- package/dist/src/polly-ui/index.css +10 -0
- package/dist/src/polly-ui/index.js +81 -32
- package/dist/src/polly-ui/index.js.map +10 -10
- package/dist/src/polly-ui/styles.css +10 -0
- package/dist/src/shared/adapters/index.js.map +3 -3
- package/dist/src/shared/lib/context-helpers.js.map +3 -3
- package/dist/src/shared/lib/mesh-client.d.ts +38 -0
- package/dist/src/shared/lib/mesh-signaling-client.d.ts +6 -5
- package/dist/src/shared/lib/mesh-state.d.ts +21 -0
- package/dist/src/shared/lib/message-bus.js.map +3 -3
- package/dist/src/shared/lib/peer-relay-adapter.d.ts +5 -0
- package/dist/src/shared/lib/peer-repo-server.d.ts +15 -0
- package/dist/src/shared/lib/resource.js +11 -2
- package/dist/src/shared/lib/resource.js.map +6 -6
- package/dist/src/shared/lib/state.d.ts +20 -0
- package/dist/src/shared/lib/state.js +11 -1
- package/dist/src/shared/lib/state.js.map +5 -5
- package/dist/src/shared/state/app-state.js +10 -1
- package/dist/src/shared/state/app-state.js.map +5 -5
- package/dist/tools/init/src/cli.js +23 -2
- package/dist/tools/init/src/cli.js.map +4 -4
- package/dist/tools/init/templates/pwa/package.json.template +1 -1
- package/dist/tools/init/templates/pwa/src/service-worker.ts.template +26 -15
- package/dist/tools/init/templates/pwa/src/shared-worker.ts.template +13 -3
- package/dist/tools/init/templates/pwa/tsconfig.json.template +2 -2
- package/dist/tools/init/templates/pwa/tsconfig.worker.json.template +17 -0
- package/dist/tools/test/src/browser/index.js +5 -2
- package/dist/tools/test/src/browser/index.js.map +3 -3
- package/dist/tools/test/src/contrast/index.js +20 -15
- package/dist/tools/test/src/contrast/index.js.map +3 -3
- package/dist/tools/test/src/e2e-cli/index.d.ts +10 -0
- package/dist/tools/test/src/e2e-cli/run-cli.d.ts +25 -0
- package/dist/tools/test/src/e2e-cli/with-temp-dir.d.ts +15 -0
- package/dist/tools/test/src/e2e-mesh/index.js +12 -7
- package/dist/tools/test/src/e2e-mesh/index.js.map +4 -4
- package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +7 -1
- package/dist/tools/test/src/e2e-relay/index.d.ts +12 -0
- package/dist/tools/test/src/e2e-relay/wait-for-relay-convergence.d.ts +27 -0
- package/dist/tools/test/src/e2e-relay/with-repo-server.d.ts +24 -0
- package/dist/tools/test/src/e2e-shared/assert.d.ts +18 -0
- package/dist/tools/test/src/e2e-shared/contract.d.ts +40 -0
- package/dist/tools/test/src/e2e-shared/index.d.ts +2 -0
- package/dist/tools/test/src/tiers/args.d.ts +23 -0
- package/dist/tools/test/src/tiers/cli.d.ts +2 -0
- package/dist/tools/test/src/tiers/cli.js +490 -0
- package/dist/tools/test/src/tiers/cli.js.map +16 -0
- package/dist/tools/test/src/tiers/detect.d.ts +12 -0
- package/dist/tools/test/src/tiers/discover.d.ts +2 -0
- package/dist/tools/test/src/tiers/engine.d.ts +3 -0
- package/dist/tools/test/src/tiers/index.d.ts +14 -0
- package/dist/tools/test/src/tiers/protocol.d.ts +10 -0
- package/dist/tools/test/src/tiers/reporter.d.ts +12 -0
- package/dist/tools/test/src/tiers/types.d.ts +94 -0
- package/dist/tools/test/src/tiers/worker.d.ts +2 -0
- package/dist/tools/test/src/tiers/worker.js +60 -0
- package/dist/tools/test/src/tiers/worker.js.map +12 -0
- package/dist/tools/verify/src/cli.js +322 -27
- package/dist/tools/verify/src/cli.js.map +13 -10
- package/dist/tools/verify/src/config.d.ts +10 -0
- package/dist/tools/verify/src/config.js.map +2 -2
- package/dist/tools/verify/src/stryker/index.js +20 -11
- package/dist/tools/verify/src/stryker/index.js.map +3 -3
- package/dist/tools/visualize/src/cli.js +8 -5
- package/dist/tools/visualize/src/cli.js.map +4 -4
- package/package.json +16 -6
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __returnValue = (v) => v;
|
|
5
|
+
function __exportSetter(name, newValue) {
|
|
6
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
+
}
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
set: __exportSetter.bind(all, name)
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
// tools/test/src/tiers/cli.ts
|
|
21
|
+
import { mkdirSync } from "node:fs";
|
|
22
|
+
import { dirname as dirname2 } from "node:path";
|
|
23
|
+
|
|
24
|
+
// tools/test/src/tiers/args.ts
|
|
25
|
+
var DEFAULT_JSON = "test-results/tiers.json";
|
|
26
|
+
var BOOL_FLAGS = {
|
|
27
|
+
"--all": "all",
|
|
28
|
+
"--full": "full",
|
|
29
|
+
"--list": "list",
|
|
30
|
+
"--bail": "bail",
|
|
31
|
+
"--strict-needs": "strictNeeds"
|
|
32
|
+
};
|
|
33
|
+
function listValue(arg, name) {
|
|
34
|
+
return arg.startsWith(name) ? arg.slice(name.length).split(",") : [];
|
|
35
|
+
}
|
|
36
|
+
function parseTierArgs(argv) {
|
|
37
|
+
const args = {
|
|
38
|
+
tiers: [],
|
|
39
|
+
only: [],
|
|
40
|
+
all: false,
|
|
41
|
+
full: false,
|
|
42
|
+
list: false,
|
|
43
|
+
bail: false,
|
|
44
|
+
strictNeeds: false,
|
|
45
|
+
json: null
|
|
46
|
+
};
|
|
47
|
+
for (const arg of argv) {
|
|
48
|
+
const boolKey = BOOL_FLAGS[arg];
|
|
49
|
+
if (boolKey) {
|
|
50
|
+
args[boolKey] = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (arg === "--json") {
|
|
54
|
+
args.json = DEFAULT_JSON;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (arg.startsWith("--json=")) {
|
|
58
|
+
args.json = arg.slice("--json=".length);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (arg.startsWith("--tier=")) {
|
|
62
|
+
args.tiers.push(...listValue(arg, "--tier="));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (arg.startsWith("--only=")) {
|
|
66
|
+
args.only.push(...listValue(arg, "--only="));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg.startsWith("-")) {
|
|
70
|
+
console.log(`Unknown flag: ${arg}`);
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
args.tiers.push(arg);
|
|
74
|
+
}
|
|
75
|
+
return args;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// tools/test/src/tiers/discover.ts
|
|
79
|
+
import { existsSync } from "node:fs";
|
|
80
|
+
import { dirname, join } from "node:path";
|
|
81
|
+
async function globFiles(root, pattern) {
|
|
82
|
+
const glob = new Bun.Glob(pattern);
|
|
83
|
+
const out = [];
|
|
84
|
+
for await (const file of glob.scan({ cwd: root, onlyFiles: true })) {
|
|
85
|
+
if (file.includes("node_modules") || file.startsWith("dist/"))
|
|
86
|
+
continue;
|
|
87
|
+
out.push(file);
|
|
88
|
+
}
|
|
89
|
+
return out.sort();
|
|
90
|
+
}
|
|
91
|
+
function browserRunner() {
|
|
92
|
+
const bundled = `${import.meta.dir}/../browser/run.js`;
|
|
93
|
+
const source = `${import.meta.dir}/../browser/run.ts`;
|
|
94
|
+
return existsSync(bundled) ? bundled : source;
|
|
95
|
+
}
|
|
96
|
+
function browserDir(root, files) {
|
|
97
|
+
if (existsSync(join(root, "tests/browser")))
|
|
98
|
+
return "tests/browser";
|
|
99
|
+
if (existsSync(join(root, "tests")))
|
|
100
|
+
return "tests";
|
|
101
|
+
return dirname(files[0] ?? ".") || ".";
|
|
102
|
+
}
|
|
103
|
+
function e2eId(file) {
|
|
104
|
+
const base = file.replace(/^.*\//, "").replace(/\.(ts|tsx)$/, "");
|
|
105
|
+
return base.replace(/^e2e-/, "") || base;
|
|
106
|
+
}
|
|
107
|
+
async function discoverPlan(root) {
|
|
108
|
+
const tiers = [];
|
|
109
|
+
const hasUnitDir = existsSync(join(root, "tests/unit"));
|
|
110
|
+
const hasIntegrationDir = existsSync(join(root, "tests/integration"));
|
|
111
|
+
const testFiles = await globFiles(root, "**/*.test.{ts,tsx}");
|
|
112
|
+
if (hasUnitDir) {
|
|
113
|
+
tiers.push({
|
|
114
|
+
name: "unit",
|
|
115
|
+
description: "bun test tests/unit",
|
|
116
|
+
cases: [
|
|
117
|
+
{ id: "unit", exec: { kind: "command", argv: ["bun", "test", "tests/unit"], cwd: root } }
|
|
118
|
+
]
|
|
119
|
+
});
|
|
120
|
+
} else if (testFiles.length > 0) {
|
|
121
|
+
tiers.push({
|
|
122
|
+
name: "unit",
|
|
123
|
+
description: `bun test (${testFiles.length} files)`,
|
|
124
|
+
cases: [{ id: "unit", exec: { kind: "command", argv: ["bun", "test"], cwd: root } }]
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (hasIntegrationDir) {
|
|
128
|
+
tiers.push({
|
|
129
|
+
name: "integration",
|
|
130
|
+
description: "bun test tests/integration",
|
|
131
|
+
cases: [
|
|
132
|
+
{
|
|
133
|
+
id: "integration",
|
|
134
|
+
exec: { kind: "command", argv: ["bun", "test", "tests/integration"], cwd: root }
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const browserFiles = await globFiles(root, "**/*.browser.{ts,tsx}");
|
|
140
|
+
if (browserFiles.length > 0) {
|
|
141
|
+
tiers.push({
|
|
142
|
+
name: "browser",
|
|
143
|
+
description: `Puppeteer over ${browserFiles.length} *.browser file(s)`,
|
|
144
|
+
timeoutMs: 180000,
|
|
145
|
+
cases: [
|
|
146
|
+
{
|
|
147
|
+
id: "browser",
|
|
148
|
+
needs: ["browser"],
|
|
149
|
+
exec: {
|
|
150
|
+
kind: "command",
|
|
151
|
+
argv: ["bun", browserRunner(), browserDir(root, browserFiles)],
|
|
152
|
+
cwd: root
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const e2eFiles = await globFiles(root, "scripts/e2e-*.{ts,tsx}");
|
|
159
|
+
if (e2eFiles.length > 0) {
|
|
160
|
+
const cases = e2eFiles.map((file) => ({
|
|
161
|
+
id: e2eId(file),
|
|
162
|
+
tags: ["e2e"],
|
|
163
|
+
exec: { kind: "module", modulePath: join(root, file) }
|
|
164
|
+
}));
|
|
165
|
+
tiers.push({
|
|
166
|
+
name: "e2e",
|
|
167
|
+
description: `run()-export scripts (${e2eFiles.length})`,
|
|
168
|
+
concurrency: 2,
|
|
169
|
+
timeoutMs: 240000,
|
|
170
|
+
cases
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return { tiers };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// tools/test/src/tiers/detect.ts
|
|
177
|
+
var cache = new Map;
|
|
178
|
+
async function commandSucceeds(argv) {
|
|
179
|
+
try {
|
|
180
|
+
const proc = Bun.spawn(argv, { stdout: "ignore", stderr: "ignore", stdin: "ignore" });
|
|
181
|
+
return await proc.exited === 0;
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function probe(need) {
|
|
187
|
+
switch (need) {
|
|
188
|
+
case "docker":
|
|
189
|
+
return commandSucceeds(["docker", "info"]);
|
|
190
|
+
case "browser":
|
|
191
|
+
return true;
|
|
192
|
+
case "network":
|
|
193
|
+
return true;
|
|
194
|
+
default:
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function hasNeed(need) {
|
|
199
|
+
const cached = cache.get(need);
|
|
200
|
+
if (cached !== undefined)
|
|
201
|
+
return cached;
|
|
202
|
+
const result = await probe(need);
|
|
203
|
+
cache.set(need, result);
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
async function firstUnmetNeed(needs) {
|
|
207
|
+
for (const need of needs ?? []) {
|
|
208
|
+
if (!await hasNeed(need))
|
|
209
|
+
return need;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// tools/test/src/tiers/protocol.ts
|
|
215
|
+
var SENTINEL = "__TIER_RESULT__";
|
|
216
|
+
|
|
217
|
+
// tools/test/src/tiers/engine.ts
|
|
218
|
+
var DEFAULT_TIMEOUT_MS = 120000;
|
|
219
|
+
async function workerPath() {
|
|
220
|
+
const bundled = `${import.meta.dir}/worker.js`;
|
|
221
|
+
const source = `${import.meta.dir}/worker.ts`;
|
|
222
|
+
return await Bun.file(bundled).exists() ? bundled : source;
|
|
223
|
+
}
|
|
224
|
+
function caseLabel(spec) {
|
|
225
|
+
return spec.label ?? spec.id;
|
|
226
|
+
}
|
|
227
|
+
function caseMatches(spec, only) {
|
|
228
|
+
if (!only || only.length === 0)
|
|
229
|
+
return true;
|
|
230
|
+
const haystacks = [spec.id, caseLabel(spec), ...spec.tags ?? []].map((s) => s.toLowerCase());
|
|
231
|
+
return only.some((needle) => {
|
|
232
|
+
const n = needle.toLowerCase();
|
|
233
|
+
return haystacks.some((h) => h.includes(n));
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
async function runSubprocess(argv, opts, onLine) {
|
|
237
|
+
const proc = Bun.spawn(argv, {
|
|
238
|
+
cwd: opts.cwd,
|
|
239
|
+
env: opts.env,
|
|
240
|
+
stdout: "pipe",
|
|
241
|
+
stderr: "pipe",
|
|
242
|
+
stdin: "ignore"
|
|
243
|
+
});
|
|
244
|
+
let timedOut = false;
|
|
245
|
+
const timer = setTimeout(() => {
|
|
246
|
+
timedOut = true;
|
|
247
|
+
proc.kill();
|
|
248
|
+
}, opts.timeoutMs);
|
|
249
|
+
const chunks = [];
|
|
250
|
+
const pump = async (stream) => {
|
|
251
|
+
const decoder = new TextDecoder;
|
|
252
|
+
let buffered = "";
|
|
253
|
+
for await (const chunk of stream) {
|
|
254
|
+
buffered += decoder.decode(chunk, { stream: true });
|
|
255
|
+
let nl = buffered.indexOf(`
|
|
256
|
+
`);
|
|
257
|
+
while (nl !== -1) {
|
|
258
|
+
const line = buffered.slice(0, nl);
|
|
259
|
+
chunks.push(line);
|
|
260
|
+
onLine(line);
|
|
261
|
+
buffered = buffered.slice(nl + 1);
|
|
262
|
+
nl = buffered.indexOf(`
|
|
263
|
+
`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (buffered) {
|
|
267
|
+
chunks.push(buffered);
|
|
268
|
+
onLine(buffered);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
await Promise.all([pump(proc.stdout), pump(proc.stderr)]);
|
|
272
|
+
const exitCode = await proc.exited;
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
return { exitCode, stdout: chunks.join(`
|
|
275
|
+
`), timedOut };
|
|
276
|
+
}
|
|
277
|
+
function parseSentinel(stdout) {
|
|
278
|
+
const idx = stdout.lastIndexOf(SENTINEL);
|
|
279
|
+
if (idx === -1)
|
|
280
|
+
return null;
|
|
281
|
+
const after = stdout.slice(idx + SENTINEL.length);
|
|
282
|
+
const newline = after.indexOf(`
|
|
283
|
+
`);
|
|
284
|
+
const json = newline === -1 ? after : after.slice(0, newline);
|
|
285
|
+
try {
|
|
286
|
+
const parsed = JSON.parse(json.trim());
|
|
287
|
+
return { pass: Boolean(parsed.pass), message: parsed.message };
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
var VERDICT_ICON = {
|
|
293
|
+
pass: "✓",
|
|
294
|
+
fail: "✗",
|
|
295
|
+
skip: "⊘",
|
|
296
|
+
timeout: "⏱"
|
|
297
|
+
};
|
|
298
|
+
function buildInvocation(spec, worker, options) {
|
|
299
|
+
if (spec.exec.kind === "module") {
|
|
300
|
+
return { argv: ["bun", worker, spec.exec.modulePath, spec.id], cwd: options.cwd };
|
|
301
|
+
}
|
|
302
|
+
return { argv: spec.exec.argv, cwd: spec.exec.cwd ?? options.cwd };
|
|
303
|
+
}
|
|
304
|
+
function decideVerdict(spec, result, timeoutMs) {
|
|
305
|
+
if (result.timedOut)
|
|
306
|
+
return { outcome: "timeout", message: `timed out after ${timeoutMs}ms` };
|
|
307
|
+
const sentinel = spec.exec.kind === "module" ? parseSentinel(result.stdout) : null;
|
|
308
|
+
const pass = sentinel ? sentinel.pass : result.exitCode === 0;
|
|
309
|
+
if (pass)
|
|
310
|
+
return { outcome: "pass" };
|
|
311
|
+
return { outcome: "fail", message: sentinel?.message ?? `exit code ${result.exitCode}` };
|
|
312
|
+
}
|
|
313
|
+
async function runOneCase(spec, tier, options, worker) {
|
|
314
|
+
const label = caseLabel(spec);
|
|
315
|
+
const log = options.log ?? ((m) => console.log(m));
|
|
316
|
+
const unmet = await firstUnmetNeed(spec.needs);
|
|
317
|
+
if (unmet && !options.strictNeeds) {
|
|
318
|
+
log(` ⊘ ${label} — skipped (needs ${unmet})`);
|
|
319
|
+
return {
|
|
320
|
+
tier: tier.name,
|
|
321
|
+
id: spec.id,
|
|
322
|
+
label,
|
|
323
|
+
outcome: "skip",
|
|
324
|
+
durationMs: 0,
|
|
325
|
+
skipReason: `needs ${unmet}`
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const timeoutMs = spec.timeoutMs ?? tier.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
329
|
+
const env = { ...process.env, ...options.env };
|
|
330
|
+
const { argv, cwd } = buildInvocation(spec, worker, options);
|
|
331
|
+
const started = performance.now();
|
|
332
|
+
const prefix = ` [${spec.id}] `;
|
|
333
|
+
const result = await runSubprocess(argv, { cwd, env, timeoutMs }, (line) => {
|
|
334
|
+
if (!line.includes(SENTINEL) && line.trim())
|
|
335
|
+
log(prefix + line);
|
|
336
|
+
});
|
|
337
|
+
const durationMs = Math.round(performance.now() - started);
|
|
338
|
+
const verdict = decideVerdict(spec, result, timeoutMs);
|
|
339
|
+
const tail = verdict.message ? ` — ${verdict.message}` : "";
|
|
340
|
+
log(` ${VERDICT_ICON[verdict.outcome]} ${label} (${durationMs}ms)${tail}`);
|
|
341
|
+
return {
|
|
342
|
+
tier: tier.name,
|
|
343
|
+
id: spec.id,
|
|
344
|
+
label,
|
|
345
|
+
outcome: verdict.outcome,
|
|
346
|
+
durationMs,
|
|
347
|
+
message: verdict.message
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
async function runTier(tier, options, worker) {
|
|
351
|
+
const log = options.log ?? ((m) => console.log(m));
|
|
352
|
+
const selected = tier.cases.filter((c) => caseMatches(c, options.only));
|
|
353
|
+
if (selected.length === 0)
|
|
354
|
+
return [];
|
|
355
|
+
log(`
|
|
356
|
+
▶ ${tier.name}${tier.description ? ` — ${tier.description}` : ""} (${selected.length} case${selected.length === 1 ? "" : "s"})`);
|
|
357
|
+
const concurrency = Math.max(1, tier.concurrency ?? 1);
|
|
358
|
+
const reports = new Array(selected.length);
|
|
359
|
+
let next = 0;
|
|
360
|
+
async function worker_loop() {
|
|
361
|
+
while (true) {
|
|
362
|
+
const i = next++;
|
|
363
|
+
const spec = selected[i];
|
|
364
|
+
if (!spec)
|
|
365
|
+
return;
|
|
366
|
+
reports[i] = await runOneCase(spec, tier, options, worker);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, selected.length) }, worker_loop));
|
|
370
|
+
return reports;
|
|
371
|
+
}
|
|
372
|
+
async function runPlan(plan, options = {}) {
|
|
373
|
+
const log = options.log ?? ((m) => console.log(m));
|
|
374
|
+
const worker = await workerPath();
|
|
375
|
+
const wanted = options.tiers && options.tiers.length > 0 ? new Set(options.tiers) : null;
|
|
376
|
+
const started = performance.now();
|
|
377
|
+
const all = [];
|
|
378
|
+
for (const tier of plan.tiers) {
|
|
379
|
+
if (wanted && !wanted.has(tier.name))
|
|
380
|
+
continue;
|
|
381
|
+
const reports = await runTier(tier, options, worker);
|
|
382
|
+
all.push(...reports);
|
|
383
|
+
if (options.bail && reports.some((r) => r.outcome === "fail" || r.outcome === "timeout")) {
|
|
384
|
+
log(`
|
|
385
|
+
⏹ bailing after failing tier "${tier.name}"`);
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const durationMs = Math.round(performance.now() - started);
|
|
390
|
+
const passed = all.filter((r) => r.outcome === "pass").length;
|
|
391
|
+
const failed = all.filter((r) => r.outcome === "fail" || r.outcome === "timeout").length;
|
|
392
|
+
const skipped = all.filter((r) => r.outcome === "skip").length;
|
|
393
|
+
return { cases: all, passed, failed, skipped, durationMs, ok: failed === 0 };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// tools/test/src/tiers/reporter.ts
|
|
397
|
+
var ICON = { pass: "✓", fail: "✗", skip: "⊘", timeout: "⏱" };
|
|
398
|
+
function formatSummary(report) {
|
|
399
|
+
const lines = [];
|
|
400
|
+
lines.push("");
|
|
401
|
+
lines.push("── results ─────────────────────────────────");
|
|
402
|
+
for (const c of report.cases) {
|
|
403
|
+
const time = c.outcome === "skip" ? "" : `${c.durationMs}ms`;
|
|
404
|
+
const tail = c.outcome === "skip" ? ` (${c.skipReason})` : c.message ? ` — ${c.message}` : "";
|
|
405
|
+
lines.push(`${ICON[c.outcome]} ${c.tier} › ${c.label} ${time}${tail}`);
|
|
406
|
+
}
|
|
407
|
+
const ran = report.cases.filter((c) => c.outcome !== "skip");
|
|
408
|
+
const slowest = [...ran].sort((a, b) => b.durationMs - a.durationMs).slice(0, 5);
|
|
409
|
+
if (slowest.length > 0) {
|
|
410
|
+
lines.push("");
|
|
411
|
+
lines.push("slowest:");
|
|
412
|
+
for (const c of slowest)
|
|
413
|
+
lines.push(` ${c.durationMs}ms ${c.tier} › ${c.label}`);
|
|
414
|
+
}
|
|
415
|
+
lines.push("");
|
|
416
|
+
lines.push(`${report.ok ? "PASS" : "FAIL"} ${report.passed} passed, ${report.failed} failed, ` + `${report.skipped} skipped in ${(report.durationMs / 1000).toFixed(1)}s`);
|
|
417
|
+
return lines.join(`
|
|
418
|
+
`);
|
|
419
|
+
}
|
|
420
|
+
function toJSON(report) {
|
|
421
|
+
return JSON.stringify(report, null, 2);
|
|
422
|
+
}
|
|
423
|
+
function formatPlan(plan) {
|
|
424
|
+
const lines = ["Tiers (run order):", ""];
|
|
425
|
+
for (const tier of plan.tiers) {
|
|
426
|
+
const conc = tier.concurrency && tier.concurrency > 1 ? ` ×${tier.concurrency}` : "";
|
|
427
|
+
lines.push(`${tier.name}${conc}${tier.description ? ` — ${tier.description}` : ""}`);
|
|
428
|
+
for (const c of tier.cases) {
|
|
429
|
+
const needs = c.needs && c.needs.length > 0 ? ` (needs ${c.needs.join(", ")})` : "";
|
|
430
|
+
lines.push(` ${c.id}${needs}`);
|
|
431
|
+
}
|
|
432
|
+
lines.push("");
|
|
433
|
+
}
|
|
434
|
+
return lines.join(`
|
|
435
|
+
`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// tools/test/src/tiers/cli.ts
|
|
439
|
+
var DEFAULT_TIERS = ["unit", "integration"];
|
|
440
|
+
async function main() {
|
|
441
|
+
const args = parseTierArgs(process.argv.slice(2));
|
|
442
|
+
const root = process.cwd();
|
|
443
|
+
const plan = await discoverPlan(root);
|
|
444
|
+
if (plan.tiers.length === 0) {
|
|
445
|
+
console.log(`polly test: no tiers discovered.
|
|
446
|
+
` + " Looked for *.test.{ts,tsx}, *.browser.{ts,tsx}, and scripts/e2e-*.{ts,tsx}.");
|
|
447
|
+
process.exit(0);
|
|
448
|
+
}
|
|
449
|
+
if (args.list) {
|
|
450
|
+
console.log(formatPlan(plan));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const discovered = plan.tiers.map((t) => t.name);
|
|
454
|
+
const known = new Set(discovered);
|
|
455
|
+
let tiers;
|
|
456
|
+
if (args.tiers.length > 0) {
|
|
457
|
+
const unknown = args.tiers.filter((t) => !known.has(t));
|
|
458
|
+
if (unknown.length > 0) {
|
|
459
|
+
console.log(`Unknown tier(s): ${unknown.join(", ")}`);
|
|
460
|
+
console.log(`Discovered tiers: ${discovered.join(", ")}`);
|
|
461
|
+
process.exit(2);
|
|
462
|
+
}
|
|
463
|
+
tiers = args.tiers;
|
|
464
|
+
} else if (args.all || args.full) {
|
|
465
|
+
tiers = discovered;
|
|
466
|
+
} else {
|
|
467
|
+
const fast = discovered.filter((t) => DEFAULT_TIERS.includes(t));
|
|
468
|
+
tiers = fast.length > 0 ? fast : discovered;
|
|
469
|
+
}
|
|
470
|
+
console.log(`polly test — tiers: ${tiers.join(" → ")}${args.only.length ? ` only: ${args.only.join(", ")}` : ""}`);
|
|
471
|
+
const report = await runPlan(plan, {
|
|
472
|
+
tiers,
|
|
473
|
+
only: args.only,
|
|
474
|
+
bail: args.bail,
|
|
475
|
+
strictNeeds: args.strictNeeds,
|
|
476
|
+
cwd: root,
|
|
477
|
+
log: (m) => console.log(m)
|
|
478
|
+
});
|
|
479
|
+
console.log(formatSummary(report));
|
|
480
|
+
if (args.json) {
|
|
481
|
+
mkdirSync(dirname2(args.json), { recursive: true });
|
|
482
|
+
await Bun.write(args.json, toJSON(report));
|
|
483
|
+
console.log(`
|
|
484
|
+
wrote ${args.json}`);
|
|
485
|
+
}
|
|
486
|
+
process.exit(report.ok ? 0 : 1);
|
|
487
|
+
}
|
|
488
|
+
await main();
|
|
489
|
+
|
|
490
|
+
//# debugId=170074AB01C8CBEF64756E2164756E21
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../tools/test/src/tiers/cli.ts", "../tools/test/src/tiers/args.ts", "../tools/test/src/tiers/discover.ts", "../tools/test/src/tiers/detect.ts", "../tools/test/src/tiers/protocol.ts", "../tools/test/src/tiers/engine.ts", "../tools/test/src/tiers/reporter.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"#!/usr/bin/env bun\n/**\n * Consumer-facing tiered test runner — what `polly test --tier …` runs from a\n * consumer's own Polly project. It auto-discovers the project's tiers (see\n * discover.ts) and runs them through the shared engine, exactly as Polly's own\n * internal suite does.\n *\n * Usage (from a Polly project root):\n * polly test --tier # discovered unit + integration (fast loop)\n * polly test --tier --all # every discovered tier\n * polly test --tier=browser # one tier\n * polly test --tier --list # show what was discovered, run nothing\n * polly test --tier --json # write test-results/tiers.json\n *\n * (Bare `polly test` still delegates to `bun test`; the --tier-family flags\n * switch into this runner.)\n */\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { parseTierArgs } from \"./args\";\nimport { discoverPlan } from \"./discover\";\nimport { runPlan } from \"./engine\";\nimport { formatPlan, formatSummary, toJSON } from \"./reporter\";\n\n/** Tiers run when no tier is named (fast inner loop), if they were discovered. */\nconst DEFAULT_TIERS = [\"unit\", \"integration\"];\n\nasync function main(): Promise<void> {\n const args = parseTierArgs(process.argv.slice(2));\n const root = process.cwd();\n const plan = await discoverPlan(root);\n\n if (plan.tiers.length === 0) {\n console.log(\n \"polly test: no tiers discovered.\\n\" +\n \" Looked for *.test.{ts,tsx}, *.browser.{ts,tsx}, and scripts/e2e-*.{ts,tsx}.\"\n );\n process.exit(0);\n }\n\n if (args.list) {\n console.log(formatPlan(plan));\n return;\n }\n\n const discovered = plan.tiers.map((t) => t.name);\n const known = new Set(discovered);\n let tiers: string[];\n if (args.tiers.length > 0) {\n const unknown = args.tiers.filter((t) => !known.has(t));\n if (unknown.length > 0) {\n console.log(`Unknown tier(s): ${unknown.join(\", \")}`);\n console.log(`Discovered tiers: ${discovered.join(\", \")}`);\n process.exit(2);\n }\n tiers = args.tiers;\n } else if (args.all || args.full) {\n tiers = discovered;\n } else {\n const fast = discovered.filter((t) => DEFAULT_TIERS.includes(t));\n tiers = fast.length > 0 ? fast : discovered;\n }\n\n console.log(\n `polly test — tiers: ${tiers.join(\" → \")}${args.only.length ? ` only: ${args.only.join(\", \")}` : \"\"}`\n );\n\n const report = await runPlan(plan, {\n tiers,\n only: args.only,\n bail: args.bail,\n strictNeeds: args.strictNeeds,\n cwd: root,\n log: (m) => console.log(m),\n });\n\n console.log(formatSummary(report));\n\n if (args.json) {\n mkdirSync(dirname(args.json), { recursive: true });\n await Bun.write(args.json, toJSON(report));\n console.log(`\\nwrote ${args.json}`);\n }\n\n process.exit(report.ok ? 0 : 1);\n}\n\nawait main();\n",
|
|
6
|
+
"/**\n * Shared flag parsing for the tiered runner CLIs.\n *\n * Both front-ends (Polly's internal scripts/test/cli.ts and the consumer-facing\n * tools/test/src/tiers/cli.ts) accept the same flags; only their tier *sets*\n * differ. This keeps the surface identical.\n */\n\nexport interface TierArgs {\n /** Explicit tier names (positional or via --tier=a,b). */\n tiers: string[];\n /** --only=substr filters across tiers. */\n only: string[];\n all: boolean;\n full: boolean;\n list: boolean;\n bail: boolean;\n strictNeeds: boolean;\n /** Path to write JSON results, or null. */\n json: string | null;\n}\n\nexport const DEFAULT_JSON = \"test-results/tiers.json\";\n\nconst BOOL_FLAGS: Record<string, \"all\" | \"full\" | \"list\" | \"bail\" | \"strictNeeds\"> = {\n \"--all\": \"all\",\n \"--full\": \"full\",\n \"--list\": \"list\",\n \"--bail\": \"bail\",\n \"--strict-needs\": \"strictNeeds\",\n};\n\nfunction listValue(arg: string, name: string): string[] {\n return arg.startsWith(name) ? arg.slice(name.length).split(\",\") : [];\n}\n\n/** Parse argv into {@link TierArgs}. Exits(2) on an unknown `--flag`. */\nexport function parseTierArgs(argv: string[]): TierArgs {\n const args: TierArgs = {\n tiers: [],\n only: [],\n all: false,\n full: false,\n list: false,\n bail: false,\n strictNeeds: false,\n json: null,\n };\n for (const arg of argv) {\n const boolKey = BOOL_FLAGS[arg];\n if (boolKey) {\n args[boolKey] = true;\n continue;\n }\n if (arg === \"--json\") {\n args.json = DEFAULT_JSON;\n continue;\n }\n if (arg.startsWith(\"--json=\")) {\n args.json = arg.slice(\"--json=\".length);\n continue;\n }\n if (arg.startsWith(\"--tier=\")) {\n args.tiers.push(...listValue(arg, \"--tier=\"));\n continue;\n }\n if (arg.startsWith(\"--only=\")) {\n args.only.push(...listValue(arg, \"--only=\"));\n continue;\n }\n if (arg.startsWith(\"-\")) {\n console.log(`Unknown flag: ${arg}`);\n process.exit(2);\n }\n args.tiers.push(arg);\n }\n return args;\n}\n",
|
|
7
|
+
"/**\n * Zero-config tier discovery for a consumer's Polly project.\n *\n * `polly test --tier` from a consumer's project builds its {@link TierPlan} by\n * convention — no config required:\n * - unit `bun test` (or `bun test tests/unit` if that dir exists)\n * - integration `bun test tests/integration` (when the dir exists)\n * - browser the Polly browser runner over `*.browser.{ts,tsx}`\n * - e2e `scripts/e2e-*.{ts,tsx}` exporting `run(ctx)` — the same\n * contract Polly dogfoods, so consumer e2e scripts written with\n * `@fairfox/polly/test` helpers slot straight in.\n *\n * A tier only appears when its inputs exist, so a project with just unit tests\n * gets just a unit tier.\n */\nimport { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { CaseSpec, Tier, TierPlan } from \"./types\";\n\n/** Files matching `pattern` under `root`, excluding node_modules, sorted. */\nasync function globFiles(root: string, pattern: string): Promise<string[]> {\n const glob = new Bun.Glob(pattern);\n const out: string[] = [];\n for await (const file of glob.scan({ cwd: root, onlyFiles: true })) {\n if (file.includes(\"node_modules\") || file.startsWith(\"dist/\")) continue;\n out.push(file);\n }\n return out.sort();\n}\n\n/** Resolve the browser runner, preferring the bundled build. */\nfunction browserRunner(): string {\n const bundled = `${import.meta.dir}/../browser/run.js`;\n const source = `${import.meta.dir}/../browser/run.ts`;\n return existsSync(bundled) ? bundled : source;\n}\n\n/** A safe directory to hand the browser runner (never the project root, which\n * would make it scan node_modules). */\nfunction browserDir(root: string, files: string[]): string {\n if (existsSync(join(root, \"tests/browser\"))) return \"tests/browser\";\n if (existsSync(join(root, \"tests\"))) return \"tests\";\n return dirname(files[0] ?? \".\") || \".\";\n}\n\n/** `scripts/e2e-foo-bar.ts` → `foo-bar`. */\nfunction e2eId(file: string): string {\n const base = file.replace(/^.*\\//, \"\").replace(/\\.(ts|tsx)$/, \"\");\n return base.replace(/^e2e-/, \"\") || base;\n}\n\nexport async function discoverPlan(root: string): Promise<TierPlan> {\n const tiers: Tier[] = [];\n\n const hasUnitDir = existsSync(join(root, \"tests/unit\"));\n const hasIntegrationDir = existsSync(join(root, \"tests/integration\"));\n const testFiles = await globFiles(root, \"**/*.test.{ts,tsx}\");\n\n if (hasUnitDir) {\n tiers.push({\n name: \"unit\",\n description: \"bun test tests/unit\",\n cases: [\n { id: \"unit\", exec: { kind: \"command\", argv: [\"bun\", \"test\", \"tests/unit\"], cwd: root } },\n ],\n });\n } else if (testFiles.length > 0) {\n tiers.push({\n name: \"unit\",\n description: `bun test (${testFiles.length} files)`,\n cases: [{ id: \"unit\", exec: { kind: \"command\", argv: [\"bun\", \"test\"], cwd: root } }],\n });\n }\n\n if (hasIntegrationDir) {\n tiers.push({\n name: \"integration\",\n description: \"bun test tests/integration\",\n cases: [\n {\n id: \"integration\",\n exec: { kind: \"command\", argv: [\"bun\", \"test\", \"tests/integration\"], cwd: root },\n },\n ],\n });\n }\n\n const browserFiles = await globFiles(root, \"**/*.browser.{ts,tsx}\");\n if (browserFiles.length > 0) {\n tiers.push({\n name: \"browser\",\n description: `Puppeteer over ${browserFiles.length} *.browser file(s)`,\n timeoutMs: 180_000,\n cases: [\n {\n id: \"browser\",\n needs: [\"browser\"],\n exec: {\n kind: \"command\",\n argv: [\"bun\", browserRunner(), browserDir(root, browserFiles)],\n cwd: root,\n },\n },\n ],\n });\n }\n\n const e2eFiles = await globFiles(root, \"scripts/e2e-*.{ts,tsx}\");\n if (e2eFiles.length > 0) {\n const cases: CaseSpec[] = e2eFiles.map((file) => ({\n id: e2eId(file),\n tags: [\"e2e\"],\n exec: { kind: \"module\", modulePath: join(root, file) },\n }));\n tiers.push({\n name: \"e2e\",\n description: `run()-export scripts (${e2eFiles.length})`,\n concurrency: 2,\n timeoutMs: 240_000,\n cases,\n });\n }\n\n return { tiers };\n}\n",
|
|
8
|
+
"/**\n * Host capability detection for need-gating.\n *\n * A case declaring `needs: [\"docker\"]` is skipped (not failed) when the host\n * can't satisfy it, unless `--strict-needs` is set. Detection is cached for the\n * lifetime of the process so a 30-case run probes `docker info` once.\n */\nimport type { Need } from \"./types\";\n\nconst cache = new Map<Need, boolean>();\n\nasync function commandSucceeds(argv: string[]): Promise<boolean> {\n try {\n const proc = Bun.spawn(argv, { stdout: \"ignore\", stderr: \"ignore\", stdin: \"ignore\" });\n return (await proc.exited) === 0;\n } catch {\n return false;\n }\n}\n\nasync function probe(need: Need): Promise<boolean> {\n switch (need) {\n case \"docker\":\n // `docker info` fails fast when the daemon is down, unlike `docker --version`.\n return commandSucceeds([\"docker\", \"info\"]);\n case \"browser\":\n // Puppeteer ships Chromium as a devDependency; assume present in-repo.\n // A real probe would resolve the executable path, but that pulls puppeteer\n // into the engine. Front-ends that need a stricter check can override.\n return true;\n case \"network\":\n return true;\n default:\n return false;\n }\n}\n\n/** Resolve whether a need is satisfied, memoised. */\nexport async function hasNeed(need: Need): Promise<boolean> {\n const cached = cache.get(need);\n if (cached !== undefined) return cached;\n const result = await probe(need);\n cache.set(need, result);\n return result;\n}\n\n/** First unmet need among `needs`, or null if all are satisfied. */\nexport async function firstUnmetNeed(needs: Need[] | undefined): Promise<Need | null> {\n for (const need of needs ?? []) {\n if (!(await hasNeed(need))) return need;\n }\n return null;\n}\n",
|
|
9
|
+
"/**\n * The one shared constant between the engine and its subprocess worker.\n *\n * It lives in its own leaf module so the engine never has to *import* the\n * worker (only spawn it by path). If the engine imported the worker, a\n * `splitting: false` bundle would inline the worker's `import.meta.main`\n * self-exec block into the CLI bundle and run it on startup. Keeping the\n * sentinel here avoids that.\n */\nexport const SENTINEL = \"__TIER_RESULT__\";\n",
|
|
10
|
+
"/**\n * The tiered test engine.\n *\n * Runs a {@link TierPlan} tier-by-tier in order. Within a tier, cases run with\n * bounded concurrency. Each case is its own subprocess (see worker.ts) so the\n * isolation the e2e scripts assume is preserved. Cases whose host needs are\n * unmet are skipped (logged), not failed, unless `strictNeeds` is set.\n */\nimport { firstUnmetNeed } from \"./detect\";\nimport { SENTINEL } from \"./protocol\";\nimport type {\n CaseOutcome,\n CaseReport,\n CaseSpec,\n EngineOptions,\n RunReport,\n Tier,\n TierPlan,\n} from \"./types\";\n\nconst DEFAULT_TIMEOUT_MS = 120_000;\n\nasync function workerPath(): Promise<string> {\n const bundled = `${import.meta.dir}/worker.js`;\n const source = `${import.meta.dir}/worker.ts`;\n return (await Bun.file(bundled).exists()) ? bundled : source;\n}\n\nfunction caseLabel(spec: CaseSpec): string {\n return spec.label ?? spec.id;\n}\n\nfunction caseMatches(spec: CaseSpec, only: string[] | undefined): boolean {\n if (!only || only.length === 0) return true;\n const haystacks = [spec.id, caseLabel(spec), ...(spec.tags ?? [])].map((s) => s.toLowerCase());\n return only.some((needle) => {\n const n = needle.toLowerCase();\n return haystacks.some((h) => h.includes(n));\n });\n}\n\ninterface SubprocOutcome {\n exitCode: number;\n stdout: string;\n timedOut: boolean;\n}\n\nasync function runSubprocess(\n argv: string[],\n opts: { cwd?: string; env: Record<string, string | undefined>; timeoutMs: number },\n onLine: (line: string) => void\n): Promise<SubprocOutcome> {\n const proc = Bun.spawn(argv, {\n cwd: opts.cwd,\n env: opts.env,\n stdout: \"pipe\",\n stderr: \"pipe\",\n stdin: \"ignore\",\n });\n\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n proc.kill();\n }, opts.timeoutMs);\n\n const chunks: string[] = [];\n const pump = async (stream: ReadableStream<Uint8Array>): Promise<void> => {\n const decoder = new TextDecoder();\n let buffered = \"\";\n for await (const chunk of stream) {\n buffered += decoder.decode(chunk, { stream: true });\n let nl = buffered.indexOf(\"\\n\");\n while (nl !== -1) {\n const line = buffered.slice(0, nl);\n chunks.push(line);\n onLine(line);\n buffered = buffered.slice(nl + 1);\n nl = buffered.indexOf(\"\\n\");\n }\n }\n if (buffered) {\n chunks.push(buffered);\n onLine(buffered);\n }\n };\n\n await Promise.all([pump(proc.stdout), pump(proc.stderr)]);\n const exitCode = await proc.exited;\n clearTimeout(timer);\n\n return { exitCode, stdout: chunks.join(\"\\n\"), timedOut };\n}\n\nfunction parseSentinel(stdout: string): { pass: boolean; message?: string } | null {\n const idx = stdout.lastIndexOf(SENTINEL);\n if (idx === -1) return null;\n const after = stdout.slice(idx + SENTINEL.length);\n const newline = after.indexOf(\"\\n\");\n const json = newline === -1 ? after : after.slice(0, newline);\n try {\n const parsed = JSON.parse(json.trim());\n return { pass: Boolean(parsed.pass), message: parsed.message };\n } catch {\n return null;\n }\n}\n\ninterface Verdict {\n outcome: CaseOutcome;\n message?: string;\n}\n\nconst VERDICT_ICON: Record<CaseOutcome, string> = {\n pass: \"✓\",\n fail: \"✗\",\n skip: \"⊘\",\n timeout: \"⏱\",\n};\n\n/** Resolve argv + cwd for a case, falling back to the run-wide cwd. */\nfunction buildInvocation(\n spec: CaseSpec,\n worker: string,\n options: EngineOptions\n): { argv: string[]; cwd?: string } {\n if (spec.exec.kind === \"module\") {\n return { argv: [\"bun\", worker, spec.exec.modulePath, spec.id], cwd: options.cwd };\n }\n return { argv: spec.exec.argv, cwd: spec.exec.cwd ?? options.cwd };\n}\n\n/** Decide pass/fail/timeout, preferring the sentinel over the exit code. */\nfunction decideVerdict(spec: CaseSpec, result: SubprocOutcome, timeoutMs: number): Verdict {\n if (result.timedOut) return { outcome: \"timeout\", message: `timed out after ${timeoutMs}ms` };\n const sentinel = spec.exec.kind === \"module\" ? parseSentinel(result.stdout) : null;\n const pass = sentinel ? sentinel.pass : result.exitCode === 0;\n if (pass) return { outcome: \"pass\" };\n return { outcome: \"fail\", message: sentinel?.message ?? `exit code ${result.exitCode}` };\n}\n\nasync function runOneCase(\n spec: CaseSpec,\n tier: Tier,\n options: EngineOptions,\n worker: string\n): Promise<CaseReport> {\n const label = caseLabel(spec);\n const log = options.log ?? ((m: string) => console.log(m));\n\n const unmet = await firstUnmetNeed(spec.needs);\n if (unmet && !options.strictNeeds) {\n log(` ⊘ ${label} — skipped (needs ${unmet})`);\n return {\n tier: tier.name,\n id: spec.id,\n label,\n outcome: \"skip\",\n durationMs: 0,\n skipReason: `needs ${unmet}`,\n };\n }\n\n const timeoutMs = spec.timeoutMs ?? tier.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const env: Record<string, string | undefined> = { ...process.env, ...options.env };\n const { argv, cwd } = buildInvocation(spec, worker, options);\n\n const started = performance.now();\n const prefix = ` [${spec.id}] `;\n const result = await runSubprocess(argv, { cwd, env, timeoutMs }, (line) => {\n if (!line.includes(SENTINEL) && line.trim()) log(prefix + line);\n });\n const durationMs = Math.round(performance.now() - started);\n\n const verdict = decideVerdict(spec, result, timeoutMs);\n const tail = verdict.message ? ` — ${verdict.message}` : \"\";\n log(` ${VERDICT_ICON[verdict.outcome]} ${label} (${durationMs}ms)${tail}`);\n return {\n tier: tier.name,\n id: spec.id,\n label,\n outcome: verdict.outcome,\n durationMs,\n message: verdict.message,\n };\n}\n\nasync function runTier(tier: Tier, options: EngineOptions, worker: string): Promise<CaseReport[]> {\n const log = options.log ?? ((m: string) => console.log(m));\n const selected = tier.cases.filter((c) => caseMatches(c, options.only));\n if (selected.length === 0) return [];\n\n log(\n `\\n▶ ${tier.name}${tier.description ? ` — ${tier.description}` : \"\"} (${selected.length} case${selected.length === 1 ? \"\" : \"s\"})`\n );\n\n const concurrency = Math.max(1, tier.concurrency ?? 1);\n const reports: CaseReport[] = new Array(selected.length);\n let next = 0;\n async function worker_loop(): Promise<void> {\n while (true) {\n const i = next++;\n const spec = selected[i];\n if (!spec) return;\n reports[i] = await runOneCase(spec, tier, options, worker);\n }\n }\n await Promise.all(Array.from({ length: Math.min(concurrency, selected.length) }, worker_loop));\n return reports;\n}\n\n/** Run a plan and return a structured report. Does not exit the process. */\nexport async function runPlan(plan: TierPlan, options: EngineOptions = {}): Promise<RunReport> {\n const log = options.log ?? ((m: string) => console.log(m));\n const worker = await workerPath();\n const wanted = options.tiers && options.tiers.length > 0 ? new Set(options.tiers) : null;\n\n const started = performance.now();\n const all: CaseReport[] = [];\n for (const tier of plan.tiers) {\n if (wanted && !wanted.has(tier.name)) continue;\n const reports = await runTier(tier, options, worker);\n all.push(...reports);\n if (options.bail && reports.some((r) => r.outcome === \"fail\" || r.outcome === \"timeout\")) {\n log(`\\n⏹ bailing after failing tier \"${tier.name}\"`);\n break;\n }\n }\n\n const durationMs = Math.round(performance.now() - started);\n const passed = all.filter((r) => r.outcome === \"pass\").length;\n const failed = all.filter((r) => r.outcome === \"fail\" || r.outcome === \"timeout\").length;\n const skipped = all.filter((r) => r.outcome === \"skip\").length;\n return { cases: all, passed, failed, skipped, durationMs, ok: failed === 0 };\n}\n",
|
|
11
|
+
"/**\n * Reporting for engine runs: a human summary table (with per-case timing and a\n * slowest-N list, the data the old `&&` chains never captured) and a JSON\n * artefact for tooling. No cloud reporters — everything lands on disk locally.\n */\nimport type { RunReport, TierPlan } from \"./types\";\n\nconst ICON = { pass: \"✓\", fail: \"✗\", skip: \"⊘\", timeout: \"⏱\" } as const;\n\n/** One-line-per-case summary plus totals and slowest cases. */\nexport function formatSummary(report: RunReport): string {\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(\"── results ─────────────────────────────────\");\n for (const c of report.cases) {\n const time = c.outcome === \"skip\" ? \"\" : `${c.durationMs}ms`;\n const tail = c.outcome === \"skip\" ? ` (${c.skipReason})` : c.message ? ` — ${c.message}` : \"\";\n lines.push(`${ICON[c.outcome]} ${c.tier} › ${c.label} ${time}${tail}`);\n }\n\n const ran = report.cases.filter((c) => c.outcome !== \"skip\");\n const slowest = [...ran].sort((a, b) => b.durationMs - a.durationMs).slice(0, 5);\n if (slowest.length > 0) {\n lines.push(\"\");\n lines.push(\"slowest:\");\n for (const c of slowest) lines.push(` ${c.durationMs}ms ${c.tier} › ${c.label}`);\n }\n\n lines.push(\"\");\n lines.push(\n `${report.ok ? \"PASS\" : \"FAIL\"} ${report.passed} passed, ${report.failed} failed, ` +\n `${report.skipped} skipped in ${(report.durationMs / 1000).toFixed(1)}s`\n );\n return lines.join(\"\\n\");\n}\n\n/** Stable JSON artefact written to test-results/. */\nexport function toJSON(report: RunReport): string {\n return JSON.stringify(report, null, 2);\n}\n\n/** `--list` output: tiers and their cases without running anything. */\nexport function formatPlan(plan: TierPlan): string {\n const lines: string[] = [\"Tiers (run order):\", \"\"];\n for (const tier of plan.tiers) {\n const conc = tier.concurrency && tier.concurrency > 1 ? ` ×${tier.concurrency}` : \"\";\n lines.push(`${tier.name}${conc}${tier.description ? ` — ${tier.description}` : \"\"}`);\n for (const c of tier.cases) {\n const needs = c.needs && c.needs.length > 0 ? ` (needs ${c.needs.join(\", \")})` : \"\";\n lines.push(` ${c.id}${needs}`);\n }\n lines.push(\"\");\n }\n return lines.join(\"\\n\");\n}\n"
|
|
12
|
+
],
|
|
13
|
+
"mappings": "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",
|
|
14
|
+
"debugId": "170074AB01C8CBEF64756E2164756E21",
|
|
15
|
+
"names": []
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host capability detection for need-gating.
|
|
3
|
+
*
|
|
4
|
+
* A case declaring `needs: ["docker"]` is skipped (not failed) when the host
|
|
5
|
+
* can't satisfy it, unless `--strict-needs` is set. Detection is cached for the
|
|
6
|
+
* lifetime of the process so a 30-case run probes `docker info` once.
|
|
7
|
+
*/
|
|
8
|
+
import type { Need } from "./types";
|
|
9
|
+
/** Resolve whether a need is satisfied, memoised. */
|
|
10
|
+
export declare function hasNeed(need: Need): Promise<boolean>;
|
|
11
|
+
/** First unmet need among `needs`, or null if all are satisfied. */
|
|
12
|
+
export declare function firstUnmetNeed(needs: Need[] | undefined): Promise<Need | null>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the tiered test engine.
|
|
3
|
+
*
|
|
4
|
+
* The engine is manifest-driven and reusable: feed it a {@link TierPlan} and it
|
|
5
|
+
* runs the cases as isolated subprocesses with need-gating, parallelism, timing
|
|
6
|
+
* and reporting. Polly's own dev suite (scripts/test) and the consumer-facing
|
|
7
|
+
* `polly test` command are both front-ends over this.
|
|
8
|
+
*/
|
|
9
|
+
export { DEFAULT_JSON, parseTierArgs, type TierArgs } from "./args";
|
|
10
|
+
export { firstUnmetNeed, hasNeed } from "./detect";
|
|
11
|
+
export { discoverPlan } from "./discover";
|
|
12
|
+
export { runPlan } from "./engine";
|
|
13
|
+
export { formatPlan, formatSummary, toJSON } from "./reporter";
|
|
14
|
+
export type { CaseExec, CaseReport, CaseSpec, EngineOptions, Need, RunReport, Tier, TierPlan, } from "./types";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one shared constant between the engine and its subprocess worker.
|
|
3
|
+
*
|
|
4
|
+
* It lives in its own leaf module so the engine never has to *import* the
|
|
5
|
+
* worker (only spawn it by path). If the engine imported the worker, a
|
|
6
|
+
* `splitting: false` bundle would inline the worker's `import.meta.main`
|
|
7
|
+
* self-exec block into the CLI bundle and run it on startup. Keeping the
|
|
8
|
+
* sentinel here avoids that.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SENTINEL = "__TIER_RESULT__";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reporting for engine runs: a human summary table (with per-case timing and a
|
|
3
|
+
* slowest-N list, the data the old `&&` chains never captured) and a JSON
|
|
4
|
+
* artefact for tooling. No cloud reporters — everything lands on disk locally.
|
|
5
|
+
*/
|
|
6
|
+
import type { RunReport, TierPlan } from "./types";
|
|
7
|
+
/** One-line-per-case summary plus totals and slowest cases. */
|
|
8
|
+
export declare function formatSummary(report: RunReport): string;
|
|
9
|
+
/** Stable JSON artefact written to test-results/. */
|
|
10
|
+
export declare function toJSON(report: RunReport): string;
|
|
11
|
+
/** `--list` output: tiers and their cases without running anything. */
|
|
12
|
+
export declare function formatPlan(plan: TierPlan): string;
|