mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-26 11:12:53 +00:00
Open configured terminals after worktree setup (#6999)
* fix(server): open worktree.terminals when a workspace is created on its own runWorktreeSetupInBackground only opened the paseo.json worktree.terminals when its caller passed runAutoTerminals, and only the fork-PR "allow setup" handler did. workspace.create, the request the New Workspace screen sends, ran worktree.setup and then skipped the terminals. Both callers want the terminals, so the flag goes away: terminals follow a successful setup on a newly created worktree. Reused worktrees stay excluded by shouldBootstrap, and the agent setup continuation never reaches this function, so nothing opens twice. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(server): run automatic terminal regressions in CI Include workspace creation in the existing integration command, and append to the terminal marker so repeated command execution fails the regression. * test(server): make workspace fixtures portable on Windows Run the setup fixture through a command accepted by PowerShell, await its readiness, and fix the fixture Git line-ending policy. Allow Windows directory locks to settle during cleanup. * test(server): expose setup failures and await fixture cleanup Check the public setup status before terminal startup so failures distinguish setup errors from terminal creation. Use asynchronous directory removal so Windows cleanup retries leave the event loop available. * test(server): start the Windows setup service asynchronously The Windows regression starts its service but setup stays running, so terminal bootstrap is never reached. Launch the fixture service explicitly with Start-Process and retain the readiness and setup-completion assertions. --------- Co-authored-by: David Yang <9925+davidyang@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
David Yang
parent
208e323035
commit
f56c013928
@@ -89,7 +89,7 @@
|
||||
"speech:transcribe:local": "tsx scripts/transcribe-local-wav.ts",
|
||||
"test": "npm run test:unit && npm run test:integration",
|
||||
"test:unit": "vitest run --fileParallelism --exclude \"**/*.e2e.test.ts\"",
|
||||
"test:integration": "vitest run --maxWorkers=1 src/server/git-filename-diffs.e2e.test.ts src/server/daemon-e2e/checkout-diff-content.e2e.test.ts src/server/daemon-e2e/filesystem.e2e.test.ts src/server/daemon-e2e/file-download.e2e.test.ts src/server/daemon-e2e/cli-logs-follow.e2e.test.ts src/server/creation/creation.e2e.test.ts src/server/daemon-e2e/terminal-workspace-sdk.e2e.test.ts src/server/daemon-e2e/models.e2e.test.ts src/server/daemon-e2e/live-preferences.e2e.test.ts src/server/daemon-e2e/oversized-message.e2e.test.ts src/server/agent/model-catalog.e2e.test.ts src/server/plugins/plugin-paseo-api.e2e.test.ts && vitest run --maxWorkers=1 src/server/daemon-e2e/git-operations.e2e.test.ts -t \"merges into\" && vitest run --maxWorkers=1 src/server/session.create-agent-worktree-autoarchive.e2e.test.ts -t \"creates a worktree and auto-archives both\" && vitest run --maxWorkers=1 src/server/agent/providers/opencode-bridge.local.e2e.test.ts -t \"versioned runtime\" && vitest run --maxWorkers=1 src/server/daemon-e2e/relay-transport.e2e.test.ts -t \"oversized attachments\"",
|
||||
"test:integration": "vitest run --maxWorkers=1 src/server/git-filename-diffs.e2e.test.ts src/server/daemon-e2e/checkout-diff-content.e2e.test.ts src/server/daemon-e2e/filesystem.e2e.test.ts src/server/daemon-e2e/file-download.e2e.test.ts src/server/daemon-e2e/cli-logs-follow.e2e.test.ts src/server/creation/creation.e2e.test.ts src/server/daemon-e2e/terminal-workspace-sdk.e2e.test.ts src/server/workspace-create-worktree-source.e2e.test.ts src/server/daemon-e2e/models.e2e.test.ts src/server/daemon-e2e/live-preferences.e2e.test.ts src/server/daemon-e2e/oversized-message.e2e.test.ts src/server/agent/model-catalog.e2e.test.ts src/server/plugins/plugin-paseo-api.e2e.test.ts && vitest run --maxWorkers=1 src/server/daemon-e2e/git-operations.e2e.test.ts -t \"merges into\" && vitest run --maxWorkers=1 src/server/session.create-agent-worktree-autoarchive.e2e.test.ts -t \"creates a worktree and auto-archives both\" && vitest run --maxWorkers=1 src/server/agent/providers/opencode-bridge.local.e2e.test.ts -t \"versioned runtime\" && vitest run --maxWorkers=1 src/server/daemon-e2e/relay-transport.e2e.test.ts -t \"oversized attachments\"",
|
||||
"test:hub-cli-contract": "vitest run --maxWorkers=1 src/server/hub/hub-cli-contract.test.ts",
|
||||
"test:integration:all": "npm run test:e2e",
|
||||
"test:integration:real": "vitest run real.e2e.test.ts",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { expect, onTestFinished, test } from "vitest";
|
||||
@@ -18,6 +19,7 @@ function createGitRepoWithBranch(): { repoDir: string; tempRoot: string } {
|
||||
const tempRoot = mkdtempSync(path.join(tmpdir(), "workspace-create-worktree-source-"));
|
||||
const repoDir = path.join(tempRoot, "repo");
|
||||
execFileSync("git", ["init", "-b", "main", repoDir], { stdio: "pipe" });
|
||||
execFileSync("git", ["config", "core.autocrlf", "false"], { cwd: repoDir, stdio: "pipe" });
|
||||
execFileSync("git", ["config", "user.email", "test@getpaseo.local"], {
|
||||
cwd: repoDir,
|
||||
stdio: "pipe",
|
||||
@@ -60,10 +62,134 @@ test("workspace.create worktree source forwards action=checkout + refName into t
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
await daemon.close();
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
await rm(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
}
|
||||
}, 180000);
|
||||
|
||||
test.each([false, true])(
|
||||
"workspace.create opens worktree.terminals (setup service: %s)",
|
||||
async (withSetupService) => {
|
||||
const daemon = await createTestPaseoDaemon();
|
||||
const { repoDir, tempRoot } = createGitRepoWithBranch();
|
||||
writeFileSync(
|
||||
path.join(repoDir, "paseo.json"),
|
||||
JSON.stringify({
|
||||
worktree: {
|
||||
...(withSetupService
|
||||
? {
|
||||
// Launch the Windows service asynchronously so it outlives
|
||||
// the setup command instead of holding setup open.
|
||||
setup:
|
||||
process.platform === "win32"
|
||||
? "Start-Process -FilePath node -ArgumentList 'setup.cjs','serve' -WorkingDirectory (Get-Location).Path -WindowStyle Hidden; while (!(Test-Path setup-pid.txt)) { Start-Sleep -Milliseconds 25 }"
|
||||
: "node setup.cjs",
|
||||
}
|
||||
: {}),
|
||||
terminals: [
|
||||
{ name: "preview", command: "echo paseo-auto-terminal >> terminal-marker.txt" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(repoDir, "setup.cjs"),
|
||||
`
|
||||
const fs = require("node:fs");
|
||||
if (process.argv[2] === "serve") {
|
||||
require("node:net").createServer().listen(Number(process.env.PASEO_WORKTREE_PORT), "127.0.0.1", () => {
|
||||
fs.writeFileSync("setup-pid.txt", String(process.pid));
|
||||
process.send?.("ready");
|
||||
});
|
||||
} else {
|
||||
const child = require("node:child_process").spawn(process.execPath, [__filename, "serve"], {
|
||||
detached: true,
|
||||
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
||||
});
|
||||
child.once("message", () => { child.disconnect(); child.unref(); });
|
||||
}
|
||||
`,
|
||||
);
|
||||
execFileSync("git", ["add", "paseo.json", "setup.cjs"], { cwd: repoDir, stdio: "pipe" });
|
||||
execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "add paseo.json"], {
|
||||
cwd: repoDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
const client = new DaemonClient({
|
||||
url: `ws://127.0.0.1:${daemon.port}/ws`,
|
||||
appVersion: "0.1.82",
|
||||
});
|
||||
|
||||
let workspaceCwd: string | undefined;
|
||||
try {
|
||||
await client.connect();
|
||||
|
||||
// This is the request the New Workspace screen sends: the workspace is
|
||||
// created on its own, with no agent in the same request.
|
||||
const result = await client.createWorkspace({
|
||||
source: {
|
||||
kind: "worktree",
|
||||
cwd: repoDir,
|
||||
action: "branch-off",
|
||||
branchName: "feature/auto-terminals",
|
||||
worktreeSlug: "feature-auto-terminals",
|
||||
baseBranch: "main",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.error).toBeNull();
|
||||
const workspace = result.workspace;
|
||||
if (!workspace) {
|
||||
throw new Error("Expected a created workspace");
|
||||
}
|
||||
|
||||
workspaceCwd = workspace.workspaceDirectory;
|
||||
if (withSetupService) {
|
||||
await expect
|
||||
.poll(() => existsSync(path.join(workspaceCwd!, "setup-pid.txt")), { timeout: 30000 })
|
||||
.toBe(true);
|
||||
}
|
||||
await expect
|
||||
.poll(async () => (await client.fetchWorkspaceSetupStatus(workspace.id)).snapshot, {
|
||||
timeout: 30000,
|
||||
})
|
||||
.toMatchObject({ status: "completed", error: null });
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(
|
||||
await client.listTerminals(workspace.workspaceDirectory, undefined, {
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
).terminals.map((terminal) => terminal.name),
|
||||
{ timeout: 30000 },
|
||||
)
|
||||
.toContain("preview");
|
||||
await expect
|
||||
.poll(
|
||||
() => {
|
||||
const marker = path.join(workspaceCwd!, "terminal-marker.txt");
|
||||
return existsSync(marker) ? readFileSync(marker, "utf8").trim() : null;
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
)
|
||||
.toBe("paseo-auto-terminal");
|
||||
expect(
|
||||
(await client.listTerminals(workspaceCwd, undefined, { workspaceId: workspace.id }))
|
||||
.terminals,
|
||||
).toHaveLength(1);
|
||||
} finally {
|
||||
const pidFile = workspaceCwd && path.join(workspaceCwd, "setup-pid.txt");
|
||||
if (pidFile && existsSync(pidFile)) {
|
||||
process.kill(Number(readFileSync(pidFile, "utf8")));
|
||||
}
|
||||
await client.close().catch(() => undefined);
|
||||
await daemon.close();
|
||||
await rm(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
}
|
||||
},
|
||||
180000,
|
||||
);
|
||||
|
||||
test("existing branch changes remain visible after archive and restore", async () => {
|
||||
const daemon = await createTestPaseoDaemon();
|
||||
const { repoDir, tempRoot } = createGitRepoWithBranch();
|
||||
@@ -74,7 +200,7 @@ test("existing branch changes remain visible after archive and restore", async (
|
||||
onTestFinished(async () => {
|
||||
await client.close().catch(() => undefined);
|
||||
await daemon.close();
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
await rm(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
});
|
||||
runGit(repoDir, "switch", "feature/existing-branch");
|
||||
commitFile(repoDir, "feature.txt", "feature change\n");
|
||||
@@ -134,7 +260,7 @@ test("workspace.create keeps a branch-off name separate from its worktree slug",
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
await daemon.close();
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
await rm(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
}
|
||||
}, 180000);
|
||||
|
||||
@@ -168,7 +294,7 @@ test("workspace.create accepts a Git-valid branch-off name outside Paseo slug sy
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
await daemon.close();
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
await rm(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
}
|
||||
}, 180000);
|
||||
|
||||
@@ -210,7 +336,7 @@ test("workspace.create always creates a new worktree when the slug is already oc
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
await daemon.close();
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
await rm(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
}
|
||||
}, 180000);
|
||||
|
||||
@@ -255,7 +381,7 @@ test("workspace.create suffixes past an occupied detached worktree", async () =>
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
await daemon.close();
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
await rm(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
}
|
||||
}, 180000);
|
||||
|
||||
@@ -297,7 +423,7 @@ test("workspace.create suffixes an occupied checkout branch", async () => {
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
await daemon.close();
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
await rm(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
}
|
||||
}, 180000);
|
||||
|
||||
@@ -340,7 +466,7 @@ async function createRestorableWorkspace(baseRef: string) {
|
||||
onTestFinished(async () => {
|
||||
await client.close().catch(() => undefined);
|
||||
await daemon.close();
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
await rm(tempRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
});
|
||||
createDivergentBases(repoDir);
|
||||
await client.connect();
|
||||
|
||||
@@ -593,13 +593,10 @@ export async function runWorktreeAutoTerminals(options: {
|
||||
workspaceId: string;
|
||||
worktree: WorktreeConfig;
|
||||
workspaceCwd: string;
|
||||
runtimeEnv: WorktreeRuntimeEnv;
|
||||
terminalManager: TerminalManager | null;
|
||||
logger?: Logger;
|
||||
}): Promise<void> {
|
||||
const runtimeEnv = await resolveWorktreeRuntimeEnv({
|
||||
worktreePath: options.worktree.worktreePath,
|
||||
branchName: options.worktree.branchName,
|
||||
});
|
||||
await runWorktreeTerminalBootstrap(
|
||||
{
|
||||
agentId: options.workspaceId,
|
||||
@@ -610,7 +607,7 @@ export async function runWorktreeAutoTerminals(options: {
|
||||
appendTimelineItem: async () => true,
|
||||
logger: options.logger,
|
||||
},
|
||||
runtimeEnv,
|
||||
options.runtimeEnv,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -817,7 +817,6 @@ export async function handleWorkspaceSetupRunRequest(
|
||||
slug: basename(worktree.worktreePath),
|
||||
worktreePath: worktree.worktreePath,
|
||||
workspaceCwd: workspace.cwd,
|
||||
runAutoTerminals: true,
|
||||
},
|
||||
signal,
|
||||
),
|
||||
@@ -857,7 +856,6 @@ export async function runWorktreeSetupInBackground(
|
||||
slug: string;
|
||||
worktreePath: string;
|
||||
workspaceCwd?: string;
|
||||
runAutoTerminals?: boolean;
|
||||
},
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
@@ -898,18 +896,18 @@ export async function runWorktreeSetupInBackground(
|
||||
} else {
|
||||
const workspaceCwd = options.workspaceCwd ?? worktree.worktreePath;
|
||||
const setupCommands = getWorktreeSetupCommands(workspaceCwd);
|
||||
const runtimeEnv = await resolveWorktreeRuntimeEnv({
|
||||
worktreePath: worktree.worktreePath,
|
||||
branchName: worktree.branchName,
|
||||
repoRootPath: options.repoRoot,
|
||||
});
|
||||
dependencies.terminalManager?.registerCwdEnv({
|
||||
cwd: workspaceCwd,
|
||||
env: runtimeEnv,
|
||||
});
|
||||
if (setupCommands.length === 0) {
|
||||
emitSetupProgress("completed", null);
|
||||
} else {
|
||||
const runtimeEnv = await resolveWorktreeRuntimeEnv({
|
||||
worktreePath: worktree.worktreePath,
|
||||
branchName: worktree.branchName,
|
||||
repoRootPath: options.repoRoot,
|
||||
});
|
||||
dependencies.terminalManager?.registerCwdEnv({
|
||||
cwd: workspaceCwd,
|
||||
env: runtimeEnv,
|
||||
});
|
||||
setupResults = await runWorktreeSetupCommands({
|
||||
worktreePath: workspaceCwd,
|
||||
branchName: worktree.branchName,
|
||||
@@ -924,15 +922,14 @@ export async function runWorktreeSetupInBackground(
|
||||
});
|
||||
emitSetupProgress("completed", null);
|
||||
}
|
||||
if (options.runAutoTerminals) {
|
||||
await runWorktreeAutoTerminals({
|
||||
workspaceId,
|
||||
worktree,
|
||||
workspaceCwd,
|
||||
terminalManager: dependencies.terminalManager,
|
||||
logger: dependencies.sessionLogger,
|
||||
});
|
||||
}
|
||||
await runWorktreeAutoTerminals({
|
||||
workspaceId,
|
||||
worktree,
|
||||
workspaceCwd,
|
||||
runtimeEnv,
|
||||
terminalManager: dependencies.terminalManager,
|
||||
logger: dependencies.sessionLogger,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof WorktreeSetupError) {
|
||||
|
||||
Reference in New Issue
Block a user