@mooloo-ai/eutaxy 0.0.0-stage → 0.2.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 +202 -0
- package/README.md +16 -2
- package/dist/aws.d.mts +15464 -0
- package/dist/aws.d.mts.map +1 -0
- package/dist/aws.mjs +24057 -0
- package/dist/aws.mjs.map +1 -0
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +17 -0
- package/dist/bin.mjs.map +1 -0
- package/dist/checksum-DhS6ofSm.mjs +6049 -0
- package/dist/checksum-DhS6ofSm.mjs.map +1 -0
- package/dist/cli-DnPkIL8h.mjs +2 -0
- package/dist/cli.d.mts +8 -0
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +2 -0
- package/dist/cloudflare.d.mts +141 -0
- package/dist/cloudflare.d.mts.map +1 -0
- package/dist/cloudflare.mjs +847 -0
- package/dist/cloudflare.mjs.map +1 -0
- package/dist/es5-DtZuPEgt.mjs +1721 -0
- package/dist/es5-DtZuPEgt.mjs.map +1 -0
- package/dist/event-streams-BANyO9xt.mjs +801 -0
- package/dist/event-streams-BANyO9xt.mjs.map +1 -0
- package/dist/explore-C5fDFOoY.d.mts +63 -0
- package/dist/explore-C5fDFOoY.d.mts.map +1 -0
- package/dist/index.d.mts +24 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +5 -0
- package/dist/platform-CGVkTa16.d.mts +267 -0
- package/dist/platform-CGVkTa16.d.mts.map +1 -0
- package/dist/rolldown-runtime-C1I3wEg2.mjs +43 -0
- package/dist/src-B66we2mG.mjs +311 -0
- package/dist/src-B66we2mG.mjs.map +1 -0
- package/dist/src-BP6Iqc_9.mjs +117 -0
- package/dist/src-BP6Iqc_9.mjs.map +1 -0
- package/dist/src-BrCsF5RZ.mjs +1286 -0
- package/dist/src-BrCsF5RZ.mjs.map +1 -0
- package/dist/src-s6uYyC0B.mjs +89 -0
- package/dist/src-s6uYyC0B.mjs.map +1 -0
- package/dist/verdict-evEM5Gxx.mjs +12 -0
- package/dist/verdict-evEM5Gxx.mjs.map +1 -0
- package/package.json +76 -3
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { i as canonicalJson } from "./src-s6uYyC0B.mjs";
|
|
2
|
+
import { i as emitRegressionTest, n as renderText } from "./src-BP6Iqc_9.mjs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
7
|
+
// packages/cli/src/args.ts
|
|
8
|
+
function parseArgs(argv, booleans = []) {
|
|
9
|
+
const positionals = [];
|
|
10
|
+
const flags = {};
|
|
11
|
+
for (let i = 0; i < argv.length; i++) {
|
|
12
|
+
const a = argv[i];
|
|
13
|
+
if (!a.startsWith("--")) {
|
|
14
|
+
positionals.push(a);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const eq = a.indexOf("=");
|
|
18
|
+
if (eq !== -1) {
|
|
19
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const name = a.slice(2);
|
|
23
|
+
const next = argv[i + 1];
|
|
24
|
+
if (booleans.includes(name) || next === void 0 || next.startsWith("--")) flags[name] = true;
|
|
25
|
+
else {
|
|
26
|
+
flags[name] = next;
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
positionals,
|
|
32
|
+
flags
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// packages/cli/src/generate.ts
|
|
36
|
+
/**
|
|
37
|
+
* The files `eutaxy check` writes into `<scenario dir>/.eutaxy/` before
|
|
38
|
+
* running Vitest: a test that performs the check inside workerd and prints
|
|
39
|
+
* the verdict on one marked line, and a Vitest config with the Cloudflare
|
|
40
|
+
* plugin pointed at the scenario's wrangler config.
|
|
41
|
+
*/
|
|
42
|
+
/** Key under which the generated test stores its result in the Vitest task meta, read back from the JSON reporter. */
|
|
43
|
+
const metaKey = "eutaxy";
|
|
44
|
+
function checkTestSource(g) {
|
|
45
|
+
const maxRuns = g.maxRuns === void 0 ? "" : `, maxRuns: ${g.maxRuns}`;
|
|
46
|
+
return `// Generated by eutaxy check; deleted after the run.
|
|
47
|
+
import { renderJson } from "@mooloo-ai/eutaxy";
|
|
48
|
+
import { check, createCloudflarePlatform, type QueueHandler } from "@mooloo-ai/eutaxy/cloudflare";
|
|
49
|
+
import { env } from "cloudflare:workers";
|
|
50
|
+
import { test } from "vitest";
|
|
51
|
+
import scenario from ${JSON.stringify(g.scenarioImport)};
|
|
52
|
+
|
|
53
|
+
const sqlFiles = import.meta.glob("../**/*.sql", { query: "?raw", import: "default", eager: true }) as Record<string, string>;
|
|
54
|
+
const modules = import.meta.glob(["../**/*.ts", "!../**/*.test.ts", "!../**/*.sim.ts", "!../**/node_modules/**", "!../**/.eutaxy/**"]) as Record<string, () => Promise<{ default: QueueHandler }>>;
|
|
55
|
+
|
|
56
|
+
function key(path: string): string {
|
|
57
|
+
return \`../\${path.replace(/^\\.\\//, "")}\`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("eutaxy check", async ({ task }) => {
|
|
61
|
+
const schemas: Record<string, string> = {};
|
|
62
|
+
for (const step of scenario.setup) {
|
|
63
|
+
if (step.kind !== "d1") continue;
|
|
64
|
+
const sql = sqlFiles[key(String(step.schemaFile))];
|
|
65
|
+
if (sql === undefined) throw new Error(\`schema file \${String(step.schemaFile)} not found next to the scenario\`);
|
|
66
|
+
schemas[String(step.binding)] = sql;
|
|
67
|
+
}
|
|
68
|
+
const load = modules[key(scenario.worker.entry)];
|
|
69
|
+
if (!load) throw new Error(\`worker entry \${scenario.worker.entry} not found next to the scenario\`);
|
|
70
|
+
const handler = (await load()).default;
|
|
71
|
+
const platform = createCloudflarePlatform({ env, handler, schemas });
|
|
72
|
+
const verdict = await check(scenario, platform, { seed: ${JSON.stringify(g.seed)}${maxRuns} });
|
|
73
|
+
task.meta[${JSON.stringify(metaKey)}] = { report: JSON.parse(renderJson(verdict)), worker: scenario.worker, setup: scenario.setup };
|
|
74
|
+
});
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
function vitestConfigSource(wranglerConfigPath, testPath, outputFile) {
|
|
78
|
+
return `// Generated by eutaxy check; deleted after the run.
|
|
79
|
+
import { cloudflareTest } from "@cloudflare/vitest-plugin";
|
|
80
|
+
import { defineConfig } from "vitest/config";
|
|
81
|
+
|
|
82
|
+
export default defineConfig({
|
|
83
|
+
plugins: [cloudflareTest({ wrangler: { configPath: ${JSON.stringify(wranglerConfigPath)} } })],
|
|
84
|
+
test: {
|
|
85
|
+
include: [${JSON.stringify(testPath)}],
|
|
86
|
+
testTimeout: 600_000,
|
|
87
|
+
reporters: ["json"],
|
|
88
|
+
outputFile: ${JSON.stringify(outputFile)},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
// packages/cli/src/regression.ts
|
|
94
|
+
/** A Vitest regression test for a Cloudflare scenario, to run under the Cloudflare Vitest plugin. */
|
|
95
|
+
function cloudflareRegressionTest(verdict, options) {
|
|
96
|
+
const bindings = Object.keys(options.schemaPaths).sort();
|
|
97
|
+
const imports = [
|
|
98
|
+
"import { createCloudflarePlatform } from \"@mooloo-ai/eutaxy/cloudflare\";",
|
|
99
|
+
"import { env } from \"cloudflare:workers\";",
|
|
100
|
+
`import scenario from ${JSON.stringify(options.scenarioPath)};`,
|
|
101
|
+
...bindings.map((b) => `import schema_${b} from ${JSON.stringify(`${options.schemaPaths[b]}?raw`)};`),
|
|
102
|
+
`import handler from ${JSON.stringify(options.workerPath)};`
|
|
103
|
+
];
|
|
104
|
+
const schemas = `{ ${bindings.map((b) => `${b}: schema_${b}`).join(", ")} }`;
|
|
105
|
+
const retries = options.maxRetries === void 0 ? "" : `, maxRetries: ${options.maxRetries}`;
|
|
106
|
+
return emitRegressionTest(verdict, {
|
|
107
|
+
imports,
|
|
108
|
+
platform: `createCloudflarePlatform({ env, handler, schemas: ${schemas}${retries} })`,
|
|
109
|
+
scenario: "scenario",
|
|
110
|
+
...options.expect ? { expect: options.expect } : {},
|
|
111
|
+
...options.engineModule ? { engineModule: options.engineModule } : {}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// packages/cli/src/check.ts
|
|
115
|
+
/**
|
|
116
|
+
* `eutaxy check <scenario.sim.ts>`: run a scenario inside workerd through
|
|
117
|
+
* the user's Vitest and the Cloudflare plugin, print the verdict as text or
|
|
118
|
+
* JSON, and on a violation write a regression test next to the scenario.
|
|
119
|
+
*/
|
|
120
|
+
const exitCodes = {
|
|
121
|
+
BOUNDED: 0,
|
|
122
|
+
VIOLATED: 1,
|
|
123
|
+
UNSUPPORTED: 2,
|
|
124
|
+
error: 3
|
|
125
|
+
};
|
|
126
|
+
function findWrangler(dir, explicit, cwd) {
|
|
127
|
+
if (explicit) return resolve(cwd, explicit);
|
|
128
|
+
for (const name of [
|
|
129
|
+
"wrangler.jsonc",
|
|
130
|
+
"wrangler.json",
|
|
131
|
+
"wrangler.toml"
|
|
132
|
+
]) {
|
|
133
|
+
const p = join(dir, name);
|
|
134
|
+
if (existsSync(p)) return p;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
function vitestBin(from) {
|
|
139
|
+
const req = createRequire(join(from, "package.json"));
|
|
140
|
+
return join(dirname(req.resolve("vitest/package.json")), "vitest.mjs");
|
|
141
|
+
}
|
|
142
|
+
function runVitest(bin, configPath, cwd) {
|
|
143
|
+
return new Promise((resolveRun) => {
|
|
144
|
+
const child = spawn(process.execPath, [
|
|
145
|
+
bin,
|
|
146
|
+
"run",
|
|
147
|
+
"--config",
|
|
148
|
+
configPath
|
|
149
|
+
], {
|
|
150
|
+
cwd,
|
|
151
|
+
env: {
|
|
152
|
+
...process.env,
|
|
153
|
+
CI: "1",
|
|
154
|
+
FORCE_COLOR: "0"
|
|
155
|
+
},
|
|
156
|
+
stdio: [
|
|
157
|
+
"ignore",
|
|
158
|
+
"pipe",
|
|
159
|
+
"pipe"
|
|
160
|
+
]
|
|
161
|
+
});
|
|
162
|
+
let output = "";
|
|
163
|
+
child.stdout.on("data", (d) => {
|
|
164
|
+
output += d.toString();
|
|
165
|
+
});
|
|
166
|
+
child.stderr.on("data", (d) => {
|
|
167
|
+
output += d.toString();
|
|
168
|
+
});
|
|
169
|
+
child.on("close", (code) => resolveRun({
|
|
170
|
+
code: code ?? 1,
|
|
171
|
+
output
|
|
172
|
+
}));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/** The transport the generated test left in its task meta, via Vitest's JSON reporter. */
|
|
176
|
+
function readTransport(reportPath) {
|
|
177
|
+
if (!existsSync(reportPath)) return null;
|
|
178
|
+
const report = JSON.parse(readFileSync(reportPath, "utf8"));
|
|
179
|
+
for (const file of report.testResults ?? []) for (const t of file.assertionResults ?? []) {
|
|
180
|
+
const m = t.meta?.[metaKey];
|
|
181
|
+
if (m) return m;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function toPosix(p) {
|
|
186
|
+
const s = p.split("\\").join("/");
|
|
187
|
+
return s.startsWith(".") ? s : `./${s}`;
|
|
188
|
+
}
|
|
189
|
+
async function runCheck(o) {
|
|
190
|
+
const scenarioPath = resolve(o.cwd, o.scenario);
|
|
191
|
+
if (!existsSync(scenarioPath)) {
|
|
192
|
+
o.stderr(`scenario not found: ${scenarioPath}\n`);
|
|
193
|
+
return exitCodes.error;
|
|
194
|
+
}
|
|
195
|
+
const dir = dirname(scenarioPath);
|
|
196
|
+
const name = basename(scenarioPath).replace(/\.sim\.ts$/, "").replace(/\.ts$/, "");
|
|
197
|
+
const wrangler = findWrangler(dir, o.wrangler, o.cwd);
|
|
198
|
+
if (!wrangler || !existsSync(wrangler)) {
|
|
199
|
+
o.stderr(`no wrangler config found next to the scenario; pass --wrangler <path>\n`);
|
|
200
|
+
return exitCodes.error;
|
|
201
|
+
}
|
|
202
|
+
const workDir = join(dir, ".eutaxy");
|
|
203
|
+
const testPath = join(workDir, `${name}.check.test.ts`);
|
|
204
|
+
const configPath = join(workDir, "vitest.config.mts");
|
|
205
|
+
mkdirSync(workDir, { recursive: true });
|
|
206
|
+
try {
|
|
207
|
+
writeFileSync(testPath, checkTestSource({
|
|
208
|
+
seed: o.seed ?? name,
|
|
209
|
+
...o.maxRuns === void 0 ? {} : { maxRuns: o.maxRuns },
|
|
210
|
+
scenarioImport: `../${basename(scenarioPath)}`
|
|
211
|
+
}));
|
|
212
|
+
const reportPath = join(workDir, "vitest-report.json");
|
|
213
|
+
rmSync(reportPath, { force: true });
|
|
214
|
+
writeFileSync(configPath, vitestConfigSource(wrangler, testPath, reportPath));
|
|
215
|
+
const { code, output } = await runVitest(vitestBin(dir), configPath, dir);
|
|
216
|
+
const transport = code === 0 ? readTransport(reportPath) : null;
|
|
217
|
+
if (!transport) {
|
|
218
|
+
o.stderr(`eutaxy check could not read a verdict from Vitest (exit ${code}):\n${output}\n`);
|
|
219
|
+
return exitCodes.error;
|
|
220
|
+
}
|
|
221
|
+
const verdict = transport.report.verdict;
|
|
222
|
+
if (o.json) o.stdout(`${canonicalJson(transport.report)}\n`);
|
|
223
|
+
else o.stdout(`${renderText(verdict)}\n`);
|
|
224
|
+
if (verdict.kind === "VIOLATED" && !o.noRegression) {
|
|
225
|
+
const outPath = o.out ? resolve(o.cwd, o.out) : join(dir, `${name}.regression.test.ts`);
|
|
226
|
+
const outDir = dirname(outPath);
|
|
227
|
+
const schemaPaths = {};
|
|
228
|
+
for (const step of transport.setup) if (step.kind === "d1") schemaPaths[String(step["binding"])] = toPosix(relative(outDir, resolve(dir, String(step["schemaFile"]))));
|
|
229
|
+
const source = cloudflareRegressionTest(verdict, {
|
|
230
|
+
scenarioPath: toPosix(relative(outDir, scenarioPath)),
|
|
231
|
+
workerPath: toPosix(relative(outDir, resolve(dir, transport.worker.entry))),
|
|
232
|
+
schemaPaths,
|
|
233
|
+
...o.expectViolation ? { expect: "violates" } : {}
|
|
234
|
+
});
|
|
235
|
+
mkdirSync(outDir, { recursive: true });
|
|
236
|
+
writeFileSync(outPath, source);
|
|
237
|
+
if (!o.json) o.stdout(`\nRegression test written to ${relative(o.cwd, outPath) || outPath}\n`);
|
|
238
|
+
}
|
|
239
|
+
return exitCodes[verdict.kind];
|
|
240
|
+
} finally {
|
|
241
|
+
if (!o.keep) rmSync(workDir, {
|
|
242
|
+
recursive: true,
|
|
243
|
+
force: true
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// packages/cli/src/main.ts
|
|
248
|
+
const usage = `eutaxy — deterministic simulation checks for Cloudflare Workers
|
|
249
|
+
|
|
250
|
+
Usage:
|
|
251
|
+
eutaxy check <scenario.sim.ts> [options]
|
|
252
|
+
|
|
253
|
+
Options:
|
|
254
|
+
--wrangler <path> wrangler config (default: wrangler.jsonc/json/toml beside the scenario)
|
|
255
|
+
--seed <seed> exploration seed (default: the scenario file name)
|
|
256
|
+
--max-runs <n> cap on exploration runs in addition to the scenario's time budget
|
|
257
|
+
--json print the versioned JSON report instead of text
|
|
258
|
+
--out <path> where to write the regression test on a violation
|
|
259
|
+
--no-regression do not write a regression test
|
|
260
|
+
--expect-violation write a regression test that asserts the violation (for corpora)
|
|
261
|
+
--keep keep the generated .eutaxy/ files
|
|
262
|
+
|
|
263
|
+
Exit codes: 0 BOUNDED, 1 VIOLATED, 2 UNSUPPORTED, 3 error.
|
|
264
|
+
`;
|
|
265
|
+
async function main(argv, io) {
|
|
266
|
+
const { positionals, flags } = parseArgs(argv, [
|
|
267
|
+
"json",
|
|
268
|
+
"no-regression",
|
|
269
|
+
"expect-violation",
|
|
270
|
+
"keep",
|
|
271
|
+
"help"
|
|
272
|
+
]);
|
|
273
|
+
const command = positionals[0];
|
|
274
|
+
if (!command || command === "help" || flags["help"] === true) {
|
|
275
|
+
io.stdout(usage);
|
|
276
|
+
return command ? exitCodes.BOUNDED : exitCodes.error;
|
|
277
|
+
}
|
|
278
|
+
if (command !== "check") {
|
|
279
|
+
io.stderr(`unknown command ${command}\n\n${usage}`);
|
|
280
|
+
return exitCodes.error;
|
|
281
|
+
}
|
|
282
|
+
const scenario = positionals[1];
|
|
283
|
+
if (!scenario) {
|
|
284
|
+
io.stderr(`check needs a scenario file\n\n${usage}`);
|
|
285
|
+
return exitCodes.error;
|
|
286
|
+
}
|
|
287
|
+
const str = (k) => typeof flags[k] === "string" ? flags[k] : void 0;
|
|
288
|
+
const maxRunsText = str("max-runs");
|
|
289
|
+
const maxRuns = maxRunsText === void 0 ? void 0 : Number(maxRunsText);
|
|
290
|
+
if (maxRuns !== void 0 && !(Number.isInteger(maxRuns) && maxRuns > 0)) {
|
|
291
|
+
io.stderr(`--max-runs must be a positive integer, got ${JSON.stringify(maxRunsText)}\n`);
|
|
292
|
+
return exitCodes.error;
|
|
293
|
+
}
|
|
294
|
+
return runCheck({
|
|
295
|
+
scenario,
|
|
296
|
+
cwd: io.cwd,
|
|
297
|
+
...str("wrangler") ? { wrangler: str("wrangler") } : {},
|
|
298
|
+
...str("seed") ? { seed: str("seed") } : {},
|
|
299
|
+
...maxRuns === void 0 ? {} : { maxRuns },
|
|
300
|
+
json: flags["json"] === true,
|
|
301
|
+
...str("out") ? { out: str("out") } : {},
|
|
302
|
+
noRegression: flags["no-regression"] === true,
|
|
303
|
+
expectViolation: flags["expect-violation"] === true,
|
|
304
|
+
keep: flags["keep"] === true,
|
|
305
|
+
stdout: io.stdout,
|
|
306
|
+
stderr: io.stderr
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
export { cloudflareRegressionTest as n, main as t };
|
|
310
|
+
|
|
311
|
+
//# sourceMappingURL=src-B66we2mG.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src-B66we2mG.mjs","names":[],"sources":["../../cli/src/args.ts","../../cli/src/generate.ts","../../cli/src/regression.ts","../../cli/src/check.ts","../../cli/src/main.ts"],"sourcesContent":["/** A small argument parser: positionals, `--flag value`, `--flag=value` and boolean `--flag`. */\n\nexport interface ParsedArgs {\n readonly positionals: readonly string[];\n readonly flags: Readonly<Record<string, string | true>>;\n}\n\nexport function parseArgs(argv: readonly string[], booleans: readonly string[] = []): ParsedArgs {\n const positionals: string[] = [];\n const flags: Record<string, string | true> = {};\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i] as string;\n if (!a.startsWith(\"--\")) {\n positionals.push(a);\n continue;\n }\n const eq = a.indexOf(\"=\");\n if (eq !== -1) {\n flags[a.slice(2, eq)] = a.slice(eq + 1);\n continue;\n }\n const name = a.slice(2);\n const next = argv[i + 1];\n if (booleans.includes(name) || next === undefined || next.startsWith(\"--\")) {\n flags[name] = true;\n } else {\n flags[name] = next;\n i++;\n }\n }\n return { positionals, flags };\n}\n","/**\n * The files `eutaxy check` writes into `<scenario dir>/.eutaxy/` before\n * running Vitest: a test that performs the check inside workerd and prints\n * the verdict on one marked line, and a Vitest config with the Cloudflare\n * plugin pointed at the scenario's wrangler config.\n */\n\n/** Key under which the generated test stores its result in the Vitest task meta, read back from the JSON reporter. */\nexport const metaKey = \"eutaxy\";\n\nexport interface GeneratedCheck {\n readonly seed: string;\n readonly maxRuns?: number;\n /** Import specifier of the scenario from the generated test (e.g. \"../scenario.sim.ts\"). */\n readonly scenarioImport: string;\n}\n\n/** What the generated test stores in its task meta. */\nexport interface CheckTransport {\n readonly report: unknown;\n readonly worker: { readonly entry: string; readonly config: string };\n readonly setup: readonly { readonly kind: string; readonly [key: string]: unknown }[];\n}\n\nexport function checkTestSource(g: GeneratedCheck): string {\n const maxRuns = g.maxRuns === undefined ? \"\" : `, maxRuns: ${g.maxRuns}`;\n return `// Generated by eutaxy check; deleted after the run.\nimport { renderJson } from \"@mooloo-ai/eutaxy\";\nimport { check, createCloudflarePlatform, type QueueHandler } from \"@mooloo-ai/eutaxy/cloudflare\";\nimport { env } from \"cloudflare:workers\";\nimport { test } from \"vitest\";\nimport scenario from ${JSON.stringify(g.scenarioImport)};\n\nconst sqlFiles = import.meta.glob(\"../**/*.sql\", { query: \"?raw\", import: \"default\", eager: true }) as Record<string, string>;\nconst modules = import.meta.glob([\"../**/*.ts\", \"!../**/*.test.ts\", \"!../**/*.sim.ts\", \"!../**/node_modules/**\", \"!../**/.eutaxy/**\"]) as Record<string, () => Promise<{ default: QueueHandler }>>;\n\nfunction key(path: string): string {\n return \\`../\\${path.replace(/^\\\\.\\\\//, \"\")}\\`;\n}\n\ntest(\"eutaxy check\", async ({ task }) => {\n const schemas: Record<string, string> = {};\n for (const step of scenario.setup) {\n if (step.kind !== \"d1\") continue;\n const sql = sqlFiles[key(String(step.schemaFile))];\n if (sql === undefined) throw new Error(\\`schema file \\${String(step.schemaFile)} not found next to the scenario\\`);\n schemas[String(step.binding)] = sql;\n }\n const load = modules[key(scenario.worker.entry)];\n if (!load) throw new Error(\\`worker entry \\${scenario.worker.entry} not found next to the scenario\\`);\n const handler = (await load()).default;\n const platform = createCloudflarePlatform({ env, handler, schemas });\n const verdict = await check(scenario, platform, { seed: ${JSON.stringify(g.seed)}${maxRuns} });\n task.meta[${JSON.stringify(metaKey)}] = { report: JSON.parse(renderJson(verdict)), worker: scenario.worker, setup: scenario.setup };\n});\n`;\n}\n\nexport function vitestConfigSource(\n wranglerConfigPath: string,\n testPath: string,\n outputFile: string,\n): string {\n return `// Generated by eutaxy check; deleted after the run.\nimport { cloudflareTest } from \"@cloudflare/vitest-plugin\";\nimport { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n plugins: [cloudflareTest({ wrangler: { configPath: ${JSON.stringify(wranglerConfigPath)} } })],\n test: {\n include: [${JSON.stringify(testPath)}],\n testTimeout: 600_000,\n reporters: [\"json\"],\n outputFile: ${JSON.stringify(outputFile)},\n },\n});\n`;\n}\n","import type { Violated } from \"@eutaxy/core\";\nimport { emitRegressionTest } from \"@eutaxy/report\";\n\nexport interface CloudflareRegressionOptions {\n /** Import path of the scenario module, relative to the emitted file. */\n readonly scenarioPath: string;\n /** Import path of the Worker module. */\n readonly workerPath: string;\n /** D1 binding name to schema file path, imported with `?raw`. */\n readonly schemaPaths: Readonly<Record<string, string>>;\n /** The platform's retry limit, when the scenario was checked with a non-default one. */\n readonly maxRetries?: number;\n readonly expect?: \"holds\" | \"violates\";\n readonly engineModule?: string;\n}\n\n/** A Vitest regression test for a Cloudflare scenario, to run under the Cloudflare Vitest plugin. */\nexport function cloudflareRegressionTest(\n verdict: Violated,\n options: CloudflareRegressionOptions,\n): string {\n const bindings = Object.keys(options.schemaPaths).sort();\n const imports = [\n 'import { createCloudflarePlatform } from \"@mooloo-ai/eutaxy/cloudflare\";',\n 'import { env } from \"cloudflare:workers\";',\n `import scenario from ${JSON.stringify(options.scenarioPath)};`,\n ...bindings.map(\n (b) => `import schema_${b} from ${JSON.stringify(`${options.schemaPaths[b]}?raw`)};`,\n ),\n `import handler from ${JSON.stringify(options.workerPath)};`,\n ];\n const schemas = `{ ${bindings.map((b) => `${b}: schema_${b}`).join(\", \")} }`;\n const retries = options.maxRetries === undefined ? \"\" : `, maxRetries: ${options.maxRetries}`;\n return emitRegressionTest(verdict, {\n imports,\n platform: `createCloudflarePlatform({ env, handler, schemas: ${schemas}${retries} })`,\n scenario: \"scenario\",\n ...(options.expect ? { expect: options.expect } : {}),\n ...(options.engineModule ? { engineModule: options.engineModule } : {}),\n });\n}\n","/**\n * `eutaxy check <scenario.sim.ts>`: run a scenario inside workerd through\n * the user's Vitest and the Cloudflare plugin, print the verdict as text or\n * JSON, and on a violation write a regression test next to the scenario.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { basename, dirname, join, relative, resolve } from \"node:path\";\nimport { canonicalJson, type JsonValue, type ScenarioVerdict, type Violated } from \"@eutaxy/core\";\nimport { renderText } from \"@eutaxy/report\";\nimport { type CheckTransport, checkTestSource, metaKey, vitestConfigSource } from \"./generate.ts\";\nimport { cloudflareRegressionTest } from \"./regression.ts\";\n\nexport interface CheckOptions {\n readonly scenario: string;\n readonly cwd: string;\n readonly wrangler?: string;\n readonly seed?: string;\n readonly maxRuns?: number;\n readonly json: boolean;\n /** Where to write the regression test on a violation; default `<name>.regression.test.ts` beside the scenario. */\n readonly out?: string;\n readonly noRegression: boolean;\n /** Emit a regression test that asserts the violation (for corpora) rather than one that expects it fixed. */\n readonly expectViolation: boolean;\n /** Keep the generated `.eutaxy/` files for inspection. */\n readonly keep: boolean;\n readonly stdout: (text: string) => void;\n readonly stderr: (text: string) => void;\n}\n\nexport const exitCodes = { BOUNDED: 0, VIOLATED: 1, UNSUPPORTED: 2, error: 3 } as const;\n\nfunction findWrangler(dir: string, explicit: string | undefined, cwd: string): string | null {\n if (explicit) return resolve(cwd, explicit);\n for (const name of [\"wrangler.jsonc\", \"wrangler.json\", \"wrangler.toml\"]) {\n const p = join(dir, name);\n if (existsSync(p)) return p;\n }\n return null;\n}\n\nfunction vitestBin(from: string): string {\n const req = createRequire(join(from, \"package.json\"));\n return join(dirname(req.resolve(\"vitest/package.json\")), \"vitest.mjs\");\n}\n\nfunction runVitest(\n bin: string,\n configPath: string,\n cwd: string,\n): Promise<{ code: number; output: string }> {\n return new Promise((resolveRun) => {\n const child = spawn(process.execPath, [bin, \"run\", \"--config\", configPath], {\n cwd,\n env: { ...process.env, CI: \"1\", FORCE_COLOR: \"0\" },\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n let output = \"\";\n child.stdout.on(\"data\", (d: Buffer) => {\n output += d.toString();\n });\n child.stderr.on(\"data\", (d: Buffer) => {\n output += d.toString();\n });\n child.on(\"close\", (code) => resolveRun({ code: code ?? 1, output }));\n });\n}\n\n/** The transport the generated test left in its task meta, via Vitest's JSON reporter. */\nfunction readTransport(reportPath: string): CheckTransport | null {\n if (!existsSync(reportPath)) return null;\n const report = JSON.parse(readFileSync(reportPath, \"utf8\")) as {\n testResults?: { assertionResults?: { meta?: Record<string, unknown> }[] }[];\n };\n for (const file of report.testResults ?? []) {\n for (const t of file.assertionResults ?? []) {\n const m = t.meta?.[metaKey];\n if (m) return m as CheckTransport;\n }\n }\n return null;\n}\n\nfunction toPosix(p: string): string {\n const s = p.split(\"\\\\\").join(\"/\");\n return s.startsWith(\".\") ? s : `./${s}`;\n}\n\nexport async function runCheck(o: CheckOptions): Promise<number> {\n const scenarioPath = resolve(o.cwd, o.scenario);\n if (!existsSync(scenarioPath)) {\n o.stderr(`scenario not found: ${scenarioPath}\\n`);\n return exitCodes.error;\n }\n const dir = dirname(scenarioPath);\n const name = basename(scenarioPath)\n .replace(/\\.sim\\.ts$/, \"\")\n .replace(/\\.ts$/, \"\");\n const wrangler = findWrangler(dir, o.wrangler, o.cwd);\n if (!wrangler || !existsSync(wrangler)) {\n o.stderr(`no wrangler config found next to the scenario; pass --wrangler <path>\\n`);\n return exitCodes.error;\n }\n const workDir = join(dir, \".eutaxy\");\n const testPath = join(workDir, `${name}.check.test.ts`);\n const configPath = join(workDir, \"vitest.config.mts\");\n mkdirSync(workDir, { recursive: true });\n try {\n writeFileSync(\n testPath,\n checkTestSource({\n seed: o.seed ?? name,\n ...(o.maxRuns === undefined ? {} : { maxRuns: o.maxRuns }),\n scenarioImport: `../${basename(scenarioPath)}`,\n }),\n );\n const reportPath = join(workDir, \"vitest-report.json\");\n rmSync(reportPath, { force: true }); // never read a report left by an earlier --keep run\n writeFileSync(configPath, vitestConfigSource(wrangler, testPath, reportPath));\n const { code, output } = await runVitest(vitestBin(dir), configPath, dir);\n const transport = code === 0 ? readTransport(reportPath) : null;\n if (!transport) {\n o.stderr(`eutaxy check could not read a verdict from Vitest (exit ${code}):\\n${output}\\n`);\n return exitCodes.error;\n }\n const report = transport.report as { verdict: ScenarioVerdict };\n const verdict = report.verdict;\n if (o.json) o.stdout(`${canonicalJson(transport.report as JsonValue)}\\n`);\n else o.stdout(`${renderText(verdict)}\\n`);\n\n if (verdict.kind === \"VIOLATED\" && !o.noRegression) {\n const outPath = o.out ? resolve(o.cwd, o.out) : join(dir, `${name}.regression.test.ts`);\n const outDir = dirname(outPath);\n const schemaPaths: Record<string, string> = {};\n for (const step of transport.setup) {\n if (step.kind === \"d1\") {\n schemaPaths[String(step[\"binding\"])] = toPosix(\n relative(outDir, resolve(dir, String(step[\"schemaFile\"]))),\n );\n }\n }\n const source = cloudflareRegressionTest(verdict as Violated, {\n scenarioPath: toPosix(relative(outDir, scenarioPath)),\n workerPath: toPosix(relative(outDir, resolve(dir, transport.worker.entry))),\n schemaPaths,\n ...(o.expectViolation ? { expect: \"violates\" as const } : {}),\n });\n mkdirSync(outDir, { recursive: true });\n writeFileSync(outPath, source);\n if (!o.json)\n o.stdout(`\\nRegression test written to ${relative(o.cwd, outPath) || outPath}\\n`);\n }\n return exitCodes[verdict.kind];\n } finally {\n if (!o.keep) rmSync(workDir, { recursive: true, force: true });\n }\n}\n","import { parseArgs } from \"./args.ts\";\nimport { exitCodes, runCheck } from \"./check.ts\";\n\nconst usage = `eutaxy — deterministic simulation checks for Cloudflare Workers\n\nUsage:\n eutaxy check <scenario.sim.ts> [options]\n\nOptions:\n --wrangler <path> wrangler config (default: wrangler.jsonc/json/toml beside the scenario)\n --seed <seed> exploration seed (default: the scenario file name)\n --max-runs <n> cap on exploration runs in addition to the scenario's time budget\n --json print the versioned JSON report instead of text\n --out <path> where to write the regression test on a violation\n --no-regression do not write a regression test\n --expect-violation write a regression test that asserts the violation (for corpora)\n --keep keep the generated .eutaxy/ files\n\nExit codes: 0 BOUNDED, 1 VIOLATED, 2 UNSUPPORTED, 3 error.\n`;\n\nexport interface Io {\n readonly cwd: string;\n readonly stdout: (text: string) => void;\n readonly stderr: (text: string) => void;\n}\n\nexport async function main(argv: readonly string[], io: Io): Promise<number> {\n const { positionals, flags } = parseArgs(argv, [\n \"json\",\n \"no-regression\",\n \"expect-violation\",\n \"keep\",\n \"help\",\n ]);\n const command = positionals[0];\n if (!command || command === \"help\" || flags[\"help\"] === true) {\n io.stdout(usage);\n return command ? exitCodes.BOUNDED : exitCodes.error;\n }\n if (command !== \"check\") {\n io.stderr(`unknown command ${command}\\n\\n${usage}`);\n return exitCodes.error;\n }\n const scenario = positionals[1];\n if (!scenario) {\n io.stderr(`check needs a scenario file\\n\\n${usage}`);\n return exitCodes.error;\n }\n const str = (k: string): string | undefined =>\n typeof flags[k] === \"string\" ? (flags[k] as string) : undefined;\n const maxRunsText = str(\"max-runs\");\n const maxRuns = maxRunsText === undefined ? undefined : Number(maxRunsText);\n if (maxRuns !== undefined && !(Number.isInteger(maxRuns) && maxRuns > 0)) {\n io.stderr(`--max-runs must be a positive integer, got ${JSON.stringify(maxRunsText)}\\n`);\n return exitCodes.error;\n }\n return runCheck({\n scenario,\n cwd: io.cwd,\n ...(str(\"wrangler\") ? { wrangler: str(\"wrangler\") as string } : {}),\n ...(str(\"seed\") ? { seed: str(\"seed\") as string } : {}),\n ...(maxRuns === undefined ? {} : { maxRuns }),\n json: flags[\"json\"] === true,\n ...(str(\"out\") ? { out: str(\"out\") as string } : {}),\n noRegression: flags[\"no-regression\"] === true,\n expectViolation: flags[\"expect-violation\"] === true,\n keep: flags[\"keep\"] === true,\n stdout: io.stdout,\n stderr: io.stderr,\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { i as canonicalJson } from "./src-s6uYyC0B.mjs";
|
|
2
|
+
// packages/report/src/regression.ts
|
|
3
|
+
/**
|
|
4
|
+
* The Vitest regression emitter. Given a violation, produce a test file that
|
|
5
|
+
* replays its shrunk schedule on the restricted scenario. By default the
|
|
6
|
+
* test expects the invariants to hold, so it fails while the bug exists and
|
|
7
|
+
* passes once it is fixed. With `expect: "violates"` it asserts the recorded
|
|
8
|
+
* violation instead, which is how Eutaxy's own corpus exercises the emitter.
|
|
9
|
+
*
|
|
10
|
+
* The emitter knows nothing about any platform: the caller supplies the
|
|
11
|
+
* import lines and the expression that builds the platform.
|
|
12
|
+
*/
|
|
13
|
+
function emitRegressionTest(verdict, options) {
|
|
14
|
+
const engineModule = options.engineModule ?? "@mooloo-ai/eutaxy";
|
|
15
|
+
const mode = options.expect ?? "holds";
|
|
16
|
+
const schedule = canonicalJson(verdict.schedule);
|
|
17
|
+
const kept = canonicalJson([...verdict.keptEvents]);
|
|
18
|
+
const keys = canonicalJson([...verdict.violation.keys]);
|
|
19
|
+
const title = mode === "holds" ? `${verdict.scenario}: ${verdict.violation.invariant} holds on the recorded schedule` : `${verdict.scenario}: ${verdict.violation.invariant} is still violated on the recorded schedule`;
|
|
20
|
+
const assertion = mode === "holds" ? ` expect(violations.map((v) => \`\${v.invariant}: \${v.message}\`)).toEqual([]);` : ` expect(violations.map((v) => [v.invariant, v.keys])).toContainEqual([${JSON.stringify(verdict.violation.invariant)}, ${keys}]);`;
|
|
21
|
+
return [
|
|
22
|
+
"// Generated by Eutaxy. Replays a recorded violation; regenerate rather than edit.",
|
|
23
|
+
`// Scenario ${verdict.scenario}, seed ${String(verdict.seed)}: ${verdict.violation.message}`,
|
|
24
|
+
`import { checkInvariants, replay, restrictSpec } from ${JSON.stringify(engineModule)};`,
|
|
25
|
+
...options.imports,
|
|
26
|
+
"import { expect, test } from \"vitest\";",
|
|
27
|
+
"",
|
|
28
|
+
`const schedule = ${schedule} as const;`,
|
|
29
|
+
`const keptEvents = ${kept} as const;`,
|
|
30
|
+
"",
|
|
31
|
+
`test(${JSON.stringify(title)}, async () => {`,
|
|
32
|
+
` const platform = ${options.platform};`,
|
|
33
|
+
` const spec = restrictSpec(platform, ${options.scenario}, keptEvents);`,
|
|
34
|
+
" const log = await replay(spec, platform, schedule);",
|
|
35
|
+
" const violations = checkInvariants(log, spec.invariants);",
|
|
36
|
+
assertion,
|
|
37
|
+
"});",
|
|
38
|
+
""
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
/** The JSON report: one object, canonical, with a schema version at the top. */
|
|
42
|
+
function renderJson(verdict) {
|
|
43
|
+
return canonicalJson({
|
|
44
|
+
schemaVersion: 1,
|
|
45
|
+
generator: "eutaxy",
|
|
46
|
+
verdict
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function clip(s, max) {
|
|
50
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}…`;
|
|
51
|
+
}
|
|
52
|
+
function pad(s, width) {
|
|
53
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
54
|
+
}
|
|
55
|
+
/** The trace as a fixed-width table; evidence rows are marked with `*`. */
|
|
56
|
+
function renderTrace(log, evidence = []) {
|
|
57
|
+
const rows = log.entries.map((e) => [
|
|
58
|
+
evidence.includes(e.seq) ? "*" : " ",
|
|
59
|
+
String(e.seq),
|
|
60
|
+
e.invocation,
|
|
61
|
+
e.kind,
|
|
62
|
+
e.target,
|
|
63
|
+
e.op,
|
|
64
|
+
clip(canonicalJson(e.payload), 72)
|
|
65
|
+
]);
|
|
66
|
+
const header = [
|
|
67
|
+
" ",
|
|
68
|
+
"seq",
|
|
69
|
+
"invocation",
|
|
70
|
+
"kind",
|
|
71
|
+
"target",
|
|
72
|
+
"op",
|
|
73
|
+
"payload"
|
|
74
|
+
];
|
|
75
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
76
|
+
const line = (cells) => cells.map((c, i) => i === cells.length - 1 ? c : pad(c, widths[i])).join(" ").trimEnd();
|
|
77
|
+
return [line(header), ...rows.map(line)].join("\n");
|
|
78
|
+
}
|
|
79
|
+
/** A readable report: verdict, what it rests on, and for a violation the schedule and trace. */
|
|
80
|
+
function renderText(verdict) {
|
|
81
|
+
const out = [];
|
|
82
|
+
const r = verdict.reached;
|
|
83
|
+
out.push(`${verdict.kind} ${verdict.scenario} (seed ${String(verdict.seed)})`);
|
|
84
|
+
switch (verdict.kind) {
|
|
85
|
+
case "VIOLATED":
|
|
86
|
+
out.push("");
|
|
87
|
+
out.push(`Invariant ${verdict.violation.invariant} is violated: ${verdict.violation.message}`);
|
|
88
|
+
out.push(`Keys: ${verdict.violation.keys.join(", ")}`);
|
|
89
|
+
out.push("");
|
|
90
|
+
out.push(`Schedule (run seed ${verdict.schedule.seed}), events kept: ${verdict.keptEvents.join(", ")}`);
|
|
91
|
+
if (verdict.schedule.faults.length === 0) out.push(" no injected faults");
|
|
92
|
+
for (const f of verdict.schedule.faults) out.push(` ${f.fault} at ${f.invocation} effect #${f.ordinal} (${f.target} ${f.op})`);
|
|
93
|
+
out.push(`Shrunk: ${verdict.shrunk.eventsRemoved} event(s) and ${verdict.shrunk.faultsRemoved} fault(s) removed in ${verdict.shrunk.runs} run(s)`);
|
|
94
|
+
out.push("");
|
|
95
|
+
out.push("Trace:");
|
|
96
|
+
out.push(renderTrace(verdict.trace, verdict.violation.evidence));
|
|
97
|
+
break;
|
|
98
|
+
case "BOUNDED": {
|
|
99
|
+
const b = verdict.bounds;
|
|
100
|
+
out.push("");
|
|
101
|
+
out.push("No violation found within the stated bounds. This is not a proof.");
|
|
102
|
+
out.push(`Bounds: faults per run ≤ ${b.maxFaultsPerRun}, redeliveries ≤ ${b.maxRedeliveries}, concurrent invocations ≤ ${b.maxConcurrentInvocations}, budget ${b.budgetMs} ms`);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "UNSUPPORTED":
|
|
106
|
+
out.push("");
|
|
107
|
+
out.push(`Eutaxy could not decide this scenario (${verdict.cause}): ${verdict.reason}`);
|
|
108
|
+
}
|
|
109
|
+
out.push("");
|
|
110
|
+
out.push(`Explored: ${r.runs} run(s), ${r.effectPoints} effect point(s), ${r.faultsInjected} fault(s) injected, ${r.elapsedMs} ms`);
|
|
111
|
+
out.push("Assumptions:");
|
|
112
|
+
for (const a of verdict.assumptions) out.push(` - ${a}`);
|
|
113
|
+
return out.join("\n");
|
|
114
|
+
}
|
|
115
|
+
export { emitRegressionTest as i, renderText as n, renderTrace as r, renderJson as t };
|
|
116
|
+
|
|
117
|
+
//# sourceMappingURL=src-BP6Iqc_9.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src-BP6Iqc_9.mjs","names":[],"sources":["../../report/src/regression.ts","../../report/src/render.ts"],"sourcesContent":["/**\n * The Vitest regression emitter. Given a violation, produce a test file that\n * replays its shrunk schedule on the restricted scenario. By default the\n * test expects the invariants to hold, so it fails while the bug exists and\n * passes once it is fixed. With `expect: \"violates\"` it asserts the recorded\n * violation instead, which is how Eutaxy's own corpus exercises the emitter.\n *\n * The emitter knows nothing about any platform: the caller supplies the\n * import lines and the expression that builds the platform.\n */\n\nimport { canonicalJson, type JsonValue, type Violated } from \"@eutaxy/core\";\n\nexport interface RegressionTestOptions {\n /** Import lines for the scenario, the platform's dependencies, and anything the platform expression needs. */\n readonly imports: readonly string[];\n /** An expression evaluating to the `Platform` (may reference the imports). */\n readonly platform: string;\n /** An expression evaluating to the `ScenarioSpec`. */\n readonly scenario: string;\n /** Module that exports `replay`, `restrictSpec` and `checkInvariants`; the public facade by default. */\n readonly engineModule?: string;\n readonly expect?: \"holds\" | \"violates\";\n}\n\nexport function emitRegressionTest(verdict: Violated, options: RegressionTestOptions): string {\n const engineModule = options.engineModule ?? \"@mooloo-ai/eutaxy\";\n const mode = options.expect ?? \"holds\";\n const schedule = canonicalJson(verdict.schedule as unknown as JsonValue);\n const kept = canonicalJson([...verdict.keptEvents]);\n const keys = canonicalJson([...verdict.violation.keys]);\n const title =\n mode === \"holds\"\n ? `${verdict.scenario}: ${verdict.violation.invariant} holds on the recorded schedule`\n : `${verdict.scenario}: ${verdict.violation.invariant} is still violated on the recorded schedule`;\n const assertion =\n mode === \"holds\"\n ? ` expect(violations.map((v) => \\`\\${v.invariant}: \\${v.message}\\`)).toEqual([]);`\n : ` expect(violations.map((v) => [v.invariant, v.keys])).toContainEqual([${JSON.stringify(verdict.violation.invariant)}, ${keys}]);`;\n const lines = [\n \"// Generated by Eutaxy. Replays a recorded violation; regenerate rather than edit.\",\n `// Scenario ${verdict.scenario}, seed ${String(verdict.seed)}: ${verdict.violation.message}`,\n `import { checkInvariants, replay, restrictSpec } from ${JSON.stringify(engineModule)};`,\n ...options.imports,\n 'import { expect, test } from \"vitest\";',\n \"\",\n `const schedule = ${schedule} as const;`,\n `const keptEvents = ${kept} as const;`,\n \"\",\n `test(${JSON.stringify(title)}, async () => {`,\n ` const platform = ${options.platform};`,\n ` const spec = restrictSpec(platform, ${options.scenario}, keptEvents);`,\n \" const log = await replay(spec, platform, schedule);\",\n \" const violations = checkInvariants(log, spec.invariants);\",\n assertion,\n \"});\",\n \"\",\n ];\n return lines.join(\"\\n\");\n}\n","/**\n * Rendering verdicts: a text form for people and a versioned JSON form for\n * CI and coding agents. Both are pure functions over the verdict data.\n */\n\nimport { canonicalJson, type EffectLog, type JsonValue, type ScenarioVerdict } from \"@eutaxy/core\";\n\nexport const reportSchemaVersion = 1;\n\n/** The JSON report: one object, canonical, with a schema version at the top. */\nexport function renderJson(verdict: ScenarioVerdict): string {\n return canonicalJson({\n schemaVersion: reportSchemaVersion,\n generator: \"eutaxy\",\n verdict: verdict as unknown as JsonValue,\n });\n}\n\nfunction clip(s: string, max: number): string {\n return s.length <= max ? s : `${s.slice(0, max - 1)}…`;\n}\n\nfunction pad(s: string, width: number): string {\n return s.length >= width ? s : s + \" \".repeat(width - s.length);\n}\n\n/** The trace as a fixed-width table; evidence rows are marked with `*`. */\nexport function renderTrace(log: EffectLog, evidence: readonly number[] = []): string {\n const rows = log.entries.map((e) => [\n evidence.includes(e.seq) ? \"*\" : \" \",\n String(e.seq),\n e.invocation,\n e.kind,\n e.target,\n e.op,\n clip(canonicalJson(e.payload), 72),\n ]);\n const header = [\" \", \"seq\", \"invocation\", \"kind\", \"target\", \"op\", \"payload\"];\n const widths = header.map((h, i) =>\n Math.max(h.length, ...rows.map((r) => (r[i] as string).length)),\n );\n const line = (cells: readonly string[]) =>\n cells\n .map((c, i) => (i === cells.length - 1 ? c : pad(c, widths[i] as number)))\n .join(\" \")\n .trimEnd();\n return [line(header), ...rows.map(line)].join(\"\\n\");\n}\n\n/** A readable report: verdict, what it rests on, and for a violation the schedule and trace. */\nexport function renderText(verdict: ScenarioVerdict): string {\n const out: string[] = [];\n const r = verdict.reached;\n out.push(`${verdict.kind} ${verdict.scenario} (seed ${String(verdict.seed)})`);\n switch (verdict.kind) {\n case \"VIOLATED\": {\n out.push(\"\");\n out.push(\n `Invariant ${verdict.violation.invariant} is violated: ${verdict.violation.message}`,\n );\n out.push(`Keys: ${verdict.violation.keys.join(\", \")}`);\n out.push(\"\");\n out.push(\n `Schedule (run seed ${verdict.schedule.seed}), events kept: ${verdict.keptEvents.join(\", \")}`,\n );\n if (verdict.schedule.faults.length === 0) out.push(\" no injected faults\");\n for (const f of verdict.schedule.faults) {\n out.push(` ${f.fault} at ${f.invocation} effect #${f.ordinal} (${f.target} ${f.op})`);\n }\n out.push(\n `Shrunk: ${verdict.shrunk.eventsRemoved} event(s) and ${verdict.shrunk.faultsRemoved} fault(s) removed in ${verdict.shrunk.runs} run(s)`,\n );\n out.push(\"\");\n out.push(\"Trace:\");\n out.push(renderTrace(verdict.trace, verdict.violation.evidence));\n break;\n }\n case \"BOUNDED\": {\n const b = verdict.bounds;\n out.push(\"\");\n out.push(\"No violation found within the stated bounds. This is not a proof.\");\n out.push(\n `Bounds: faults per run ≤ ${b.maxFaultsPerRun}, redeliveries ≤ ${b.maxRedeliveries}, concurrent invocations ≤ ${b.maxConcurrentInvocations}, budget ${b.budgetMs} ms`,\n );\n break;\n }\n case \"UNSUPPORTED\": {\n out.push(\"\");\n out.push(`Eutaxy could not decide this scenario (${verdict.cause}): ${verdict.reason}`);\n break;\n }\n }\n out.push(\"\");\n out.push(\n `Explored: ${r.runs} run(s), ${r.effectPoints} effect point(s), ${r.faultsInjected} fault(s) injected, ${r.elapsedMs} ms`,\n );\n out.push(\"Assumptions:\");\n for (const a of verdict.assumptions) out.push(` - ${a}`);\n return out.join(\"\\n\");\n}\n"],"mappings":"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"}
|