Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302// FS slice acceptance proofs (FS-SLICE-BRIEF §Tests): the host fs facade over the// kernel's pid-0 host process, driven through a real page. The fixture page// (demo/fixture.html) exists only so these tests don't depend on the not-yet-built// demo harness; every assertion runs against the public Kernel.fs contract.//// Error-shape ground truth was captured from real node (rmSync/unlinkSync/// renameSync errno matrix). Two deliberate brief-over-Node choices are pinned// here: unlink of a dir → EISDIR (Node: EPERM) and rm of a non-empty dir without// recursive → ENOTEMPTY (Node: ERR_FS_EISDIR).import { test, expect, type Page } from "@playwright/test";import type { Kernel } from "../src/host/index";import { runGuest } from "./guest";
declare global { interface Window { __runtime?: Kernel }}
async function bootFixture(page: Page): Promise<void> { await page.goto("/demo/fixture.html"); await page.waitForFunction(() => window.__runtime !== undefined, undefined, { timeout: 20_000 });}
test("readdir of a seeded dir returns names+kinds; stat sizes match content length", async ({ page }) => { await bootFixture(page); const res = await page.evaluate(async () => { const runtime = window.__runtime!; return { entries: await runtime.fs.readdir("/app"), one: await runtime.fs.stat("/app/one.txt"), dir: await runtime.fs.stat("/app/dir"), nested: await runtime.fs.stat("/app/dir/nested.txt"), }; }); const byName = new Map(res.entries.map((e) => [e.name, e.kind])); expect(byName.get("one.txt")).toBe("file"); expect(byName.get("dir")).toBe("dir"); expect(res.one).toMatchObject({ kind: "file", size: 3 }); // "one" expect(res.nested).toMatchObject({ kind: "file", size: 6 }); // "nested" expect(res.dir).toMatchObject({ kind: "dir", size: 0 }); expect(typeof res.one.mode).toBe("number"); expect(typeof res.one.mtimeMs).toBe("number");});
test("writeFile then readFile round-trips string and Uint8Array payloads", async ({ page }) => { await bootFixture(page); const res = await page.evaluate(async () => { const runtime = window.__runtime!; await runtime.fs.writeFile("/app/round.txt", "hello page"); const text = new TextDecoder().decode(await runtime.fs.readFile("/app/round.txt")); const payload = new Uint8Array([0, 1, 2, 254, 255]); await runtime.fs.writeFile("/app/bin.txt", payload); const bytes = Array.from(await runtime.fs.readFile("/app/bin.txt")); // Overwriting truncates: a shorter write must not leave the old tail. await runtime.fs.writeFile("/app/round.txt", "hi"); const truncated = new TextDecoder().decode(await runtime.fs.readFile("/app/round.txt")); return { text, bytes, truncated, stat: await runtime.fs.stat("/app/round.txt") }; }); expect(res.text).toBe("hello page"); expect(res.bytes).toEqual([0, 1, 2, 254, 255]); expect(res.truncated).toBe("hi"); expect(res.stat).toMatchObject({ kind: "file", size: 2 });});
test("mkdir recursive + nested writeFile + readdir nesting", async ({ page }) => { await bootFixture(page); await page.evaluate(async () => { const runtime = window.__runtime!; await runtime.fs.mkdir("/app/a/b/c", { recursive: true }); await runtime.fs.writeFile("/app/a/b/c/leaf.txt", "leaf"); }); const tree = await page.evaluate(async () => { const runtime = window.__runtime!; return { a: await runtime.fs.readdir("/app/a"), b: await runtime.fs.readdir("/app/a/b"), c: await runtime.fs.readdir("/app/a/b/c"), }; }); expect(tree.a).toContainEqual({ name: "b", kind: "dir" }); expect(tree.b).toContainEqual({ name: "c", kind: "dir" }); expect(tree.c).toContainEqual({ name: "leaf.txt", kind: "file" });});
test("rename moves a file (and a dir with its children); the old path is gone", async ({ page }) => { await bootFixture(page); await page.evaluate(async () => { const runtime = window.__runtime!; await runtime.fs.rename("/app/one.txt", "/app/two.txt"); await runtime.fs.rename("/app/dir", "/app/dir2"); }); const after = await page.evaluate(async () => { const runtime = window.__runtime!; let oldPath: string | null = null; try { await runtime.fs.stat("/app/one.txt"); } catch (e) { oldPath = (e as { code?: string }).code ?? "THREW"; } return { oldPath, moved: new TextDecoder().decode(await runtime.fs.readFile("/app/two.txt")), children: await runtime.fs.readdir("/app/dir2"), }; }); expect(after.oldPath).toBe("ENOENT"); expect(after.moved).toBe("one"); // The Dentry moves — children travel with it and still resolve. expect(after.children).toContainEqual({ name: "nested.txt", kind: "file" });});
test("unlink file; rmdir empty dir; rm recursive removes a subtree; rmdir non-empty without recursive rejects ENOTEMPTY", async ({ page }) => { await bootFixture(page); const res = await page.evaluate(async () => { const runtime = window.__runtime!; const code = async (fn: () => Promise<unknown>): Promise<string | null> => { try { await fn(); return null; } catch (e) { return (e as { code?: string }).code ?? "THREW"; } }; await runtime.fs.writeFile("/app/tmp.txt", "x"); await runtime.fs.unlink("/app/tmp.txt"); const unlinked = await code(() => runtime.fs.stat("/app/tmp.txt"));
await runtime.fs.mkdir("/app/empty"); await runtime.fs.rm("/app/empty"); // non-recursive rm on an empty dir removes it const rmdirEmpty = await code(() => runtime.fs.stat("/app/empty"));
await runtime.fs.mkdir("/app/sub/inner", { recursive: true }); await runtime.fs.writeFile("/app/sub/inner/deep.txt", "deep"); await runtime.fs.rm("/app/sub", { recursive: true }); const subtreeGone = await code(() => runtime.fs.stat("/app/sub/inner/deep.txt"));
await runtime.fs.mkdir("/app/stay"); await runtime.fs.writeFile("/app/stay/keep.txt", "k"); const nonEmpty = await code(() => runtime.fs.rm("/app/stay")); const survivor = await runtime.fs.stat("/app/stay/keep.txt"); // untouched by the refusal return { unlinked, rmdirEmpty, subtreeGone, nonEmpty, survivor }; }); expect(res.unlinked).toBe("ENOENT"); expect(res.rmdirEmpty).toBe("ENOENT"); expect(res.subtreeGone).toBe("ENOENT"); expect(res.nonEmpty).toBe("ENOTEMPTY"); expect(res.survivor).toMatchObject({ kind: "file", size: 1 });});
test("error shapes: readFile missing → ENOENT; unlink a dir → EISDIR; readdir a file → ENOTDIR", async ({ page }) => { await bootFixture(page); const errs = await page.evaluate(async () => { const runtime = window.__runtime!; const shape = async (fn: () => Promise<unknown>) => { try { await fn(); return null; } catch (e) { const err = e as { code?: string; errno?: number; syscall?: string; path?: string }; return { code: err.code, errno: err.errno, syscall: err.syscall, path: err.path }; } }; return { missing: await shape(() => runtime.fs.readFile("/app/nope.txt")), unlinkDir: await shape(() => runtime.fs.unlink("/app/dir")), readdirFile: await shape(() => runtime.fs.readdir("/app/one.txt")), }; }); expect(errs.missing).toMatchObject({ code: "ENOENT", errno: 2, syscall: "open", path: "/app/nope.txt" }); expect(errs.unlinkDir).toMatchObject({ code: "EISDIR", syscall: "unlink", path: "/app/dir" }); expect(errs.readdirFile).toMatchObject({ code: "ENOTDIR", syscall: "readdir" });});
test("cross-writer visibility: guest writes → page readdir; page writes → guest readFileSync", async ({ page }) => { await bootFixture(page); const res = await page.evaluate(async () => { const runtime = window.__runtime!; // Seed the two guest scripts through the host fs (awaited, so no seed race). await runtime.fs.writeFile("/app/guest-write.cjs", `const fs = require("node:fs");\nfs.writeFileSync("/app/guest.txt", "written-by-guest");\nconsole.log("guest wrote");\n`); await runtime.fs.writeFile("/app/guest-read.cjs", `const fs = require("node:fs");\nconst s = fs.readFileSync("/app/reply.txt", "utf8");\nconsole.log("guest read:", s);\n`);
// Listen BEFORE the guest runs — its write must surface via onFsChanged // (single-writer notification), with a timeout fallback so a missed event // degrades to a poll instead of a hang. let resolveChanged: (v: string) => void = () => {}; const changed = new Promise<string>((resolve) => { resolveChanged = resolve; }); const off = runtime.onFsChanged(() => resolveChanged("changed")); setTimeout(() => resolveChanged("timeout"), 5000);
const code = await runtime.exec("/app/guest-write.cjs").exited; const notice = await changed; off(); const names = (await runtime.fs.readdir("/app")).map((e) => e.name);
// Direction 2: the page writes, a second guest reads the bytes back. await runtime.fs.writeFile("/app/reply.txt", "hello-from-page"); const chunks: string[] = []; const proc = runtime.exec("/app/guest-read.cjs"); proc.stdout.pipeTo(new WritableStream({ write(c) { chunks.push(new TextDecoder().decode(c)); } })); const readCode = await proc.exited; return { code, notice, names, readCode, stdout: chunks.join("") }; }); expect(res.code).toBe(0); expect(res.notice).toBe("changed"); expect(res.names).toContain("guest.txt"); expect(res.readCode).toBe(0); expect(res.stdout).toContain("guest read: hello-from-page");});
test("guest sync builtins: mkdir/readdir/stat/rename/unlink/rmdir round-trip inside the guest", async ({ page }) => { await bootFixture(page); const res = await page.evaluate(async () => { const runtime = window.__runtime!; await runtime.fs.writeFile("/app/builtins.cjs", [ `const fs = require("node:fs");`, `fs.mkdirSync("/app/gb/nested", { recursive: true });`, `fs.writeFileSync("/app/gb/nested/f.txt", "payload");`, `const names = fs.readdirSync("/app/gb").sort().join(",");`, `const st = fs.statSync("/app/gb/nested/f.txt");`, `fs.renameSync("/app/gb/nested/f.txt", "/app/gb/moved.txt");`, `const moved = fs.readFileSync("/app/gb/moved.txt", "utf8");`, `fs.unlinkSync("/app/gb/moved.txt");`, `fs.rmdirSync("/app/gb", { recursive: true });`, `const gone = (() => { try { fs.statSync("/app/gb"); return "exists"; } catch (e) { return e.code; } })();`, `console.log("names=" + names, "size=" + st.size, "dir=" + st.isDirectory(),`, ` "moved=" + moved, "gone=" + gone);`, ].join("\n")); const chunks: string[] = []; const proc = runtime.exec("/app/builtins.cjs"); proc.stdout.pipeTo(new WritableStream({ write(c) { chunks.push(new TextDecoder().decode(c)); } })); const stderr: string[] = []; proc.stderr.pipeTo(new WritableStream({ write(c) { stderr.push(new TextDecoder().decode(c)); } })); const code = await proc.exited; return { code, stdout: chunks.join(""), stderr: stderr.join("") }; }); expect(res.stderr).toBe(""); expect(res.code).toBe(0); expect(res.stdout).toContain("names=nested"); expect(res.stdout).toContain("size=7"); // "payload" expect(res.stdout).toContain("dir=false"); expect(res.stdout).toContain("moved=payload"); // renameSync moved it, readFileSync found it expect(res.stdout).toContain("gone=ENOENT"); // rmdirSync recursive removed the subtree});
test("rename edge cases + seed notifies onFsChanged", async ({ page }) => { await bootFixture(page); const res = await page.evaluate(async () => { const runtime = window.__runtime!; const code = async (fn: () => Promise<unknown>): Promise<string | null> => { try { await fn(); return null; } catch (e) { return (e as { code?: string }).code ?? "THREW"; } }; await runtime.fs.mkdir("/app/edir"); await runtime.fs.writeFile("/app/efile.txt", "e"); return { fileOntoDir: await code(() => runtime.fs.rename("/app/efile.txt", "/app/edir")), // restore for the next case: the failed rename left both paths intact dirOntoFile: await code(() => runtime.fs.rename("/app/edir", "/app/efile.txt")), intoOwnSubtree: await code(() => runtime.fs.rename("/app/edir", "/app/edir/sub")), missing: await code(() => runtime.fs.rename("/app/nope.txt", "/app/elsewhere.txt")), missingParent: await code(() => runtime.fs.rename("/app/efile.txt", "/app/nodir/f.txt")), // The seed path (writeTree) is a writer too and must notify. seedNotice: await new Promise<string>((resolve) => { const off = runtime.onFsChanged(() => { off(); resolve("changed"); }); setTimeout(() => { off(); resolve("timeout"); }, 5000); runtime.writeTree({ "/app/seeded-by-tree.txt": "seed" }); }), seeded: await runtime.fs.readFile("/app/seeded-by-tree.txt").then( (b) => new TextDecoder().decode(b), (e) => e.code, ), }; }); expect(res.fileOntoDir).toBe("EISDIR"); // brief contract (Node agrees for file→dir) expect(res.dirOntoFile).toBe("ENOTDIR"); // Node semantics expect(res.intoOwnSubtree).toBe("EINVAL"); // POSIX/Node semantics expect(res.missing).toBe("ENOENT"); expect(res.missingParent).toBe("ENOENT"); // target parent must exist expect(res.seedNotice).toBe("changed"); // writeTree goes through the notification path expect(res.seeded).toBe("seed");});
// Real Node 24.16's lines for the same script.test("{ bigint: true } answers BigIntStats from every stat form, as Node does", async ({ page }) => { const res = await runGuest(page, { "/app/main.cjs": [ "const fs = require(\"node:fs\");", "fs.writeFileSync(\"/app/f.txt\", \"hello\");", "const s = fs.statSync(\"/app/f.txt\", { bigint: true });", "console.log(Object.keys(s).join(\",\"));", "console.log([typeof s.size, typeof s.mtimeNs, s.size, s.isFile(), s.isDirectory(), s.mtimeNs / 1000000n === s.mtimeMs, s.mtime.getTime() === Number(s.mtimeMs)].join(\" \"));", "const fd = fs.openSync(\"/app/f.txt\", \"r\");", "console.log(typeof fs.fstatSync(fd, { bigint: true }).ino, typeof fs.lstatSync(\"/app/f.txt\", { bigint: true }).mode, typeof fs.statSync(\"/app/f.txt\").size);", "fs.stat(\"/app/f.txt\", { bigint: true }, (e, st) => {", " console.log(\"cb\", typeof st.blocks);", " fs.promises.stat(\"/app/f.txt\", { bigint: true }).then(async (p) => {", " const h = await fs.promises.open(\"/app/f.txt\");", " console.log(\"promise\", typeof p.dev, \"handle\", typeof (await h.stat({ bigint: true })).nlink);", " await h.close();", " });", "});", ].join("\n"), }, "/app/main.cjs"); expect(res.stdout.split("\n")).toEqual([ "dev,mode,nlink,uid,gid,rdev,blksize,ino,size,blocks,atimeMs,mtimeMs,ctimeMs,birthtimeMs,atimeNs,mtimeNs,ctimeNs,birthtimeNs", "bigint bigint 5 true false true true", "bigint bigint number", "cb bigint", "promise bigint handle bigint", "", ]);});