2026-06-10 18:06:33 +02:00
|
|
|
|
import type { BrowserContext, Page } from "@playwright/test";
|
|
|
|
|
|
import { test, expect } from "./fixtures";
|
|
|
|
|
|
import { sexprDiff } from "../../web/standalone/src/wasm/collab/sexpr-diff";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-06-11 15:25:30 +02:00
|
|
|
|
* Round-trip integration tests (feature 0004; v2 items wire since ysync 0008
|
|
|
|
|
|
* Stage D): for each collab-capable, file-bearing tool prove the Y.Doc
|
|
|
|
|
|
* representation is lossless —
|
2026-06-10 18:06:33 +02:00
|
|
|
|
*
|
|
|
|
|
|
* load a fixture file → save it (ORIG, normalized through the serializer)
|
2026-06-11 15:25:30 +02:00
|
|
|
|
* → kicadCollabSnapshotItems (per-item s-expr blobs, the Slot-model wire)
|
2026-06-10 18:06:33 +02:00
|
|
|
|
* → RELOAD the page (fresh process-global wasm; boot.ts) → open an EMPTY doc
|
2026-06-11 15:25:30 +02:00
|
|
|
|
* → kicadCollabApplyItems(snapshot) (rebuild the model purely from the blobs)
|
2026-06-10 18:06:33 +02:00
|
|
|
|
* → save (REGEN)
|
|
|
|
|
|
* → assert sexprDiff(ORIG, REGEN).equal
|
|
|
|
|
|
*
|
|
|
|
|
|
* Both ORIG and REGEN come out of the SAME tool serializer, so `sexprDiff` (which
|
|
|
|
|
|
* compares uuid-keyed items as multisets of normalized children) is false-positive
|
|
|
|
|
|
* free by construction — non-item structure (setup, layers, paper) carries no uuid
|
|
|
|
|
|
* and is intentionally not compared.
|
|
|
|
|
|
*
|
|
|
|
|
|
* The empty fixture shares the full fixture's top-level identity uuid(s) (where a
|
|
|
|
|
|
* tool has one, e.g. the schematic root) so only the synced ITEMS differ between
|
|
|
|
|
|
* ORIG and REGEN.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
interface ToolCfg {
|
|
|
|
|
|
tool: string;
|
|
|
|
|
|
html: string;
|
|
|
|
|
|
ext: string;
|
|
|
|
|
|
/** The per-tool save embind export (README §A). */
|
|
|
|
|
|
saveFn: "kicadSaveDrawingSheet" | "kicadSaveSchematic" | "kicadSaveBoard";
|
|
|
|
|
|
/** Full document with explicit uuid items to round-trip. */
|
|
|
|
|
|
fixture: string;
|
|
|
|
|
|
/** Valid, openable document of the same type carrying NO synced items (but the
|
|
|
|
|
|
* same top-level identity uuids as `fixture`). */
|
|
|
|
|
|
empty: string;
|
|
|
|
|
|
/** Known-volatile top-level tokens to ignore (serializer nondeterminism). */
|
|
|
|
|
|
ignoreTokens?: string[];
|
fix(ysync): wire dialect == file dialect, uuid churn, drift noise
The Y.Doc is the source of truth for the FILE, but both live wires wrote
KiCad's CLIPBOARD dialect — a lossy, paste-oriented format. Every
difference was permanent, unfixable drift.
- pcbnew: serialize footprint blobs with CTL_FOR_BOARD, not CLIPBOARD_IO's
CTL_FOR_CLIPBOARD, which emitted (version)(generator)(generator_version)
inside every (footprint …). Keep (locked yes).
- eeschema: aForClipboard=false — clipboard mode collapsed every symbol's
(instances … (path "/sheet")) to (path "").
- Re-supply (version) at PARSE time only (withFootprintVersion): the token
is invalid file content but load-bearing on decode — without it the
parser starts at m_requiredVersion=0 and stamps (hide yes) on every
mandatory field.
- FOOTPRINT copy ctor: restore mandatory-field uuids (EDA_ITEM::operator=
keeps the target's const m_Uuid, so Clone() rerolled all four).
- drift: classify order-only diffs as `reordered` — y-sexpr v2 reorders
legitimately; excluded from counts, report-worthiness and dedupe hashes.
Migration 0017.
- fpedit from eeschema: AsyncLoad()+BlockUntilLoaded() in initLibraryTree —
FACE_PCB starts lazily there and never preloaded its libraries.
Guards: wire-vs-file round-trip tests (pcbnew + eeschema),
fpedit-from-eeschema (verified red without the fix), symedit-from-eeschema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9kjdGBdcpwUSjJ3q5xg2
2026-07-20 18:29:52 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* uuids that live in the FILE but are legitimately absent from the items wire,
|
|
|
|
|
|
* because they identify the document rather than an item — e.g. a schematic's
|
|
|
|
|
|
* root `(uuid …)`, which travels in kdoc_meta/kdoc_layout, not kdoc_items.
|
|
|
|
|
|
* Listed explicitly per fixture so `expectWireMatchesFile` can subtract them
|
|
|
|
|
|
* without blanket-ignoring "the wire dropped an item", which is a real defect.
|
|
|
|
|
|
*/
|
|
|
|
|
|
wireOmitsUuids?: string[];
|
2026-06-10 18:06:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type Mod = {
|
|
|
|
|
|
kicadOpenFile(p: string): unknown;
|
2026-06-11 15:25:30 +02:00
|
|
|
|
kicadCollabSnapshotItems(): string;
|
|
|
|
|
|
kicadCollabApplyItems(j: string): unknown;
|
2026-06-10 18:06:33 +02:00
|
|
|
|
} & Record<string, (p: string) => unknown>;
|
|
|
|
|
|
type FS = {
|
|
|
|
|
|
mkdirTree(p: string): void;
|
|
|
|
|
|
writeFile(p: string, d: string): void;
|
|
|
|
|
|
readFile(p: string, o: { encoding: "utf8" }): string;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Heavy tools (pcbnew ~190MB wasm) can take well over a minute to download +
|
|
|
|
|
|
// compile + boot, especially the SECOND instance of a run; keep this generous.
|
|
|
|
|
|
const BOOT_TIMEOUT = 150000;
|
|
|
|
|
|
|
|
|
|
|
|
function hasAbort(l: { consoleLogs: string[]; errors: string[] }): boolean {
|
|
|
|
|
|
return [...l.consoleLogs, ...l.errors].some((s) => s.includes("Aborted("));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Boot a fresh tool page and open `content` from MEMFS. */
|
|
|
|
|
|
async function bootOpen(page: Page, cfg: ToolCfg, content: string, name: string): Promise<void> {
|
|
|
|
|
|
await page.goto(`/kicad/${cfg.html}`);
|
|
|
|
|
|
await expect(page.locator("#canvas")).toBeVisible({ timeout: BOOT_TIMEOUT });
|
|
|
|
|
|
await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: BOOT_TIMEOUT });
|
|
|
|
|
|
await page.waitForFunction(
|
|
|
|
|
|
(saveFn) => {
|
|
|
|
|
|
const m = (window as unknown as { Module?: Mod }).Module;
|
|
|
|
|
|
return (
|
|
|
|
|
|
typeof m?.kicadOpenFile === "function" &&
|
2026-06-11 15:25:30 +02:00
|
|
|
|
typeof m?.kicadCollabSnapshotItems === "function" &&
|
|
|
|
|
|
typeof m?.kicadCollabApplyItems === "function" &&
|
2026-06-10 18:06:33 +02:00
|
|
|
|
typeof m?.[saveFn] === "function"
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
cfg.saveFn,
|
|
|
|
|
|
{ timeout: BOOT_TIMEOUT },
|
|
|
|
|
|
);
|
|
|
|
|
|
await page.waitForFunction(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
!!window.wxElementRegistry &&
|
|
|
|
|
|
window.wxElementRegistry
|
|
|
|
|
|
.findAll({ visible: true })
|
|
|
|
|
|
.some((e) => /Frame$/.test(e.typeName) || (e.name || "").endsWith("Frame")),
|
|
|
|
|
|
null,
|
|
|
|
|
|
{ timeout: BOOT_TIMEOUT },
|
|
|
|
|
|
);
|
|
|
|
|
|
await page.evaluate(
|
|
|
|
|
|
({ content, name, ext }) => {
|
|
|
|
|
|
const w = window as unknown as { FS: FS; Module: Mod };
|
|
|
|
|
|
try {
|
|
|
|
|
|
w.FS.mkdirTree("/home/kicad/documents");
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* exists */
|
|
|
|
|
|
}
|
|
|
|
|
|
const p = `/home/kicad/documents/${name}.${ext}`;
|
|
|
|
|
|
w.FS.writeFile(p, content);
|
|
|
|
|
|
w.Module.kicadOpenFile(p);
|
|
|
|
|
|
},
|
|
|
|
|
|
{ content, name, ext: cfg.ext },
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Save the current model to MEMFS via the tool's save export and read it back. */
|
|
|
|
|
|
async function saveRead(page: Page, cfg: ToolCfg, name: string): Promise<string> {
|
|
|
|
|
|
return page.evaluate(
|
|
|
|
|
|
({ saveFn, ext, name }) => {
|
|
|
|
|
|
const w = window as unknown as { FS: FS; Module: Mod };
|
|
|
|
|
|
const out = `/home/kicad/documents/${name}.${ext}`;
|
|
|
|
|
|
w.Module[saveFn](out);
|
|
|
|
|
|
return w.FS.readFile(out, { encoding: "utf8" });
|
|
|
|
|
|
},
|
|
|
|
|
|
{ saveFn: cfg.saveFn, ext: cfg.ext, name },
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* The full load → yjs → reload → apply → save loop. Uses TWO separate pages — the
|
|
|
|
|
|
* extract page is CLOSED before the rebuild page opens — so the process-global wasm
|
|
|
|
|
|
* heap of the first instance is freed before the second boots (otherwise the heavy
|
|
|
|
|
|
* tools, e.g. pcbnew at ~190MB, boot the second instance under memory pressure and
|
|
|
|
|
|
* stall). `snap` crosses the boundary as a plain JS string in test scope.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function roundTrip(
|
|
|
|
|
|
context: BrowserContext,
|
|
|
|
|
|
cfg: ToolCfg,
|
|
|
|
|
|
): Promise<{ orig: string; regen: string }> {
|
|
|
|
|
|
// 1–3: load the fixture, normalize via a save, extract the yjs snapshot.
|
|
|
|
|
|
// Open from a FIXED filename for both sides: some serializers embed the opened
|
|
|
|
|
|
// file name (e.g. eeschema's root "Sheetfile" property), so ORIG and REGEN must
|
|
|
|
|
|
// share it or that artifact reads as a (spurious) diff.
|
|
|
|
|
|
const extract = await context.newPage();
|
|
|
|
|
|
await bootOpen(extract, cfg, cfg.fixture, "rt");
|
|
|
|
|
|
const orig = await saveRead(extract, cfg, "orig_dump");
|
2026-06-11 15:25:30 +02:00
|
|
|
|
const snap = await extract.evaluate(() => window.Module.kicadCollabSnapshotItems());
|
2026-06-10 18:06:33 +02:00
|
|
|
|
await extract.close();
|
|
|
|
|
|
|
2026-06-11 15:25:30 +02:00
|
|
|
|
// v2 wire: { added: [{ sexpr, parent }] } — uuids live inside the blobs.
|
|
|
|
|
|
const blobs: string[] = JSON.parse(snap).added.map((w: { sexpr: string }) => w.sexpr);
|
|
|
|
|
|
const ids = [...new Set(blobs.flatMap((s) => [...s.matchAll(/\(uuid "([^"]+)"\)/g)].map((m) => m[1]!)))];
|
2026-06-10 18:06:33 +02:00
|
|
|
|
expect(ids.length, "fixture must contain uuid items").toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
|
|
// 4–5: fresh page (fresh wasm), open an empty doc, rebuild the model from the snapshot.
|
|
|
|
|
|
const rebuild = await context.newPage();
|
|
|
|
|
|
await bootOpen(rebuild, cfg, cfg.empty, "rt");
|
2026-06-11 15:25:30 +02:00
|
|
|
|
await rebuild.evaluate((s) => window.Module.kicadCollabApplyItems(s), snap);
|
2026-06-10 18:06:33 +02:00
|
|
|
|
|
|
|
|
|
|
// apply() runs async for eeschema/pcbnew (CallAfter + coroutine). Best-effort wait
|
|
|
|
|
|
// for the first applied item to materialize — but DON'T fail here: proceed to save
|
|
|
|
|
|
// REGEN regardless, so sexprDiff reports exactly which items failed to round-trip
|
|
|
|
|
|
// (a poll timeout would hide that signal). pl_editor settles within a tick.
|
|
|
|
|
|
const deadline = Date.now() + 15000;
|
|
|
|
|
|
for (;;) {
|
|
|
|
|
|
const probe = await saveRead(rebuild, cfg, "regen_probe");
|
|
|
|
|
|
if (probe.includes(`(uuid "${ids[0]}")`) || Date.now() >= deadline) break;
|
2026-07-07 10:50:24 +02:00
|
|
|
|
await rebuild.waitForTimeout(300); // eslint-disable-line -- deliberate best-effort poll (a hard wait would hide which items failed)
|
2026-06-10 18:06:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const regen = await saveRead(rebuild, cfg, "regen_dump");
|
|
|
|
|
|
await rebuild.close();
|
|
|
|
|
|
return { orig, regen };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
fix(ysync): wire dialect == file dialect, uuid churn, drift noise
The Y.Doc is the source of truth for the FILE, but both live wires wrote
KiCad's CLIPBOARD dialect — a lossy, paste-oriented format. Every
difference was permanent, unfixable drift.
- pcbnew: serialize footprint blobs with CTL_FOR_BOARD, not CLIPBOARD_IO's
CTL_FOR_CLIPBOARD, which emitted (version)(generator)(generator_version)
inside every (footprint …). Keep (locked yes).
- eeschema: aForClipboard=false — clipboard mode collapsed every symbol's
(instances … (path "/sheet")) to (path "").
- Re-supply (version) at PARSE time only (withFootprintVersion): the token
is invalid file content but load-bearing on decode — without it the
parser starts at m_requiredVersion=0 and stamps (hide yes) on every
mandatory field.
- FOOTPRINT copy ctor: restore mandatory-field uuids (EDA_ITEM::operator=
keeps the target's const m_Uuid, so Clone() rerolled all four).
- drift: classify order-only diffs as `reordered` — y-sexpr v2 reorders
legitimately; excluded from counts, report-worthiness and dedupe hashes.
Migration 0017.
- fpedit from eeschema: AsyncLoad()+BlockUntilLoaded() in initLibraryTree —
FACE_PCB starts lazily there and never preloaded its libraries.
Guards: wire-vs-file round-trip tests (pcbnew + eeschema),
fpedit-from-eeschema (verified red without the fix), symedit-from-eeschema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9kjdGBdcpwUSjJ3q5xg2
2026-07-20 18:29:52 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* One boot: the file the tool would SAVE, and the per-item blobs it would put on
|
|
|
|
|
|
* the collab WIRE, taken from the same model at the same moment.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function fileAndWire(
|
|
|
|
|
|
context: BrowserContext,
|
|
|
|
|
|
cfg: ToolCfg,
|
|
|
|
|
|
): Promise<{ file: string; wire: string }> {
|
|
|
|
|
|
const page = await context.newPage();
|
|
|
|
|
|
await bootOpen(page, cfg, cfg.fixture, "rt");
|
|
|
|
|
|
const file = await saveRead(page, cfg, "orig_dump");
|
|
|
|
|
|
const snap = await page.evaluate(() => window.Module.kicadCollabSnapshotItems());
|
|
|
|
|
|
await page.close();
|
|
|
|
|
|
|
|
|
|
|
|
// Splice the blobs into one synthetic document so sexprDiff can index them by
|
|
|
|
|
|
// uuid. Non-footprint blobs already arrive wrapped in their own `(kicad_pcb …)`
|
|
|
|
|
|
// envelope; nesting is harmless — only uuid-bearing forms are compared, and the
|
|
|
|
|
|
// envelope's layers/version carry none.
|
|
|
|
|
|
const blobs: string[] = JSON.parse(snap).added.map((w: { sexpr: string }) => w.sexpr);
|
|
|
|
|
|
return { file, wire: `(${cfg.tool}_wire ${blobs.join("\n")})` };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* THE invariant this suite was missing. `roundTrip` compares two FILE saves, so a
|
|
|
|
|
|
* wire blob written in a different dialect than the file writer round-trips
|
|
|
|
|
|
* perfectly and still corrupts the Y.Doc — which is exactly what happened:
|
|
|
|
|
|
*
|
|
|
|
|
|
* - pcbnew serialized wire footprints through CLIPBOARD_IO (CTL_FOR_CLIPBOARD),
|
|
|
|
|
|
* which unlike CTL_FOR_BOARD emits `(version …) (generator …)
|
|
|
|
|
|
* (generator_version …)` INSIDE every `(footprint …)`. Every footprint of
|
|
|
|
|
|
* every board drifted, permanently.
|
|
|
|
|
|
* - eeschema serialized symbols with `aForClipboard=true`, whose
|
|
|
|
|
|
* `MakeRelativeTo(currentSheet)` collapses `(instances … (path "/<sheet>" …))`
|
|
|
|
|
|
* to `(path "")` — so materializing the Y.Doc would strip every symbol's sheet
|
|
|
|
|
|
* path, reference and unit.
|
|
|
|
|
|
*
|
|
|
|
|
|
* The Y.Doc is the source of truth for the FILE, so the two must agree token for
|
|
|
|
|
|
* token. NOTE: no `ignoreTokens` here on purpose — `generator_version` is exactly
|
|
|
|
|
|
* one of the tokens the pcbnew bug leaked, and ignoring it would re-mask it. (The
|
|
|
|
|
|
* fixture-vs-build version mismatch that `ignoreTokens` exists for lives on the
|
|
|
|
|
|
* ROOT form, which carries no uuid and is therefore never compared.)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function expectWireMatchesFile(context: BrowserContext, cfg: ToolCfg): Promise<void> {
|
|
|
|
|
|
const { file, wire } = await fileAndWire(context, cfg);
|
|
|
|
|
|
const raw = sexprDiff(file, wire);
|
|
|
|
|
|
const omitted = new Set(cfg.wireOmitsUuids ?? []);
|
|
|
|
|
|
const diff = {
|
|
|
|
|
|
...raw,
|
|
|
|
|
|
removed: raw.removed.filter((u) => !omitted.has(u)),
|
|
|
|
|
|
};
|
|
|
|
|
|
expect(
|
|
|
|
|
|
diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0,
|
|
|
|
|
|
`wire blobs disagree with the file save:\n${JSON.stringify(diff, null, 2)}`,
|
|
|
|
|
|
).toBe(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 18:06:33 +02:00
|
|
|
|
// ── Fixtures ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const PL: ToolCfg = {
|
|
|
|
|
|
tool: "pl_editor",
|
|
|
|
|
|
html: "pl_editor.html",
|
|
|
|
|
|
ext: "kicad_wks",
|
|
|
|
|
|
saveFn: "kicadSaveDrawingSheet",
|
|
|
|
|
|
// No root uuid in a .kicad_wks; rect + tbtext carry the uuids we compare.
|
|
|
|
|
|
fixture: `(kicad_wks (version 20220228) (generator "pl_editor") (generator_version "9.0")
|
|
|
|
|
|
(setup (textsize 1.5 1.5)(linewidth 0.15)(textlinewidth 0.15)
|
|
|
|
|
|
(left_margin 10)(right_margin 10)(top_margin 10)(bottom_margin 10))
|
|
|
|
|
|
(rect (uuid "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") (name border) (start 0 0 ltcorner) (end 0 0 rbcorner))
|
|
|
|
|
|
(tbtext "Title" (uuid "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") (name title) (pos 100 20 ltcorner) (font (size 2 2)))
|
|
|
|
|
|
)
|
|
|
|
|
|
`,
|
|
|
|
|
|
empty: `(kicad_wks (version 20220228) (generator "pl_editor") (generator_version "9.0")
|
|
|
|
|
|
(setup (textsize 1.5 1.5)(linewidth 0.15)(textlinewidth 0.15)
|
|
|
|
|
|
(left_margin 10)(right_margin 10)(top_margin 10)(bottom_margin 10))
|
|
|
|
|
|
)
|
|
|
|
|
|
`,
|
|
|
|
|
|
ignoreTokens: ["generator_version"],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const SCH: ToolCfg = {
|
|
|
|
|
|
tool: "eeschema",
|
|
|
|
|
|
html: "eeschema.html",
|
|
|
|
|
|
ext: "kicad_sch",
|
|
|
|
|
|
saveFn: "kicadSaveSchematic",
|
|
|
|
|
|
// Root (uuid …) shared with the empty doc so only the wires differ.
|
|
|
|
|
|
fixture: `(kicad_sch
|
|
|
|
|
|
(version 20250114)
|
|
|
|
|
|
(generator "eeschema")
|
|
|
|
|
|
(generator_version "9.0")
|
|
|
|
|
|
(uuid "11111111-1111-1111-1111-111111111111")
|
|
|
|
|
|
(paper "A4")
|
|
|
|
|
|
(lib_symbols)
|
|
|
|
|
|
(wire (pts (xy 50.8 50.8) (xy 101.6 50.8)) (stroke (width 0) (type default)) (uuid "22222222-0000-0000-0000-000000000001"))
|
|
|
|
|
|
(wire (pts (xy 50.8 76.2) (xy 101.6 76.2)) (stroke (width 0) (type default)) (uuid "22222222-0000-0000-0000-000000000002"))
|
|
|
|
|
|
(sheet_instances (path "/" (page "1")))
|
|
|
|
|
|
)
|
|
|
|
|
|
`,
|
|
|
|
|
|
empty: `(kicad_sch
|
|
|
|
|
|
(version 20250114)
|
|
|
|
|
|
(generator "eeschema")
|
|
|
|
|
|
(generator_version "9.0")
|
|
|
|
|
|
(uuid "11111111-1111-1111-1111-111111111111")
|
|
|
|
|
|
(paper "A4")
|
|
|
|
|
|
(lib_symbols)
|
|
|
|
|
|
(sheet_instances (path "/" (page "1")))
|
|
|
|
|
|
)
|
|
|
|
|
|
`,
|
|
|
|
|
|
ignoreTokens: ["generator_version"],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
fix(ysync): wire dialect == file dialect, uuid churn, drift noise
The Y.Doc is the source of truth for the FILE, but both live wires wrote
KiCad's CLIPBOARD dialect — a lossy, paste-oriented format. Every
difference was permanent, unfixable drift.
- pcbnew: serialize footprint blobs with CTL_FOR_BOARD, not CLIPBOARD_IO's
CTL_FOR_CLIPBOARD, which emitted (version)(generator)(generator_version)
inside every (footprint …). Keep (locked yes).
- eeschema: aForClipboard=false — clipboard mode collapsed every symbol's
(instances … (path "/sheet")) to (path "").
- Re-supply (version) at PARSE time only (withFootprintVersion): the token
is invalid file content but load-bearing on decode — without it the
parser starts at m_requiredVersion=0 and stamps (hide yes) on every
mandatory field.
- FOOTPRINT copy ctor: restore mandatory-field uuids (EDA_ITEM::operator=
keeps the target's const m_Uuid, so Clone() rerolled all four).
- drift: classify order-only diffs as `reordered` — y-sexpr v2 reorders
legitimately; excluded from counts, report-worthiness and dedupe hashes.
Migration 0017.
- fpedit from eeschema: AsyncLoad()+BlockUntilLoaded() in initLibraryTree —
FACE_PCB starts lazily there and never preloaded its libraries.
Guards: wire-vs-file round-trip tests (pcbnew + eeschema),
fpedit-from-eeschema (verified red without the fix), symedit-from-eeschema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9kjdGBdcpwUSjJ3q5xg2
2026-07-20 18:29:52 +02:00
|
|
|
|
// A schematic carrying a real SYMBOL with `(instances …)`. The wire-vs-file test
|
|
|
|
|
|
// below needs one: the eeschema clipboard dialect rewrote the instance path
|
|
|
|
|
|
// relative to the current sheet, collapsing it to `(path "")`. A wire-only
|
|
|
|
|
|
// fixture (SCH above) cannot catch that — only symbols have instances.
|
|
|
|
|
|
const SCH_SYM: ToolCfg = {
|
|
|
|
|
|
...SCH,
|
|
|
|
|
|
// The schematic's own root uuid is document identity (kdoc_meta/kdoc_layout),
|
|
|
|
|
|
// never an item on the items wire — so the file has it and the wire does not.
|
|
|
|
|
|
wireOmitsUuids: ["11111111-1111-1111-1111-111111111111"],
|
|
|
|
|
|
fixture: `(kicad_sch
|
|
|
|
|
|
(version 20250114)
|
|
|
|
|
|
(generator "eeschema")
|
|
|
|
|
|
(generator_version "9.0")
|
|
|
|
|
|
(uuid "11111111-1111-1111-1111-111111111111")
|
|
|
|
|
|
(paper "A4")
|
|
|
|
|
|
(lib_symbols
|
|
|
|
|
|
(symbol "Device:R"
|
|
|
|
|
|
(pin_numbers (hide yes))
|
|
|
|
|
|
(pin_names (offset 0))
|
|
|
|
|
|
(exclude_from_sim no)
|
|
|
|
|
|
(in_bom yes)
|
|
|
|
|
|
(on_board yes)
|
|
|
|
|
|
(property "Reference" "R" (at 2.032 0 90) (effects (font (size 1.27 1.27))))
|
|
|
|
|
|
(property "Value" "R" (at 0 0 90) (effects (font (size 1.27 1.27))))
|
|
|
|
|
|
(symbol "R_0_1"
|
|
|
|
|
|
(rectangle (start -1.016 -2.54) (end 1.016 2.54)
|
|
|
|
|
|
(stroke (width 0.254) (type default)) (fill (type none)))
|
|
|
|
|
|
)
|
|
|
|
|
|
(symbol "R_1_1"
|
|
|
|
|
|
(pin passive line (at 0 3.81 270) (length 1.27)
|
|
|
|
|
|
(name "~" (effects (font (size 1.27 1.27))))
|
|
|
|
|
|
(number "1" (effects (font (size 1.27 1.27)))))
|
|
|
|
|
|
(pin passive line (at 0 -3.81 90) (length 1.27)
|
|
|
|
|
|
(name "~" (effects (font (size 1.27 1.27))))
|
|
|
|
|
|
(number "2" (effects (font (size 1.27 1.27)))))
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
(symbol
|
|
|
|
|
|
(lib_id "Device:R")
|
|
|
|
|
|
(at 100 100 0)
|
|
|
|
|
|
(unit 1)
|
|
|
|
|
|
(exclude_from_sim no)
|
|
|
|
|
|
(in_bom yes)
|
|
|
|
|
|
(on_board yes)
|
|
|
|
|
|
(dnp no)
|
|
|
|
|
|
(uuid "33333333-0000-0000-0000-000000000001")
|
|
|
|
|
|
(property "Reference" "R1" (at 102 99 0) (effects (font (size 1.27 1.27)) (justify left)))
|
|
|
|
|
|
(property "Value" "R" (at 102 101 0) (effects (font (size 1.27 1.27)) (justify left)))
|
|
|
|
|
|
(pin "1" (uuid "33333333-0000-0000-0000-0000000000a1"))
|
|
|
|
|
|
(pin "2" (uuid "33333333-0000-0000-0000-0000000000a2"))
|
|
|
|
|
|
(instances
|
|
|
|
|
|
(project "rt"
|
|
|
|
|
|
(path "/11111111-1111-1111-1111-111111111111" (reference "R1") (unit 1))
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
(sheet_instances (path "/" (page "1")))
|
|
|
|
|
|
)
|
|
|
|
|
|
`,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-10 18:06:33 +02:00
|
|
|
|
const PCB: ToolCfg = {
|
|
|
|
|
|
tool: "pcbnew",
|
|
|
|
|
|
// pcbnew-collab.html seeds kicad_common.json to skip the first-run setup wizard;
|
|
|
|
|
|
// plain pcbnew.html keeps the wizard, whose modal blocks boot (see pcbnew-collab.spec).
|
|
|
|
|
|
html: "pcbnew-collab.html",
|
|
|
|
|
|
ext: "kicad_pcb",
|
|
|
|
|
|
saveFn: "kicadSaveBoard",
|
|
|
|
|
|
// No root uuid in a .kicad_pcb; footprint / via / zone / gr_text / segments carry the uuids.
|
|
|
|
|
|
fixture: `(kicad_pcb
|
|
|
|
|
|
(version 20241229)
|
|
|
|
|
|
(generator "pcbnew")
|
|
|
|
|
|
(generator_version "9.0")
|
|
|
|
|
|
(general (thickness 1.6))
|
|
|
|
|
|
(paper "A4")
|
|
|
|
|
|
(layers
|
|
|
|
|
|
(0 "F.Cu" signal)
|
|
|
|
|
|
(2 "B.Cu" signal)
|
|
|
|
|
|
(37 "F.SilkS" user)
|
|
|
|
|
|
(25 "Edge.Cuts" user)
|
|
|
|
|
|
)
|
|
|
|
|
|
(setup)
|
|
|
|
|
|
(net 0 "")
|
|
|
|
|
|
(footprint "TestLib:R"
|
|
|
|
|
|
(layer "F.Cu")
|
|
|
|
|
|
(uuid "66666666-0000-0000-0000-000000000001")
|
|
|
|
|
|
(at 100 100)
|
|
|
|
|
|
(attr smd)
|
|
|
|
|
|
(property "Reference" "R1" (at 0 -4.2 0) (layer "F.SilkS") (uuid "66666666-0000-0000-0000-0000000000aa") (effects (font (size 1 1) (thickness 0.15))))
|
|
|
|
|
|
(property "Value" "R" (at 0 4.6 0) (layer "F.Fab") (uuid "66666666-0000-0000-0000-0000000000bb") (effects (font (size 1 1) (thickness 0.15))))
|
|
|
|
|
|
(fp_text user "HELLO" (at 0 0 0) (layer "F.SilkS") (uuid "66666666-0000-0000-0000-0000000000cc") (effects (font (size 1 1) (thickness 0.15))))
|
|
|
|
|
|
)
|
|
|
|
|
|
(via (at 80 80) (size 1.4) (drill 0.6) (layers "F.Cu" "B.Cu") (net 0) (uuid "77777777-0000-0000-0000-000000000001"))
|
|
|
|
|
|
(gr_text "BOARDTEXT" (at 90 95 0) (layer "F.SilkS") (uuid "77777777-0000-0000-0000-000000000003") (effects (font (size 1 1) (thickness 0.15)) (justify left bottom)))
|
|
|
|
|
|
(segment (start 50.8 50.8) (end 101.6 50.8) (width 0.2) (layer "F.Cu") (net 0) (uuid "88888888-0000-0000-0000-000000000001"))
|
|
|
|
|
|
(segment (start 50.8 76.2) (end 101.6 76.2) (width 0.2) (layer "F.Cu") (net 0) (uuid "88888888-0000-0000-0000-000000000002"))
|
|
|
|
|
|
)
|
|
|
|
|
|
`,
|
|
|
|
|
|
empty: `(kicad_pcb
|
|
|
|
|
|
(version 20241229)
|
|
|
|
|
|
(generator "pcbnew")
|
|
|
|
|
|
(generator_version "9.0")
|
|
|
|
|
|
(general (thickness 1.6))
|
|
|
|
|
|
(paper "A4")
|
|
|
|
|
|
(layers
|
|
|
|
|
|
(0 "F.Cu" signal)
|
|
|
|
|
|
(2 "B.Cu" signal)
|
|
|
|
|
|
(37 "F.SilkS" user)
|
|
|
|
|
|
(25 "Edge.Cuts" user)
|
|
|
|
|
|
)
|
|
|
|
|
|
(setup)
|
|
|
|
|
|
(net 0 "")
|
|
|
|
|
|
)
|
|
|
|
|
|
`,
|
|
|
|
|
|
ignoreTokens: ["generator_version"],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-11 15:25:30 +02:00
|
|
|
|
// Footprint-only board: the v2 items wire's PROVEN pcbnew apply path (bare
|
|
|
|
|
|
// footprint blob parse + children — the 0004 containment win). Kept separate
|
|
|
|
|
|
// from PCB so the fragile-envelope types (via/text/segment, see the fixme
|
|
|
|
|
|
// below) don't mask the passing footprint coverage.
|
|
|
|
|
|
const PCB_FP: ToolCfg = {
|
|
|
|
|
|
...PCB,
|
|
|
|
|
|
fixture: `(kicad_pcb
|
|
|
|
|
|
(version 20241229)
|
|
|
|
|
|
(generator "pcbnew")
|
|
|
|
|
|
(generator_version "9.0")
|
|
|
|
|
|
(general (thickness 1.6))
|
|
|
|
|
|
(paper "A4")
|
|
|
|
|
|
(layers
|
|
|
|
|
|
(0 "F.Cu" signal)
|
|
|
|
|
|
(2 "B.Cu" signal)
|
|
|
|
|
|
(37 "F.SilkS" user)
|
|
|
|
|
|
(25 "Edge.Cuts" user)
|
|
|
|
|
|
)
|
|
|
|
|
|
(setup)
|
|
|
|
|
|
(net 0 "")
|
|
|
|
|
|
(footprint "TestLib:R"
|
|
|
|
|
|
(layer "F.Cu")
|
|
|
|
|
|
(uuid "66666666-0000-0000-0000-000000000001")
|
|
|
|
|
|
(at 100 100)
|
|
|
|
|
|
(attr smd)
|
|
|
|
|
|
(property "Reference" "R1" (at 0 -4.2 0) (layer "F.SilkS") (uuid "66666666-0000-0000-0000-0000000000aa") (effects (font (size 1 1) (thickness 0.15))))
|
|
|
|
|
|
(property "Value" "R" (at 0 4.6 0) (layer "F.Fab") (uuid "66666666-0000-0000-0000-0000000000bb") (effects (font (size 1 1) (thickness 0.15))))
|
|
|
|
|
|
(fp_text user "HELLO" (at 0 0 0) (layer "F.SilkS") (uuid "66666666-0000-0000-0000-0000000000cc") (effects (font (size 1 1) (thickness 0.15))))
|
|
|
|
|
|
)
|
|
|
|
|
|
(footprint "TestLib:C"
|
|
|
|
|
|
(layer "F.Cu")
|
|
|
|
|
|
(uuid "99999999-0000-0000-0000-000000000009")
|
|
|
|
|
|
(at 50 50)
|
|
|
|
|
|
(attr smd)
|
|
|
|
|
|
(property "Reference" "C1" (at 0 -2 0) (layer "F.SilkS") (uuid "99999999-0000-0000-0000-0000000000aa") (effects (font (size 1 1) (thickness 0.15))))
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
`,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-10 18:06:33 +02:00
|
|
|
|
test.beforeAll(() => {
|
|
|
|
|
|
// No reconciler bundle needed — the round trip drives only the C++ Module.*
|
|
|
|
|
|
// exports (open / snapshot / apply / save).
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test.describe("round trip: file → yjs → file", () => {
|
|
|
|
|
|
// Each test boots the tool TWICE (extract + rebuild); the heavy tools need well
|
|
|
|
|
|
// beyond the config's 180s per-test budget.
|
|
|
|
|
|
test.describe.configure({ timeout: 420000 });
|
|
|
|
|
|
|
|
|
|
|
|
test("pl_editor preserves items through a yjs round trip", async ({ context, testLogger }) => {
|
|
|
|
|
|
const { orig, regen } = await roundTrip(context, PL);
|
|
|
|
|
|
const diff = sexprDiff(orig, regen, { ignoreTokens: PL.ignoreTokens });
|
|
|
|
|
|
expect(
|
|
|
|
|
|
diff.equal,
|
|
|
|
|
|
`round trip lost data:\n${JSON.stringify(diff, null, 2)}`,
|
|
|
|
|
|
).toBe(true);
|
|
|
|
|
|
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("eeschema preserves items through a yjs round trip", async ({ context, testLogger }) => {
|
|
|
|
|
|
const { orig, regen } = await roundTrip(context, SCH);
|
|
|
|
|
|
const diff = sexprDiff(orig, regen, { ignoreTokens: SCH.ignoreTokens });
|
|
|
|
|
|
expect(
|
|
|
|
|
|
diff.equal,
|
|
|
|
|
|
`round trip lost data:\n${JSON.stringify(diff, null, 2)}`,
|
|
|
|
|
|
).toBe(true);
|
|
|
|
|
|
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-11 15:25:30 +02:00
|
|
|
|
// THE 0004 CONTAINMENT WIN (ysync 0008 Stage C/D): footprints round-trip via the
|
|
|
|
|
|
// items wire — the bare-footprint blob parse recreates the footprint WITH its
|
|
|
|
|
|
// children (properties, fp_text) in place, which the scalar wire never could.
|
|
|
|
|
|
test("pcbnew preserves footprint containment through a yjs round trip", async ({
|
|
|
|
|
|
context,
|
|
|
|
|
|
testLogger,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const { orig, regen } = await roundTrip(context, PCB_FP);
|
|
|
|
|
|
const diff = sexprDiff(orig, regen, { ignoreTokens: PCB_FP.ignoreTokens });
|
|
|
|
|
|
expect(
|
|
|
|
|
|
diff.equal,
|
|
|
|
|
|
`round trip lost data:\n${JSON.stringify(diff, null, 2)}`,
|
|
|
|
|
|
).toBe(true);
|
|
|
|
|
|
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
fix(ysync): wire dialect == file dialect, uuid churn, drift noise
The Y.Doc is the source of truth for the FILE, but both live wires wrote
KiCad's CLIPBOARD dialect — a lossy, paste-oriented format. Every
difference was permanent, unfixable drift.
- pcbnew: serialize footprint blobs with CTL_FOR_BOARD, not CLIPBOARD_IO's
CTL_FOR_CLIPBOARD, which emitted (version)(generator)(generator_version)
inside every (footprint …). Keep (locked yes).
- eeschema: aForClipboard=false — clipboard mode collapsed every symbol's
(instances … (path "/sheet")) to (path "").
- Re-supply (version) at PARSE time only (withFootprintVersion): the token
is invalid file content but load-bearing on decode — without it the
parser starts at m_requiredVersion=0 and stamps (hide yes) on every
mandatory field.
- FOOTPRINT copy ctor: restore mandatory-field uuids (EDA_ITEM::operator=
keeps the target's const m_Uuid, so Clone() rerolled all four).
- drift: classify order-only diffs as `reordered` — y-sexpr v2 reorders
legitimately; excluded from counts, report-worthiness and dedupe hashes.
Migration 0017.
- fpedit from eeschema: AsyncLoad()+BlockUntilLoaded() in initLibraryTree —
FACE_PCB starts lazily there and never preloaded its libraries.
Guards: wire-vs-file round-trip tests (pcbnew + eeschema),
fpedit-from-eeschema (verified red without the fix), symedit-from-eeschema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9kjdGBdcpwUSjJ3q5xg2
2026-07-20 18:29:52 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// The wire dialect must BE the file dialect — see expectWireMatchesFile. These
|
|
|
|
|
|
// two would both have failed before the CTL_FOR_BOARD / aForClipboard=false fix:
|
|
|
|
|
|
// pcbnew on the `(version)(generator)(generator_version)` triple inside every
|
|
|
|
|
|
// footprint, eeschema on `(instances … (path ""))`.
|
|
|
|
|
|
test("pcbnew wire blobs match the file save token for token", async ({
|
|
|
|
|
|
context,
|
|
|
|
|
|
testLogger,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
await expectWireMatchesFile(context, PCB_FP);
|
|
|
|
|
|
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("eeschema wire blobs keep the symbol's instance path", async ({
|
|
|
|
|
|
context,
|
|
|
|
|
|
testLogger,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
await expectWireMatchesFile(context, SCH_SYM);
|
|
|
|
|
|
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
2026-06-11 15:25:30 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
// FORMER KNOWN GAP (ysync 0008 status, known limit 1): pcbnew
|
|
|
|
|
|
// track/via/zone/text APPLY rides the `(kicad_pcb …)` envelope parse, which
|
|
|
|
|
|
// was asyncify-fragile in wasm (even a verbatim SaveSelection envelope for a
|
|
|
|
|
|
// segment died silently in the commit) and lost those items on the rebuild
|
|
|
|
|
|
// side. Healthy under JSPI — this test is the pin.
|
2026-08-13 18:43:36 +02:00
|
|
|
|
test( // re-enabled 2026-08-13: the asyncify-fragile envelope parse is gone with JSPI — passes both engines
|
2026-06-10 18:06:33 +02:00
|
|
|
|
"pcbnew preserves items through a yjs round trip",
|
|
|
|
|
|
async ({ context, testLogger }) => {
|
|
|
|
|
|
const { orig, regen } = await roundTrip(context, PCB);
|
|
|
|
|
|
const diff = sexprDiff(orig, regen, { ignoreTokens: PCB.ignoreTokens });
|
|
|
|
|
|
expect(
|
|
|
|
|
|
diff.equal,
|
|
|
|
|
|
`round trip lost data:\n${JSON.stringify(diff, null, 2)}`,
|
|
|
|
|
|
).toBe(true);
|
|
|
|
|
|
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|