mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 18:52:39 +00:00
* 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.
429 lines
15 KiB
TypeScript
429 lines
15 KiB
TypeScript
import { daemonTest, test, expect } from "../support/fixtures";
|
|
import { createTempGitRepo } from "../support/helpers/workspace";
|
|
import {
|
|
closeSetupTab,
|
|
waitForWorkspaceTabsVisible,
|
|
expectFailedSetupTabSeededInMainPane,
|
|
expectSetupTabNotSeeded,
|
|
expectNoTerminalTabs,
|
|
clickFirstTerminalTab,
|
|
expectFirstTerminalTabContains,
|
|
} from "../support/helpers/workspace-tabs";
|
|
import { clickNewChat } from "../support/helpers/launcher";
|
|
import { expectComposerVisible } from "../support/helpers/composer";
|
|
import { openFileExplorer, expectExplorerEntryVisible } from "../support/helpers/file-explorer";
|
|
import {
|
|
expectTerminalSurfaceVisible,
|
|
waitForTerminalAttached,
|
|
} from "../support/helpers/terminal-perf";
|
|
import {
|
|
connectWorkspaceSetupClient,
|
|
setupCommandOutput,
|
|
createWorkspaceThroughDaemon,
|
|
expectSetupPanel,
|
|
openHomeWithProject,
|
|
navigateToWorkspaceViaSidebar,
|
|
leaveWorkspaceViaHistory,
|
|
openWorkspaceScriptsMenu,
|
|
startWorkspaceScriptFromMenu,
|
|
closeWorkspaceScriptsMenu,
|
|
seedProjectForWorkspaceSetup,
|
|
waitForWorkspaceSetupProgress,
|
|
observeSetupPanelTraffic,
|
|
} from "../support/helpers/workspace-setup";
|
|
|
|
interface WorkspaceScriptStarter {
|
|
startWorkspaceScript(
|
|
workspaceId: string,
|
|
scriptName: string,
|
|
): Promise<{
|
|
workspaceId: string;
|
|
scriptName: string;
|
|
terminalId: string | null;
|
|
error: string | null;
|
|
}>;
|
|
}
|
|
|
|
test.describe("Workspace setup streaming", () => {
|
|
test("does not seed the setup tab while workspace setup is running", async ({ page }) => {
|
|
const client = await connectWorkspaceSetupClient();
|
|
const repo = await createTempGitRepo("setup-open-", {
|
|
paseoConfig: {
|
|
worktree: {
|
|
setup: [
|
|
"sh -c 'echo starting setup; for i in $(seq 1 30); do echo tick $i; sleep 1; done; echo setup complete'",
|
|
],
|
|
},
|
|
},
|
|
});
|
|
|
|
try {
|
|
await seedProjectForWorkspaceSetup(client, repo.path);
|
|
const workspace = await createWorkspaceThroughDaemon(client, {
|
|
cwd: repo.path,
|
|
worktreeSlug: `setup-open-${Date.now()}`,
|
|
});
|
|
await openHomeWithProject(page, repo.path);
|
|
await navigateToWorkspaceViaSidebar(page, workspace.id);
|
|
|
|
await expectSetupTabNotSeeded(page, workspace.id);
|
|
} finally {
|
|
await client.close();
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("runs setup through the sidebar and leaves the workspace usable", async ({ page }) => {
|
|
const client = await connectWorkspaceSetupClient();
|
|
const repo = await createTempGitRepo("setup-ui-flow-", {
|
|
paseoConfig: {
|
|
worktree: {
|
|
setup: [
|
|
"sh -c 'echo starting setup; sleep 1; echo loading dependencies; sleep 1; echo setup complete'",
|
|
],
|
|
},
|
|
},
|
|
files: [{ path: "src/index.ts", content: "export const ready = true;\n" }],
|
|
});
|
|
|
|
try {
|
|
await seedProjectForWorkspaceSetup(client, repo.path);
|
|
|
|
// The setup client owns progress before creating the workspace.
|
|
const completed = waitForWorkspaceSetupProgress(
|
|
client,
|
|
(payload) =>
|
|
payload.status === "completed" &&
|
|
setupCommandOutput(payload.detail).includes("setup complete"),
|
|
);
|
|
const workspace = await createWorkspaceThroughDaemon(client, {
|
|
cwd: repo.path,
|
|
worktreeSlug: `setup-ui-flow-${Date.now()}`,
|
|
});
|
|
await completed;
|
|
|
|
await openHomeWithProject(page, repo.path);
|
|
await navigateToWorkspaceViaSidebar(page, workspace.id);
|
|
|
|
await expectSetupTabNotSeeded(page, workspace.id);
|
|
await expectSetupPanel(page);
|
|
await waitForWorkspaceTabsVisible(page);
|
|
await clickNewChat(page);
|
|
await expectComposerVisible(page, { timeout: 30_000 });
|
|
await openFileExplorer(page);
|
|
await expectExplorerEntryVisible(page, "README.md");
|
|
await expectExplorerEntryVisible(page, "src");
|
|
} finally {
|
|
await client.close();
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("setup panel releases details while retained behind another tab and resubscribes when shown", async ({
|
|
page,
|
|
}) => {
|
|
const client = await connectWorkspaceSetupClient();
|
|
const repo = await createTempGitRepo("setup-membership-", {
|
|
paseoConfig: { worktree: { setup: ["echo setup complete"] } },
|
|
});
|
|
const traffic = observeSetupPanelTraffic(page);
|
|
try {
|
|
await seedProjectForWorkspaceSetup(client, repo.path);
|
|
const completed = waitForWorkspaceSetupProgress(
|
|
client,
|
|
(payload) => payload.status === "completed",
|
|
);
|
|
const workspace = await createWorkspaceThroughDaemon(client, {
|
|
cwd: repo.path,
|
|
worktreeSlug: "setup-membership",
|
|
});
|
|
await completed;
|
|
await openHomeWithProject(page, repo.path);
|
|
await navigateToWorkspaceViaSidebar(page, workspace.id);
|
|
await expectSetupTabNotSeeded(page, workspace.id);
|
|
await traffic.expectNoDemand();
|
|
await expectSetupPanel(page);
|
|
await traffic.expectSubscribed(workspace.id, 1);
|
|
await expect(page.getByTestId("workspace-setup-log").first()).toContainText("setup complete");
|
|
await clickNewChat(page);
|
|
await expectComposerVisible(page);
|
|
await traffic.expectReleased(0);
|
|
await expectSetupPanel(page);
|
|
await traffic.expectSubscribed(workspace.id, 2);
|
|
await closeSetupTab(page, workspace.id);
|
|
await traffic.expectReleased(1);
|
|
} finally {
|
|
await client.close();
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
daemonTest("streams running and completed setup snapshots for a successful setup", async () => {
|
|
const client = await connectWorkspaceSetupClient();
|
|
const repo = await createTempGitRepo("setup-success-", {
|
|
paseoConfig: {
|
|
worktree: {
|
|
setup: ["sh -c 'echo starting setup; sleep 2; echo setup complete'"],
|
|
},
|
|
},
|
|
});
|
|
|
|
try {
|
|
await seedProjectForWorkspaceSetup(client, repo.path);
|
|
const initialRunning = waitForWorkspaceSetupProgress(
|
|
client,
|
|
(payload) => payload.status === "running" && payload.detail.commands.length === 0,
|
|
);
|
|
const runningWithOutput = waitForWorkspaceSetupProgress(
|
|
client,
|
|
(payload) =>
|
|
payload.status === "running" &&
|
|
setupCommandOutput(payload.detail).includes("starting setup"),
|
|
);
|
|
const completed = waitForWorkspaceSetupProgress(
|
|
client,
|
|
(payload) =>
|
|
payload.status === "completed" &&
|
|
setupCommandOutput(payload.detail).includes("setup complete"),
|
|
);
|
|
|
|
await createWorkspaceThroughDaemon(client, {
|
|
cwd: repo.path,
|
|
worktreeSlug: "workspace-setup-success",
|
|
});
|
|
|
|
const initialPayload = await initialRunning;
|
|
const runningPayload = await runningWithOutput;
|
|
const completedPayload = await completed;
|
|
|
|
expect(initialPayload.detail.log).toBe("");
|
|
expect(setupCommandOutput(runningPayload.detail)).toContain("starting setup");
|
|
expect(setupCommandOutput(completedPayload.detail)).toContain("setup complete");
|
|
expect(completedPayload.error).toBeNull();
|
|
} finally {
|
|
await client.close();
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("seeds a failed setup tab once in the main pane", async ({ page }) => {
|
|
const client = await connectWorkspaceSetupClient();
|
|
const repo = await createTempGitRepo("setup-failure-", {
|
|
paseoConfig: {
|
|
worktree: {
|
|
setup: ["sh -c 'echo starting setup; sleep 2; echo setup failed 1>&2; exit 1'"],
|
|
},
|
|
},
|
|
});
|
|
|
|
try {
|
|
await seedProjectForWorkspaceSetup(client, repo.path);
|
|
const failed = waitForWorkspaceSetupProgress(
|
|
client,
|
|
(payload) =>
|
|
payload.status === "failed" &&
|
|
setupCommandOutput(payload.detail).includes("setup failed"),
|
|
);
|
|
|
|
const workspace = await createWorkspaceThroughDaemon(client, {
|
|
cwd: repo.path,
|
|
worktreeSlug: "workspace-setup-failure",
|
|
});
|
|
|
|
const failedPayload = await failed;
|
|
expect(setupCommandOutput(failedPayload.detail)).toContain("starting setup");
|
|
expect(setupCommandOutput(failedPayload.detail)).toContain("setup failed");
|
|
expect(failedPayload.error).toMatch(/failed/i);
|
|
|
|
await openHomeWithProject(page, repo.path);
|
|
await navigateToWorkspaceViaSidebar(page, workspace.id);
|
|
await waitForWorkspaceTabsVisible(page);
|
|
await expectFailedSetupTabSeededInMainPane(page, workspace.id);
|
|
|
|
await closeSetupTab(page, workspace.id);
|
|
await leaveWorkspaceViaHistory(page);
|
|
await navigateToWorkspaceViaSidebar(page, workspace.id);
|
|
await expectSetupTabNotSeeded(page, workspace.id);
|
|
} finally {
|
|
await client.close();
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("keeps a workspace usable when its setup config cannot be parsed", async ({ page }) => {
|
|
const client = await connectWorkspaceSetupClient();
|
|
const repo = await createTempGitRepo("setup-invalid-config-", {
|
|
files: [{ path: "paseo.json", content: "{ invalid json\n" }],
|
|
});
|
|
|
|
try {
|
|
await seedProjectForWorkspaceSetup(client, repo.path);
|
|
const failed = waitForWorkspaceSetupProgress(
|
|
client,
|
|
(payload) => payload.status === "failed",
|
|
);
|
|
const workspace = await createWorkspaceThroughDaemon(client, {
|
|
cwd: repo.path,
|
|
worktreeSlug: "workspace-setup-invalid-config",
|
|
});
|
|
const failedPayload = await failed;
|
|
expect(failedPayload.error).toContain("Failed to parse paseo.json");
|
|
expect(failedPayload.detail.commands).toEqual([]);
|
|
|
|
await openHomeWithProject(page, repo.path);
|
|
await navigateToWorkspaceViaSidebar(page, workspace.id);
|
|
await expectFailedSetupTabSeededInMainPane(page, workspace.id);
|
|
await expect(page.getByText(/Failed to parse paseo\.json at/)).toBeVisible();
|
|
await closeSetupTab(page, workspace.id);
|
|
await clickNewChat(page);
|
|
await expectComposerVisible(page);
|
|
await openFileExplorer(page);
|
|
await expectExplorerEntryVisible(page, "paseo.json");
|
|
} finally {
|
|
await client.close();
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
daemonTest("emits a completed empty snapshot when no setup commands exist", async () => {
|
|
const client = await connectWorkspaceSetupClient();
|
|
const repo = await createTempGitRepo("setup-none-");
|
|
|
|
try {
|
|
await seedProjectForWorkspaceSetup(client, repo.path);
|
|
const completed = waitForWorkspaceSetupProgress(
|
|
client,
|
|
(payload) =>
|
|
payload.status === "completed" &&
|
|
payload.detail.commands.length === 0 &&
|
|
payload.detail.log === "",
|
|
);
|
|
|
|
await createWorkspaceThroughDaemon(client, {
|
|
cwd: repo.path,
|
|
worktreeSlug: "workspace-setup-none",
|
|
});
|
|
|
|
const completedPayload = await completed;
|
|
expect(completedPayload.error).toBeNull();
|
|
expect(completedPayload.detail.commands).toEqual([]);
|
|
expect(completedPayload.detail.log).toBe("");
|
|
} finally {
|
|
await client.close();
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("launches script terminals from the workspace scripts menu", async ({ page }) => {
|
|
test.setTimeout(90_000);
|
|
const client = await connectWorkspaceSetupClient();
|
|
const repo = await createTempGitRepo("setup-svc-ui-", {
|
|
paseoConfig: {
|
|
worktree: {
|
|
setup: ["sh -c 'echo bootstrapping; sleep 1; echo setup complete'"],
|
|
},
|
|
scripts: {
|
|
web: {
|
|
command:
|
|
"node -e \"const http = require('http'); const s = http.createServer((q,r) => r.end('ok')); s.listen(process.env.PORT || 3000, () => console.log('listening on ' + s.address().port))\"",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
try {
|
|
await seedProjectForWorkspaceSetup(client, repo.path);
|
|
|
|
// Wait for setup completion via daemon (setup snapshots are per-session)
|
|
const completed = waitForWorkspaceSetupProgress(
|
|
client,
|
|
(payload) =>
|
|
payload.status === "completed" &&
|
|
setupCommandOutput(payload.detail).includes("setup complete"),
|
|
);
|
|
const workspace = await createWorkspaceThroughDaemon(client, {
|
|
cwd: repo.path,
|
|
worktreeSlug: `setup-svc-${Date.now()}`,
|
|
});
|
|
await completed;
|
|
|
|
await openHomeWithProject(page, repo.path);
|
|
await navigateToWorkspaceViaSidebar(page, workspace.id);
|
|
|
|
await waitForWorkspaceTabsVisible(page);
|
|
await expectNoTerminalTabs(page);
|
|
await openWorkspaceScriptsMenu(page);
|
|
await startWorkspaceScriptFromMenu(page, "web");
|
|
await closeWorkspaceScriptsMenu(page);
|
|
await clickFirstTerminalTab(page);
|
|
await expectTerminalSurfaceVisible(page, { timeout: 10_000 });
|
|
await waitForTerminalAttached(page);
|
|
await expectFirstTerminalTabContains(page, "web");
|
|
} finally {
|
|
await client.close();
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
daemonTest("launches workspace scripts through an explicit daemon request", async () => {
|
|
const client = await connectWorkspaceSetupClient();
|
|
const repo = await createTempGitRepo("setup-scripts-", {
|
|
paseoConfig: {
|
|
worktree: {
|
|
setup: ["sh -c 'echo bootstrapping; sleep 1; echo setup complete'"],
|
|
},
|
|
scripts: {
|
|
editor: {
|
|
command: "node -e \"console.log('editor ready'); setInterval(() => {}, 1000)\"",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
try {
|
|
await seedProjectForWorkspaceSetup(client, repo.path);
|
|
const completed = waitForWorkspaceSetupProgress(
|
|
client,
|
|
(payload) =>
|
|
payload.status === "completed" &&
|
|
setupCommandOutput(payload.detail).includes("setup complete"),
|
|
);
|
|
|
|
const result = await client.createPaseoWorktree({
|
|
cwd: repo.path,
|
|
worktreeSlug: "workspace-setup-scripts",
|
|
});
|
|
if (!result.workspace) {
|
|
throw new Error(result.error ?? "Failed to create workspace");
|
|
}
|
|
const workspaceDir = result.workspace.workspaceDirectory;
|
|
const workspaceId = result.workspace.id;
|
|
|
|
await completed;
|
|
|
|
const scriptClient = client as typeof client & WorkspaceScriptStarter;
|
|
const startResult = await scriptClient.startWorkspaceScript(workspaceId, "editor");
|
|
expect(startResult).toMatchObject({
|
|
workspaceId,
|
|
scriptName: "editor",
|
|
terminalId: expect.any(String),
|
|
error: null,
|
|
});
|
|
|
|
const findEditorTerminal = (terminal: { name: string }) => terminal.name === "editor";
|
|
await expect
|
|
.poll(async () => {
|
|
const terminals = await client.listTerminals(workspaceDir);
|
|
return terminals.terminals.find(findEditorTerminal) ?? null;
|
|
})
|
|
.toMatchObject({
|
|
id: expect.any(String),
|
|
name: "editor",
|
|
});
|
|
} finally {
|
|
await client.close();
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
});
|