diff --git a/packages/server/src/server/daemon-e2e/project-becomes-git.e2e.test.ts b/packages/server/src/server/daemon-e2e/project-becomes-git.e2e.test.ts index e7843f78ae..24cbfcedb9 100644 --- a/packages/server/src/server/daemon-e2e/project-becomes-git.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/project-becomes-git.e2e.test.ts @@ -1,6 +1,6 @@ import { execFile as execFileCallback } from "node:child_process"; import { mkdtempSync, realpathSync } from "node:fs"; -import { readFile, rm } from "node:fs/promises"; +import { readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; @@ -18,6 +18,7 @@ const cleanupListeners = new Set<() => void>(); const execFile = promisify(execFileCallback); type ProjectUpdatePayload = Extract["payload"]; +type WorkspaceUpdatePayload = Extract["payload"]; function waitForProjectUpdate( client: DaemonClient, @@ -34,6 +35,21 @@ function waitForProjectUpdate( }); } +function waitForWorkspaceUpdate( + client: DaemonClient, + predicate: (payload: WorkspaceUpdatePayload) => boolean, +): Promise { + return new Promise((resolve) => { + const unsubscribe = client.on("workspace_update", (message) => { + if (!predicate(message.payload)) return; + cleanupListeners.delete(unsubscribe); + unsubscribe(); + resolve(message.payload); + }); + cleanupListeners.add(unsubscribe); + }); +} + afterEach(async () => { for (const unsubscribe of cleanupListeners) unsubscribe(); cleanupListeners.clear(); @@ -121,3 +137,82 @@ test("an empty project becomes Git without changing its identity or creating a w archivedAt: null, }); }, 30_000); + +test("an opened directory workspace becomes Git without restarting the daemon", async () => { + const projectRoot = realpathSync( + mkdtempSync(path.join(os.tmpdir(), "paseo-workspace-becomes-git-")), + ); + const paseoHomeRoot = realpathSync( + mkdtempSync(path.join(os.tmpdir(), "paseo-workspace-becomes-git-home-")), + ); + cleanupPaths.add(projectRoot); + cleanupPaths.add(paseoHomeRoot); + + const daemon = await createTestPaseoDaemon({ paseoHomeRoot, cleanup: false }); + cleanupDaemons.add(daemon); + const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); + cleanupClients.add(client); + await client.connect(); + + const opened = await client.openProject(projectRoot); + expect(opened.error).toBeNull(); + expect(opened.workspace).toMatchObject({ + workspaceDirectory: projectRoot, + projectRootPath: projectRoot, + projectKind: "non_git", + workspaceKind: "directory", + }); + const workspace = opened.workspace!; + + await client.fetchWorkspaces({ + subscribe: { subscriptionId: "workspace-becomes-git" }, + filter: { projectId: workspace.projectId }, + }); + + const gitWorkspaceUpdate = waitForWorkspaceUpdate( + client, + (payload) => + payload.kind === "upsert" && + payload.workspace.id === workspace.id && + payload.workspace.projectId === workspace.projectId && + payload.workspace.projectKind === "git" && + payload.workspace.workspaceKind === "local_checkout" && + Boolean(payload.workspace.gitRuntime), + ); + await execFile("git", ["init", "-b", "main"], { cwd: projectRoot }); + await execFile("git", ["config", "user.email", "test@getpaseo.dev"], { cwd: projectRoot }); + await execFile("git", ["config", "user.name", "Paseo Test"], { cwd: projectRoot }); + await writeFile(path.join(projectRoot, "README.md"), "# Test\n", "utf8"); + await execFile("git", ["add", "README.md"], { cwd: projectRoot }); + await execFile("git", ["-c", "commit.gpgSign=false", "commit", "-m", "initial"], { + cwd: projectRoot, + }); + const update = await withTimeout({ + promise: gitWorkspaceUpdate, + timeoutMs: 10_000, + label: "workspace_update after git init", + }); + + expect(update).toMatchObject({ + kind: "upsert", + workspace: { + id: workspace.id, + projectId: workspace.projectId, + projectRootPath: projectRoot, + workspaceDirectory: projectRoot, + projectKind: "git", + workspaceKind: "local_checkout", + gitRuntime: expect.any(Object), + }, + }); + + const afterGitInit = await client.fetchWorkspaces({ filter: { projectId: workspace.projectId } }); + expect(afterGitInit.entries).toContainEqual( + expect.objectContaining({ + id: workspace.id, + projectKind: "git", + workspaceKind: "local_checkout", + gitRuntime: expect.objectContaining({ currentBranch: "main" }), + }), + ); +}, 30_000); diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 2192ad69b9..1c96dd5902 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -1167,6 +1167,13 @@ export class Session { const workspace = await this.workspaceRegistry.get(workspaceId); if (workspace && !workspace.archivedAt) { await this.workspaceGitObserver.syncObserverForWorkspace(workspace); + if (workspace.kind !== "directory") { + await this.workspaceGitService.getSnapshot(workspace.cwd, { + force: true, + includeForge: true, + reason: "workspace_reconciliation", + }); + } } }), );