mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-11 02:46:29 +00:00
* Revert "Restore cached conversations before reconnecting (#4421)"
This reverts commit 1c51b52d07.
* fix(app): restore provider loading on native
* fix(app): preserve native navigators across appearance changes
Reapply the isolated appearance boundaries and native menu transform origin from #4421 so appearance hydration preserves native navigator and gesture host lifetimes.
* fix(app): retain cached baselines during live updates
Read the timeline concurrently with agent preparation and reconcile display-only cache rows beneath the live head using the existing reducer. Merge cached directory entries beneath live state while preserving the existing checkpoint race guard.
192 lines
8.7 KiB
TypeScript
192 lines
8.7 KiB
TypeScript
import { test, expect, type Page } from "../support/fixtures";
|
|
import { gotoAppShell } from "../support/helpers/app";
|
|
import { gotoWorkspace, clickNewTerminal } from "../support/helpers/launcher";
|
|
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
|
import { seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
|
import { getServerId } from "../support/helpers/server-id";
|
|
import { projectEquivalenceViewKey } from "../support/helpers/project-view-key";
|
|
import { selectSidebarStatusGrouping } from "../support/helpers/sidebar";
|
|
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
|
import { getVisibleWorkspaceAgentTabIds } from "../support/helpers/workspace-tabs";
|
|
|
|
// Model B sidebar shape: every project — git or non-git, single- or
|
|
// multi-workspace — renders as the same expandable parent, the deepest sidebar
|
|
// level is the workspace row, and tabs/agents/terminals NEVER appear in the
|
|
// sidebar. These specs prove all three invariants end to end.
|
|
|
|
function workspaceRow(page: Page, workspaceId: string) {
|
|
return page.getByTestId(`sidebar-workspace-row-${getServerId()}:${workspaceId}`);
|
|
}
|
|
|
|
function projectRow(page: Page, projectKey: string) {
|
|
return page.getByTestId(`sidebar-project-row-${projectEquivalenceViewKey(projectKey)}`);
|
|
}
|
|
|
|
function projectNewWorktreeIcon(page: Page, projectKey: string) {
|
|
return page.getByTestId(`sidebar-project-new-worktree-${projectEquivalenceViewKey(projectKey)}`);
|
|
}
|
|
|
|
async function seedSecondWorkspace(seeded: SeededWorkspace, title: string): Promise<string> {
|
|
const created = await seeded.client.createWorkspace({
|
|
source: { kind: "directory", path: seeded.repoPath, projectId: seeded.projectId },
|
|
title,
|
|
});
|
|
if (!created.workspace) {
|
|
throw new Error(created.error ?? `Failed to create second workspace for ${seeded.projectId}`);
|
|
}
|
|
return created.workspace.id;
|
|
}
|
|
|
|
test.describe("Model B sidebar shape", () => {
|
|
test.describe.configure({ timeout: 180_000 });
|
|
|
|
test("git and non-git projects both render as expandable parents, both show a per-row New workspace icon, and the global button covers both", async ({
|
|
page,
|
|
}) => {
|
|
const gitProject = await seedWorkspace({ repoPrefix: "model-b-git-" });
|
|
const nonGitProject = await seedWorkspace({ repoPrefix: "model-b-nongit-", git: false });
|
|
|
|
try {
|
|
const gitSecondId = await seedSecondWorkspace(gitProject, "Git second");
|
|
const nonGitSecondId = await seedSecondWorkspace(nonGitProject, "Non-git second");
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
|
|
// Both projects are expandable parents — the non-git one is NOT flattened
|
|
// into a bare workspace link.
|
|
await expect(projectRow(page, gitProject.projectKey)).toBeVisible({ timeout: 30_000 });
|
|
await expect(projectRow(page, nonGitProject.projectKey)).toBeVisible({ timeout: 30_000 });
|
|
|
|
// Each parent shows both of its workspace rows underneath.
|
|
await expect(workspaceRow(page, gitProject.workspaceId)).toBeVisible({ timeout: 30_000 });
|
|
await expect(workspaceRow(page, gitSecondId)).toBeVisible({ timeout: 30_000 });
|
|
await expect(workspaceRow(page, nonGitProject.workspaceId)).toBeVisible({ timeout: 30_000 });
|
|
await expect(workspaceRow(page, nonGitSecondId)).toBeVisible({ timeout: 30_000 });
|
|
|
|
// Both projects show a per-row New workspace icon (revealed on hover): the
|
|
// git project can branch off a worktree, and the non-git project can add
|
|
// another workspace because the host supports workspaceMultiplicity.
|
|
await projectRow(page, gitProject.projectKey).hover();
|
|
await expect(projectNewWorktreeIcon(page, gitProject.projectKey)).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
await projectRow(page, nonGitProject.projectKey).hover();
|
|
await expect(projectNewWorktreeIcon(page, nonGitProject.projectKey)).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
|
|
// The global new-workspace button is the universal entry — present for both
|
|
// kinds regardless of their per-row affordance.
|
|
await expect(page.getByTestId("sidebar-global-new-workspace")).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
} finally {
|
|
await gitProject.cleanup();
|
|
await nonGitProject.cleanup();
|
|
}
|
|
});
|
|
|
|
test("no tab, agent, or terminal ever renders as a sidebar row", async ({ page }) => {
|
|
const mock = await seedMockAgentWorkspace({
|
|
repoPrefix: "model-b-leaf-",
|
|
title: "Leaf workspace",
|
|
});
|
|
|
|
try {
|
|
// Open the workspace and materialize both an agent tab and a terminal tab.
|
|
await gotoWorkspace(page, mock.workspaceId);
|
|
const agentTabs = await getVisibleWorkspaceAgentTabIds(page);
|
|
expect(agentTabs).toContain(`workspace-tab-agent_${mock.agentId}`);
|
|
|
|
await clickNewTerminal(page);
|
|
await expect(
|
|
page.locator('[data-testid^="workspace-tab-terminal_"]').filter({ visible: true }).first(),
|
|
).toBeVisible({ timeout: 30_000 });
|
|
|
|
// The deepest level inside the sidebar is the workspace row: no tab,
|
|
// agent, or terminal element appears as a sidebar descendant.
|
|
const sidebar = page.getByTestId("sidebar-sessions").filter({ visible: true }).first();
|
|
await expect(workspaceRow(page, mock.workspaceId).first()).toBeVisible({ timeout: 30_000 });
|
|
await expect(sidebar.locator('[data-testid^="workspace-tab-"]')).toHaveCount(0);
|
|
await expect(sidebar.locator('[data-testid^="sidebar-agent-row-"]')).toHaveCount(0);
|
|
await expect(sidebar.locator('[data-testid^="sidebar-terminal-row-"]')).toHaveCount(0);
|
|
} finally {
|
|
await mock.cleanup();
|
|
}
|
|
});
|
|
|
|
test("status grouping shows only workspace rows and moves a single row when its status changes", async ({
|
|
page,
|
|
}) => {
|
|
const idleProject = await seedWorkspace({ repoPrefix: "model-b-status-idle-" });
|
|
const activeMock = await seedMockAgentWorkspace({
|
|
repoPrefix: "model-b-status-active-",
|
|
title: "Working workspace",
|
|
initialPrompt: "stay busy",
|
|
model: "one-minute-stream",
|
|
});
|
|
|
|
try {
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await expect(workspaceRow(page, idleProject.workspaceId)).toBeVisible({ timeout: 30_000 });
|
|
|
|
// Switch to status grouping.
|
|
await selectSidebarStatusGrouping(page);
|
|
|
|
const sidebar = page.getByTestId("sidebar-sessions").filter({ visible: true }).first();
|
|
|
|
// The idle workspace lands in the Done bucket; the busy mock-agent workspace
|
|
// lands in the Working bucket. Each workspace is bucketed independently.
|
|
await expect(page.getByTestId("sidebar-status-group-done")).toBeVisible({ timeout: 30_000 });
|
|
await expect(page.getByTestId("sidebar-status-group-running")).toBeVisible({
|
|
timeout: 60_000,
|
|
});
|
|
await expect(workspaceRow(page, idleProject.workspaceId).first()).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
await expect(workspaceRow(page, activeMock.workspaceId).first()).toBeVisible({
|
|
timeout: 60_000,
|
|
});
|
|
await expect(workspaceRow(page, idleProject.workspaceId).first()).toHaveAccessibleName(
|
|
`${idleProject.projectDisplayName}, ${idleProject.workspaceName}`,
|
|
);
|
|
await expect(workspaceRow(page, activeMock.workspaceId).first()).toHaveAccessibleName(
|
|
/Working$/,
|
|
);
|
|
|
|
// Only workspace rows are shown — no tab/agent/terminal leaves leak into
|
|
// the status view.
|
|
await expect(sidebar.locator('[data-testid^="workspace-tab-"]')).toHaveCount(0);
|
|
|
|
// Status mode drops the project grouping, so each row leads its subtitle
|
|
// with the project's icon to keep projects distinguishable.
|
|
for (const workspaceId of [idleProject.workspaceId, activeMock.workspaceId]) {
|
|
await expect(
|
|
page.getByTestId(`sidebar-row-project-icon-${getServerId()}:${workspaceId}`).first(),
|
|
).toBeVisible({ timeout: 60_000 });
|
|
}
|
|
|
|
// The busy workspace is grouped under Working, the idle one under Done:
|
|
// changing one workspace's status moved only that row.
|
|
const workingRows = page.getByTestId("sidebar-status-group-rows-running");
|
|
const doneRows = page.getByTestId("sidebar-status-group-rows-done");
|
|
await expect(
|
|
workingRows.getByTestId(`sidebar-workspace-row-${getServerId()}:${activeMock.workspaceId}`),
|
|
).toBeVisible({ timeout: 60_000 });
|
|
await expect(
|
|
doneRows.getByTestId(`sidebar-workspace-row-${getServerId()}:${idleProject.workspaceId}`),
|
|
).toBeVisible({ timeout: 30_000 });
|
|
// The busy workspace is NOT also sitting in the Done bucket — only its own
|
|
// row moved.
|
|
await expect(
|
|
doneRows.getByTestId(`sidebar-workspace-row-${getServerId()}:${activeMock.workspaceId}`),
|
|
).toHaveCount(0);
|
|
} finally {
|
|
await idleProject.cleanup();
|
|
await activeMock.cleanup();
|
|
}
|
|
});
|
|
});
|