import test from "node:test"; import assert from "node:assert/strict"; import {mkdtemp, rm} from "node:fs/promises"; import path from "node:path"; import os from "node:os"; import {spawn} from "node:child_process"; import {once} from "node:events"; import {withFileLock} from "../src/capability-language/file-lock.js"; test("authoring lock excludes concurrent mutations and survives owner death", async context => { const root = await mkdtemp(path.join(os.tmpdir(), "qx-lock-test-")); context.after(() => rm(root, {recursive: true, force: true})); const filename = path.join(root, "lock"); await withFileLock(filename, async () => { await assert.rejects(withFileLock(filename, async () => assert.fail("concurrent mutation")), /Another authoring command/); }); const module = new URL("../src/capability-language/file-lock.js", import.meta.url).href; const owner = spawn(process.execPath, ["--input-type=module", "-e", `import {withFileLock} from ${JSON.stringify(module)}; await withFileLock(${JSON.stringify(filename)}, async () => {process.stdout.write('ready'); await new Promise(() => {});});`], {stdio: ["ignore", "pipe", "pipe"]}); context.after(() => owner.kill("SIGKILL")); await once(owner.stdout, "data"); const exited = once(owner, "exit"); owner.kill("SIGKILL"); await exited; // EOF release happens in the helper; wait a bounded amount for scheduling. let acquired = false; for (let attempt = 0; attempt < 30 && !acquired; attempt++) { try { await withFileLock(filename, async () => {acquired = true;}); } catch (error) { if (!/Another authoring command/.test(String(error))) throw error; } } assert.equal(acquired, true); });