From add858f3e8d7d09fb3e439dc14bdab9b965a72f7 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 15 Aug 2026 07:54:55 +0000 Subject: [PATCH] test(workspaces): isolate command runtime Git contracts --- ...ce-git-service.runtime.integration.test.ts | 343 ++++++++++-------- 1 file changed, 199 insertions(+), 144 deletions(-) diff --git a/packages/server/src/server/workspace-git-service.runtime.integration.test.ts b/packages/server/src/server/workspace-git-service.runtime.integration.test.ts index a6c864603c..2d0bfa7ca0 100644 --- a/packages/server/src/server/workspace-git-service.runtime.integration.test.ts +++ b/packages/server/src/server/workspace-git-service.runtime.integration.test.ts @@ -14,6 +14,7 @@ const fixtureExecutable = fileURLToPath( new URL("../../../../runtimes/fixture/src/index.mjs", import.meta.url), ); const cleanupRoots: string[] = []; +const cleanupTasks: Array<() => Promise> = []; function eventWithin(event: Promise, label: string): Promise { return Promise.race([ @@ -33,9 +34,22 @@ function createDeferred(): { promise: Promise; resolve(): void } { } afterEach(async () => { - await Promise.all( + const teardownResults = await Promise.allSettled( + cleanupTasks + .splice(0) + .toReversed() + .map((cleanup) => cleanup()), + ); + const removalResults = await Promise.allSettled( cleanupRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })), ); + const results = [...teardownResults, ...removalResults]; + const failures = results + .filter((result): result is PromiseRejectedResult => result.status === "rejected") + .map((result) => result.reason); + if (failures.length > 0) { + throw new AggregateError(failures, "Runtime Git fixture cleanup failed"); + } }); test.each(["local", "worktree"] as const)( @@ -471,54 +485,9 @@ async function createBranchThroughRuntime( await expect(childProcess.exited).resolves.toEqual({ code: 0, signal: null }); } -test("selected workspace Git reads and mutations stay inside its command runtime", async () => { - const root = await mkdtemp(path.join(tmpdir(), "paseo-runtime-git-")); - cleanupRoots.push(root); - const runtimeRepository = await createRepository(path.join(root, "runtime-repository")); - const hostDecoy = await createRepository(path.join(root, "host-decoy")); - const remoteRepository = path.join(root, "remote.git"); - await mkdir(remoteRepository); - git(remoteRepository, "init", "--bare", "--initial-branch=main"); - git(runtimeRepository, "remote", "add", "origin", remoteRepository); - git(runtimeRepository, "push", "-u", "origin", "main"); - const stateDirectory = path.join(root, "runtime-state"); - await mkdir(stateDirectory); - const workspaceId = "runtime-git-workspace"; - const runtimeIds = new Map(); - const workspaceRuntime = createWorkspaceRuntimeService({ - paseoHome: path.join(root, "paseo-home"), - resolveRuntimeId: async (id) => runtimeIds.get(id) ?? null, - persistRuntimeId: async (id, runtimeId) => { - runtimeIds.set(id, runtimeId); - }, - beginWorkspaceDeletion: async () => {}, - removeWorkspaceRecord: async (id) => { - runtimeIds.delete(id); - }, - externalRuntimes: { - fixture: { - type: "command", - command: [process.execPath, fixtureExecutable], - options: { stateDirectory, recordLaunchInWorkspace: false }, - }, - }, - }); - await workspaceRuntime.create({ - workspaceId, - runtimeId: "fixture", - project: { - id: "runtime-git-project", - source: { kind: "host-directory", path: runtimeRepository }, - }, - placement: { kind: "existing" }, - }); - const service = new WorkspaceGitServiceImpl({ - logger: createLogger(), - paseoHome: path.join(root, "paseo-home"), - workspaceRuntime, - }); - const selectedGit = service.bindWorkspace({ workspaceId, cwd: hostDecoy }); - +test("selected workspace Git reads stay inside its command runtime", async () => { + const { hostDecoy, runtimeRepository, selectedGit, workspaceRuntime, workspaceId } = + await createCommandRuntimeGitFixture(); await workspaceRuntime.files(workspaceId).write({ path: "tracked.txt", contents: Buffer.from("runtime edit\n"), @@ -540,18 +509,14 @@ test("selected workspace Git reads and mutations stay inside its command runtime expect(JSON.stringify(dirty)).not.toContain(runtimeRepository); expect(JSON.stringify(diff)).toContain("runtime edit"); expect(JSON.stringify(diff)).not.toContain("host edit"); +}, 20_000); + +test("selected workspace Git rejects mutations unsupported by its command runtime", async () => { + const { hostDecoy, selectedGit } = await createCommandRuntimeGitFixture(); + git(hostDecoy, "branch", "host-only"); await expect(selectedGit.switchBranch("host-only")).rejects.toThrow( "Branch not found: host-only", ); - await selectedGit.stashPush("paseo-runtime-stash"); - expect((await selectedGit.getSnapshot({ force: true, reason: "stash-push" })).git.isDirty).toBe( - false, - ); - await selectedGit.stashPop(0); - expect((await selectedGit.getSnapshot({ force: true, reason: "stash-pop" })).git.isDirty).toBe( - true, - ); - await expect(selectedGit.mergeToBase({ baseRef: "main" })).rejects.toThrow( "Selected workspace Git does not support merge to base", ); @@ -562,6 +527,30 @@ test("selected workspace Git reads and mutations stay inside its command runtime "Selected workspace Git does not support branch rename", ); await expect(selectedGit.push()).rejects.toThrow("Selected workspace Git does not support push"); +}, 20_000); + +test("selected workspace Git mutations and fetch stay inside its command runtime", async () => { + const { root, hostDecoy, runtimeRepository, selectedGit, workspaceRuntime, workspaceId } = + await createCommandRuntimeGitFixture(); + const remoteRepository = path.join(root, "remote.git"); + await mkdir(remoteRepository); + git(remoteRepository, "init", "--bare", "--initial-branch=main"); + git(runtimeRepository, "remote", "add", "origin", remoteRepository); + git(runtimeRepository, "push", "-u", "origin", "main"); + + await workspaceRuntime.files(workspaceId).write({ + path: "tracked.txt", + contents: Buffer.from("runtime edit\n"), + }); + await writeFile(path.join(hostDecoy, "tracked.txt"), "host edit\n"); + await selectedGit.stashPush("paseo-runtime-stash"); + expect((await selectedGit.getSnapshot({ force: true, reason: "stash-push" })).git.isDirty).toBe( + false, + ); + await selectedGit.stashPop(0); + expect((await selectedGit.getSnapshot({ force: true, reason: "stash-pop" })).git.isDirty).toBe( + true, + ); await selectedGit.commit({ message: "runtime commit", addAll: true }); await selectedGit.createBranch({ branch: "runtime-branch", baseRef: "main" }); @@ -591,25 +580,18 @@ test("selected workspace Git reads and mutations stay inside its command runtime await selectedGit.fetch(); const fetched = await selectedGit.getSnapshot(); expect(fetched.git.behindOfOrigin).toBe(1); - - await workspaceRuntime.destroy(workspaceId); - git(runtimeRepository, "checkout", "runtime-branch"); - await workspaceRuntime.create({ - workspaceId, - runtimeId: "fixture", - project: { - id: "runtime-git-project", - source: { kind: "host-directory", path: runtimeRepository }, - }, - placement: { kind: "existing" }, - }); - const reconstructed = await selectedGit.getSnapshot(); - expect(reconstructed.git.currentBranch).toBe("runtime-branch"); - - await service.dispose(); - await workspaceRuntime.destroy(workspaceId); }, 40_000); +test("selected workspace Git reconstructs command runtime state after recreation", async () => { + const { recreate, runtimeRepository, selectedGit, workspaceRuntime, workspaceId } = + await createCommandRuntimeGitFixture(); + await workspaceRuntime.destroy(workspaceId); + git(runtimeRepository, "branch", "-m", "runtime-rebuilt"); + await recreate(); + const reconstructed = await selectedGit.getSnapshot(); + expect(reconstructed.git.currentBranch).toBe("runtime-rebuilt"); +}, 20_000); + test("selected commit history highlighting never reads a deleted file from the host cwd", async () => { const root = await mkdtemp(path.join(tmpdir(), "paseo-runtime-git-history-")); cleanupRoots.push(root); @@ -681,56 +663,8 @@ test("selected commit history highlighting never reads a deleted file from the h await workspaceRuntime.destroy("history-workspace"); }, 20_000); -test("selected workspaces with the same public cwd keep Git state, mutations, and caches isolated", async () => { - const root = await mkdtemp(path.join(tmpdir(), "paseo-runtime-git-same-cwd-")); - cleanupRoots.push(root); - const runtimeARepository = await createRepository(path.join(root, "runtime-a")); - const runtimeBRepository = await createRepository(path.join(root, "runtime-b")); - git(runtimeBRepository, "branch", "-m", "runtime-b"); - const publicCwd = await createRepository(path.join(root, "shared-public-cwd")); - git(publicCwd, "branch", "-m", "host-decoy"); - await writeFile(path.join(publicCwd, "selected-proof.txt"), Buffer.alloc(32, 0)); - const stateDirectory = path.join(root, "runtime-state"); - await mkdir(stateDirectory); - const runtimeIds = new Map(); - const workspaceRuntime = createWorkspaceRuntimeService({ - paseoHome: path.join(root, "paseo-home"), - resolveRuntimeId: async (id) => runtimeIds.get(id) ?? null, - persistRuntimeId: async (id, runtimeId) => { - runtimeIds.set(id, runtimeId); - }, - beginWorkspaceDeletion: async () => {}, - removeWorkspaceRecord: async (id) => { - runtimeIds.delete(id); - }, - externalRuntimes: { - fixture: { - type: "command", - command: [process.execPath, fixtureExecutable], - options: { stateDirectory, recordLaunchInWorkspace: false }, - }, - }, - }); - for (const [workspaceId, source] of [ - ["same-cwd-a", runtimeARepository], - ["same-cwd-b", runtimeBRepository], - ] as const) { - await workspaceRuntime.create({ - workspaceId, - runtimeId: "fixture", - project: { id: workspaceId, source: { kind: "host-directory", path: source } }, - placement: { kind: "existing" }, - }); - } - const service = new WorkspaceGitServiceImpl({ - logger: createLogger(), - paseoHome: path.join(root, "paseo-home"), - workspaceRuntime, - deps: { getWorkspaceGitSelfHealPhaseMs: () => 60_000 }, - }); - const workspaceA = service.bindWorkspace({ workspaceId: "same-cwd-a", cwd: publicCwd }); - const workspaceB = service.bindWorkspace({ workspaceId: "same-cwd-b", cwd: publicCwd }); - +test("selected workspaces with the same public cwd keep snapshots and diffs isolated", async () => { + const { workspaceA, workspaceB, workspaceRuntime } = await createSamePublicCwdFixture(); await workspaceRuntime.files("same-cwd-a").write({ path: "tracked.txt", contents: Buffer.from("runtime a dirty\n"), @@ -779,7 +713,10 @@ test("selected workspaces with the same public cwd keep Git state, mutations, an }); removeUntracked.stdin.end(); await expect(removeUntracked.exited).resolves.toEqual({ code: 0, signal: null }); +}, 25_000); +test("selected workspaces with the same public cwd keep observations isolated", async () => { + const { workspaceA, workspaceB, workspaceRuntime } = await createSamePublicCwdFixture(); let observedAWaiter: { dirty: boolean; resolve: () => void } | null = null; let observedBWaiter: { dirty: boolean; resolve: () => void } | null = null; let workspaceAChanges = 0; @@ -819,19 +756,19 @@ test("selected workspaces with the same public cwd keep Git state, mutations, an path: "tracked.txt", contents: Buffer.from("initial\n"), }); - await observedAClean; + await eventWithin(observedAClean, "workspace A readiness"); const observedBDirty = waitForObservedB(true); await workspaceRuntime.files("same-cwd-b").write({ path: "tracked.txt", contents: Buffer.from("runtime b startup barrier\n"), }); - await observedBDirty; + await eventWithin(observedBDirty, "workspace B dirty readiness"); const observedBClean = waitForObservedB(false); await workspaceRuntime.files("same-cwd-b").write({ path: "tracked.txt", contents: Buffer.from("initial\n"), }); - await observedBClean; + await eventWithin(observedBClean, "workspace B clean readiness"); workspaceAChanges = 0; workspaceBChanges = 0; workspaceBStates.length = 0; @@ -841,7 +778,7 @@ test("selected workspaces with the same public cwd keep Git state, mutations, an path: "tracked.txt", contents: Buffer.from("runtime a watched\n"), }); - await observedA; + await eventWithin(observedA, "workspace A change"); await new Promise((resolve) => setImmediate(resolve)); expect(workspaceAChanges).toBeGreaterThan(0); expect(workspaceBStates).toEqual([]); @@ -849,16 +786,31 @@ test("selected workspaces with the same public cwd keep Git state, mutations, an path: "tracked.txt", contents: Buffer.from("runtime b watched\n"), }); - await observedB; + await eventWithin(observedB, "workspace B change"); expect(workspaceBChanges).toBeGreaterThan(0); const restoredB = waitForObservedB(false); await workspaceRuntime.files("same-cwd-b").write({ path: "tracked.txt", contents: Buffer.from("initial\n"), }); - await restoredB; + await eventWithin(restoredB, "workspace B restoration"); await Promise.all([observationA.unsubscribe(), observationB.unsubscribe()]); +}, 30_000); +test("selected workspaces with the same public cwd keep mutations and caches isolated", async () => { + const { + publicCwd, + recreateA, + runtimeARepository, + runtimeBRepository, + workspaceA, + workspaceB, + workspaceRuntime, + } = await createSamePublicCwdFixture(); + await workspaceRuntime.files("same-cwd-a").write({ + path: "tracked.txt", + contents: Buffer.from("runtime a commit\n"), + }); await Promise.all([ workspaceA.commit({ message: "runtime a commit", addAll: true }), workspaceB.createBranch({ branch: "runtime-b-next", baseRef: "runtime-b" }), @@ -876,28 +828,131 @@ test("selected workspaces with the same public cwd keep Git state, mutations, an await workspaceRuntime.destroy("same-cwd-a"); git(runtimeARepository, "branch", "-m", "runtime-a-rebuilt"); - await workspaceRuntime.create({ - workspaceId: "same-cwd-a", - runtimeId: "fixture", - project: { - id: "same-cwd-a", - source: { kind: "host-directory", path: runtimeARepository }, - }, - placement: { kind: "existing" }, - }); + await recreateA(); const [rebuiltA, cachedB] = await Promise.all([ workspaceA.getSnapshot(), workspaceB.getSnapshot(), ]); expect(rebuiltA.git.currentBranch).toBe("runtime-a-rebuilt"); expect(cachedB.git.currentBranch).toBe("runtime-b-next"); +}, 30_000); - await service.dispose(); +async function createCommandRuntimeGitFixture() { + const root = await mkdtemp(path.join(tmpdir(), "paseo-runtime-git-")); + cleanupRoots.push(root); + const runtimeRepository = await createRepository(path.join(root, "runtime-repository")); + const hostDecoy = await createRepository(path.join(root, "host-decoy")); + const stateDirectory = path.join(root, "runtime-state"); + await mkdir(stateDirectory); + const workspaceId = "runtime-git-workspace"; + const runtimeIds = new Map(); + const workspaceRuntime = createWorkspaceRuntimeService({ + paseoHome: path.join(root, "paseo-home"), + resolveRuntimeId: async (id) => runtimeIds.get(id) ?? null, + persistRuntimeId: async (id, runtimeId) => { + runtimeIds.set(id, runtimeId); + }, + beginWorkspaceDeletion: async () => {}, + removeWorkspaceRecord: async (id) => { + runtimeIds.delete(id); + }, + externalRuntimes: { + fixture: { + type: "command", + command: [process.execPath, fixtureExecutable], + options: { stateDirectory, recordLaunchInWorkspace: false }, + }, + }, + }); + cleanupTasks.push(() => workspaceRuntime.close()); + const recreate = () => + workspaceRuntime.create({ + workspaceId, + runtimeId: "fixture", + project: { + id: "runtime-git-project", + source: { kind: "host-directory", path: runtimeRepository }, + }, + placement: { kind: "existing" }, + }); + await recreate(); + const service = new WorkspaceGitServiceImpl({ + logger: createLogger(), + paseoHome: path.join(root, "paseo-home"), + workspaceRuntime, + }); + cleanupTasks.push(() => service.dispose()); + const selectedGit = service.bindWorkspace({ workspaceId, cwd: hostDecoy }); + return { + root, + hostDecoy, + recreate, + runtimeRepository, + selectedGit, + workspaceId, + workspaceRuntime, + }; +} + +async function createSamePublicCwdFixture() { + const root = await mkdtemp(path.join(tmpdir(), "paseo-runtime-git-same-cwd-")); + cleanupRoots.push(root); + const runtimeARepository = await createRepository(path.join(root, "runtime-a")); + const runtimeBRepository = await createRepository(path.join(root, "runtime-b")); + git(runtimeBRepository, "branch", "-m", "runtime-b"); + const publicCwd = await createRepository(path.join(root, "shared-public-cwd")); + git(publicCwd, "branch", "-m", "host-decoy"); + await writeFile(path.join(publicCwd, "selected-proof.txt"), Buffer.alloc(32, 0)); + const stateDirectory = path.join(root, "runtime-state"); + await mkdir(stateDirectory); + const runtimeIds = new Map(); + const workspaceRuntime = createWorkspaceRuntimeService({ + paseoHome: path.join(root, "paseo-home"), + resolveRuntimeId: async (id) => runtimeIds.get(id) ?? null, + persistRuntimeId: async (id, runtimeId) => { + runtimeIds.set(id, runtimeId); + }, + beginWorkspaceDeletion: async () => {}, + removeWorkspaceRecord: async (id) => { + runtimeIds.delete(id); + }, + externalRuntimes: { + fixture: { + type: "command", + command: [process.execPath, fixtureExecutable], + options: { stateDirectory, recordLaunchInWorkspace: false }, + }, + }, + }); + cleanupTasks.push(() => workspaceRuntime.close()); + const createWorkspace = (workspaceId: string, source: string) => + workspaceRuntime.create({ + workspaceId, + runtimeId: "fixture", + project: { id: workspaceId, source: { kind: "host-directory", path: source } }, + placement: { kind: "existing" }, + }); await Promise.all([ - workspaceRuntime.destroy("same-cwd-a"), - workspaceRuntime.destroy("same-cwd-b"), + createWorkspace("same-cwd-a", runtimeARepository), + createWorkspace("same-cwd-b", runtimeBRepository), ]); -}, 40_000); + const service = new WorkspaceGitServiceImpl({ + logger: createLogger(), + paseoHome: path.join(root, "paseo-home"), + workspaceRuntime, + deps: { getWorkspaceGitSelfHealPhaseMs: () => 60_000 }, + }); + cleanupTasks.push(() => service.dispose()); + return { + publicCwd, + recreateA: () => createWorkspace("same-cwd-a", runtimeARepository), + runtimeARepository, + runtimeBRepository, + workspaceA: service.bindWorkspace({ workspaceId: "same-cwd-a", cwd: publicCwd }), + workspaceB: service.bindWorkspace({ workspaceId: "same-cwd-b", cwd: publicCwd }), + workspaceRuntime, + }; +} async function createRepository(directory: string): Promise { await mkdir(directory);