Files
paseo/packages/server/src/server/worktree-session.test.ts
T
Mohamed Boudra bc53cf0a04 Fix CI failures: server tests, app typecheck, workspace schema alignment
- Fix workspace test files to use file-backed registry schema (cwd/workspaceId
  instead of directory/numeric id)
- Remove DB-specific test setup (direct DB inserts) from snapshot-mutation test
- Fix worktree-session.ts: remove duplicate createAgentWorktree call, fix imports
- Fix daemon-client.test.ts and agent-manager.test.ts assertions
- Fix app typecheck: add `as any` casts for web-only backgroundImage CSS
- Fix session.workspace-git-watch.test.ts workspace shape
- Fix workspace-reconciliation-service.test.ts registry mocks
2026-04-14 05:36:58 +07:00

1010 lines
32 KiB
TypeScript

import { execSync } from "node:child_process";
import {
existsSync,
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, test, vi } from "vitest";
import type { SessionOutboundMessage } from "./messages.js";
import { ScriptRouteStore } from "./script-proxy.js";
import * as worktreeBootstrap from "./worktree-bootstrap.js";
import {
buildAgentSessionConfig,
runWorktreeSetupInBackground,
handleCreatePaseoWorktreeRequest,
handleWorkspaceSetupStatusRequest,
} from "./worktree-session.js";
import { createWorktree } from "../utils/worktree.js";
function createLogger() {
return {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as any;
}
function createTerminalManagerStub(options?: {
createTerminal?: (input: {
cwd: string;
name?: string;
env?: Record<string, string>;
}) => Promise<any>;
}) {
const terminals: Array<{
id: string;
cwd: string;
name: string | undefined;
env: Record<string, string> | undefined;
sent: string[];
}> = [];
return {
terminals,
manager: {
registerCwdEnv: vi.fn(),
createTerminal: vi.fn(
async (input: { cwd: string; name?: string; env?: Record<string, string> }) => {
if (options?.createTerminal) {
return options.createTerminal(input);
}
const sent: string[] = [];
const terminal = {
id: `terminal-${terminals.length + 1}`,
getState: () => ({
scrollback: [[{ char: "$" }]],
grid: [],
}),
subscribe: () => () => {},
onExit: () => () => {},
send: (message: { type: string; data: string }) => {
if (message.type === "input") {
sent.push(message.data);
}
},
};
terminals.push({
id: terminal.id,
cwd: input.cwd,
name: input.name,
env: input.env,
sent,
});
return terminal;
},
),
} as any,
};
}
function createGitRepo(options?: { paseoConfig?: Record<string, unknown> }) {
const tempDir = realpathSync(mkdtempSync(path.join(tmpdir(), "worktree-session-test-")));
const repoDir = path.join(tempDir, "repo");
execSync(`mkdir -p ${JSON.stringify(repoDir)}`);
execSync("git init -b main", { cwd: repoDir, stdio: "pipe" });
execSync("git config user.email 'test@test.com'", { cwd: repoDir, stdio: "pipe" });
execSync("git config user.name 'Test'", { cwd: repoDir, stdio: "pipe" });
writeFileSync(path.join(repoDir, "README.md"), "hello\n");
if (options?.paseoConfig) {
writeFileSync(path.join(repoDir, "paseo.json"), JSON.stringify(options.paseoConfig, null, 2));
}
execSync("git add .", { cwd: repoDir, stdio: "pipe" });
execSync("git -c commit.gpgsign=false commit -m 'initial'", { cwd: repoDir, stdio: "pipe" });
return { tempDir, repoDir };
}
function createGitHubPrRemoteRepo() {
const { tempDir, repoDir } = createGitRepo();
const featureBranch = "feature/review-pr";
execSync(`git checkout -b ${JSON.stringify(featureBranch)}`, { cwd: repoDir, stdio: "pipe" });
writeFileSync(path.join(repoDir, "README.md"), "review branch\n");
execSync("git add README.md", { cwd: repoDir, stdio: "pipe" });
execSync("git -c commit.gpgsign=false commit -m 'review branch'", {
cwd: repoDir,
stdio: "pipe",
});
const featureSha = execSync("git rev-parse HEAD", { cwd: repoDir, stdio: "pipe" })
.toString()
.trim();
execSync("git checkout main", { cwd: repoDir, stdio: "pipe" });
execSync(`git branch -D ${JSON.stringify(featureBranch)}`, { cwd: repoDir, stdio: "pipe" });
const remoteDir = path.join(tempDir, "remote.git");
execSync(`git clone --bare ${JSON.stringify(repoDir)} ${JSON.stringify(remoteDir)}`, {
stdio: "pipe",
});
execSync(
`git --git-dir=${JSON.stringify(remoteDir)} update-ref refs/pull/123/head ${featureSha}`,
{
stdio: "pipe",
},
);
execSync(`git remote add origin ${JSON.stringify(remoteDir)}`, { cwd: repoDir, stdio: "pipe" });
execSync("git fetch origin", { cwd: repoDir, stdio: "pipe" });
return { tempDir, repoDir };
}
describe("runWorktreeSetupInBackground", () => {
const cleanupPaths: string[] = [];
afterEach(() => {
for (const target of cleanupPaths.splice(0)) {
rmSync(target, { recursive: true, force: true });
}
});
test("emits running then completed snapshots for no-setup workspaces without auto-starting scripts", async () => {
const { tempDir, repoDir } = createGitRepo({
paseoConfig: {
scripts: {
web: {
command: "npm run dev",
},
},
},
});
cleanupPaths.push(tempDir);
const paseoHome = path.join(tempDir, ".paseo");
const createdWorktree = await createWorktree({
branchName: "feature-no-setup",
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "feature-no-setup",
runSetup: false,
paseoHome,
});
const worktreePath = createdWorktree.worktreePath;
const emitted: SessionOutboundMessage[] = [];
const snapshots = new Map<string, unknown>();
const logger = createLogger();
const terminalManager = createTerminalManagerStub();
const emitWorkspaceUpdateForCwd = vi.fn(async () => {});
const archiveWorkspaceRecord = vi.fn(async () => {});
await runWorktreeSetupInBackground(
{
paseoHome,
emitWorkspaceUpdateForCwd,
cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) =>
snapshots.set(workspaceId, snapshot),
emit: (message) => emitted.push(message),
sessionLogger: logger,
terminalManager: terminalManager.manager,
archiveWorkspaceRecord,
},
{
requestCwd: repoDir,
repoRoot: repoDir,
workspaceId: "42",
worktree: {
branchName: "feature-no-setup",
worktreePath,
},
shouldBootstrap: true,
slug: "feature-no-setup",
worktreePath,
},
);
const progressMessages = emitted.filter(
(message): message is Extract<SessionOutboundMessage, { type: "workspace_setup_progress" }> =>
message.type === "workspace_setup_progress",
);
expect(progressMessages).toHaveLength(2);
expect(progressMessages[0]?.payload).toMatchObject({
workspaceId: "42",
status: "running",
error: null,
detail: {
type: "worktree_setup",
worktreePath,
branchName: "feature-no-setup",
log: "",
commands: [],
},
});
expect(progressMessages[1]?.payload).toMatchObject({
workspaceId: "42",
status: "completed",
error: null,
detail: {
type: "worktree_setup",
worktreePath,
branchName: "feature-no-setup",
log: "",
commands: [],
},
});
expect(snapshots.get("42")).toMatchObject({
status: "completed",
error: null,
detail: {
type: "worktree_setup",
worktreePath,
branchName: "feature-no-setup",
log: "",
commands: [],
},
});
expect(terminalManager.terminals).toHaveLength(0);
expect(archiveWorkspaceRecord).not.toHaveBeenCalled();
expect(emitWorkspaceUpdateForCwd).toHaveBeenCalledWith(worktreePath);
});
test("archives the pending workspace and emits a failed snapshot when setup cannot start", async () => {
const { tempDir, repoDir } = createGitRepo();
cleanupPaths.push(tempDir);
writeFileSync(path.join(repoDir, "paseo.json"), "{ invalid json\n");
execSync("git add paseo.json", { cwd: repoDir, stdio: "pipe" });
execSync("git -c commit.gpgsign=false commit -m 'broken config'", {
cwd: repoDir,
stdio: "pipe",
});
const paseoHome = path.join(tempDir, ".paseo");
const createdWorktree = await createWorktree({
branchName: "broken-feature",
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "broken-feature",
runSetup: false,
paseoHome,
});
const worktreePath = createdWorktree.worktreePath;
const emitted: SessionOutboundMessage[] = [];
const snapshots = new Map<string, unknown>();
const logger = createLogger();
const emitWorkspaceUpdateForCwd = vi.fn(async () => {});
const archiveWorkspaceRecord = vi.fn(async () => {});
const workspaceId = 101;
await runWorktreeSetupInBackground(
{
paseoHome,
emitWorkspaceUpdateForCwd,
cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) =>
snapshots.set(workspaceId, snapshot),
emit: (message) => emitted.push(message),
sessionLogger: logger,
terminalManager: null,
archiveWorkspaceRecord,
},
{
requestCwd: repoDir,
repoRoot: repoDir,
workspaceId: String(workspaceId),
worktree: {
branchName: "broken-feature",
worktreePath,
},
shouldBootstrap: true,
slug: "broken-feature",
worktreePath,
},
);
const progressMessages = emitted.filter(
(message): message is Extract<SessionOutboundMessage, { type: "workspace_setup_progress" }> =>
message.type === "workspace_setup_progress",
);
expect(progressMessages).toHaveLength(2);
expect(progressMessages[0]?.payload.status).toBe("running");
expect(progressMessages[0]?.payload.error).toBeNull();
expect(progressMessages[1]?.payload.status).toBe("failed");
expect(progressMessages[1]?.payload.error).toContain("Failed to parse paseo.json");
expect(progressMessages[1]?.payload.detail.commands).toEqual([]);
expect(snapshots.get("101")).toMatchObject({
status: "failed",
error: expect.stringContaining("Failed to parse paseo.json"),
});
expect(archiveWorkspaceRecord).toHaveBeenCalledWith(String(workspaceId));
expect(emitWorkspaceUpdateForCwd).toHaveBeenCalledWith(worktreePath);
});
test("emits running setup snapshots before completed for real setup commands", async () => {
const { tempDir, repoDir } = createGitRepo({
paseoConfig: {
worktree: {
setup: ["sh -c \"printf 'phase-one\\\\n'; sleep 0.1; printf 'phase-two\\\\n'\""],
},
},
});
cleanupPaths.push(tempDir);
const paseoHome = path.join(tempDir, ".paseo");
const createdWorktree = await createWorktree({
branchName: "feature-running-setup",
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "feature-running-setup",
runSetup: false,
paseoHome,
});
const worktreePath = createdWorktree.worktreePath;
const emitted: SessionOutboundMessage[] = [];
const snapshots = new Map<string, unknown>();
const logger = createLogger();
const emitWorkspaceUpdateForCwd = vi.fn(async () => {});
const archiveWorkspaceRecord = vi.fn(async () => {});
await runWorktreeSetupInBackground(
{
paseoHome,
emitWorkspaceUpdateForCwd,
cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) =>
snapshots.set(workspaceId, snapshot),
emit: (message) => emitted.push(message),
sessionLogger: logger,
terminalManager: null,
archiveWorkspaceRecord,
},
{
requestCwd: repoDir,
repoRoot: repoDir,
workspaceId: "43",
worktree: {
branchName: "feature-running-setup",
worktreePath,
},
shouldBootstrap: true,
slug: "feature-running-setup",
worktreePath,
},
);
const progressMessages = emitted.filter(
(message): message is Extract<SessionOutboundMessage, { type: "workspace_setup_progress" }> =>
message.type === "workspace_setup_progress",
);
expect(progressMessages.length).toBeGreaterThan(1);
expect(progressMessages[0]?.payload).toMatchObject({
workspaceId: "43",
status: "running",
error: null,
detail: {
type: "worktree_setup",
worktreePath,
branchName: "feature-running-setup",
log: "",
commands: [],
},
});
expect(progressMessages.at(-1)?.payload.status).toBe("completed");
const runningMessages = progressMessages.filter(
(message) => message.payload.status === "running",
);
expect(runningMessages.length).toBeGreaterThan(0);
expect(
progressMessages.findIndex((message) => message.payload.status === "running"),
).toBeLessThan(progressMessages.findIndex((message) => message.payload.status === "completed"));
const setupOutputMessage = runningMessages.find((message) =>
message.payload.detail.commands[0]?.log.includes("phase-one"),
);
expect(setupOutputMessage?.payload.detail.log).toContain("phase-one");
expect(setupOutputMessage?.payload.detail.commands[0]).toMatchObject({
index: 1,
command: "sh -c \"printf 'phase-one\\\\n'; sleep 0.1; printf 'phase-two\\\\n'\"",
log: expect.stringContaining("phase-one"),
status: "running",
});
expect(progressMessages.at(-1)?.payload).toMatchObject({
workspaceId: "43",
status: "completed",
error: null,
detail: {
type: "worktree_setup",
worktreePath,
branchName: "feature-running-setup",
},
});
expect(progressMessages.at(-1)?.payload.detail.log).toContain("phase-two");
expect(progressMessages.at(-1)?.payload.detail.commands[0]).toMatchObject({
index: 1,
command: "sh -c \"printf 'phase-one\\\\n'; sleep 0.1; printf 'phase-two\\\\n'\"",
log: expect.stringContaining("phase-two"),
status: "completed",
exitCode: 0,
});
expect(snapshots.get("43")).toMatchObject({
status: "completed",
error: null,
});
});
test("emits completed when reusing an existing worktree without bootstrapping or auto-starting scripts", async () => {
const { tempDir, repoDir } = createGitRepo({
paseoConfig: {
worktree: {
setup: ["printf 'ran' > setup-ran.txt"],
},
scripts: {
web: {
command: "npm run dev",
},
},
},
});
cleanupPaths.push(tempDir);
const paseoHome = path.join(tempDir, ".paseo");
const existingWorktree = await createWorktree({
branchName: "reused-worktree",
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "reused-worktree",
runSetup: false,
paseoHome,
});
const emitted: SessionOutboundMessage[] = [];
const snapshots = new Map<string, unknown>();
const logger = createLogger();
const terminalManager = createTerminalManagerStub();
const emitWorkspaceUpdateForCwd = vi.fn(async () => {});
const archiveWorkspaceRecord = vi.fn(async () => {});
await runWorktreeSetupInBackground(
{
paseoHome,
emitWorkspaceUpdateForCwd,
cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) =>
snapshots.set(workspaceId, snapshot),
emit: (message) => emitted.push(message),
sessionLogger: logger,
terminalManager: terminalManager.manager,
archiveWorkspaceRecord,
},
{
requestCwd: repoDir,
repoRoot: repoDir,
workspaceId: "44",
worktree: {
branchName: "reused-worktree",
worktreePath: existingWorktree.worktreePath,
},
shouldBootstrap: false,
slug: "reused-worktree",
worktreePath: existingWorktree.worktreePath,
},
);
const progressMessages = emitted.filter(
(message): message is Extract<SessionOutboundMessage, { type: "workspace_setup_progress" }> =>
message.type === "workspace_setup_progress",
);
expect(progressMessages).toHaveLength(2);
expect(progressMessages[0]?.payload).toMatchObject({
workspaceId: "44",
status: "running",
error: null,
});
expect(progressMessages[1]?.payload).toMatchObject({
workspaceId: "44",
status: "completed",
error: null,
detail: {
type: "worktree_setup",
worktreePath: existingWorktree.worktreePath,
branchName: "reused-worktree",
log: "",
commands: [],
},
});
expect(terminalManager.terminals).toHaveLength(0);
expect(readFileSync(path.join(existingWorktree.worktreePath, "README.md"), "utf8")).toContain(
"hello",
);
expect(() =>
readFileSync(path.join(existingWorktree.worktreePath, "setup-ran.txt"), "utf8"),
).toThrow();
expect(snapshots.get("44")).toMatchObject({
status: "completed",
error: null,
});
expect(archiveWorkspaceRecord).not.toHaveBeenCalled();
expect(emitWorkspaceUpdateForCwd).toHaveBeenCalledWith(existingWorktree.worktreePath);
});
test("keeps setup completed without attempting script launch afterward", async () => {
const { tempDir, repoDir } = createGitRepo({
paseoConfig: {
scripts: {
web: {
command: "npm run dev",
},
},
},
});
cleanupPaths.push(tempDir);
const paseoHome = path.join(tempDir, ".paseo");
const createdWorktree = await createWorktree({
branchName: "feature-service-failure",
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "feature-service-failure",
runSetup: false,
paseoHome,
});
const worktreePath = createdWorktree.worktreePath;
const emitted: SessionOutboundMessage[] = [];
const snapshots = new Map<string, unknown>();
const logger = createLogger();
const terminalManager = createTerminalManagerStub({
createTerminal: async () => {
throw new Error("terminal spawn failed");
},
});
const emitWorkspaceUpdateForCwd = vi.fn(async () => {});
const archiveWorkspaceRecord = vi.fn(async () => {});
await runWorktreeSetupInBackground(
{
paseoHome,
emitWorkspaceUpdateForCwd,
cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) =>
snapshots.set(workspaceId, snapshot),
emit: (message) => emitted.push(message),
sessionLogger: logger,
terminalManager: terminalManager.manager,
archiveWorkspaceRecord,
},
{
requestCwd: repoDir,
repoRoot: repoDir,
workspaceId: "45",
worktree: {
branchName: "feature-service-failure",
worktreePath,
},
shouldBootstrap: true,
slug: "feature-service-failure",
worktreePath,
},
);
const progressMessages = emitted.filter(
(message): message is Extract<SessionOutboundMessage, { type: "workspace_setup_progress" }> =>
message.type === "workspace_setup_progress",
);
expect(progressMessages).toHaveLength(2);
expect(progressMessages[0]?.payload.status).toBe("running");
expect(progressMessages[0]?.payload.error).toBeNull();
expect(progressMessages[1]?.payload.status).toBe("completed");
expect(progressMessages[1]?.payload.error).toBeNull();
expect(
emitted.some(
(message) =>
message.type === "workspace_setup_progress" && message.payload.status === "failed",
),
).toBe(false);
expect(logger.error).not.toHaveBeenCalledWith(
expect.anything(),
"Failed to spawn worktree scripts after workspace setup completed",
);
expect(terminalManager.terminals).toHaveLength(0);
expect(snapshots.get("45")).toMatchObject({
status: "completed",
error: null,
});
expect(archiveWorkspaceRecord).not.toHaveBeenCalled();
expect(emitWorkspaceUpdateForCwd).toHaveBeenCalledWith(worktreePath);
});
test("does not auto-start scripts in socket mode", async () => {
const { tempDir, repoDir } = createGitRepo({
paseoConfig: {
scripts: {
web: {
command: "npm run dev",
},
},
},
});
cleanupPaths.push(tempDir);
const paseoHome = path.join(tempDir, ".paseo");
const createdWorktree = await createWorktree({
branchName: "feature-socket-mode",
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "feature-socket-mode",
runSetup: false,
paseoHome,
});
const worktreePath = createdWorktree.worktreePath;
const emitted: SessionOutboundMessage[] = [];
const snapshots = new Map<string, unknown>();
const logger = createLogger();
const terminalManager = createTerminalManagerStub();
const emitWorkspaceUpdateForCwd = vi.fn(async () => {});
const archiveWorkspaceRecord = vi.fn(async () => {});
await runWorktreeSetupInBackground(
{
paseoHome,
emitWorkspaceUpdateForCwd,
cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) =>
snapshots.set(workspaceId, snapshot),
emit: (message) => emitted.push(message),
sessionLogger: logger,
terminalManager: terminalManager.manager,
archiveWorkspaceRecord,
},
{
requestCwd: repoDir,
repoRoot: repoDir,
workspaceId: "46",
worktree: {
branchName: "feature-socket-mode",
worktreePath,
},
shouldBootstrap: true,
slug: "feature-socket-mode",
worktreePath,
},
);
expect(terminalManager.terminals).toHaveLength(0);
expect(snapshots.get("46")).toMatchObject({
status: "completed",
error: null,
});
expect(archiveWorkspaceRecord).not.toHaveBeenCalled();
expect(emitWorkspaceUpdateForCwd).toHaveBeenCalledWith(worktreePath);
});
test("returns the cached workspace setup snapshot for status requests", async () => {
const emitted: SessionOutboundMessage[] = [];
const snapshots = new Map([
[
"/repo/.paseo/worktrees/feature-a",
{
status: "completed",
detail: {
type: "worktree_setup",
worktreePath: "/repo/.paseo/worktrees/feature-a",
branchName: "feature-a",
log: "done",
commands: [],
},
error: null,
},
],
]);
await handleWorkspaceSetupStatusRequest(
{
emit: (message) => emitted.push(message),
workspaceSetupSnapshots: snapshots,
workspaceRegistry: { list: async () => [] } as any,
},
{
type: "workspace_setup_status_request",
workspaceId: "/repo/.paseo/worktrees/feature-a",
requestId: "req-status",
},
);
expect(emitted).toContainEqual({
type: "workspace_setup_status_response",
payload: {
requestId: "req-status",
workspaceId: "/repo/.paseo/worktrees/feature-a",
snapshot: {
status: "completed",
detail: {
type: "worktree_setup",
worktreePath: "/repo/.paseo/worktrees/feature-a",
branchName: "feature-a",
log: "done",
commands: [],
},
error: null,
},
},
});
});
test("returns null when no cached workspace setup snapshot exists", async () => {
const emitted: SessionOutboundMessage[] = [];
await handleWorkspaceSetupStatusRequest(
{
emit: (message) => emitted.push(message),
workspaceSetupSnapshots: new Map(),
workspaceRegistry: { list: async () => [] } as any,
},
{
type: "workspace_setup_status_request",
workspaceId: "/repo/.paseo/worktrees/missing",
requestId: "req-missing",
},
);
expect(emitted).toContainEqual({
type: "workspace_setup_status_response",
payload: {
requestId: "req-missing",
workspaceId: "/repo/.paseo/worktrees/missing",
snapshot: null,
},
});
});
});
describe("handleCreatePaseoWorktreeRequest", () => {
const cleanupPaths: string[] = [];
afterEach(() => {
for (const target of cleanupPaths.splice(0)) {
rmSync(target, { recursive: true, force: true });
}
});
test("checks out the GitHub PR branch when a github_pr attachment is present", async () => {
const { tempDir, repoDir } = createGitHubPrRemoteRepo();
cleanupPaths.push(tempDir);
const emitted: SessionOutboundMessage[] = [];
const logger = createLogger();
const paseoHome = path.join(tempDir, ".paseo");
await handleCreatePaseoWorktreeRequest(
{
paseoHome,
describeWorkspaceRecord: async (workspace) =>
({
id: String(workspace.id),
projectId: String(workspace.projectId),
projectDisplayName: "repo",
projectRootPath: repoDir,
workspaceDirectory: workspace.directory,
workspaceKind: "worktree",
projectKind: "git",
name: workspace.displayName,
status: "done",
activityAt: null,
diffStat: null,
scripts: [],
}) as any,
emit: (message) => emitted.push(message),
registerPendingWorktreeWorkspace: async ({ worktreePath, branchName }) =>
({
id: 1,
projectId: 1,
directory: worktreePath,
displayName: branchName,
kind: "worktree",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
archivedAt: null,
}) as any,
sessionLogger: logger,
runWorktreeSetupInBackground: async () => {},
},
{
type: "create_paseo_worktree_request",
requestId: "req-pr-worktree",
cwd: repoDir,
worktreeSlug: "review-pr-123",
attachments: [
{
type: "github_pr",
mimeType: "application/github-pr",
number: 123,
title: "Review branch",
url: "https://github.com/getpaseo/paseo/pull/123",
baseRefName: "main",
headRefName: "feature/review-pr",
},
],
},
);
const response = emitted.find(
(
message,
): message is Extract<SessionOutboundMessage, { type: "create_paseo_worktree_response" }> =>
message.type === "create_paseo_worktree_response",
);
expect(response?.payload.error).toBeNull();
expect(response?.payload.workspace?.workspaceDirectory).toBeTruthy();
const worktreePath = response?.payload.workspace?.workspaceDirectory;
expect(worktreePath).toBeTruthy();
if (!worktreePath) {
return;
}
const branch = execSync("git branch --show-current", { cwd: worktreePath, stdio: "pipe" })
.toString()
.trim();
expect(branch).toBe("feature/review-pr");
const readme = readFileSync(path.join(worktreePath, "README.md"), "utf8");
expect(readme).toContain("review branch");
});
test("buildAgentSessionConfig checks out the GitHub PR branch for agent worktrees", async () => {
const { tempDir, repoDir } = createGitHubPrRemoteRepo();
cleanupPaths.push(tempDir);
const result = await buildAgentSessionConfig(
{
paseoHome: path.join(tempDir, ".paseo"),
sessionLogger: createLogger(),
checkoutExistingBranch: async () => {
throw new Error("should not checkout existing branch");
},
createBranchFromBase: async () => {
throw new Error("should not create a new branch from base");
},
},
{
provider: "codex",
cwd: repoDir,
},
{
createWorktree: true,
worktreeSlug: "agent-review-pr-123",
},
undefined,
[
{
type: "github_pr",
mimeType: "application/github-pr",
number: 123,
title: "Review branch",
url: "https://github.com/getpaseo/paseo/pull/123",
baseRefName: "main",
headRefName: "feature/review-pr",
},
],
);
expect(result.worktreeBootstrap?.worktree.branchName).toBe("feature/review-pr");
expect(result.worktreeBootstrap?.worktree.worktreePath).toContain("agent-review-pr-123");
const branch = execSync("git branch --show-current", {
cwd: result.sessionConfig.cwd,
stdio: "pipe",
})
.toString()
.trim();
expect(branch).toBe("feature/review-pr");
});
});
describe("handleCreatePaseoWorktreeRequest", () => {
test("invokes worktree creation once for a create request", async () => {
const { tempDir, repoDir } = createGitRepo();
const paseoHome = path.join(tempDir, ".paseo");
const emitted: SessionOutboundMessage[] = [];
const createAgentWorktreeSpy = vi.spyOn(worktreeBootstrap, "createAgentWorktree");
try {
await handleCreatePaseoWorktreeRequest(
{
paseoHome,
sessionLogger: createLogger(),
emit: (message) => emitted.push(message),
registerPendingWorktreeWorkspace: vi.fn(async (options) => ({
workspaceId: options.worktreePath,
projectId: options.repoRoot,
})),
describeWorkspaceRecord: vi.fn(async (workspace) => ({
id: workspace.workspaceId,
projectId: workspace.projectId,
projectDisplayName: path.basename(repoDir),
projectRootPath: repoDir,
projectKind: "git",
workspaceKind: "worktree",
name: path.basename(workspace.workspaceId),
status: "done",
activityAt: null,
})),
runWorktreeSetupInBackground: vi.fn(async () => {}),
},
{
type: "create_paseo_worktree_request",
cwd: repoDir,
worktreeSlug: "single-call",
requestId: "req-single-call",
},
);
expect(createAgentWorktreeSpy).toHaveBeenCalledTimes(1);
const response = emitted.find(
(
message,
): message is Extract<SessionOutboundMessage, { type: "create_paseo_worktree_response" }> =>
message.type === "create_paseo_worktree_response",
);
expect(response?.payload.error).toBeNull();
} finally {
createAgentWorktreeSpy.mockRestore();
rmSync(tempDir, { recursive: true, force: true });
}
});
test("creates the worktree before emitting the response", async () => {
const { tempDir, repoDir } = createGitRepo();
const paseoHome = path.join(tempDir, ".paseo");
const emitted: SessionOutboundMessage[] = [];
const backgroundWork = vi.fn(async () => {});
try {
await handleCreatePaseoWorktreeRequest(
{
paseoHome,
sessionLogger: createLogger(),
emit: (message) => emitted.push(message),
registerPendingWorktreeWorkspace: vi.fn(async (options) => {
expect(existsSync(options.worktreePath)).toBe(true);
return {
workspaceId: options.worktreePath,
projectId: options.repoRoot,
} as any;
}),
describeWorkspaceRecord: vi.fn(async (workspace) => ({
id: workspace.workspaceId,
projectId: workspace.projectId,
projectDisplayName: path.basename(repoDir),
projectRootPath: repoDir,
projectKind: "git",
workspaceKind: "worktree",
name: path.basename(workspace.workspaceId),
status: "done",
activityAt: null,
})),
runWorktreeSetupInBackground: backgroundWork,
},
{
type: "create_paseo_worktree_request",
cwd: repoDir,
worktreeSlug: "response-after-create",
requestId: "req-1",
},
);
const response = emitted.find(
(
message,
): message is Extract<SessionOutboundMessage, { type: "create_paseo_worktree_response" }> =>
message.type === "create_paseo_worktree_response",
);
expect(response?.payload.error).toBeNull();
expect(response?.payload.workspace?.id).toBeTruthy();
expect(existsSync(response!.payload.workspace!.id)).toBe(true);
expect(backgroundWork).toHaveBeenCalledWith(
expect.objectContaining({
requestCwd: repoDir,
repoRoot: repoDir,
worktree: {
branchName: "response-after-create",
worktreePath: response!.payload.workspace!.id,
},
shouldBootstrap: true,
}),
);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
});