diff --git a/tests/bun/npm-protocol-resolution.test.ts b/tests/bun/npm-protocol-resolution.test.ts index fff2539b8c..0d3388ab55 100644 --- a/tests/bun/npm-protocol-resolution.test.ts +++ b/tests/bun/npm-protocol-resolution.test.ts @@ -3,6 +3,7 @@ import { JSDOM } from "npm:jsdom@28.0.0"; import { assertEquals } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { rewriteNpmProtocolImports } from "./npm-protocol-imports.ts"; +import { bunPreloadRewriteFilter, rewriteBunPreloadSource } from "./preload-rewrite.ts"; describe("Bun npm protocol resolution", () => { it("loads versioned scoped and unscoped npm imports", () => { @@ -30,4 +31,45 @@ describe("Bun npm protocol resolution", () => { ].join("\n"), ); }); + + it("rewrites extension and test sources after normalizing path separators", () => { + const extensionSource = + 'import { defineExtension } from "veryfront/extensions";\nexport const marker = "kept";\n'; + const testSource = + 'import { BasicTracerProvider } from "npm:@opentelemetry/sdk-trace-base@2.9.0";\nexport const marker = "kept";\n'; + + for ( + const extensionPath of [ + "/repo/extensions/ext-yaml/src/adapter.ts", + String.raw`C:\repo\extensions\ext-yaml\src\adapter.ts`, + ] + ) { + assertEquals(bunPreloadRewriteFilter.test(extensionPath), true); + assertEquals( + rewriteBunPreloadSource(extensionPath, extensionSource, (source) => + source.replace( + '"veryfront/extensions"', + '"../../../src/extensions/types.ts"', + )), + 'import { defineExtension } from "../../../src/extensions/types.ts";\nexport const marker = "kept";\n', + ); + } + + for ( + const testPath of [ + "/repo/tests/bun/npm-protocol-resolution.test.ts", + String.raw`C:\repo\tests\bun\npm-protocol-resolution.test.ts`, + "/repo/extensions/fixtures/npm-protocol-resolution.test.ts", + String.raw`C:\repo\extensions\fixtures\npm-protocol-resolution.test.ts`, + ] + ) { + assertEquals(bunPreloadRewriteFilter.test(testPath), true); + assertEquals( + rewriteBunPreloadSource(testPath, testSource, () => { + throw new Error("test files must not use extension import rewriting"); + }), + 'import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";\nexport const marker = "kept";\n', + ); + } + }); }); diff --git a/tests/bun/preload-rewrite.ts b/tests/bun/preload-rewrite.ts new file mode 100644 index 0000000000..775ac40803 --- /dev/null +++ b/tests/bun/preload-rewrite.ts @@ -0,0 +1,27 @@ +import { rewriteNpmProtocolImports } from "./npm-protocol-imports.ts"; + +export const bunPreloadRewriteFilter = + /(\.test\.[cm]?[jt]sx?|[/\\]extensions[/\\]ext-[^/\\]+[/\\]src[/\\].*\.[cm]?[jt]sx?)$/; + +function normalizePreloadPath(path: string): string { + return path.replaceAll("\\", "/"); +} + +function isExtensionSourcePath(path: string): boolean { + return /(?:^|\/)extensions\/ext-[^/]+\/src\/.*\.[cm]?[jt]sx?$/.test(path); +} + +export function rewriteBunPreloadSource( + path: string, + source: string, + rewriteExtensionImports: (source: string) => string | null, +): string { + const posixPath = normalizePreloadPath(path); + let contents = isExtensionSourcePath(posixPath) + ? rewriteExtensionImports(source) ?? source + : source; + if (/\.test\.[cm]?[jt]sx?$/.test(posixPath)) { + contents = rewriteNpmProtocolImports(contents) ?? contents; + } + return contents; +} diff --git a/tests/bun/preload.ts b/tests/bun/preload.ts index 965fe1ed3d..c8e0ca00de 100644 --- a/tests/bun/preload.ts +++ b/tests/bun/preload.ts @@ -12,7 +12,8 @@ import { plugin } from "bun"; import { existsSync, readFileSync, statSync } from "fs"; import { dirname, extname, relative, resolve, sep } from "path"; import { fileURLToPath } from "url"; -import { rewriteModuleSpecifiers, rewriteNpmProtocolImports } from "./npm-protocol-imports.ts"; +import { rewriteModuleSpecifiers } from "./npm-protocol-imports.ts"; +import { bunPreloadRewriteFilter, rewriteBunPreloadSource } from "./preload-rewrite.ts"; const projectRoot = resolve(import.meta.dir, "../.."); @@ -144,21 +145,19 @@ plugin({ // import-looking fixture strings and comments untouched. build.onLoad( { - filter: - /(\.test\.[cm]?[jt]sx?|[/\\]extensions[/\\]ext-[^/\\]+[/\\]src[/\\].*\.[cm]?[jt]sx?)$/, + filter: bunPreloadRewriteFilter, }, (args) => { - const posixPath = args.path.split(sep).join("/"); const source = readFileSync(args.path, "utf8"); - let contents = posixPath.includes("/extensions/") - ? rewriteModuleSpecifiers( - source, - (specifier) => workspaceModuleSpecifier(args.path, specifier), - ) ?? source - : source; - if (/\.test\.[cm]?[jt]sx?$/.test(posixPath)) { - contents = rewriteNpmProtocolImports(contents) ?? contents; - } + const contents = rewriteBunPreloadSource( + args.path, + source, + (extensionSource) => + rewriteModuleSpecifiers( + extensionSource, + (specifier) => workspaceModuleSpecifier(args.path, specifier), + ), + ); const extension = extname(args.path).toLowerCase(); const loader = extension === ".tsx" ? "tsx" diff --git a/tests/bun/runner-args.test.mjs b/tests/bun/runner-args.test.mjs index d89e9c4292..e5b1210313 100644 --- a/tests/bun/runner-args.test.mjs +++ b/tests/bun/runner-args.test.mjs @@ -3,12 +3,15 @@ import { spawnSync } from "node:child_process"; import { EventEmitter } from "node:events"; import { readFileSync } from "node:fs"; import test from "node:test"; +import { fileURLToPath } from "node:url"; import { buildBunTestArgs, buildIsolatedBunTestRuns, registerBunWorkspaceCleanup, } from "./runner-args.mjs"; +const runTestsPath = fileURLToPath(new URL("./run-tests.mjs", import.meta.url)); + test("buildBunTestArgs caps concurrency without enabling concurrent test semantics", () => { const args = buildBunTestArgs(["one.test.ts", "two.test.ts"], 3); @@ -50,7 +53,7 @@ test("the Bun runner drains child output and exits naturally", () => { test("the Bun runner fails loudly when filters select no files", () => { const result = spawnSync( process.execPath, - [new URL("./run-tests.mjs", import.meta.url).pathname], + [runTestsPath], { env: { ...process.env, BUN_TEST_INCLUDE: "missing-bun-fixture.test.ts" }, encoding: "utf8", @@ -62,6 +65,18 @@ test("the Bun runner fails loudly when filters select no files", () => { assert.doesNotMatch(result.stdout, /0 passed, 0 failed/); }); +test("the empty-selection spawn path is decoded through fileURLToPath", () => { + const source = readFileSync(fileURLToPath(import.meta.url), "utf8"); + const runnerPathnameAccess = 'run-tests.mjs", import.meta.url)' + + ".pathname"; + + assert.match( + source, + /const runTestsPath = fileURLToPath\(new URL\("\.\/run-tests\.mjs", import\.meta\.url\)\)/, + ); + assert.equal(source.includes(runnerPathnameAccess), false); +}); + test("Bun workspace cleanup runs before termination signals are re-raised", () => { const runtimeProcess = new EventEmitter(); runtimeProcess.pid = 123; diff --git a/tests/bun/workspace-packages.mjs b/tests/bun/workspace-packages.mjs index 218ce39c61..cba75c272b 100644 --- a/tests/bun/workspace-packages.mjs +++ b/tests/bun/workspace-packages.mjs @@ -1,31 +1,109 @@ -import { existsSync, mkdirSync, readFileSync, rmdirSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + readFileSync, + renameSync, + rmdirSync, + rmSync, + writeFileSync, +} from "node:fs"; import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; -import { randomUUID } from "node:crypto"; +import { createHash, randomUUID } from "node:crypto"; import { ensureDirectoryLink } from "../ensure-npm-links.mjs"; const MARKER_NAME = ".veryfront-bun-workspace-package.json"; const LOCK_NAME = ".veryfront-bun-workspace-packages.lock"; +const RECLAIMER_GUARD_PREFIX = ".veryfront-bun-workspace-packages.reclaiming-"; const LOCK_OWNER = "veryfront-bun-tests"; function readJson(path) { return JSON.parse(readFileSync(path, "utf8")); } +function activePreparationError() { + return new Error("Bun workspace package preparation is already active"); +} + +function waitForReclaimRaceTestBarrier() { + const barrierPath = process.env.VF_BUN_WORKSPACE_RECLAIM_BARRIER_PATH; + if (!barrierPath) return; + mkdirSync(barrierPath, { recursive: true }); + writeFileSync( + join(barrierPath, `ready-${process.pid}-${randomUUID()}`), + "ready\n", + { flag: "wx" }, + ); + const releasePath = join(barrierPath, "release"); + const deadline = Date.now() + 10_000; + const signal = new Int32Array(new SharedArrayBuffer(4)); + while (!existsSync(releasePath)) { + if (Date.now() >= deadline) { + throw new Error("Timed out waiting for the Bun workspace reclaim barrier"); + } + Atomics.wait(signal, 0, 0, 10); + } +} + +function reclaimerGuardPath(nodeModulesPath, token) { + const digest = createHash("sha256").update(token).digest("hex"); + return join(nodeModulesPath, `${RECLAIMER_GUARD_PREFIX}${digest}`); +} + +function createPreparationLock(nodeModulesPath, lockPath, token) { + const stagingPath = join( + nodeModulesPath, + `${LOCK_NAME}.staging-${process.pid}-${token}`, + ); + try { + mkdirSync(stagingPath); + writeFileSync( + join(stagingPath, MARKER_NAME), + `${JSON.stringify({ owner: LOCK_OWNER, pid: process.pid, token })}\n`, + ); + if (process.env.VF_BUN_WORKSPACE_INTERRUPT_BEFORE_LOCK_PUBLISH === "1") { + process.exit(18); + } + if (existsSync(lockPath)) { + const conflict = new Error("Bun workspace package lock already exists"); + conflict.code = "EEXIST"; + throw conflict; + } + renameSync(stagingPath, lockPath); + } catch (error) { + rmSync(stagingPath, { recursive: true, force: true }); + throw error; + } +} + +export function isDirectoryPathConflict(error, path) { + return existsSync(path) && + ["EACCES", "EEXIST", "ENOTEMPTY", "EPERM"].includes(error?.code); +} + function acquirePreparationLock(nodeModulesPath) { mkdirSync(nodeModulesPath, { recursive: true }); const lockPath = join(nodeModulesPath, LOCK_NAME); + const token = randomUUID(); + let reclaimedGuardPath; try { - mkdirSync(lockPath); + createPreparationLock(nodeModulesPath, lockPath, token); } catch (error) { - if (error?.code === "EEXIST") { - if (!reclaimStalePreparationLock(lockPath)) { - throw new Error("Bun workspace package preparation is already active"); + if (isDirectoryPathConflict(error, lockPath)) { + reclaimedGuardPath = reclaimStalePreparationLock( + nodeModulesPath, + lockPath, + ); + if (!reclaimedGuardPath) { + throw activePreparationError(); } try { - mkdirSync(lockPath); + createPreparationLock(nodeModulesPath, lockPath, token); } catch (retryError) { - if (retryError?.code === "EEXIST") { - throw new Error("Bun workspace package preparation is already active"); + if (isDirectoryPathConflict(retryError, lockPath)) { + // This generation tombstone must remain permanent. Otherwise an + // arbitrarily delayed stale-generation reader could reuse it after a + // fresh runner acquires lockPath and move that fresh live lock. + throw activePreparationError(); } throw retryError; } @@ -34,21 +112,28 @@ function acquirePreparationLock(nodeModulesPath) { } } - const token = randomUUID(); - try { - writeFileSync( - join(lockPath, MARKER_NAME), - `${JSON.stringify({ owner: LOCK_OWNER, pid: process.pid, token })}\n`, - ); - } catch (error) { - rmSync(lockPath, { recursive: true, force: true }); - throw error; - } - return { lockPath, token }; } -export function reclaimStalePreparationLock(lockPath, runtimeProcess = process) { +function isOwnedLockMarker(marker) { + return marker?.owner === LOCK_OWNER && + Number.isSafeInteger(marker.pid) && + marker.pid >= 1 && + typeof marker.token === "string" && + marker.token.length > 0; +} + +function sameLockGeneration(left, right) { + return left?.owner === right?.owner && + left?.pid === right?.pid && + left?.token === right?.token; +} + +function reclaimStalePreparationLock( + nodeModulesPath, + lockPath, + runtimeProcess = process, +) { let marker; try { marker = readJson(join(lockPath, MARKER_NAME)); @@ -56,10 +141,7 @@ export function reclaimStalePreparationLock(lockPath, runtimeProcess = process) return false; } - if (marker?.owner !== LOCK_OWNER) { - return false; - } - if (!Number.isSafeInteger(marker.pid) || marker.pid < 1) { + if (!isOwnedLockMarker(marker)) { return false; } try { @@ -67,8 +149,33 @@ export function reclaimStalePreparationLock(lockPath, runtimeProcess = process) return false; } catch (error) { if (error?.code !== "ESRCH") return false; - rmSync(lockPath, { recursive: true, force: true }); - return true; + waitForReclaimRaceTestBarrier(); + const guardPath = reclaimerGuardPath(nodeModulesPath, marker.token); + try { + renameSync(lockPath, guardPath); + } catch (renameError) { + if ( + renameError?.code === "ENOENT" || + isDirectoryPathConflict(renameError, guardPath) + ) { + return false; + } + throw renameError; + } + + let claimedMarker; + try { + claimedMarker = readJson(join(guardPath, MARKER_NAME)); + } catch { + return false; + } + if (!sameLockGeneration(marker, claimedMarker)) { + return false; + } + // Lock markers are immutable after publication. Matching the generation + // after the atomic rename proves this process claimed the stale directory; + // keep that path as a permanent tombstone for delayed readers. + return guardPath; } } diff --git a/tests/bun/workspace-packages.test.mjs b/tests/bun/workspace-packages.test.mjs index 986dbc7a56..0386615338 100644 --- a/tests/bun/workspace-packages.test.mjs +++ b/tests/bun/workspace-packages.test.mjs @@ -1,16 +1,154 @@ import assert from "node:assert/strict"; -import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { Buffer } from "node:buffer"; +import { spawn } from "node:child_process"; +import { createHash, randomUUID } from "node:crypto"; +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + realpathSync, + rmSync, + writeFileSync, +} from "node:fs"; import { join, resolve } from "node:path"; +import { setTimeout as delay } from "node:timers/promises"; import { fileURLToPath } from "node:url"; import test from "node:test"; -import { prepareBunWorkspacePackages } from "./workspace-packages.mjs"; +import { isDirectoryPathConflict, prepareBunWorkspacePackages } from "./workspace-packages.mjs"; const projectRoot = fileURLToPath(new URL("../..", import.meta.url)); +const workspacePackagesModuleUrl = new URL("./workspace-packages.mjs", import.meta.url).href; +const PREPARATION_ACTIVE_EXIT_CODE = 17; +const PREPARATION_PUBLISH_INTERRUPTED_EXIT_CODE = 18; +const LOCK_STAGING_PREFIX = ".veryfront-bun-workspace-packages.lock.staging-"; +const RECLAIMER_GUARD_PREFIX = ".veryfront-bun-workspace-packages.reclaiming-"; +const RECLAIM_BARRIER_PREFIX = ".veryfront-bun-workspace-packages.barrier-"; + +test("directory conflicts include Windows access errors for existing paths", () => { + const guardPath = reclaimerGuardPath(randomUUID()); + mkdirSync(guardPath, { recursive: true }); + + try { + assert.equal(isDirectoryPathConflict({ code: "EPERM" }, guardPath), true); + assert.equal(isDirectoryPathConflict({ code: "EACCES" }, guardPath), true); + assert.equal(isDirectoryPathConflict({ code: "EINVAL" }, guardPath), false); + } finally { + rmSync(guardPath, { recursive: true, force: true }); + } + + assert.equal(isDirectoryPathConflict({ code: "EPERM" }, guardPath), false); +}); function readJson(path) { return JSON.parse(readFileSync(path, "utf8")); } +function staleLockPath() { + return join( + projectRoot, + "node_modules", + ".veryfront-bun-workspace-packages.lock", + ); +} + +function reclaimerGuardPath(token) { + const digest = createHash("sha256").update(token).digest("hex"); + return join(projectRoot, "node_modules", `${RECLAIMER_GUARD_PREFIX}${digest}`); +} + +function writeOwnedLockMarker(directoryPath, token, pid = 9_999_999) { + writeFileSync( + join(directoryPath, ".veryfront-bun-workspace-package.json"), + `${JSON.stringify({ owner: "veryfront-bun-tests", pid, token })}\n`, + ); +} + +function writeStaleLock(lockPath, token = "stale") { + rmSync(lockPath, { recursive: true, force: true }); + mkdirSync(lockPath, { recursive: true }); + writeOwnedLockMarker(lockPath, token); +} + +function runWorkspacePreparationChild(extraEnv = {}) { + const source = ` + import { setTimeout } from "node:timers/promises"; + import { prepareBunWorkspacePackages } from ${JSON.stringify(workspacePackagesModuleUrl)}; + + try { + const prepared = prepareBunWorkspacePackages(${JSON.stringify(projectRoot)}); + await setTimeout(Number(process.env.VF_BUN_WORKSPACE_HOLD_MS ?? 0)); + prepared.cleanup(); + process.exit(0); + } catch (error) { + if (error?.message === "Bun workspace package preparation is already active") { + process.exit(${PREPARATION_ACTIVE_EXIT_CODE}); + } + console.error(error); + process.exit(1); + } + `; + const evalArgs = process.versions.deno + ? ["eval", source] + : ["--input-type=module", "--eval", source]; + + return new Promise((resolvePromise) => { + const child = spawn(process.execPath, evalArgs, { + env: { + ...process.env, + VF_BUN_WORKSPACE_HOLD_MS: "500", + ...extraEnv, + }, + stdio: ["ignore", "pipe", "pipe"], + }); + const stdout = []; + const stderr = []; + child.stdout.on("data", (chunk) => stdout.push(chunk)); + child.stderr.on("data", (chunk) => stderr.push(chunk)); + child.on("error", (error) => { + resolvePromise({ + code: 1, + stdout: Buffer.concat(stdout).toString(), + stderr: `${Buffer.concat(stderr).toString()}${error.stack ?? error.message}`, + }); + }); + child.on("close", (code) => { + resolvePromise({ + code: code ?? 1, + stdout: Buffer.concat(stdout).toString(), + stderr: Buffer.concat(stderr).toString(), + }); + }); + }); +} + +function stagingLockPaths() { + const nodeModulesPath = join(projectRoot, "node_modules"); + return readdirSync(nodeModulesPath) + .filter((name) => name.startsWith(LOCK_STAGING_PREFIX)) + .map((name) => join(nodeModulesPath, name)); +} + +async function waitForReclaimerArrivals(barrierPath, expectedCount) { + const deadline = Date.now() + 10_000; + while ( + readdirSync(barrierPath).filter((name) => name.startsWith("ready-")).length < expectedCount + ) { + if (Date.now() >= deadline) { + throw new Error("Timed out waiting for Bun workspace reclaimers"); + } + await delay(10); + } +} + +function releaseReclaimers(barrierPath) { + try { + writeFileSync(join(barrierPath, "release"), "release\n", { flag: "wx" }); + } catch (error) { + if (error?.code !== "EEXIST") throw error; + } +} + test("prepareBunWorkspacePackages derives native packages from every workspace export", () => { const prepared = prepareBunWorkspacePackages(projectRoot); const generatedPackageRoots = []; @@ -109,28 +247,133 @@ test("workspace package preparation rejects an overlapping run without disturbin assert.equal(existsSync(rootPackagePath), false); }); +test("workspace package preparation survives interrupted lock publication", async () => { + const lockPath = staleLockPath(); + rmSync(lockPath, { recursive: true, force: true }); + for (const path of stagingLockPaths()) { + rmSync(path, { recursive: true, force: true }); + } + + try { + const result = await runWorkspacePreparationChild({ + VF_BUN_WORKSPACE_HOLD_MS: "0", + VF_BUN_WORKSPACE_INTERRUPT_BEFORE_LOCK_PUBLISH: "1", + }); + + assert.equal(result.code, PREPARATION_PUBLISH_INTERRUPTED_EXIT_CODE); + assert.equal(existsSync(lockPath), false); + assert.equal(stagingLockPaths().length, 1); + + const prepared = prepareBunWorkspacePackages(projectRoot); + prepared.cleanup(); + } finally { + rmSync(lockPath, { recursive: true, force: true }); + for (const path of stagingLockPaths()) { + rmSync(path, { recursive: true, force: true }); + } + } +}); + test("workspace package preparation reclaims a stale lock with a dead owner", () => { - const lockPath = join( + const lockPath = staleLockPath(); + const staleToken = "single-stale-generation"; + const guardPath = reclaimerGuardPath(staleToken); + rmSync(guardPath, { recursive: true, force: true }); + writeStaleLock(lockPath, staleToken); + + const prepared = prepareBunWorkspacePackages(projectRoot); + let guardExistsAfterCleanup = false; + try { + assert.equal( + existsSync(join(prepared.nodeModulesPath, "veryfront/package.json")), + true, + ); + } finally { + prepared.cleanup(); + guardExistsAfterCleanup = existsSync(guardPath); + rmSync(guardPath, { recursive: true, force: true }); + } + + assert.equal(guardExistsAfterCleanup, true); +}); + +test("workspace package preparation serializes concurrent stale-lock reclaimers", async () => { + const lockPath = staleLockPath(); + const staleToken = "raced-stale-generation"; + const guardPath = reclaimerGuardPath(staleToken); + const barrierPath = join( projectRoot, "node_modules", - ".veryfront-bun-workspace-packages.lock", + `${RECLAIM_BARRIER_PREFIX}${randomUUID()}`, ); + rmSync(guardPath, { recursive: true, force: true }); + mkdirSync(barrierPath, { recursive: true }); + writeStaleLock(lockPath, staleToken); + const children = [ + runWorkspacePreparationChild({ + VF_BUN_WORKSPACE_RECLAIM_BARRIER_PATH: barrierPath, + }), + runWorkspacePreparationChild({ + VF_BUN_WORKSPACE_RECLAIM_BARRIER_PATH: barrierPath, + }), + ]; + + try { + await waitForReclaimerArrivals(barrierPath, children.length); + releaseReclaimers(barrierPath); + const results = await Promise.all(children); + const exitCodes = results.map((result) => result.code).sort((a, b) => a - b); + + assert.deepEqual( + exitCodes, + [0, PREPARATION_ACTIVE_EXIT_CODE], + JSON.stringify(results, null, 2), + ); + assert.equal(existsSync(lockPath), false); + assert.equal(existsSync(guardPath), true); + assert.equal( + existsSync(join(projectRoot, "node_modules", "veryfront/package.json")), + false, + ); + } finally { + releaseReclaimers(barrierPath); + await Promise.allSettled(children); + rmSync(lockPath, { recursive: true, force: true }); + rmSync(guardPath, { recursive: true, force: true }); + rmSync(barrierPath, { recursive: true, force: true }); + } +}); + +test("workspace package preparation ignores an orphaned stale-generation guard", () => { + const lockPath = staleLockPath(); + const orphanGuardPath = reclaimerGuardPath("orphaned-generation"); + const reclaimedGuardPath = reclaimerGuardPath("new-stale-generation"); rmSync(lockPath, { recursive: true, force: true }); - mkdirSync(lockPath, { recursive: true }); - writeFileSync( - join(lockPath, ".veryfront-bun-workspace-package.json"), - `${JSON.stringify({ owner: "veryfront-bun-tests", pid: 9_999_999, token: "stale" })}\n`, - ); + rmSync(orphanGuardPath, { recursive: true, force: true }); + rmSync(reclaimedGuardPath, { recursive: true, force: true }); + mkdirSync(orphanGuardPath, { recursive: true }); + writeOwnedLockMarker(orphanGuardPath, "orphaned-generation"); + writeStaleLock(lockPath, "new-stale-generation"); const prepared = prepareBunWorkspacePackages(projectRoot); + let reclaimedExistsAfterCleanup = false; try { + assert.equal(existsSync(orphanGuardPath), true); + assert.equal(existsSync(reclaimedGuardPath), true); assert.equal( existsSync(join(prepared.nodeModulesPath, "veryfront/package.json")), true, ); } finally { prepared.cleanup(); + reclaimedExistsAfterCleanup = existsSync(reclaimedGuardPath); + rmSync(orphanGuardPath, { recursive: true, force: true }); + rmSync(reclaimedGuardPath, { recursive: true, force: true }); + rmSync(lockPath, { recursive: true, force: true }); } + + assert.equal(existsSync(lockPath), false); + assert.equal(reclaimedExistsAfterCleanup, true); }); test("workspace package preparation preserves a live lock owner", () => {