Skip unrelated required checks without losing their names (#2542)
* 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
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
|
||||
import { test } from "../support/fixtures";
|
||||
import { seedWorkspace, type SeedDaemonClient } from "../support/helpers/seed-client";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { observeTimelineSubscriptions } from "../support/helpers/timeline-delivery";
|
||||
import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs";
|
||||
import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
|
||||
import {
|
||||
expectAgentIdle,
|
||||
expectInlineWorkingIndicator,
|
||||
expectTurnCopyButton,
|
||||
} from "../support/helpers/agent-stream";
|
||||
import {
|
||||
expectReconnectingToastGone,
|
||||
expectReconnectingToastVisible,
|
||||
} from "../support/helpers/workspace-ui";
|
||||
|
||||
interface ViewedTimelineScenario {
|
||||
client: SeedDaemonClient;
|
||||
workspaceId: string;
|
||||
firstAgentId: string;
|
||||
secondAgentId: string;
|
||||
cleanup(): Promise<void>;
|
||||
}
|
||||
|
||||
async function seedViewedTimelineScenario(
|
||||
options: { firstAgentModel?: string } = {},
|
||||
): Promise<ViewedTimelineScenario> {
|
||||
const workspace = await seedWorkspace({ repoPrefix: "viewed-timelines-" });
|
||||
const createAgent = (title: string, model = "ten-second-stream") =>
|
||||
workspace.client.createAgent({
|
||||
provider: "mock",
|
||||
cwd: workspace.repoPath,
|
||||
workspaceId: workspace.workspaceId,
|
||||
title,
|
||||
modeId: "load-test",
|
||||
model,
|
||||
});
|
||||
const [firstAgent, secondAgent] = await Promise.all([
|
||||
createAgent("First viewed chat", options.firstAgentModel),
|
||||
createAgent("Second viewed chat"),
|
||||
]);
|
||||
return {
|
||||
client: workspace.client,
|
||||
workspaceId: workspace.workspaceId,
|
||||
firstAgentId: firstAgent.id,
|
||||
secondAgentId: secondAgent.id,
|
||||
cleanup: workspace.cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
async function openAgent(page: Page, scenario: ViewedTimelineScenario, agentId: string) {
|
||||
await page.goto(buildHostAgentDetailRoute(getServerId(), agentId, scenario.workspaceId));
|
||||
await page.waitForURL(
|
||||
(url) => url.pathname.includes("/workspace/") && !url.searchParams.has("open"),
|
||||
);
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
}
|
||||
|
||||
async function selectAgent(page: Page, title: string) {
|
||||
await page.getByRole("button", { name: title, exact: true }).click();
|
||||
}
|
||||
|
||||
async function enableMoveTabShortcut(page: Page) {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, "platform", { get: () => "MacIntel" });
|
||||
});
|
||||
}
|
||||
|
||||
async function moveActiveTabRight(page: Page) {
|
||||
await page.keyboard.press("Meta+Alt+Shift+ArrowRight");
|
||||
}
|
||||
|
||||
async function commitMessage(scenario: ViewedTimelineScenario, agentId: string, prompt: string) {
|
||||
await scenario.client.sendAgentMessage(agentId, prompt);
|
||||
const finish = await scenario.client.waitForFinish(agentId, 30_000);
|
||||
expect(finish.status).toBe("idle");
|
||||
}
|
||||
|
||||
async function startVisibleTurn(
|
||||
page: Page,
|
||||
scenario: ViewedTimelineScenario,
|
||||
prompt: string,
|
||||
): Promise<void> {
|
||||
await scenario.client.sendAgentMessage(scenario.firstAgentId, prompt);
|
||||
await expect(page.getByText(prompt, { exact: true })).toBeVisible();
|
||||
await expectInlineWorkingIndicator(page);
|
||||
}
|
||||
|
||||
async function expectAgentConsistentlyIdle(page: Page, title: string): Promise<void> {
|
||||
const tab = page.getByRole("button", { name: title, exact: true });
|
||||
await expect(tab.locator('[data-status-bucket="running"]')).toHaveCount(0);
|
||||
await expectAgentIdle(page);
|
||||
await expect(page.getByTestId("turn-working-indicator")).toHaveCount(0);
|
||||
await expectTurnCopyButton(page);
|
||||
}
|
||||
|
||||
test.describe("Viewed agent timelines", () => {
|
||||
test("a turn that finishes while unsubscribed reopens with consistently idle chrome", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(150_000);
|
||||
const subscriptions = observeTimelineSubscriptions(page);
|
||||
const scenario = await seedViewedTimelineScenario({ firstAgentModel: "one-minute-stream" });
|
||||
try {
|
||||
await openAgent(page, scenario, scenario.firstAgentId);
|
||||
await startVisibleTurn(page, scenario, "Finish after this chat becomes unsubscribed.");
|
||||
await selectAgent(page, "Second viewed chat");
|
||||
await subscriptions.waitForSubscribedAgents([scenario.secondAgentId], { timeout: 45_000 });
|
||||
const finish = await scenario.client.waitForFinish(scenario.firstAgentId, 90_000);
|
||||
expect(finish.status).toBe("idle");
|
||||
|
||||
await selectAgent(page, "First viewed chat");
|
||||
await expectAgentConsistentlyIdle(page, "First viewed chat");
|
||||
} finally {
|
||||
await scenario.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("an unsubscribed hidden chat catches up when shown", async ({ page }) => {
|
||||
test.setTimeout(90_000);
|
||||
const subscriptions = observeTimelineSubscriptions(page);
|
||||
const scenario = await seedViewedTimelineScenario();
|
||||
try {
|
||||
await openAgent(page, scenario, scenario.firstAgentId);
|
||||
await selectAgent(page, "Second viewed chat");
|
||||
await subscriptions.waitForSubscribedAgents([scenario.secondAgentId], { timeout: 45_000 });
|
||||
await commitMessage(
|
||||
scenario,
|
||||
scenario.firstAgentId,
|
||||
"Committed after the first chat unsubscribed.",
|
||||
);
|
||||
await expect(
|
||||
page.getByText("Committed after the first chat unsubscribed.", { exact: true }),
|
||||
).toHaveCount(0);
|
||||
await selectAgent(page, "First viewed chat");
|
||||
await expect(
|
||||
page.getByText("Committed after the first chat unsubscribed.", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("(end of synthetic stream)", { exact: true })).toBeVisible();
|
||||
} finally {
|
||||
await scenario.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("a hidden retained chat stays current during unsubscribe grace", async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
const subscriptions = observeTimelineSubscriptions(page);
|
||||
const scenario = await seedViewedTimelineScenario();
|
||||
try {
|
||||
await openAgent(page, scenario, scenario.firstAgentId);
|
||||
await selectAgent(page, "Second viewed chat");
|
||||
await subscriptions.waitForSubscribedAgents([scenario.firstAgentId, scenario.secondAgentId]);
|
||||
await commitMessage(
|
||||
scenario,
|
||||
scenario.firstAgentId,
|
||||
"Committed while the first chat is hidden.",
|
||||
);
|
||||
await expect(
|
||||
page.getByText("Committed while the first chat is hidden.", { exact: true }),
|
||||
).toHaveCount(0);
|
||||
await selectAgent(page, "First viewed chat");
|
||||
await expect(
|
||||
page.getByText("Committed while the first chat is hidden.", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("(end of synthetic stream)", { exact: true })).toBeVisible();
|
||||
} finally {
|
||||
await scenario.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("two visible split chats both stay current", async ({ page }) => {
|
||||
const scenario = await seedViewedTimelineScenario();
|
||||
try {
|
||||
await enableMoveTabShortcut(page);
|
||||
await openAgent(page, scenario, scenario.firstAgentId);
|
||||
await page.getByRole("button", { name: "Split pane right" }).click();
|
||||
await selectAgent(page, "Second viewed chat");
|
||||
await moveActiveTabRight(page);
|
||||
await expect(
|
||||
page.getByRole("button", { name: "First viewed chat", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Second viewed chat", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("textbox", { name: "Message agent..." })).toHaveCount(2);
|
||||
await commitMessage(scenario, scenario.firstAgentId, "First visible pane update.");
|
||||
await expect(page.getByText("First visible pane update.", { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Second viewed chat", exact: true }),
|
||||
).toBeVisible();
|
||||
} finally {
|
||||
await scenario.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("a visible chat catches up after reconnecting", async ({ page }) => {
|
||||
const gate = await installDaemonWebSocketGate(page);
|
||||
const scenario = await seedViewedTimelineScenario();
|
||||
try {
|
||||
await openAgent(page, scenario, scenario.firstAgentId);
|
||||
await expect(page.getByRole("button", { name: "First viewed chat" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
await gate.drop();
|
||||
await expectReconnectingToastVisible(page);
|
||||
await commitMessage(scenario, scenario.firstAgentId, "Committed while the chat reconnects.");
|
||||
await expect(
|
||||
page.getByText("Committed while the chat reconnects.", { exact: true }),
|
||||
).toHaveCount(0);
|
||||
gate.restore();
|
||||
await expectReconnectingToastGone(page);
|
||||
const recoveredMessage = page.getByText("Committed while the chat reconnects.", {
|
||||
exact: true,
|
||||
});
|
||||
await expect(recoveredMessage).toHaveCount(1);
|
||||
await expect(recoveredMessage).toBeVisible();
|
||||
} finally {
|
||||
gate.restore();
|
||||
await scenario.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user