feat(drift-trio): phase D — seeded fuzz harness; finding #10 pinned
drift-trio-fuzz.spec.ts: mulberry32-seeded weighted concurrent actions on A+B (C observes), K=40, marker-synced oracle sweep every 10 steps, per-tab console capture, and a replayable failure artifact (seed + action log + drift/saves/ renders/consoles) dumped to logs/kicad/drift-trio-fuzz/. DRIFT_FUZZ_SEED replays a sequence exactly. First catch (0008 §10 #10, OPEN): the actively-editing receiver's C++ apply silently drops ops under sustained bidirectional load — JS dispatched every apply, the editor never applied some; pure observer and emitter stay clean; one run hit a wasm 'memory access out of bounds' on the observer. Timing- dependent, so the fuzz tests are test.fixme'd (an expected-fail would flake CI) and run manually via DRIFT_FUZZ_SEED until the fix lands (phase E). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
This commit is contained in:
parent
23b0f43e73
commit
a5751d8542
1 changed files with 499 additions and 0 deletions
499
tests/kicad/drift-trio-fuzz.spec.ts
Normal file
499
tests/kicad/drift-trio-fuzz.spec.ts
Normal file
|
|
@ -0,0 +1,499 @@
|
|||
import { execSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { Page } from "@playwright/test";
|
||||
import { test, expect } from "./fixtures";
|
||||
import {
|
||||
TRIO_PCB,
|
||||
TRIO_SCH,
|
||||
type ToolCfg,
|
||||
type Trio,
|
||||
SYM1,
|
||||
WIRE1,
|
||||
WIRE2,
|
||||
FP1,
|
||||
PAD1,
|
||||
PAD2,
|
||||
VIA1,
|
||||
SEG1,
|
||||
SEG2,
|
||||
callHook,
|
||||
closeTrio,
|
||||
drift,
|
||||
hasAbort,
|
||||
modelText,
|
||||
openTrio,
|
||||
oracleSweep,
|
||||
renderDoc,
|
||||
settleConverged,
|
||||
} from "./utils/trio";
|
||||
|
||||
/**
|
||||
* Drift trio harness — S10 seeded fuzz (standalone-hardening 0008 §6, phase D).
|
||||
*
|
||||
* A deterministic PRNG (mulberry32) interleaves weighted catalog actions on A
|
||||
* and B — mutations > creations > deletions — with NO settling between steps;
|
||||
* C only observes. Every SYNC_EVERY steps both actors drop a marker, the run
|
||||
* waits for both markers on every tab, then settles and runs the full oracle
|
||||
* sweep. Any failure dumps a REPLAYABLE artifact (seed + full action log +
|
||||
* per-tab drift/saves/renders) to logs/kicad/drift-trio-fuzz/.
|
||||
*
|
||||
* DETERMINISM: the seed is fixed (1) unless DRIFT_FUZZ_SEED is set — CI always
|
||||
* runs the same sequence; exploration happens locally via the env var, and a
|
||||
* found bug's seed is then pinned as a named regression spec.
|
||||
*
|
||||
* Hook results are NOT asserted mid-run: an action racing a concurrent delete
|
||||
* legitimately loses (returns false/"" — finding #9's policy). The oracles are
|
||||
* the contract; the log records every action + result for triage.
|
||||
*/
|
||||
|
||||
const K_STEPS = 40;
|
||||
const SYNC_EVERY = 10;
|
||||
const SEED = Number(process.env.DRIFT_FUZZ_SEED ?? 1);
|
||||
|
||||
test.beforeAll(() => {
|
||||
execSync("node collab/build.mjs", { cwd: path.resolve(__dirname, ".."), stdio: "inherit" });
|
||||
});
|
||||
|
||||
function skipFirefox(): void {
|
||||
test.skip(
|
||||
test.info().project.name.includes("firefox"),
|
||||
"three heavy wasm tabs exceed Firefox's per-process wasm budget",
|
||||
);
|
||||
}
|
||||
|
||||
/** mulberry32 — tiny deterministic PRNG. */
|
||||
function mulberry32(seed: number): () => number {
|
||||
let a = seed >>> 0;
|
||||
return () => {
|
||||
a |= 0;
|
||||
a = (a + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
interface Rng {
|
||||
next(): number;
|
||||
int(lo: number, hi: number): number;
|
||||
pick<T>(arr: readonly T[]): T | undefined;
|
||||
}
|
||||
|
||||
function makeRng(seed: number): Rng {
|
||||
const next = mulberry32(seed);
|
||||
return {
|
||||
next,
|
||||
int: (lo, hi) => lo + Math.floor(next() * (hi - lo + 1)),
|
||||
pick: (arr) => (arr.length ? arr[Math.floor(next() * arr.length)] : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
/** Known-item registry (node-side truth; races only make actions no-op). */
|
||||
interface Reg {
|
||||
/** value-carrying primaries (symbols / footprints) */
|
||||
primaries: string[];
|
||||
/** endpoint-carrying items (wires / tracks) */
|
||||
lines: string[];
|
||||
/** everything deletable */
|
||||
all: string[];
|
||||
}
|
||||
|
||||
interface FuzzAction {
|
||||
name: string;
|
||||
weight: number;
|
||||
/** Returns a result string for the log; may register/unregister uuids. */
|
||||
run(page: Page, rng: Rng, reg: Reg, step: number): Promise<string>;
|
||||
}
|
||||
|
||||
function registerCreated(reg: Reg, uuid: string, kind: "primary" | "line" | "other"): void {
|
||||
if (!/[0-9a-f-]{36}/.test(uuid)) return;
|
||||
reg.all.push(uuid);
|
||||
if (kind === "primary") reg.primaries.push(uuid);
|
||||
if (kind === "line") reg.lines.push(uuid);
|
||||
}
|
||||
|
||||
function unregister(reg: Reg, uuid: string): void {
|
||||
for (const arr of [reg.all, reg.primaries, reg.lines]) {
|
||||
const i = arr.indexOf(uuid);
|
||||
if (i >= 0) arr.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Positions: step-scoped lanes so creations never overlap exactly (grid-ish,
|
||||
// deterministic). eeschema IU 1e4/mm; pcbnew IU 1e6/mm.
|
||||
const schX = (rng: Rng) => rng.int(40, 170) * 10000;
|
||||
const schY = (rng: Rng) => rng.int(40, 170) * 10000;
|
||||
const pcbX = (rng: Rng) => rng.int(20, 160) * 1000000;
|
||||
const pcbY = (rng: Rng) => rng.int(20, 160) * 1000000;
|
||||
|
||||
function schActions(): FuzzAction[] {
|
||||
return [
|
||||
{
|
||||
name: "moveItem",
|
||||
weight: 5,
|
||||
run: async (p, rng, reg) => {
|
||||
const t = rng.pick(reg.all);
|
||||
if (!t) return "skip:empty";
|
||||
const ok = await callHook<boolean>(p, "kicadCollabTestMoveSchItem", t, rng.int(-30, 30) * 10000, rng.int(-30, 30) * 10000);
|
||||
return `${t.slice(0, 8)}→${ok}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rotate",
|
||||
weight: 3,
|
||||
run: async (p, rng, reg) => {
|
||||
const t = rng.pick(reg.all);
|
||||
if (!t) return "skip:empty";
|
||||
const ok = await callHook<boolean>(p, "kicadCollabTestRotateItem", t, 90 * rng.int(1, 3));
|
||||
return `${t.slice(0, 8)}→${ok}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mirror",
|
||||
weight: 2,
|
||||
run: async (p, rng, reg) => {
|
||||
const t = rng.pick(reg.all);
|
||||
if (!t) return "skip:empty";
|
||||
const ok = await callHook<boolean>(p, "kicadCollabTestMirrorSchItem", t, rng.next() < 0.5);
|
||||
return `${t.slice(0, 8)}→${ok}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "setValue",
|
||||
weight: 4,
|
||||
run: async (p, rng, reg, step) => {
|
||||
const t = rng.pick(reg.primaries);
|
||||
if (!t) return "skip:empty";
|
||||
const ok = await callHook<boolean>(p, "kicadCollabTestSetFieldText", t, `fz-${step}`);
|
||||
return `${t.slice(0, 8)}→${ok}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addWire",
|
||||
weight: 3,
|
||||
run: async (p, rng, _reg, step) => {
|
||||
const x = schX(rng);
|
||||
const y = schY(rng);
|
||||
const u = await callHook<string>(p, "kicadCollabTestAddWire", x, y, x + rng.int(2, 12) * 10000, y);
|
||||
registerCreated(_reg, u, "line");
|
||||
return u ? u.slice(0, 8) : `fail@${step}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addLabel",
|
||||
weight: 2,
|
||||
run: async (p, rng, reg, step) => {
|
||||
const kind = rng.pick(["label", "global", "hier"] as const)!;
|
||||
const u = await callHook<string>(p, "kicadCollabTestAddLabel", kind, `FZ${step}`, schX(rng), schY(rng));
|
||||
registerCreated(reg, u, "other");
|
||||
return u ? `${kind}:${u.slice(0, 8)}` : "fail";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addSymbol",
|
||||
weight: 2,
|
||||
run: async (p, rng, reg, step) => {
|
||||
const u = await callHook<string>(p, "kicadCollabTestAddSymbol", "Device:R", schX(rng), schY(rng), `R${20 + step}`);
|
||||
registerCreated(reg, u, "primary");
|
||||
return u ? u.slice(0, 8) : "fail";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "duplicate",
|
||||
weight: 1,
|
||||
run: async (p, rng, reg) => {
|
||||
const t = rng.pick(reg.primaries);
|
||||
if (!t) return "skip:empty";
|
||||
const u = await callHook<string>(p, "kicadCollabTestDuplicateSchItem", t, rng.int(5, 20) * 10000, 0);
|
||||
registerCreated(reg, u, "primary");
|
||||
return u ? `${t.slice(0, 8)}→${u.slice(0, 8)}` : "fail";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove",
|
||||
weight: 1,
|
||||
run: async (p, rng, reg) => {
|
||||
const t = rng.pick(reg.all);
|
||||
if (!t) return "skip:empty";
|
||||
unregister(reg, t); // node-side intent; the race may no-op it
|
||||
const ok = await callHook<boolean>(p, "kicadCollabTestRemoveItem", t);
|
||||
return `${t.slice(0, 8)}→${ok}`;
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function pcbActions(): FuzzAction[] {
|
||||
return [
|
||||
{
|
||||
name: "moveItem",
|
||||
weight: 5,
|
||||
run: async (p, rng, reg) => {
|
||||
const t = rng.pick(reg.all);
|
||||
if (!t) return "skip:empty";
|
||||
const ok = await callHook<boolean>(p, "kicadCollabTestMoveBoardItem", t, rng.int(-20, 20) * 100000, rng.int(-20, 20) * 100000);
|
||||
return `${t.slice(0, 8)}→${ok}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rotate",
|
||||
weight: 3,
|
||||
run: async (p, rng, reg) => {
|
||||
const t = rng.pick(reg.all);
|
||||
if (!t) return "skip:empty";
|
||||
const ok = await callHook<boolean>(p, "kicadCollabTestRotateItem", t, 90 * rng.int(1, 3));
|
||||
return `${t.slice(0, 8)}→${ok}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "flip",
|
||||
weight: 2,
|
||||
run: async (p, rng, reg) => {
|
||||
const t = rng.pick(reg.primaries);
|
||||
if (!t) return "skip:empty";
|
||||
const ok = await callHook<boolean>(p, "kicadCollabTestFlipBoardItem", t);
|
||||
return `${t.slice(0, 8)}→${ok}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "setValue",
|
||||
weight: 3,
|
||||
run: async (p, rng, reg, step) => {
|
||||
const t = rng.pick(reg.primaries);
|
||||
if (!t) return "skip:empty";
|
||||
const ok = await callHook<boolean>(p, "kicadCollabTestSetFootprintField", t, "Value", `fz-${step}`);
|
||||
return `${t.slice(0, 8)}→${ok}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "setLocked",
|
||||
weight: 2,
|
||||
run: async (p, rng, reg) => {
|
||||
const t = rng.pick(reg.all);
|
||||
if (!t) return "skip:empty";
|
||||
const ok = await callHook<boolean>(p, "kicadCollabTestSetBoardItemLocked", t, rng.next() < 0.5);
|
||||
return `${t.slice(0, 8)}→${ok}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "setPadSize",
|
||||
weight: 2,
|
||||
run: async (p, rng) => {
|
||||
const pad = rng.pick([PAD1, PAD2])!;
|
||||
const ok = await callHook<boolean>(p, "kicadCollabTestSetPadSize", pad, rng.int(8, 20) * 100000, rng.int(8, 20) * 100000);
|
||||
return `${pad.slice(-2)}→${ok}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "moveEndpoint",
|
||||
weight: 2,
|
||||
run: async (p, rng, reg) => {
|
||||
const t = rng.pick(reg.lines);
|
||||
if (!t) return "skip:empty";
|
||||
const ok = await callHook<boolean>(p, "kicadCollabTestMoveEndpoint", t, rng.int(-20, 20) * 100000, rng.int(-20, 20) * 100000);
|
||||
return `${t.slice(0, 8)}→${ok}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addTrack",
|
||||
weight: 3,
|
||||
run: async (p, rng, reg) => {
|
||||
const x = pcbX(rng);
|
||||
const y = pcbY(rng);
|
||||
const u = await callHook<string>(p, "kicadCollabTestAddTrack", x, y, x + rng.int(2, 20) * 1000000, y, rng.int(2, 6) * 100000, "F.Cu");
|
||||
registerCreated(reg, u, "line");
|
||||
return u ? u.slice(0, 8) : "fail";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addVia",
|
||||
weight: 2,
|
||||
run: async (p, rng, reg) => {
|
||||
const u = await callHook<string>(p, "kicadCollabTestAddVia", pcbX(rng), pcbY(rng), 800000, 400000);
|
||||
registerCreated(reg, u, "other");
|
||||
return u ? u.slice(0, 8) : "fail";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addText",
|
||||
weight: 2,
|
||||
run: async (p, rng, reg, step) => {
|
||||
const u = await callHook<string>(p, "kicadCollabTestAddBoardText", `FZ${step}`, pcbX(rng), pcbY(rng), "F.SilkS");
|
||||
registerCreated(reg, u, "other");
|
||||
return u ? u.slice(0, 8) : "fail";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "duplicate",
|
||||
weight: 1,
|
||||
run: async (p, rng, reg) => {
|
||||
const t = rng.pick(reg.primaries);
|
||||
if (!t) return "skip:empty";
|
||||
const u = await callHook<string>(p, "kicadCollabTestDuplicateBoardItem", t, rng.int(5, 25) * 1000000, 0);
|
||||
registerCreated(reg, u, "primary");
|
||||
return u ? `${t.slice(0, 8)}→${u.slice(0, 8)}` : "fail";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove",
|
||||
weight: 1,
|
||||
run: async (p, rng, reg) => {
|
||||
const t = rng.pick(reg.all);
|
||||
if (!t) return "skip:empty";
|
||||
unregister(reg, t);
|
||||
const ok = await callHook<boolean>(p, "kicadCollabTestRemoveItem", t);
|
||||
return `${t.slice(0, 8)}→${ok}`;
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function weightedPick(rng: Rng, actions: FuzzAction[]): FuzzAction {
|
||||
const total = actions.reduce((s, a) => s + a.weight, 0);
|
||||
let roll = rng.next() * total;
|
||||
for (const a of actions) {
|
||||
roll -= a.weight;
|
||||
if (roll <= 0) return a;
|
||||
}
|
||||
return actions[actions.length - 1]!;
|
||||
}
|
||||
|
||||
interface LogEntry {
|
||||
step: number;
|
||||
actor: string;
|
||||
action: string;
|
||||
result: string;
|
||||
}
|
||||
|
||||
const ART_DIR = path.resolve(__dirname, "../logs/kicad/drift-trio-fuzz");
|
||||
|
||||
async function dumpArtifact(
|
||||
trio: Trio,
|
||||
cfg: ToolCfg,
|
||||
label: string,
|
||||
log: LogEntry[],
|
||||
err: unknown,
|
||||
consoles: Record<string, string[]> = {},
|
||||
): Promise<void> {
|
||||
fs.mkdirSync(ART_DIR, { recursive: true });
|
||||
const base = path.join(ART_DIR, `${label}-seed${SEED}`);
|
||||
const tabs: Record<string, unknown> = {};
|
||||
for (const [tabLabel, page] of trio.tabs) {
|
||||
const save = await modelText(page, cfg).catch((e) => `ERR:${e}`);
|
||||
const render = await renderDoc(page).catch((e) => ({ err: String(e) }));
|
||||
const d = await drift(page, cfg).catch((e) => `ERR:${e}`);
|
||||
fs.writeFileSync(`${base}-${tabLabel}.${cfg.ext}`, typeof save === "string" ? save : String(save));
|
||||
tabs[tabLabel] = { drift: d, renderErr: (render as { err?: string }).err ?? null };
|
||||
fs.writeFileSync(`${base}-${tabLabel}.render.txt`, (render as { ok?: string }).ok ?? `ERR: ${(render as { err?: string }).err}`);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
`${base}.json`,
|
||||
JSON.stringify({ seed: SEED, steps: K_STEPS, error: String(err), tabs, consoles, log }, null, 2),
|
||||
);
|
||||
}
|
||||
|
||||
for (const [cfg, label, mkActions, seedReg] of [
|
||||
[
|
||||
TRIO_SCH,
|
||||
"eeschema",
|
||||
schActions,
|
||||
(): Reg => ({ primaries: [SYM1], lines: [WIRE1, WIRE2], all: [SYM1, WIRE1, WIRE2] }),
|
||||
],
|
||||
[
|
||||
TRIO_PCB,
|
||||
"pcbnew",
|
||||
pcbActions,
|
||||
(): Reg => ({ primaries: [FP1], lines: [SEG1, SEG2], all: [FP1, VIA1, SEG1, SEG2] }),
|
||||
],
|
||||
] as const) {
|
||||
test.describe(`drift trio fuzz — ${label}`, () => {
|
||||
test.describe.configure({ timeout: 900000 });
|
||||
|
||||
test(`${label} S10: seeded fuzz, K=${K_STEPS}, seed=${SEED}`, async ({
|
||||
context,
|
||||
testLogger,
|
||||
}) => {
|
||||
skipFirefox();
|
||||
// FINDING #10 (standalone-hardening 0008 §10, open): under sustained
|
||||
// bidirectional load the ACTIVELY-EDITING receiver's C++ apply silently
|
||||
// drops ops (JS dispatched them; the editor never applied) — eeschema
|
||||
// reproduces at seed 1 by sync-2/3, pcbnew intermittently; one run hit a
|
||||
// wasm "memory access out of bounds" on the observer. Fixme (not
|
||||
// test.fail) because the wedge point is timing-dependent — a flaky
|
||||
// expected-fail would red CI on its lucky runs. Run manually:
|
||||
// DRIFT_FUZZ_SEED=1 npx playwright test --project=kicad-chromium kicad/drift-trio-fuzz.spec.ts
|
||||
// Un-fixme when #10 is fixed (phase E).
|
||||
test.fixme(
|
||||
!process.env.DRIFT_FUZZ_SEED,
|
||||
"finding #10: apply drops under bidirectional load — run manually via DRIFT_FUZZ_SEED",
|
||||
);
|
||||
const trio = await openTrio(context, cfg, `fuzz-${label}-${SEED}-${test.info().workerIndex}`);
|
||||
// Per-tab console capture — testLogger only hooks the default page, and
|
||||
// apply/parse failures surface as console lines on the affected TAB.
|
||||
const consoles: Record<string, string[]> = {};
|
||||
for (const [tabLabel, page] of trio.tabs) {
|
||||
consoles[tabLabel] = [];
|
||||
page.on("console", (m) => {
|
||||
const t = m.text();
|
||||
if (/collab|drift|parse|error|abort/i.test(t)) consoles[tabLabel]!.push(t.slice(0, 400));
|
||||
});
|
||||
page.on("pageerror", (e) => consoles[tabLabel]!.push(`PAGEERROR ${e.message}`));
|
||||
}
|
||||
const actions = mkActions();
|
||||
const reg = seedReg();
|
||||
const rngA = makeRng(SEED * 2 + 1);
|
||||
const rngB = makeRng(SEED * 3 + 7);
|
||||
const log: LogEntry[] = [];
|
||||
|
||||
const marker =
|
||||
label === "eeschema"
|
||||
? (p: Page, text: string, slot: number) =>
|
||||
callHook<string>(p, "kicadCollabTestAddLabel", "label", text, 200000 + slot * 40000, 200000)
|
||||
: (p: Page, text: string, slot: number) =>
|
||||
callHook<string>(p, "kicadCollabTestAddBoardText", text, 10000000 + slot * 12000000, 180000000, "F.SilkS");
|
||||
|
||||
try {
|
||||
for (let step = 0; step < K_STEPS; step++) {
|
||||
const actA = weightedPick(rngA, actions);
|
||||
const actB = weightedPick(rngB, actions);
|
||||
const [resA, resB] = await Promise.all([
|
||||
actA.run(trio.A, rngA, reg, step).catch((e) => `ERR:${e}`),
|
||||
actB.run(trio.B, rngB, reg, step).catch((e) => `ERR:${e}`),
|
||||
]);
|
||||
log.push({ step, actor: "A", action: actA.name, result: resA });
|
||||
log.push({ step, actor: "B", action: actB.name, result: resB });
|
||||
|
||||
if ((step + 1) % SYNC_EVERY === 0) {
|
||||
const slot = Math.floor(step / SYNC_EVERY);
|
||||
const [mA, mB] = await Promise.all([
|
||||
marker(trio.A, `sync-${slot}-a`, slot * 2),
|
||||
marker(trio.B, `sync-${slot}-b`, slot * 2 + 1),
|
||||
]);
|
||||
registerCreated(reg, mA, "other");
|
||||
registerCreated(reg, mB, "other");
|
||||
for (const m of [`sync-${slot}-a`, `sync-${slot}-b`]) {
|
||||
for (const [tabLabel, page] of trio.tabs) {
|
||||
await expect
|
||||
.poll(async () => (await modelText(page, cfg)).includes(m), {
|
||||
timeout: 45000,
|
||||
intervals: [500],
|
||||
message: `${tabLabel} must receive marker ${m}`,
|
||||
})
|
||||
.toBe(true);
|
||||
}
|
||||
}
|
||||
await settleConverged(trio, cfg, 45000);
|
||||
await oracleSweep(trio, cfg);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
await dumpArtifact(trio, cfg, label, log, err, consoles);
|
||||
throw err;
|
||||
}
|
||||
|
||||
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
||||
await closeTrio(trio);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue