mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 07:03:18 +00:00
* fix(workspaces): observe setup output only in the visible setup panel Setup logs were requested for the whole session, including unrelated workspaces. Use directory status for discovery and source-owned workspace observations for details, preserving legacy delivery only for clients without the negotiated capability. * fix(workspaces): publish external setup progress once per daemon MCP and scheduled setup enter through the broadcast API. Publish them to the shared setup owner before session fanout so each observation gets one update and snapshots survive periods without connected clients. Keep the setup action available on older hosts to explain the required host update. * fix(app): replace setup details with each observation snapshot A host restart can return an empty setup snapshot. Apply that authoritative snapshot so an open panel does not retain output from the previous daemon. Assert setup membership requests as well as delivery in the browser lifecycle regression. * test(workspaces): cover setup status catch-up after directory release * test(workspaces): synchronize setup output and release fixture resources
420 lines
14 KiB
TypeScript
420 lines
14 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,
|
|
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" && payload.detail.log.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.log === "",
|
|
);
|
|
const runningWithOutput = waitForWorkspaceSetupProgress(
|
|
client,
|
|
(payload) => payload.status === "running" && payload.detail.log.includes("starting setup"),
|
|
);
|
|
const completed = waitForWorkspaceSetupProgress(
|
|
client,
|
|
(payload) =>
|
|
payload.status === "completed" && payload.detail.log.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(runningPayload.detail.log).toContain("starting setup");
|
|
expect(completedPayload.detail.log).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" && payload.detail.log.includes("setup failed"),
|
|
);
|
|
|
|
const workspace = await createWorkspaceThroughDaemon(client, {
|
|
cwd: repo.path,
|
|
worktreeSlug: "workspace-setup-failure",
|
|
});
|
|
|
|
const failedPayload = await failed;
|
|
expect(failedPayload.detail.log).toContain("starting setup");
|
|
expect(failedPayload.detail.log).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" && payload.detail.log.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" && payload.detail.log.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();
|
|
}
|
|
});
|
|
});
|