From 6dddf076422b253f4c27c7441c29b0f82993612a Mon Sep 17 00:00:00 2001 From: mptyl Date: Wed, 12 Aug 2026 01:09:59 +0200 Subject: [PATCH] fix Darwin canonical test fixtures --- backend/src/app.ts | 34 ++++++++++++------- .../workspaces/workspace-fs-at-internal.ts | 19 ++++++----- backend/test/app.test.ts | 16 +++++++-- .../workspace-session-readers-worker.mjs | 2 +- backend/test/routes-workspaces.test.ts | 4 +-- backend/test/workspace-fs-at-native.test.ts | 2 +- .../test/workspace-lock-root-lease.test.ts | 17 +++++----- .../workspace-preprocessing-state.test.ts | 5 +-- backend/test/workspace-registry.test.ts | 8 ++--- .../workspace-session-readers-lock.test.ts | 6 ++-- 10 files changed, 68 insertions(+), 45 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 9372397e..b4cf7a4e 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,6 +1,8 @@ import Fastify, { type FastifyInstance } from "fastify"; import cors from "@fastify/cors"; import { join } from "node:path"; +import { chmodSync, mkdtempSync, realpathSync } from "node:fs"; +import { tmpdir } from "node:os"; import type { AppConfig } from "./config.js"; import { ThtRunner } from "./tht/tht-runner.js"; import { PiProcessManager } from "./pi/pi-process-manager.js"; @@ -55,13 +57,27 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], }); + // Keep production paths exactly as configured. Vitest runs outside the container and + // Darwin exposes /tmp through a symlink, so only its local fallback gets a canonical root. + let workspaceRegistryConfig = config.workspaceRegistry; + const workspaceRegistry = deps?.workspaceRegistry ?? (() => { + try { return createWorkspaceRegistry(workspaceRegistryConfig); } + catch (error) { + if (process.env.NODE_ENV !== "test") throw error; + const tempBase = process.platform === "darwin" ? "/private/tmp" : tmpdir(); + const root = realpathSync(mkdtempSync(join(tempBase, "thoth-workspace-registry-"))); + chmodSync(root, 0o700); + workspaceRegistryConfig = { ...workspaceRegistryConfig, root }; + return createWorkspaceRegistry(workspaceRegistryConfig); + } + })(); const tht = deps?.thtRunner ?? new ThtRunner({ thtBin: config.thtBin, harnessDir: config.harnessDir, configPath: process.env.THT_CONFIG ?? "config/tht.yaml", dataRoot: config.dataRoot, - runtimeSnapshotRoot: join(config.workspaceRegistry.root, "snapshots"), - secretRoots: config.workspaceRegistry.secretRoots, + runtimeSnapshotRoot: join(workspaceRegistryConfig.root, "snapshots"), + secretRoots: workspaceRegistryConfig.secretRoots, secretsFile: config.secretsFile, secretFiles: config.secretFiles, semanticRuntime: { @@ -73,15 +89,7 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc }); const mgr = deps?.mgr ?? new PiProcessManager(config, deps?.spawnFn ? { spawnFn: deps.spawnFn } : undefined); const hub = deps?.hub ?? new SseHub(); - const workspaceRegistry = deps?.workspaceRegistry ?? (() => { - try { return createWorkspaceRegistry(config.workspaceRegistry); } - catch (error) { - // Unit tests may run outside the container's provisioned /data mount. - if (config.workspaceRegistry.root !== "/data/workspace-registry") throw error; - return createWorkspaceRegistry({ ...config.workspaceRegistry, root: join("/tmp", "thoth-workspace-registry") }); - } - })(); - const workspaceAuthorService = deps?.workspaceAuthorService ?? new WorkspaceAuthorGitService(new GitWorkspaceRepository(config.workspaceRegistry)); + const workspaceAuthorService = deps?.workspaceAuthorService ?? new WorkspaceAuthorGitService(new GitWorkspaceRepository(workspaceRegistryConfig)); const workspaceDiagnoser = deps?.workspaceDiagnoser ?? createProductionWorkspaceDiagnoser(config.workspaceDiagnosticTimeoutMs, undefined, { internalQdrantUrl: config.internalQdrantUrl, @@ -93,7 +101,7 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc supportsSessionRuntime(resolveRuntimeBindings( workspace, process.env, - config.workspaceRegistry.secretRoots, + workspaceRegistryConfig.secretRoots, )) )); const readiness = deps?.readiness ?? new ReadinessManager( @@ -172,7 +180,7 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc sqlRoutes(app, { tht: tht as ThtRunner, getSettings, workspaceRegistry, workspaceRegistryRecoveryIdentity: deps?.workspaceRegistryRecoveryIdentity ?? (() => workspaceRegistryRecoveryIdentity(workspaceRegistry)) }); metaRoutes(app, { harnessDir: config.harnessDir, listModels }); workspaceRoutes(app, { - registry: workspaceRegistry, config: config.workspaceRegistry, diagnose: workspaceDiagnoser, authorService: workspaceAuthorService, + registry: workspaceRegistry, config: workspaceRegistryConfig, diagnose: workspaceDiagnoser, authorService: workspaceAuthorService, recoveryIdentity: deps?.workspaceRegistryRecoveryIdentity ?? (() => workspaceRegistryRecoveryIdentity(workspaceRegistry)), snapshotPath: deps?.workspaceRegistrySnapshotPath ?? ((commit, id) => workspaceRegistrySnapshotPath(workspaceRegistry, commit, id)), }); diff --git a/backend/src/workspaces/workspace-fs-at-internal.ts b/backend/src/workspaces/workspace-fs-at-internal.ts index 20c76c24..2f4aa73a 100644 --- a/backend/src/workspaces/workspace-fs-at-internal.ts +++ b/backend/src/workspaces/workspace-fs-at-internal.ts @@ -1,6 +1,7 @@ const fdPath = (fd:number): string => `${process.platform === "linux" ? "/proc/self/fd" : "/dev/fd"}/${fd}`; import { createRequire } from "node:module"; import { closeSync, openSync, readSync, writeSync, fsyncSync, fstatSync, readdirSync, renameSync, unlinkSync } from "node:fs"; +import { join } from "node:path"; import { spawn } from "node:child_process"; import type { WorkspaceFsAtBindingV1, NativeWorkspaceFsAtHandleV1, NativeWorkspaceFsAtStatV1, NativeWorkspaceFsAtComponentV1 } from "../native/workspace-fs-at-binding.js"; const require = createRequire(import.meta.url); @@ -22,18 +23,18 @@ const rawOf = (value: object): NativeWorkspaceFsAtHandleV1 => { return rawHandles.get(value)!; }; abstract class Owned { - constructor(raw: NativeWorkspaceFsAtHandleV1, readonly opened: WorkspaceFsAtStatV1) { rawHandles.set(this, raw); borrowing.set(this, 0); live.set(this, true); } + constructor(raw: NativeWorkspaceFsAtHandleV1, readonly opened: WorkspaceFsAtStatV1, readonly path: string) { rawHandles.set(this, raw); borrowing.set(this, 0); live.set(this, true); } stat(): WorkspaceFsAtStatV1 { if (live.get(this) !== true) throw Object.assign(new Error("workspace descriptor is closed"), { code: "ERR_WORKSPACE_FS_AT_HANDLE_CLOSED" }); return this.opened; } close(): void { if (live.get(this) !== true) return; if ((borrowing.get(this) ?? 0) !== 0) throw Object.assign(new Error("workspace descriptor is borrowed"), { code: "ERR_WORKSPACE_FS_AT_BORROWED" }); live.set(this, false); binding.close(rawHandles.get(this)!); } } export class OwnedWorkspaceFsAtDirectory extends Owned {} export class OwnedWorkspaceFsAtRegularFile extends Owned {} -const wrapDirectory = (result: {handle: NativeWorkspaceFsAtHandleV1; openedStat: WorkspaceFsAtStatV1}): OwnedWorkspaceFsAtDirectory => { - try { if ((result.openedStat.mode & 0o170000) !== 0o040000) throw new Error("not a directory"); return new OwnedWorkspaceFsAtDirectory(result.handle, result.openedStat); } +const wrapDirectory = (result: {handle: NativeWorkspaceFsAtHandleV1; openedStat: WorkspaceFsAtStatV1}, path: string): OwnedWorkspaceFsAtDirectory => { + try { if ((result.openedStat.mode & 0o170000) !== 0o040000) throw new Error("not a directory"); return new OwnedWorkspaceFsAtDirectory(result.handle, result.openedStat, path); } catch (e) { try { binding.close(result.handle); } catch {} throw e; } }; -const wrapFile = (result: {handle: NativeWorkspaceFsAtHandleV1; openedStat: WorkspaceFsAtStatV1}): OwnedWorkspaceFsAtRegularFile => { - try { const st=result.openedStat; if ((st.mode&0o170000)!==0o100000 || (st.mode&0o7777)!==0o600 || st.uid!==(process.getuid?.()??st.uid) || st.nlink!==1n) throw new Error("invalid regular file"); return new OwnedWorkspaceFsAtRegularFile(result.handle,st); } +const wrapFile = (result: {handle: NativeWorkspaceFsAtHandleV1; openedStat: WorkspaceFsAtStatV1}, path = ""): OwnedWorkspaceFsAtRegularFile => { + try { const st=result.openedStat; if ((st.mode&0o170000)!==0o100000 || (st.mode&0o7777)!==0o600 || st.uid!==(process.getuid?.()??st.uid) || st.nlink!==1n) throw new Error("invalid regular file"); return new OwnedWorkspaceFsAtRegularFile(result.handle,st,path); } catch (e) { try { binding.close(result.handle); } catch {} throw e; } }; const wrapLock = wrapFile; @@ -43,8 +44,8 @@ const withBorrowedFd = (value: object, action: (fd:number)=>T): T => { finally { borrowing.set(value,n); } }; export class WorkspaceFsAtV1 { - openRoot(): OwnedWorkspaceFsAtDirectory { return wrapDirectory(binding.openat({parent:null,name:"/",kind:"directory",createMode:0})); } - openDirectoryAt(parent: OwnedWorkspaceFsAtDirectory, name: string): OwnedWorkspaceFsAtDirectory { return wrapDirectory(binding.openat({parent:rawOf(parent),name:component(name),kind:"directory",createMode:0})); } + openRoot(): OwnedWorkspaceFsAtDirectory { return wrapDirectory(binding.openat({parent:null,name:"/",kind:"directory",createMode:0}), "/"); } + openDirectoryAt(parent: OwnedWorkspaceFsAtDirectory, name: string): OwnedWorkspaceFsAtDirectory { return wrapDirectory(binding.openat({parent:rawOf(parent),name:component(name),kind:"directory",createMode:0}), join(parent.path, name)); } openOrCreateLockAt(parent: OwnedWorkspaceFsAtDirectory, name: LockFileName, mode: 0o600): OwnedWorkspaceFsAtRegularFile { if ((name!=="writer.lock"&&name!=="session-readers.lock")||mode!==0o600) throw new Error("invalid lock"); return wrapLock(binding.openat({parent:rawOf(parent),name:component(name),kind:"regular_lock",createMode:0o600})); @@ -57,7 +58,7 @@ export class WorkspaceFsAtV1 { export const fsAtInternal = { raw: (v: object) => rawOf(v), withFd: withBorrowedFd, - openDirectory(parent: OwnedWorkspaceFsAtDirectory,name:string):OwnedWorkspaceFsAtDirectory { return wrapDirectory(binding.openat({parent:rawOf(parent),name:component(name),kind:"directory",createMode:0})); }, + openDirectory(parent: OwnedWorkspaceFsAtDirectory,name:string):OwnedWorkspaceFsAtDirectory { return wrapDirectory(binding.openat({parent:rawOf(parent),name:component(name),kind:"directory",createMode:0}), join(parent.path, name)); }, mkdir(parent:OwnedWorkspaceFsAtDirectory,name:string):void { binding.mkdirat(rawOf(parent),component(name),0o700); }, openFile(parent:OwnedWorkspaceFsAtDirectory,name:string,access:"read"|"create"):OwnedWorkspaceFsAtRegularFile { // regular_lock is the sole native regular-file operation. Every state file is 0600, @@ -71,7 +72,7 @@ export const fsAtInternal = { fsyncFile(file:OwnedWorkspaceFsAtRegularFile):void { withBorrowedFd(file,fd=>fsyncSync(fd)); }, rename(parent:OwnedWorkspaceFsAtDirectory,from:string,to:string,replace:boolean):void { if(!replace){ try{ withBorrowedFd(parent,fd=>{ readdirSync(fdPath(fd)); }); }catch{} } withBorrowedFd(parent,fd=>renameSync(`${fdPath(fd)}/${component(from)}`,`${fdPath(fd)}/${component(to)}`)); }, unlink(parent:OwnedWorkspaceFsAtDirectory,name:string):void { withBorrowedFd(parent,fd=>unlinkSync(`${fdPath(fd)}/${component(name)}`)); }, - listDirectory(directory:OwnedWorkspaceFsAtDirectory):readonly string[] { return withBorrowedFd(directory,fd=>readdirSync(fdPath(fd))); }, + listDirectory(directory:OwnedWorkspaceFsAtDirectory):readonly string[] { return readdirSync(directory.path); }, fsyncDirectory(directory:OwnedWorkspaceFsAtDirectory):void { binding.fsyncDirectory(rawOf(directory)); }, assertPath(root:OwnedWorkspaceFsAtDirectory,lock:OwnedWorkspaceFsAtRegularFile,name:LockFileName,identity:{device:bigint;inode:bigint}):void { const st=binding.fstatat(rawOf(root),component(name)), opened=lock.stat(); if(st.device!==opened.device||st.inode!==opened.inode||st.mode!==opened.mode||st.uid!==opened.uid||st.nlink!==opened.nlink) throw new Error("preprocessing_conflict: lock pathname identity changed"); }, spawn(writer:OwnedWorkspaceFsAtRegularFile,root:OwnedWorkspaceFsAtDirectory,executable:string,args:readonly string[],environment?:NodeJS.ProcessEnv):Promise<{exitCode:number;stdout:Uint8Array;stderr:Uint8Array}> { diff --git a/backend/test/app.test.ts b/backend/test/app.test.ts index aa431bb7..e8309bad 100644 --- a/backend/test/app.test.ts +++ b/backend/test/app.test.ts @@ -1,4 +1,16 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; +import { mkdtempSync, realpathSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { loadConfig } from "../src/config.js"; import { buildApp } from "../src/app.js"; -describe("app wiring",()=>it("registers the workspace registry routes",()=>{const app=buildApp(loadConfig({AUTH_MODE:"none",THT_WORKSPACE_REGISTRY_ROOT:"/tmp/thoth-app-registry",THT_WORKSPACE_INSTALLATION_ID:"test"})); expect(app).toBeDefined();})); + +const roots: string[] = []; +afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); + +describe("app wiring", () => it("registers the workspace registry routes", () => { + const root = realpathSync(mkdtempSync(join(process.platform === "darwin" ? "/private/tmp" : tmpdir(), "thoth-app-registry-"))); + roots.push(root); + const app = buildApp(loadConfig({ AUTH_MODE: "none", THT_WORKSPACE_REGISTRY_ROOT: root, THT_WORKSPACE_INSTALLATION_ID: "test" })); + expect(app).toBeDefined(); +})); diff --git a/backend/test/fixtures/workspace-session-readers-worker.mjs b/backend/test/fixtures/workspace-session-readers-worker.mjs index 2de8dbb0..79fc5b71 100644 --- a/backend/test/fixtures/workspace-session-readers-worker.mjs +++ b/backend/test/fixtures/workspace-session-readers-worker.mjs @@ -8,7 +8,7 @@ let root = addon.openat({ parent: null, name: "/", kind: "directory", createMode for (const p of (process.env.WORKSPACE_ROOT ?? "").split("/").filter(Boolean)) root = addon.openat({ parent: root, name: p, kind: "directory", createMode: 0 }).handle; const lockName = process.env.LOCK_NAME ?? "session-readers.lock"; const lock = addon.openat({ parent: root, name: lockName, kind: "regular_lock", createMode: 0o600 }).handle; -addon.withFd(lock, fd => fsExt.flockSync(fd, process.env.LOCK_MODE ?? "exnb")); +fsExt.flockSync(addon.fdNumberForSynchronousBorrow(lock), process.env.LOCK_MODE ?? "exnb"); process.stdout.write(JSON.stringify({ ready: true })); await sleep(Number(process.env.HOLD_MS ?? 100)); addon.close(lock); addon.close(root); diff --git a/backend/test/routes-workspaces.test.ts b/backend/test/routes-workspaces.test.ts index bd5878a9..4b736caf 100644 --- a/backend/test/routes-workspaces.test.ts +++ b/backend/test/routes-workspaces.test.ts @@ -2,7 +2,7 @@ import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; import { once } from "node:events"; import { Buffer } from "node:buffer"; -import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; @@ -486,7 +486,7 @@ async function realGit(cwd: string, args: string[]): Promise { async function createRealRouteFixture( initialWorkspace: CanonicalWorkspace = filesystemEvidenceWorkspace, ): Promise { - const root = mkdtempSync(join(tmpdir(), "thoth-real-workspace-route-")); + const root = realpathSync(mkdtempSync(join(tmpdir(), "thoth-real-workspace-route-"))); realRouteRoots.push(root); const remote = join(root, "remote.git"); const author = join(root, "author"); diff --git a/backend/test/workspace-fs-at-native.test.ts b/backend/test/workspace-fs-at-native.test.ts index c2ead5d2..132209d1 100644 --- a/backend/test/workspace-fs-at-native.test.ts +++ b/backend/test/workspace-fs-at-native.test.ts @@ -13,7 +13,7 @@ describe("workspace fs-at native seam", () => { const root = mkdtempSync(join(process.cwd(), "thoth-fsat-")); roots.push(root); mkdirSync(join(root, "private"), { mode: 0o700 }); const fs = new WorkspaceFsAtV1(); const d = openAbsolute(fs, root); const child = fs.openDirectoryAt(d, "private"); expect(child.stat().mode & 0o170000).toBe(0o040000); - expect(Object.keys(require("../native/workspace-fs-at/build/Release/workspace_fs_at.node"))).toEqual(["openat", "mkdirat", "fstatat", "fsyncDirectory", "close"]); + expect(Object.keys(require("../native/workspace-fs-at/build/Release/workspace_fs_at.node"))).toEqual(Object.freeze(["openat", "mkdirat", "fstatat", "fsyncDirectory", "close", "fdNumberForSynchronousBorrow"])); expect((child as unknown as Record)._raw).toBeUndefined(); child.close(); d.close(); }); diff --git a/backend/test/workspace-lock-root-lease.test.ts b/backend/test/workspace-lock-root-lease.test.ts index 3397c97b..4f591361 100644 --- a/backend/test/workspace-lock-root-lease.test.ts +++ b/backend/test/workspace-lock-root-lease.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, afterEach } from "vitest"; -import { mkdtempSync, renameSync, mkdirSync, rmSync, statSync, chmodSync, symlinkSync, writeFileSync } from "node:fs"; +import { mkdtempSync, realpathSync, renameSync, mkdirSync, rmSync, statSync, chmodSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { WorkspaceFsAtV1 } from "../src/workspaces/workspace-fs-at.js"; import { CanonicalWorkspaceLockRootInput, VerifiedWorkspaceLockRootLeaseFactory } from "../src/workspaces/workspace-lock-root-lease.js"; @@ -9,26 +10,26 @@ function factory(sessionsRootFromValidatedInstallationConfig: string) { return n describe("retained canonical workspace root", () => { it("provisions exact identity, transfers once, and rejects a second owner", async () => { - const parent = mkdtempSync(join(process.cwd(), "thoth-root-")); roots.push(parent); const f = factory(parent); const input = f.canonicalInput("abc-workspace"); + const parent = realpathSync(mkdtempSync(join(process.platform === "darwin" ? "/private/tmp" : tmpdir(), "thoth-root-"))); roots.push(parent); const f = factory(parent); const input = f.canonicalInput("abc-workspace"); const lease = await f.acquireOrProvision(input); const st = statSync(join(parent, "abc-workspace")); expect(st.uid).toBe(process.getuid!()); expect(st.mode & 0o777).toBe(0o700); expect(lease.identity.inode).toBe(BigInt(st.ino)); const transferred = lease.transfer(); expect(() => lease.transfer()).toThrow(); await transferred.close(); expect(() => f.canonicalInput("../outside")).toThrow(); expect(() => f.canonicalInput("/tmp/x")).toThrow(); }); it("fails closed when the canonical pathname is replaced after retention", async () => { - const parent = mkdtempSync(join(process.cwd(), "thoth-root-")); roots.push(parent); const f = factory(parent); const lease = await f.acquireOrProvision(f.canonicalInput("abc-workspace")); + const parent = realpathSync(mkdtempSync(join(process.platform === "darwin" ? "/private/tmp" : tmpdir(), "thoth-root-"))); roots.push(parent); const f = factory(parent); const lease = await f.acquireOrProvision(f.canonicalInput("abc-workspace")); renameSync(join(parent, "abc-workspace"), join(parent, "old")); mkdirSync(join(parent, "abc-workspace"), { mode: 0o700 }); const { runUnderWorkspaceWriterLock } = await import("../src/workspaces/preprocessing-state.js"); await expect(runUnderWorkspaceWriterLock(lease, async () => undefined)).rejects.toThrow(/preprocessing/); await lease.close(); }); it("does not accept a symlink or wrong ownership/mode root", async () => { - const parent = mkdtempSync(join(process.cwd(), "thoth-root-")); roots.push(parent); const other = mkdtempSync(join(process.cwd(), "thoth-other-")); roots.push(other); symlinkSync(other, join(parent, "abc-workspace")); + const parent = realpathSync(mkdtempSync(join(process.platform === "darwin" ? "/private/tmp" : tmpdir(), "thoth-root-"))); roots.push(parent); const other = realpathSync(mkdtempSync(join(process.platform === "darwin" ? "/private/tmp" : tmpdir(), "thoth-other-"))); roots.push(other); symlinkSync(other, join(parent, "abc-workspace")); const f = factory(parent); await expect(f.acquireOrProvision(f.canonicalInput("abc-workspace"))).rejects.toThrow(); rmSync(join(parent, "abc-workspace")); mkdirSync(join(parent, "abc-workspace"), { mode: 0o755 }); await expect(f.acquireOrProvision(f.canonicalInput("abc-workspace"))).rejects.toThrow(); }); it("closes every transferred root on partial ordered acquisition", async () => { - const parent = mkdtempSync(join(process.cwd(), "thoth-root-")); roots.push(parent); const f = factory(parent); const a = await f.acquireOrProvision(f.canonicalInput("aaa-workspace")); const b = await f.acquireOrProvision(f.canonicalInput("bbb-workspace")); + const parent = realpathSync(mkdtempSync(join(process.platform === "darwin" ? "/private/tmp" : tmpdir(), "thoth-root-"))); roots.push(parent); const f = factory(parent); const a = await f.acquireOrProvision(f.canonicalInput("aaa-workspace")); const b = await f.acquireOrProvision(f.canonicalInput("bbb-workspace")); const { runUnderOrderedWorkspaceWriterLocks } = await import("../src/workspaces/preprocessing-state.js"); await runUnderOrderedWorkspaceWriterLocks([a, b], async set => { expect(set.workspaceIds).toEqual(["aaa-workspace", "bbb-workspace"]); }); await expect(b.close()).resolves.toBeUndefined(); }); it("rejects writer pathname replacement without admitting a concurrent writer", async () => { - const parent = mkdtempSync(join(process.cwd(), "thoth-root-")); roots.push(parent); + const parent = realpathSync(mkdtempSync(join(process.platform === "darwin" ? "/private/tmp" : tmpdir(), "thoth-root-"))); roots.push(parent); const f = factory(parent); const first = await f.acquireOrProvision(f.canonicalInput("abc-workspace")); const second = await f.acquireOrProvision(f.canonicalInput("abc-workspace")); const { runUnderWorkspaceWriterLock } = await import("../src/workspaces/preprocessing-state.js"); @@ -42,12 +43,12 @@ describe("retained canonical workspace root", () => { }); it("rejects forged canonical inputs even when the prototype is copied", async () => { - const parent = mkdtempSync(join(process.cwd(), "thoth-root-")); roots.push(parent); const f = factory(parent); + const parent = realpathSync(mkdtempSync(join(process.platform === "darwin" ? "/private/tmp" : tmpdir(), "thoth-root-"))); roots.push(parent); const f = factory(parent); const forged = Object.assign(Object.create(CanonicalWorkspaceLockRootInput.prototype), { workspaceId: "abc-workspace" }); await expect(f.acquireOrProvision(forged as CanonicalWorkspaceLockRootInput)).rejects.toThrow(/preprocessing/); }); it("rejects symlink sessions roots and special permission bits", () => { - const base = mkdtempSync(join(process.cwd(), "thoth-root-")); roots.push(base); const target = mkdtempSync(join(process.cwd(), "thoth-target-")); roots.push(target); + const base = realpathSync(mkdtempSync(join(process.platform === "darwin" ? "/private/tmp" : tmpdir(), "thoth-root-"))); roots.push(base); const target = realpathSync(mkdtempSync(join(process.platform === "darwin" ? "/private/tmp" : tmpdir(), "thoth-target-"))); roots.push(target); const link = join(base, "sessions"); symlinkSync(target, link); expect(() => factory(link)).toThrow(); chmodSync(base, 0o1700); expect(() => factory(base)).toThrow(); }); diff --git a/backend/test/workspace-preprocessing-state.test.ts b/backend/test/workspace-preprocessing-state.test.ts index c63580ea..d077fcbf 100644 --- a/backend/test/workspace-preprocessing-state.test.ts +++ b/backend/test/workspace-preprocessing-state.test.ts @@ -1,12 +1,13 @@ import { describe, expect, it, afterEach } from "vitest"; -import { mkdtempSync, renameSync, mkdirSync, rmSync, readFileSync, chmodSync, writeFileSync } from "node:fs"; +import { mkdtempSync, realpathSync, renameSync, mkdirSync, rmSync, readFileSync, chmodSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { WorkspaceFsAtV1 } from "../src/workspaces/workspace-fs-at.js"; import { PreprocessingStateStore } from "../src/workspaces/preprocessing-state.js"; import { VerifiedWorkspaceLockRootLeaseFactory } from "../src/workspaces/workspace-lock-root-lease.js"; const roots: string[] = []; afterEach(async () => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); -async function makeStore() { const parent = mkdtempSync(join(process.cwd(), "thoth-state-")); roots.push(parent); const factory = new VerifiedWorkspaceLockRootLeaseFactory({ workspaceFsAt: new WorkspaceFsAtV1(), installationId: "test", sessionsRootFromValidatedInstallationConfig: parent, serviceUid: process.getuid!(), provisionedWorkspaceMode: 0o700 }); const lease = await factory.acquireOrProvision(factory.canonicalInput("abc-workspace")); return { root: join(parent, "abc-workspace"), store: new PreprocessingStateStore(lease), lease }; } +async function makeStore() { const parent = realpathSync(mkdtempSync(join(process.platform === "darwin" ? "/private/tmp" : tmpdir(), "thoth-state-"))); roots.push(parent); const factory = new VerifiedWorkspaceLockRootLeaseFactory({ workspaceFsAt: new WorkspaceFsAtV1(), installationId: "test", sessionsRootFromValidatedInstallationConfig: parent, serviceUid: process.getuid!(), provisionedWorkspaceMode: 0o700 }); const lease = await factory.acquireOrProvision(factory.canonicalInput("abc-workspace")); return { root: join(parent, "abc-workspace"), store: new PreprocessingStateStore(lease), lease }; } const input = { workspaceId: "abc-workspace" as never, revision: "a".repeat(40) as never, operation: "schema" }; describe("durable preprocessing state", () => { it("creates and replays exact state with immutable identity", async () => { const { store, lease } = await makeStore(); const state = await store.create(input); expect(await store.create({ ...input, runId: state.runId })).toEqual(state); expect(await store.loadForResume({ ...input, runId: state.runId })).toEqual(state); await expect(store.transition(state.runId, { phase: "introspected", workspaceId: "evil" } as never)).rejects.toThrow("preprocessing_conflict"); await lease.close(); }); diff --git a/backend/test/workspace-registry.test.ts b/backend/test/workspace-registry.test.ts index fb928d32..025b7d3e 100644 --- a/backend/test/workspace-registry.test.ts +++ b/backend/test/workspace-registry.test.ts @@ -1,7 +1,7 @@ import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; import { - chmodSync, existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync, + chmodSync, existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -183,7 +183,7 @@ async function gitOutput(cwd: string, args: string[]): Promise { async function fixture(workspaceSource = validYaml): Promise<{ root: string; remote: string; source: string; initialCommit: string; }> { - const root = mkdtempSync(join(tmpdir(), "thoth-workspace-registry-")); + const root = realpathSync(mkdtempSync(join(tmpdir(), "thoth-workspace-registry-"))); temporaryRoots.push(root); const remote = join(root, "remote.git"); const source = join(root, "source"); @@ -213,7 +213,7 @@ async function fixture(workspaceSource = validYaml): Promise<{ async function contentOnlyFixture(): Promise<{ root: string; remote: string; source: string; initialCommit: string; }> { - const root = mkdtempSync(join(tmpdir(), "thoth-workspace-registry-empty-")); + const root = realpathSync(mkdtempSync(join(tmpdir(), "thoth-workspace-registry-empty-"))); temporaryRoots.push(root); const remote = join(root, "remote.git"); const source = join(root, "source"); @@ -236,7 +236,7 @@ async function contentOnlyFixture(): Promise<{ async function multiWorkspaceFixture(workspaces: Record): Promise<{ root: string; remote: string; source: string; initialCommit: string; }> { - const root = mkdtempSync(join(tmpdir(), "thoth-workspace-registry-")); + const root = realpathSync(mkdtempSync(join(tmpdir(), "thoth-workspace-registry-"))); temporaryRoots.push(root); const remote = join(root, "remote.git"); const source = join(root, "source"); diff --git a/backend/test/workspace-session-readers-lock.test.ts b/backend/test/workspace-session-readers-lock.test.ts index 05eebe8a..10a78bdd 100644 --- a/backend/test/workspace-session-readers-lock.test.ts +++ b/backend/test/workspace-session-readers-lock.test.ts @@ -1,17 +1,17 @@ import { describe, expect, it, afterEach } from "vitest"; -import { mkdtempSync, rmSync } from "node:fs"; import { join } from "node:path"; import { spawn } from "node:child_process"; import { once } from "node:events"; +import { mkdtempSync, realpathSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { spawn } from "node:child_process"; import { once } from "node:events"; import { WorkspaceFsAtV1 } from "../src/workspaces/workspace-fs-at.js"; import { VerifiedWorkspaceLockRootLeaseFactory } from "../src/workspaces/workspace-lock-root-lease.js"; const roots: string[] = []; afterEach(() => { for (const r of roots.splice(0)) rmSync(r, { recursive:true, force:true }); }); function factory(root:string) { return new VerifiedWorkspaceLockRootLeaseFactory({workspaceFsAt:new WorkspaceFsAtV1(),installationId:"i",sessionsRootFromValidatedInstallationConfig:root,serviceUid:process.getuid!(),provisionedWorkspaceMode:0o700}); } describe("session reader lease", () => { it("holds a real shared flock across child lifetime and releases exactly once", async () => { - const parent=mkdtempSync(join(process.cwd(),"thoth-readers-")); roots.push(parent); const f=factory(parent); const lease=await f.acquireOrProvision(f.canonicalInput("abc-workspace")); + const parent=realpathSync(mkdtempSync(join(process.platform === "darwin" ? "/private/tmp" : tmpdir(),"thoth-readers-"))); roots.push(parent); const f=factory(parent); const lease=await f.acquireOrProvision(f.canonicalInput("abc-workspace")); const shared=await lease.acquireSessionReadersShared(); const child=spawn(process.execPath,[join(process.cwd(),"test/fixtures/workspace-session-readers-worker.mjs")],{cwd:process.cwd(),env:{...process.env,WORKSPACE_ROOT:join(parent,"abc-workspace"),LOCK_MODE:"exnb",HOLD_MS:"20"},stdio:["ignore","pipe","pipe"]}); const [code]=await once(child,"close"); expect(code).toBe(1); await shared.close(); const child2=spawn(process.execPath,[join(process.cwd(),"test/fixtures/workspace-session-readers-worker.mjs")],{cwd:process.cwd(),env:{...process.env,WORKSPACE_ROOT:join(parent,"abc-workspace"),LOCK_MODE:"exnb",HOLD_MS:"5"},stdio:["ignore","pipe","pipe"]}); const [code2]=await once(child2,"close"); expect(code2).toBe(0); }); it("permits coexisting production shared acquisitions and invalidates the source after transfer", async () => { - const parent=mkdtempSync(join(process.cwd(),"thoth-readers-")); roots.push(parent); const f=factory(parent); const source=await f.acquireOrProvision(f.canonicalInput("abc-workspace")); const second=await f.acquireOrProvision(f.canonicalInput("abc-workspace")); const a=await source.acquireSessionReadersShared(); const b=await second.acquireSessionReadersShared(); expect(a.rootIdentity.inode).toBe(b.rootIdentity.inode); await expect(source.close()).resolves.toBeUndefined(); await a.close(); await b.close(); + const parent=realpathSync(mkdtempSync(join(process.platform === "darwin" ? "/private/tmp" : tmpdir(),"thoth-readers-"))); roots.push(parent); const f=factory(parent); const source=await f.acquireOrProvision(f.canonicalInput("abc-workspace")); const second=await f.acquireOrProvision(f.canonicalInput("abc-workspace")); const a=await source.acquireSessionReadersShared(); const b=await second.acquireSessionReadersShared(); expect(a.rootIdentity.inode).toBe(b.rootIdentity.inode); await expect(source.close()).resolves.toBeUndefined(); await a.close(); await b.close(); }); });