mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 23:44:41 +00:00
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:
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
Reference in New Issue
Block a user