* fix(app): keep an open workspace menu from unmounting its own trigger The sidebar kebab is revealed by hover, and on a compact layout the menu it opens is a sheet that slides up over the row it came from. The row un-hovers, the trailing overlay unmounts the trigger, and the menu's open state goes with it — nothing is left that can close the surface already presented in the portal, so a full-screen backdrop swallows every click until reload. Lift the menu's open state to the component that decides whether to render the kebab, and keep the kebab rendered while the menu is up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(app): follow the sidebar and host badge redesigns in the e2e suite Three specs were still driving surfaces that moved out from under them, so CI has been red on main since the sidebar polish landed. Display preferences became one row per decision with the options a page down, so grouping and the host filter need that page walked first. Five specs had their own copy of the two-line dance; it lives in the sidebar helper now, and the host filter has its own opener next to the menu it belongs to. The host badge is plain secondary text on the meta line and its identity colour lives on the server icon alone, so the helpers stop asserting a tinted label and a pill fill that the redesign removed. Alignment rails are compared across canvas glyph metrics and SVG bounding boxes, so the residual depends on the host's font; a 1px and a 0.5px tolerance both landed on the wrong side of it in CI. Two pixels still catches the whole- pixel offsets these rails encode. The compact sheet spec also read its rails while the panel was still sliding in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(app): let a browser e2e run take a private Metro cache Metro's on-disk cache is shared by every checkout on the machine. Editing one app source file rebuilds that module against everyone else's cached output and the Unistyles styles can come back desynced, which surfaces as alignment specs failing with real-looking pixel numbers that go away when the edit is reverted. E2E_METRO_CACHE_VERSION namespaces a run's cache without deleting anyone else's entries, so the cache can be ruled out before a layout failure is believed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(app): stop the chat scroll helper from hanging on a stalled sampler `scrollChatAwayFromBottom` waited on a promise that could only ever resolve from a `requestAnimationFrame` sampler started by a `wheel` listener. rAF does not fire while the page is occluded, which is reachable on a CI runner, so the sampler could stall on its first frame and the wait never returned — the test died on its own 240s timeout with nothing to read. A missed wheel event did the same. Settling the scroll before measuring is an optimization; the assertion after it is what decides the test. Sample on a timer with a deadline so a stall falls through to that assertion instead of consuming the test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(app): remove layout geometry e2e coverage Rail and pixel-alignment assertions depend on host rendering details and should not define the browser E2E contract. Preserve the surrounding behavior coverage while dropping the Metro cache escape hatch that only supported diagnosing those assertions. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
576 lines
19 KiB
TypeScript
576 lines
19 KiB
TypeScript
import { test, expect } from "../support/fixtures";
|
|
import { expectWorkspaceTabVisible } from "../support/helpers/archive-tab";
|
|
import {
|
|
expectComposerEditable,
|
|
expectComposerVisible,
|
|
submitMessage,
|
|
} from "../support/helpers/composer";
|
|
import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher";
|
|
import {
|
|
assertNewWorkspaceSidebarAndHeader,
|
|
connectNewWorkspaceDaemonClient,
|
|
expectNewWorkspaceProjectSelected,
|
|
openGlobalNewWorkspaceComposer,
|
|
selectWorkspaceIsolation,
|
|
submitNewWorkspaceEmpty,
|
|
submitNewWorkspacePrompt,
|
|
} from "../support/helpers/new-workspace";
|
|
import { getServerId } from "../support/helpers/server-id";
|
|
import { selectSidebarStatusGrouping } from "../support/helpers/sidebar";
|
|
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
|
import {
|
|
expectSubagentRowVisible,
|
|
openSubagentsTrack,
|
|
seedParentWithCrossWorkspaceSubagent,
|
|
} from "../support/helpers/subagents";
|
|
import { expectWorkspaceHeader, waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
|
import { getVisibleWorkspaceAgentTabIds } from "../support/helpers/workspace-tabs";
|
|
|
|
type NewWorkspaceDaemonClient = Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>;
|
|
type WorkspaceIndicator = "attention" | "done" | "failed" | "loading" | "needs_input" | "running";
|
|
|
|
interface CreatedAgentAssertion {
|
|
workspaceId: string;
|
|
provider: string;
|
|
cwd: string;
|
|
modeId: string | null;
|
|
model: string | null;
|
|
}
|
|
|
|
async function fetchCreatedAgentForWorkspace(
|
|
seeded: SeededWorkspace,
|
|
workspaceId: string,
|
|
): Promise<CreatedAgentAssertion | null> {
|
|
const result = await seeded.client.fetchAgents({ scope: "active" });
|
|
const agents = result.entries
|
|
.map((entry) => entry.agent)
|
|
.filter((agent) => agent.cwd === seeded.repoPath && agent.workspaceId === workspaceId);
|
|
if (agents.length !== 1) {
|
|
return null;
|
|
}
|
|
const [agent] = agents;
|
|
return {
|
|
workspaceId,
|
|
provider: agent.provider,
|
|
cwd: agent.cwd,
|
|
modeId: agent.currentModeId,
|
|
model: agent.model,
|
|
};
|
|
}
|
|
|
|
async function fetchWorkspaceName(
|
|
client: NewWorkspaceDaemonClient,
|
|
workspaceId: string,
|
|
): Promise<string | null> {
|
|
const result = await client.fetchWorkspaces();
|
|
return result.entries.find((entry) => entry.id === workspaceId)?.name ?? null;
|
|
}
|
|
|
|
async function fetchWorkspaceStatuses(
|
|
client: NewWorkspaceDaemonClient,
|
|
workspaceIds: string[],
|
|
): Promise<Record<string, string | null>> {
|
|
const result = await client.fetchWorkspaces();
|
|
const entriesById = new Map(result.entries.map((entry) => [entry.id, entry.status]));
|
|
return Object.fromEntries(
|
|
workspaceIds.map((workspaceId) => [workspaceId, entriesById.get(workspaceId) ?? null]),
|
|
);
|
|
}
|
|
|
|
async function fetchAgentStatus(seeded: SeededWorkspace, agentId: string): Promise<string | null> {
|
|
const result = await seeded.client.fetchAgents({ scope: "active" });
|
|
return result.entries.find((entry) => entry.agent.id === agentId)?.agent.status ?? null;
|
|
}
|
|
|
|
async function switchSidebarToStatusGrouping(page: import("@playwright/test").Page) {
|
|
await selectSidebarStatusGrouping(page);
|
|
await expect(page.locator('[data-testid^="sidebar-status-group-"]').first()).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
}
|
|
|
|
function statusGroupRows(page: import("@playwright/test").Page, bucket: string) {
|
|
return page.getByTestId(`sidebar-status-group-rows-${bucket}`);
|
|
}
|
|
|
|
async function expectWorkspaceRowInStatusBucket(
|
|
page: import("@playwright/test").Page,
|
|
input: { rowTestId: string; bucket: string },
|
|
) {
|
|
await expect(statusGroupRows(page, input.bucket).getByTestId(input.rowTestId)).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
}
|
|
|
|
async function expectWorkspaceRowNotInStatusBuckets(
|
|
page: import("@playwright/test").Page,
|
|
input: { rowTestId: string; buckets: string[] },
|
|
) {
|
|
for (const bucket of input.buckets) {
|
|
await expect(statusGroupRows(page, bucket).getByTestId(input.rowTestId)).toHaveCount(0, {
|
|
timeout: 5_000,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function expectWorkspaceRowHasOnlyIndicator(
|
|
page: import("@playwright/test").Page,
|
|
input: { rowTestId: string; indicator: WorkspaceIndicator },
|
|
) {
|
|
const row = page.getByTestId(input.rowTestId);
|
|
await expect(row).toBeVisible({ timeout: 30_000 });
|
|
for (const indicator of [
|
|
"attention",
|
|
"done",
|
|
"failed",
|
|
"loading",
|
|
"needs_input",
|
|
"running",
|
|
] satisfies WorkspaceIndicator[]) {
|
|
const locator = row.locator(`[data-testid="workspace-status-indicator-${indicator}"]`);
|
|
if (indicator === input.indicator) {
|
|
await expect(locator).toBeVisible({ timeout: 30_000 });
|
|
} else {
|
|
await expect(locator).toHaveCount(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function expectWorkspaceRowDoesNotShowIndicator(
|
|
page: import("@playwright/test").Page,
|
|
input: { rowTestId: string; indicator: WorkspaceIndicator },
|
|
) {
|
|
const row = page.getByTestId(input.rowTestId);
|
|
await expect(row).toBeVisible({ timeout: 30_000 });
|
|
await expect(
|
|
row.locator(`[data-testid="workspace-status-indicator-${input.indicator}"]`),
|
|
).toHaveCount(0, { timeout: 5_000 });
|
|
}
|
|
|
|
test.describe("Workspace model regressions", () => {
|
|
let client: NewWorkspaceDaemonClient;
|
|
|
|
test.describe.configure({ timeout: 240_000 });
|
|
|
|
test.beforeEach(async () => {
|
|
client = await connectNewWorkspaceDaemonClient();
|
|
});
|
|
|
|
test.afterEach(async () => {
|
|
await client?.close().catch(() => undefined);
|
|
});
|
|
|
|
test("same-directory workspace does not show agents owned by another workspace", async ({
|
|
page,
|
|
}) => {
|
|
const seeded = await seedWorkspace({ repoPrefix: "workspace-legacy-agents-" });
|
|
|
|
try {
|
|
const ownedAgent = await seeded.client.createAgent({
|
|
provider: "mock",
|
|
cwd: seeded.repoPath,
|
|
workspaceId: seeded.workspaceId,
|
|
title: "Agent owned by original workspace",
|
|
modeId: "load-test",
|
|
model: "ten-second-stream",
|
|
});
|
|
const secondWorkspace = await seeded.client.createWorkspace({
|
|
source: { kind: "directory", path: seeded.repoPath, projectId: seeded.projectId },
|
|
title: "Fresh workspace",
|
|
});
|
|
if (!secondWorkspace.workspace) {
|
|
throw new Error(secondWorkspace.error ?? "Failed to create same-directory workspace");
|
|
}
|
|
|
|
await gotoWorkspace(page, secondWorkspace.workspace.id);
|
|
|
|
await expect
|
|
.poll(() => getVisibleWorkspaceAgentTabIds(page), { timeout: 30_000 })
|
|
.toEqual([]);
|
|
|
|
await gotoWorkspace(page, seeded.workspaceId);
|
|
await expect
|
|
.poll(() => getVisibleWorkspaceAgentTabIds(page), { timeout: 30_000 })
|
|
.toContain(`workspace-tab-agent_${ownedAgent.id}`);
|
|
} finally {
|
|
await seeded.cleanup();
|
|
}
|
|
});
|
|
|
|
test("new agent tab in a same-directory workspace picks a default model for the saved provider", async ({
|
|
page,
|
|
}) => {
|
|
const seeded: SeededWorkspace = await seedWorkspace({
|
|
repoPrefix: "workspace-new-agent-model-",
|
|
});
|
|
|
|
try {
|
|
const secondWorkspace = await seeded.client.createWorkspace({
|
|
source: { kind: "directory", path: seeded.repoPath, projectId: seeded.projectId },
|
|
title: "Fresh workspace",
|
|
});
|
|
if (!secondWorkspace.workspace) {
|
|
throw new Error(secondWorkspace.error ?? "Failed to create same-directory workspace");
|
|
}
|
|
const workspace = secondWorkspace.workspace;
|
|
|
|
await page.addInitScript(() => {
|
|
localStorage.setItem(
|
|
"@paseo:create-agent-preferences",
|
|
JSON.stringify({
|
|
provider: "mock",
|
|
providerPreferences: {
|
|
mock: { mode: "load-test" },
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
await gotoWorkspace(page, workspace.id);
|
|
await clickNewChat(page);
|
|
|
|
await expectComposerVisible(page);
|
|
await expectComposerEditable(page);
|
|
const prompt = `Create agent default model ${Date.now()}`;
|
|
await submitMessage(page, prompt);
|
|
await expect(page.getByText("No model is available for the selected provider")).toHaveCount(
|
|
0,
|
|
);
|
|
await expect
|
|
.poll(() => fetchCreatedAgentForWorkspace(seeded, workspace.id), {
|
|
timeout: 10_000,
|
|
})
|
|
.toEqual({
|
|
workspaceId: workspace.id,
|
|
provider: "mock",
|
|
cwd: seeded.repoPath,
|
|
modeId: "load-test",
|
|
model: "five-minute-stream",
|
|
});
|
|
} finally {
|
|
await seeded.cleanup();
|
|
}
|
|
});
|
|
|
|
test("local same-directory workspace with an initial prompt shows a generated title", async ({
|
|
page,
|
|
}) => {
|
|
const serverId = getServerId();
|
|
const seeded: SeededWorkspace = await seedWorkspace({
|
|
repoPrefix: "workspace-generated-local-title-",
|
|
});
|
|
const previousConfig = await client.getDaemonConfig();
|
|
|
|
try {
|
|
await client.patchDaemonConfig({
|
|
metadataGeneration: {
|
|
providers: [{ provider: "mock", model: "ten-second-stream" }],
|
|
},
|
|
});
|
|
|
|
await gotoWorkspace(page, seeded.workspaceId);
|
|
await waitForSidebarHydration(page);
|
|
await openGlobalNewWorkspaceComposer(page);
|
|
await expectNewWorkspaceProjectSelected(page, seeded.projectDisplayName);
|
|
await selectWorkspaceIsolation(page, "local");
|
|
await submitNewWorkspacePrompt(page, "Fix login bug");
|
|
|
|
const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, {
|
|
serverId,
|
|
client,
|
|
previousWorkspaceId: seeded.workspaceId,
|
|
projectDisplayName: seeded.projectDisplayName,
|
|
});
|
|
const createdRow = page.getByTestId(
|
|
`sidebar-workspace-row-${serverId}:${createdWorkspace.workspaceId}`,
|
|
);
|
|
|
|
await expect
|
|
.poll(() => fetchWorkspaceName(client, createdWorkspace.workspaceId), {
|
|
timeout: 30_000,
|
|
})
|
|
.toBe("Fix login bug");
|
|
await expectWorkspaceHeader(page, {
|
|
title: "Fix login bug",
|
|
subtitle: seeded.projectDisplayName,
|
|
});
|
|
await expect(createdRow).toContainText("Fix login bug", { timeout: 30_000 });
|
|
expect(createdWorkspace.workspaceDirectory).toBe(seeded.workspaceDirectory);
|
|
} finally {
|
|
await client
|
|
.patchDaemonConfig({
|
|
metadataGeneration: {
|
|
providers: previousConfig.config.metadataGeneration.providers,
|
|
},
|
|
})
|
|
.catch(() => undefined);
|
|
await seeded.cleanup();
|
|
}
|
|
});
|
|
|
|
test("running agent in one same-directory workspace only shows the loader on its owning row", async ({
|
|
page,
|
|
}) => {
|
|
const serverId = getServerId();
|
|
const seeded = await seedWorkspace({
|
|
repoPrefix: "workspace-same-cwd-running-",
|
|
});
|
|
|
|
try {
|
|
const secondWorkspace = await seeded.client.createWorkspace({
|
|
source: { kind: "directory", path: seeded.repoPath, projectId: seeded.projectId },
|
|
title: "Existing sibling workspace",
|
|
});
|
|
if (!secondWorkspace.workspace) {
|
|
throw new Error(secondWorkspace.error ?? "Failed to create same-directory workspace");
|
|
}
|
|
const secondWorkspaceId = secondWorkspace.workspace.id;
|
|
|
|
const runningAgent = await seeded.client.createAgent({
|
|
provider: "mock",
|
|
cwd: seeded.repoPath,
|
|
workspaceId: seeded.workspaceId,
|
|
title: "Running agent",
|
|
modeId: "load-test",
|
|
model: "five-minute-stream",
|
|
initialPrompt: "stay running",
|
|
});
|
|
await seeded.client.waitForAgentUpsert(
|
|
runningAgent.id,
|
|
(snapshot) => snapshot.status === "running",
|
|
15_000,
|
|
);
|
|
|
|
await gotoWorkspace(page, seeded.workspaceId);
|
|
await waitForSidebarHydration(page);
|
|
|
|
const firstRowTestId = `sidebar-workspace-row-${serverId}:${seeded.workspaceId}`;
|
|
const secondRowTestId = `sidebar-workspace-row-${serverId}:${secondWorkspaceId}`;
|
|
await expectWorkspaceRowHasOnlyIndicator(page, {
|
|
rowTestId: firstRowTestId,
|
|
indicator: "running",
|
|
});
|
|
await expectWorkspaceRowDoesNotShowIndicator(page, {
|
|
rowTestId: secondRowTestId,
|
|
indicator: "running",
|
|
});
|
|
|
|
await openGlobalNewWorkspaceComposer(page);
|
|
await expectNewWorkspaceProjectSelected(page, seeded.projectDisplayName);
|
|
await selectWorkspaceIsolation(page, "local");
|
|
await submitNewWorkspaceEmpty(page);
|
|
|
|
const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, {
|
|
serverId,
|
|
client,
|
|
previousWorkspaceId: seeded.workspaceId,
|
|
projectDisplayName: seeded.projectDisplayName,
|
|
});
|
|
const createdRowTestId = `sidebar-workspace-row-${serverId}:${createdWorkspace.workspaceId}`;
|
|
|
|
await expect
|
|
.poll(() => fetchAgentStatus(seeded, runningAgent.id), { timeout: 10_000 })
|
|
.toBe("running");
|
|
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
fetchWorkspaceStatuses(client, [
|
|
seeded.workspaceId,
|
|
secondWorkspaceId,
|
|
createdWorkspace.workspaceId,
|
|
]),
|
|
{ timeout: 10_000 },
|
|
)
|
|
.toEqual({
|
|
[seeded.workspaceId]: "running",
|
|
[secondWorkspaceId]: "done",
|
|
[createdWorkspace.workspaceId]: "done",
|
|
});
|
|
|
|
await expectWorkspaceRowHasOnlyIndicator(page, {
|
|
rowTestId: firstRowTestId,
|
|
indicator: "running",
|
|
});
|
|
await expectWorkspaceRowDoesNotShowIndicator(page, {
|
|
rowTestId: secondRowTestId,
|
|
indicator: "running",
|
|
});
|
|
await expectWorkspaceRowDoesNotShowIndicator(page, {
|
|
rowTestId: createdRowTestId,
|
|
indicator: "running",
|
|
});
|
|
|
|
await switchSidebarToStatusGrouping(page);
|
|
await expectWorkspaceRowInStatusBucket(page, {
|
|
rowTestId: firstRowTestId,
|
|
bucket: "running",
|
|
});
|
|
await expectWorkspaceRowInStatusBucket(page, {
|
|
rowTestId: secondRowTestId,
|
|
bucket: "done",
|
|
});
|
|
await expectWorkspaceRowInStatusBucket(page, {
|
|
rowTestId: createdRowTestId,
|
|
bucket: "done",
|
|
});
|
|
await expectWorkspaceRowNotInStatusBuckets(page, {
|
|
rowTestId: secondRowTestId,
|
|
buckets: ["running", "needs_input", "attention"],
|
|
});
|
|
await expectWorkspaceRowNotInStatusBuckets(page, {
|
|
rowTestId: createdRowTestId,
|
|
buckets: ["running", "needs_input", "attention"],
|
|
});
|
|
} finally {
|
|
await seeded.cleanup();
|
|
}
|
|
});
|
|
|
|
test("pending permission in one same-directory workspace marks only its own row needing input", async ({
|
|
page,
|
|
}) => {
|
|
const serverId = getServerId();
|
|
const seeded = await seedWorkspace({
|
|
repoPrefix: "workspace-same-cwd-permission-",
|
|
});
|
|
|
|
try {
|
|
const secondWorkspace = await seeded.client.createWorkspace({
|
|
source: { kind: "directory", path: seeded.repoPath, projectId: seeded.projectId },
|
|
title: "Permission sibling workspace",
|
|
});
|
|
if (!secondWorkspace.workspace) {
|
|
throw new Error(secondWorkspace.error ?? "Failed to create same-directory workspace");
|
|
}
|
|
const secondWorkspaceId = secondWorkspace.workspace.id;
|
|
|
|
const agent = await seeded.client.createAgent({
|
|
provider: "mock",
|
|
cwd: seeded.repoPath,
|
|
workspaceId: seeded.workspaceId,
|
|
title: "Permission agent",
|
|
modeId: "load-test",
|
|
model: "ten-second-stream",
|
|
});
|
|
await seeded.client.sendAgentMessage(agent.id, "Emit synthetic plan approval.");
|
|
const parked = await seeded.client.waitForFinish(agent.id, 15_000);
|
|
expect(parked.status).toBe("permission");
|
|
|
|
await gotoWorkspace(page, seeded.workspaceId);
|
|
await waitForSidebarHydration(page);
|
|
|
|
const firstRowTestId = `sidebar-workspace-row-${serverId}:${seeded.workspaceId}`;
|
|
const secondRowTestId = `sidebar-workspace-row-${serverId}:${secondWorkspaceId}`;
|
|
await expectWorkspaceRowHasOnlyIndicator(page, {
|
|
rowTestId: firstRowTestId,
|
|
indicator: "needs_input",
|
|
});
|
|
await expectWorkspaceRowDoesNotShowIndicator(page, {
|
|
rowTestId: secondRowTestId,
|
|
indicator: "needs_input",
|
|
});
|
|
|
|
await openGlobalNewWorkspaceComposer(page);
|
|
await expectNewWorkspaceProjectSelected(page, seeded.projectDisplayName);
|
|
await selectWorkspaceIsolation(page, "local");
|
|
await submitNewWorkspaceEmpty(page);
|
|
|
|
const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, {
|
|
serverId,
|
|
client,
|
|
previousWorkspaceId: seeded.workspaceId,
|
|
projectDisplayName: seeded.projectDisplayName,
|
|
});
|
|
const createdRowTestId = `sidebar-workspace-row-${serverId}:${createdWorkspace.workspaceId}`;
|
|
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
fetchWorkspaceStatuses(client, [
|
|
seeded.workspaceId,
|
|
secondWorkspaceId,
|
|
createdWorkspace.workspaceId,
|
|
]),
|
|
{ timeout: 10_000 },
|
|
)
|
|
.toEqual({
|
|
[seeded.workspaceId]: "needs_input",
|
|
[secondWorkspaceId]: "done",
|
|
[createdWorkspace.workspaceId]: "done",
|
|
});
|
|
|
|
await expectWorkspaceRowHasOnlyIndicator(page, {
|
|
rowTestId: firstRowTestId,
|
|
indicator: "needs_input",
|
|
});
|
|
await expectWorkspaceRowDoesNotShowIndicator(page, {
|
|
rowTestId: secondRowTestId,
|
|
indicator: "needs_input",
|
|
});
|
|
await expectWorkspaceRowDoesNotShowIndicator(page, {
|
|
rowTestId: createdRowTestId,
|
|
indicator: "needs_input",
|
|
});
|
|
|
|
await switchSidebarToStatusGrouping(page);
|
|
await expectWorkspaceRowInStatusBucket(page, {
|
|
rowTestId: firstRowTestId,
|
|
bucket: "needs_input",
|
|
});
|
|
await expectWorkspaceRowInStatusBucket(page, {
|
|
rowTestId: secondRowTestId,
|
|
bucket: "done",
|
|
});
|
|
await expectWorkspaceRowInStatusBucket(page, {
|
|
rowTestId: createdRowTestId,
|
|
bucket: "done",
|
|
});
|
|
await expectWorkspaceRowNotInStatusBuckets(page, {
|
|
rowTestId: secondRowTestId,
|
|
buckets: ["running", "needs_input", "attention"],
|
|
});
|
|
await expectWorkspaceRowNotInStatusBuckets(page, {
|
|
rowTestId: createdRowTestId,
|
|
buckets: ["running", "needs_input", "attention"],
|
|
});
|
|
} finally {
|
|
await seeded.cleanup();
|
|
}
|
|
});
|
|
|
|
test("cross-workspace subagent opens in its workspace and keeps its parent relationship", async ({
|
|
page,
|
|
}) => {
|
|
const serverId = getServerId();
|
|
const seeded = await seedWorkspace({ repoPrefix: "workspace-cross-subagent-" });
|
|
|
|
try {
|
|
const agents = await seedParentWithCrossWorkspaceSubagent(seeded, {
|
|
parentTitle: "Parent agent",
|
|
childTitle: "Cross-workspace subagent",
|
|
});
|
|
|
|
await gotoWorkspace(page, agents.child.workspaceId);
|
|
await waitForSidebarHydration(page);
|
|
await expectWorkspaceTabVisible(page, agents.child.id);
|
|
|
|
const parentRowTestId = `sidebar-workspace-row-${serverId}:${agents.parent.workspaceId}`;
|
|
const childRowTestId = `sidebar-workspace-row-${serverId}:${agents.child.workspaceId}`;
|
|
await expectWorkspaceRowHasOnlyIndicator(page, {
|
|
rowTestId: childRowTestId,
|
|
indicator: "running",
|
|
});
|
|
await expectWorkspaceRowHasOnlyIndicator(page, {
|
|
rowTestId: parentRowTestId,
|
|
indicator: "done",
|
|
});
|
|
|
|
await gotoWorkspace(page, agents.parent.workspaceId);
|
|
await openSubagentsTrack(page);
|
|
await expectSubagentRowVisible(page, agents.child.id);
|
|
} finally {
|
|
await seeded.cleanup();
|
|
}
|
|
});
|
|
});
|