Keep setup execution and output owned by the workspace (#7473)

* perf(workspaces): stop duplicating setup output in progress snapshots

Keep the required combined log field empty for setup clients, which display per-command output. Preserve concatenation only for legacy agent bootstrap timeline cards whose released renderers still read it directly.

* refactor(workspaces): own setup exclusively at the workspace boundary

Preserve keyed modern agent worktree creation while retiring legacy placement that selected an agent-owned setup lifecycle. Keep output in per-command workspace snapshots and retain empty strings required by existing wire schemas.

* fix(server): avoid duplicate cwd in worktree provisioning request

* test(e2e): keep developer toasts out of concurrent app assertions

Concurrent layout measurements could invoke the same Playwright locator handler before its first dismissal finished, leaving checks waiting for a removed button. Hide only the Expo toast host through test styling while retaining console error capture.
This commit is contained in:
Mohamed Boudra
2026-10-21 14:16:24 +07:00
committed by GitHub
parent a891155b82
commit fbd90cb88d
52 changed files with 869 additions and 3459 deletions
@@ -67,7 +67,7 @@ interface LegacyCreateWorktreeTestOptions {
cwd: string;
baseBranch: string;
worktreeSlug: string;
runSetup?: boolean;
paseoHome?: string;
}
@@ -86,7 +86,7 @@ function createLegacyWorktreeForTest(
baseBranch: options.baseBranch,
branchName: options.branchName,
},
runSetup: options.runSetup ?? true,
paseoHome: options.paseoHome,
});
}
@@ -144,7 +144,6 @@ describe("worktree shell selection", () => {
await runWorktreeSetupCommands({
worktreePath,
branchName: "main",
cleanupOnFailure: false,
runtimeEnv: {
PASEO_SOURCE_CHECKOUT_PATH: worktreePath,
PASEO_ROOT_PATH: worktreePath,
@@ -58,7 +58,7 @@ interface LegacyCreateWorktreeTestOptions {
cwd: string;
baseBranch: string;
worktreeSlug: string;
runSetup?: boolean;
paseoHome?: string;
worktreesRoot?: string;
}
@@ -78,7 +78,7 @@ function createLegacyWorktreeForTest(
baseBranch: options.baseBranch,
branchName: options.branchName,
},
runSetup: options.runSetup ?? true,
paseoHome: options.paseoHome,
worktreesRoot: options.worktreesRoot,
});
@@ -239,7 +239,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
worktreeSlug: "my-feature",
source: { kind: "branch-off", baseBranch: "main", branchName: "feature/x" },
runSetup: true,
paseoHome,
});
@@ -282,7 +282,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
worktreeSlug: "existing-feature",
source: { kind: "checkout-branch", branchName: "feature/existing" },
runSetup: false,
paseoHome,
});
const status = await getCheckoutStatus(result.worktreePath, { paseoHome });
@@ -304,7 +304,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
worktreeSlug: "dev-worktree",
source: { kind: "checkout-branch", branchName: "dev" },
runSetup: true,
paseoHome,
});
@@ -346,7 +346,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
worktreeSlug: "stale-default",
source: { kind: "checkout-branch", branchName: "feature/stale-default" },
runSetup: false,
paseoHome,
});
const diff = await getCheckoutDiff(created.worktreePath, { mode: "base" }, { paseoHome });
@@ -365,7 +365,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
worktreeSlug: "feature-x-worktree",
source: { kind: "checkout-branch", branchName: "feature-x" },
runSetup: true,
paseoHome,
});
@@ -382,7 +382,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
worktreeSlug: "release-worktree",
source: { kind: "checkout-branch", branchName: "release/1.1.15" },
runSetup: true,
paseoHome,
});
@@ -400,7 +400,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
worktreeSlug: "dev-worktree",
source: { kind: "checkout-branch", branchName: "main" },
runSetup: true,
paseoHome,
});
@@ -412,7 +412,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
});
});
it("fetches a GitHub PR branch, checks it out, writes metadata, and runs setup", async () => {
it("fetches a GitHub PR branch and writes metadata without executing setup", async () => {
const remoteDir = join(tempDir, "remote.git");
const remoteCloneDir = join(tempDir, "remote-clone");
execFileSync("git", ["clone", "--bare", repoDir, remoteDir]);
@@ -446,12 +446,12 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
headRef: "user/feature",
baseRefName: "main",
},
runSetup: true,
paseoHome,
});
expect(readFileSync(join(result.worktreePath, "file.txt"), "utf8")).toBe("from-pr\n");
expect(readFileSync(join(result.worktreePath, "setup.log"), "utf8")).toBe("setup ran\n");
expect(existsSync(join(result.worktreePath, "setup.log"))).toBe(false);
const currentBranch = execFileSync("git", ["branch", "--show-current"], {
cwd: result.worktreePath,
})
@@ -494,7 +494,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
headRef: "Feature.X",
baseRefName: "main",
},
runSetup: true,
paseoHome,
});
@@ -540,7 +540,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
baseBranch: "refs/heads/main",
worktreeSlug: "prefer-local-feature",
runSetup: false,
paseoHome,
});
const originResult = await createLegacyWorktreeForTest({
@@ -548,7 +548,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
baseBranch: "refs/remotes/origin/main",
worktreeSlug: "prefer-origin-feature",
runSetup: false,
paseoHome,
});
@@ -598,7 +598,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
baseBranch: "refs/remotes/upstream/main",
worktreeSlug: "fork-base-feature",
runSetup: false,
paseoHome,
});
@@ -689,7 +689,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
baseBranch: "refs/remotes/upstream/release+hotfix",
worktreeSlug: "release-hotfix-feature",
runSetup: false,
paseoHome,
});
@@ -717,7 +717,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "prefer-local-fallback-feature",
runSetup: false,
paseoHome,
});
@@ -731,7 +731,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
baseBranch: "does-not-exist",
worktreeSlug: "missing-base-feature",
runSetup: false,
paseoHome,
}),
).rejects.toThrow("Base branch not found: does-not-exist");
@@ -755,7 +755,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
worktreeSlug: "invalid-existing-branch",
source: { kind: "checkout-branch", branchName: "bad..name" },
runSetup: true,
paseoHome,
});
} catch (error) {
@@ -773,7 +773,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
worktreeSlug: "invalid-option-like-branch",
source: { kind: "checkout-branch", branchName: "-bad" },
runSetup: true,
paseoHome,
});
} catch (error) {
@@ -853,6 +853,10 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
expect(existsSync(result.worktreePath)).toBe(true);
await runWorktreeSetupCommands({
worktreePath: result.worktreePath,
branchName: result.branchName,
});
// Verify setup ran and env vars were available
const setupLog = readFileSync(join(result.worktreePath, "setup.log"), "utf8");
expect(setupLog).toContain(`source=${repoDir}`);
@@ -886,6 +890,10 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
paseoHome,
});
await runWorktreeSetupCommands({
worktreePath: result.worktreePath,
branchName: result.branchName,
});
expect(getWorktreeSetupCommands(result.worktreePath)).toEqual([
'greeting="hello from string setup"\necho "$greeting" > setup.log',
]);
@@ -926,7 +934,6 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
await runWorktreeSetupCommands({
worktreePath: repoDir,
branchName: "main",
cleanupOnFailure: false,
runtimeEnv: {
PASEO_SOURCE_CHECKOUT_PATH: repoDir,
PASEO_ROOT_PATH: repoDir,
@@ -1002,7 +1009,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
]);
});
it("does not run setup commands when runSetup=false", async () => {
it("only provisions the worktree without executing setup", async () => {
const paseoConfig = {
worktree: {
setup: ['echo "setup ran" > setup.log'],
@@ -1019,7 +1026,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "no-setup-test",
runSetup: false,
paseoHome,
});
@@ -1043,7 +1050,6 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
const results = await runWorktreeSetupCommands({
worktreePath: repoDir,
branchName: "main",
cleanupOnFailure: false,
onEvent: (event) => {
progressEvents.push(event);
},
@@ -1061,7 +1067,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "runtime-env-port-reuse",
runSetup: false,
paseoHome,
});
@@ -1083,7 +1089,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "runtime-env-port-conflict",
runSetup: false,
paseoHome,
});
@@ -1117,35 +1123,6 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
});
});
it("cleans up worktree if setup command fails", async () => {
// Create paseo.json with failing setup command
const paseoConfig = {
worktree: {
setup: ["exit 1"],
},
};
writeFileSync(join(repoDir, "paseo.json"), JSON.stringify(paseoConfig));
execFileSync("git", ["add", "paseo.json"], { cwd: repoDir });
execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "add paseo.json"], {
cwd: repoDir,
});
const expectedWorktreePath = join(paseoHome, "worktrees", "test-repo", "fail-test");
await expect(
createLegacyWorktreeForTest({
branchName: "main",
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "fail-test",
paseoHome,
}),
).rejects.toThrow("Worktree setup command failed");
// Verify worktree was cleaned up
expect(existsSync(expectedWorktreePath)).toBe(false);
});
it("reads worktree terminal specs from paseo.json with optional name", async () => {
const paseoConfig = {
worktree: {
@@ -1278,7 +1255,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
worktreeSlug: "seed-uncommitted",
source: { kind: "branch-off", baseBranch: "main", branchName: "feature/seed" },
runSetup: false,
paseoHome,
});
@@ -1324,7 +1301,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
baseBranch: "origin/main",
branchName: "feature/upstream-config",
},
runSetup: false,
paseoHome,
});
@@ -1364,7 +1341,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
baseBranch: "symlink-config",
branchName: "feature/symlink-config",
},
runSetup: false,
paseoHome,
});
@@ -1385,7 +1362,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: repoDir,
worktreeSlug: "no-config",
source: { kind: "branch-off", baseBranch: "main", branchName: "feature/no-config" },
runSetup: false,
paseoHome,
});
+3 -3
View File
@@ -31,7 +31,7 @@ interface LegacyCreateWorktreeTestOptions {
cwd: string;
baseBranch: string;
worktreeSlug: string;
runSetup?: boolean;
paseoHome?: string;
}
@@ -50,7 +50,7 @@ function createLegacyWorktreeForTest(
baseBranch: options.baseBranch,
branchName: options.branchName,
},
runSetup: options.runSetup ?? true,
paseoHome: options.paseoHome,
});
}
@@ -132,7 +132,7 @@ describe("paseo worktree manager", () => {
cwd: repoDir,
paseoHome,
worktreeSlug: "fork-review",
runSetup: false,
source: {
kind: "checkout-change-request",
forge: "gitea",
+1 -29
View File
@@ -1,13 +1,6 @@
import { execFile } from "child_process";
import { promisify } from "util";
import {
constants as fsConstants,
existsSync,
mkdirSync,
realpathSync,
rmSync,
statSync,
} from "fs";
import { constants as fsConstants, existsSync, mkdirSync, realpathSync, statSync } from "fs";
import { copyFile, rm, stat } from "fs/promises";
import { join, basename, dirname, isAbsolute, resolve, sep } from "path";
import net from "node:net";
@@ -217,7 +210,6 @@ export interface CreateWorktreeOptions {
cwd: string;
worktreeSlug: string;
source: WorktreeSource;
runSetup: boolean;
paseoHome?: string;
worktreesRoot?: string;
}
@@ -643,7 +635,6 @@ async function inferRepoRootPathFromWorktreePath(worktreePath: string): Promise<
export async function runWorktreeSetupCommands(options: {
worktreePath: string;
branchName: string;
cleanupOnFailure: boolean;
repoRootPath?: string;
runtimeEnv?: WorktreeRuntimeEnv;
signal?: AbortSignal;
@@ -683,16 +674,6 @@ export async function runWorktreeSetupCommands(options: {
results.push(result);
if (result.exitCode !== 0) {
if (options.cleanupOnFailure) {
try {
await runGitCommand(["worktree", "remove", options.worktreePath, "--force"], {
cwd: options.worktreePath,
timeout: 120_000,
});
} catch {
rmSync(options.worktreePath, { recursive: true, force: true });
}
}
throw new WorktreeSetupError(
`Worktree setup command failed: ${cmd}\n${result.stderr}`.trim(),
results,
@@ -1221,7 +1202,6 @@ export const createWorktree = async ({
cwd,
source,
worktreeSlug,
runSetup,
paseoHome,
worktreesRoot,
}: CreateWorktreeOptions): Promise<CreatedWorktree> => {
@@ -1269,14 +1249,6 @@ export const createWorktree = async ({
await seedPaseoConfigFile({ sourceCwd: cwd, targetCwd: worktreePath });
if (runSetup) {
await runWorktreeSetupCommands({
worktreePath,
branchName: sourcePlan.branchName,
cleanupOnFailure: true,
});
}
return {
branchName: sourcePlan.branchName,
worktreePath,