* fix(app): only surface failed workspace setup Running and successful setup snapshots were treated as reasons to seed a background Setup tab, adding noise to every workspace with setup commands. Seed only failed setup and classify that tab as main-workspace content so the failure stays visible without stealing focus. * fix(app): surface failed setup once Keep the failed-setup surfacing claim with the setup snapshot so closing its background tab remains respected after the workspace remounts. Explicitly prefer the main pane for a new failure tab without moving a user-placed existing tab. * fix(app): keep setup tabs out of side panel A preferred main-pane placement can fall back when users remove that pane. Keep that implicit fallback out of the focused Side panel when another workspace pane is available.
346 lines
12 KiB
TypeScript
346 lines
12 KiB
TypeScript
import { 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,
|
|
returnHomeFromWorkspace,
|
|
openWorkspaceScriptsMenu,
|
|
startWorkspaceScriptFromMenu,
|
|
closeWorkspaceScriptsMenu,
|
|
seedProjectForWorkspaceSetup,
|
|
waitForWorkspaceSetupProgress,
|
|
} 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);
|
|
|
|
// Wait for setup completion via daemon (setup snapshots are per-session,
|
|
// so the browser session won't receive progress events).
|
|
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("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 returnHomeFromWorkspace(page);
|
|
await navigateToWorkspaceViaSidebar(page, workspace.id);
|
|
await expectSetupTabNotSeeded(page, workspace.id);
|
|
} finally {
|
|
await client.close();
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("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();
|
|
}
|
|
});
|
|
|
|
test("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();
|
|
}
|
|
});
|
|
});
|