* fix(ci): skip unrelated required checks cleanly Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable. * fix(ci): gate changes to install patching script * fix(ci): route cross-package test contracts narrowly * fix(ci): keep routed contracts merge-blocking * fix(server): exclude nested test utilities from builds * fix(ci): cover app-owned native and browser domains * refactor(ci): align tests with runtime domains Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check. * fix(ci): keep routing and desktop suites isolated The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs. * fix(ci): isolate desktop e2e runtime * perf(test): isolate Playwright workers * perf(test): parallelize Playwright within specs * fix(test): keep Playwright file scheduling * fix(test): share dynamic Metro fixture * fix(test): preserve own-host restart fixtures * fix(ci): preserve routed contracts after rebase * Stabilize Playwright wheel-scroll assertions
223 lines
7.9 KiB
TypeScript
223 lines
7.9 KiB
TypeScript
import { expect, test as base } from "../support/fixtures";
|
|
import { awaitAssistantMessage } from "../support/helpers/agent-stream";
|
|
import {
|
|
expectChatHistoryAttachment,
|
|
expectInFlightForkAvailable,
|
|
expectLiveAssistantText,
|
|
forkInFlightTurnToNewTab,
|
|
forkMostRecentAssistantTurnToNewTab,
|
|
forkMostRecentAssistantTurnToNewWorkspace,
|
|
observeForkAttachment,
|
|
} from "../support/helpers/assistant-fork";
|
|
import { expectComposerVisible, submitMessage } from "../support/helpers/composer";
|
|
import { getE2EDaemonPort } from "../support/helpers/daemon-port";
|
|
import {
|
|
openAgentRoute,
|
|
seedMockAgentWorkspace,
|
|
type MockAgentOptions,
|
|
type MockAgentWorkspace,
|
|
} from "../support/helpers/mock-agent";
|
|
import { getServerId } from "../support/helpers/server-id";
|
|
import { seedSavedSettingsHosts } from "../support/helpers/settings";
|
|
import { submitNewWorkspaceEmpty } from "../support/helpers/new-workspace";
|
|
|
|
const test = base.extend<{
|
|
seedForkWorkspace: (options: MockAgentOptions) => Promise<MockAgentWorkspace>;
|
|
}>({
|
|
seedForkWorkspace: async ({ browserName: _browserName }, provide) => {
|
|
const sessions: MockAgentWorkspace[] = [];
|
|
await provide(async (options) => {
|
|
const session = await seedMockAgentWorkspace(options);
|
|
sessions.push(session);
|
|
return session;
|
|
});
|
|
await Promise.allSettled(sessions.map((session) => session.cleanup()));
|
|
},
|
|
});
|
|
|
|
test.describe("Assistant fork menu", () => {
|
|
test.describe.configure({ timeout: 180_000 });
|
|
|
|
test("forks a failed assistant turn that has no provider message id", async ({
|
|
page,
|
|
seedForkWorkspace,
|
|
}) => {
|
|
const session = await seedForkWorkspace({
|
|
repoPrefix: "assistant-fork-failed-turn-",
|
|
title: "Assistant fork failed turn",
|
|
model: "ten-second-stream",
|
|
});
|
|
|
|
await openAgentRoute(page, session);
|
|
await expectComposerVisible(page);
|
|
await submitMessage(page, "Emit a synthetic turn failure.");
|
|
await expect(page.getByText("[System Error] Requested mock provider failure")).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
|
|
await forkMostRecentAssistantTurnToNewTab(page);
|
|
await expectChatHistoryAttachment(page);
|
|
});
|
|
|
|
test("forks a streaming assistant turn without interrupting it", async ({
|
|
page,
|
|
seedForkWorkspace,
|
|
}) => {
|
|
const visibleBeforeFork = "where the auto-scroll logic actually lives";
|
|
const visibleAfterFork = "the first useful step is to read the relevant files";
|
|
const sourceAgentTitle = "Assistant fork in flight";
|
|
const forkAttachment = observeForkAttachment(page);
|
|
|
|
const session = await seedForkWorkspace({
|
|
repoPrefix: "assistant-fork-in-flight-",
|
|
title: sourceAgentTitle,
|
|
model: "thirty-minute-stream",
|
|
});
|
|
|
|
await openAgentRoute(page, session);
|
|
await expectComposerVisible(page);
|
|
await submitMessage(page, "Walk me through the scroll anchor behavior.");
|
|
|
|
await expectInFlightForkAvailable(page);
|
|
await expectLiveAssistantText(page, visibleBeforeFork);
|
|
|
|
await forkInFlightTurnToNewTab(page);
|
|
await expectChatHistoryAttachment(page);
|
|
expect(await forkAttachment.waitForText()).toContain(visibleBeforeFork);
|
|
|
|
await page.getByRole("button", { name: sourceAgentTitle }).click();
|
|
await expectLiveAssistantText(page, visibleAfterFork);
|
|
});
|
|
|
|
test("focuses a forked assistant turn in a new workspace draft tab", async ({
|
|
page,
|
|
seedForkWorkspace,
|
|
}) => {
|
|
const session = await seedForkWorkspace({
|
|
repoPrefix: "assistant-fork-focused-tab-",
|
|
title: "Assistant fork focused tab",
|
|
initialPrompt: "emit 1 coalesced agent stream updates for initial assistant fork turn.",
|
|
model: "ten-second-stream",
|
|
});
|
|
|
|
await openAgentRoute(page, session);
|
|
await expectComposerVisible(page);
|
|
await awaitAssistantMessage(page);
|
|
await session.client.waitForFinish(session.agentId, 45_000);
|
|
|
|
await submitMessage(page, "emit 1 coalesced agent stream updates while this tab is visible.");
|
|
await session.client.waitForFinish(session.agentId, 45_000);
|
|
await awaitAssistantMessage(page);
|
|
|
|
const agentTab = page.getByTestId(`workspace-tab-agent_${session.agentId}`);
|
|
await expect(agentTab).toHaveAttribute("aria-selected", "true");
|
|
|
|
await forkMostRecentAssistantTurnToNewTab(page);
|
|
|
|
const selectedTab = page
|
|
.getByTestId("workspace-tabs-row")
|
|
.getByRole("button")
|
|
.and(page.locator('[aria-selected="true"]'));
|
|
await expect(selectedTab).toHaveAttribute("data-testid", /^workspace-tab-draft_/, {
|
|
timeout: 30_000,
|
|
});
|
|
await expect(agentTab).toHaveAttribute("aria-selected", "false");
|
|
await expectChatHistoryAttachment(page);
|
|
});
|
|
|
|
test("keeps the fork attachment after submitting an existing-workspace draft tab", async ({
|
|
page,
|
|
seedForkWorkspace,
|
|
}) => {
|
|
const session = await seedForkWorkspace({
|
|
repoPrefix: "assistant-fork-tab-submit-",
|
|
title: "Assistant fork tab submit",
|
|
initialPrompt: "emit 1 coalesced agent stream updates for assistant fork tab submit.",
|
|
model: "ten-second-stream",
|
|
});
|
|
|
|
await openAgentRoute(page, session);
|
|
await expectComposerVisible(page);
|
|
await awaitAssistantMessage(page);
|
|
await session.client.waitForFinish(session.agentId, 45_000);
|
|
|
|
await forkMostRecentAssistantTurnToNewTab(page);
|
|
await expectChatHistoryAttachment(page);
|
|
|
|
await submitMessage(page, "");
|
|
|
|
const userMessage = page.getByTestId("user-message").filter({ hasText: "Chat history" }).last();
|
|
await expect(userMessage).toBeVisible({ timeout: 30_000 });
|
|
await expect(userMessage).not.toContainText("Source agent:");
|
|
});
|
|
|
|
test("forks an assistant turn into New Workspace and keeps the attachment across host changes", async ({
|
|
page,
|
|
seedForkWorkspace,
|
|
}) => {
|
|
await seedSavedSettingsHosts(page, [
|
|
{
|
|
serverId: getServerId(),
|
|
label: "localhost",
|
|
endpoint: `127.0.0.1:${getE2EDaemonPort()}`,
|
|
},
|
|
{
|
|
serverId: "secondary-assistant-fork-host",
|
|
label: "Secondary host",
|
|
// The host does not need to be reachable; this pins that the draft-scoped
|
|
// attachment survives changing the selected target host.
|
|
endpoint: "127.0.0.1:9",
|
|
},
|
|
]);
|
|
|
|
const session = await seedForkWorkspace({
|
|
repoPrefix: "assistant-fork-workspace-",
|
|
title: "Assistant fork workspace",
|
|
initialPrompt: "emit 1 coalesced agent stream updates for assistant fork new workspace.",
|
|
model: "ten-second-stream",
|
|
});
|
|
|
|
await openAgentRoute(page, session);
|
|
await expectComposerVisible(page);
|
|
await awaitAssistantMessage(page);
|
|
await session.client.waitForFinish(session.agentId, 45_000);
|
|
|
|
await forkMostRecentAssistantTurnToNewWorkspace(page);
|
|
|
|
await expect(page).toHaveURL(/\/new\?.*draftId=/, { timeout: 30_000 });
|
|
await expectChatHistoryAttachment(page);
|
|
|
|
await page.getByTestId("host-picker-trigger").click();
|
|
await page
|
|
.getByTestId("new-workspace-host-picker-option-secondary-assistant-fork-host")
|
|
.click();
|
|
await expectChatHistoryAttachment(page);
|
|
});
|
|
|
|
test("keeps the fork attachment after the new agent receives its user message", async ({
|
|
page,
|
|
seedForkWorkspace,
|
|
}) => {
|
|
const session = await seedForkWorkspace({
|
|
repoPrefix: "assistant-fork-submit-",
|
|
title: "Assistant fork submit",
|
|
initialPrompt: "emit 1 coalesced agent stream updates for assistant fork submit.",
|
|
model: "ten-second-stream",
|
|
});
|
|
|
|
await openAgentRoute(page, session);
|
|
await expectComposerVisible(page);
|
|
await awaitAssistantMessage(page);
|
|
await session.client.waitForFinish(session.agentId, 45_000);
|
|
|
|
await forkMostRecentAssistantTurnToNewWorkspace(page);
|
|
await expectChatHistoryAttachment(page);
|
|
|
|
await submitNewWorkspaceEmpty(page);
|
|
|
|
const userMessage = page.getByTestId("user-message").filter({ hasText: "Chat history" }).last();
|
|
await expect(userMessage).toBeVisible({ timeout: 30_000 });
|
|
await expect(userMessage).not.toContainText("Source agent:");
|
|
});
|
|
});
|