mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 08:24:46 +00:00
test(app): migrate browser fixtures to owned subscriptions
This commit is contained in:
@@ -1,86 +0,0 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { metroTest as test } from "../support/fixtures";
|
||||
import { buildCreateAgentPreferences, buildSeededHost } from "../support/helpers/daemon-registry";
|
||||
import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon";
|
||||
import type { MockAgentWorkspace } from "../support/helpers/mock-agent";
|
||||
import { seedWorkspace } from "../support/helpers/seed-client";
|
||||
import {
|
||||
expectTimelinePromptVisible,
|
||||
holdOlderHistoryPages,
|
||||
openAgentTimeline,
|
||||
scrollThroughOlderHistoryPages,
|
||||
} from "../support/helpers/timeline-pagination";
|
||||
|
||||
test("does not repeat an assistant block when the current app paginates a published 0.2.5 daemon", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(120_000);
|
||||
const serverId = `srv_old_pagination_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
||||
const daemon = await startIsolatedHostDaemon(serverId, { publishedVersion: "0.2.5" });
|
||||
const workspace = await seedWorkspace({
|
||||
repoPrefix: "timeline-old-daemon-pagination-",
|
||||
port: daemon.port,
|
||||
});
|
||||
const createdAgent = await workspace.client.createAgent({
|
||||
provider: "mock",
|
||||
cwd: workspace.repoPath,
|
||||
workspaceId: workspace.workspaceId,
|
||||
title: "Published daemon pagination regression",
|
||||
modeId: "load-test",
|
||||
model: "ten-second-stream",
|
||||
});
|
||||
const agent: MockAgentWorkspace = {
|
||||
agentId: createdAgent.id,
|
||||
workspaceId: workspace.workspaceId,
|
||||
cwd: workspace.repoPath,
|
||||
client: workspace.client,
|
||||
cleanup: workspace.cleanup,
|
||||
};
|
||||
|
||||
try {
|
||||
for (let index = 0; index < 40; index += 1) {
|
||||
await agent.client.sendAgentMessage(
|
||||
agent.agentId,
|
||||
`timeline-pagination-older-turn-${index}: emit 1 coalesced agent stream updates`,
|
||||
);
|
||||
await agent.client.waitForFinish(agent.agentId, 15_000);
|
||||
}
|
||||
await agent.client.sendAgentMessage(agent.agentId, "build a realistic long mock timeline");
|
||||
await agent.client.waitForFinish(agent.agentId, 20_000);
|
||||
for (let index = 0; index < 20; index += 1) {
|
||||
await agent.client.sendAgentMessage(
|
||||
agent.agentId,
|
||||
`timeline-pagination-turn-${index}: emit 1 coalesced agent stream updates`,
|
||||
);
|
||||
await agent.client.waitForFinish(agent.agentId, 15_000);
|
||||
}
|
||||
|
||||
const host = buildSeededHost({
|
||||
serverId,
|
||||
endpoint: `127.0.0.1:${daemon.port}`,
|
||||
nowIso: new Date().toISOString(),
|
||||
});
|
||||
await page.addInitScript(
|
||||
({ seededHost, preferences }) => {
|
||||
localStorage.setItem("@paseo:e2e", "1");
|
||||
localStorage.setItem("@paseo:daemon-registry", JSON.stringify([seededHost]));
|
||||
localStorage.setItem("@paseo:create-agent-preferences", JSON.stringify(preferences));
|
||||
},
|
||||
{ seededHost: host, preferences: buildCreateAgentPreferences() },
|
||||
);
|
||||
|
||||
const history = await holdOlderHistoryPages(page, agent, daemon.port);
|
||||
await openAgentTimeline(page, agent, serverId);
|
||||
await expectTimelinePromptVisible(
|
||||
page,
|
||||
"timeline-pagination-turn-19: emit 1 coalesced agent stream updates",
|
||||
);
|
||||
await scrollThroughOlderHistoryPages(page, 3, history);
|
||||
|
||||
history.expectRepeatedEntries();
|
||||
await history.expectOwnedTextRendered("Now I have a clearer picture.");
|
||||
} finally {
|
||||
await agent.cleanup();
|
||||
await daemon.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { metroTest as test, expect } from "../support/fixtures";
|
||||
import { buildCreateAgentPreferences, buildSeededHost } from "../support/helpers/daemon-registry";
|
||||
import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon";
|
||||
import { buildAgentRoute } from "../support/helpers/mock-agent";
|
||||
import { seedWorkspace } from "../support/helpers/seed-client";
|
||||
|
||||
test("requires a host update before observing a published 0.2.5 daemon", async ({ page }) => {
|
||||
test.setTimeout(120_000);
|
||||
const serverId = `srv_old_pagination_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
||||
const daemon = await startIsolatedHostDaemon(serverId, { publishedVersion: "0.2.5" });
|
||||
const workspace = await seedWorkspace({
|
||||
repoPrefix: "timeline-old-daemon-pagination-",
|
||||
port: daemon.port,
|
||||
});
|
||||
const createdAgent = await workspace.client.createAgent({
|
||||
provider: "mock",
|
||||
cwd: workspace.repoPath,
|
||||
workspaceId: workspace.workspaceId,
|
||||
title: "Published daemon pagination regression",
|
||||
modeId: "load-test",
|
||||
model: "ten-second-stream",
|
||||
});
|
||||
const requests: unknown[] = [];
|
||||
page.on("websocket", (socket) =>
|
||||
socket.on("framesent", ({ payload }) => {
|
||||
if (typeof payload !== "string") return;
|
||||
const frame = JSON.parse(payload);
|
||||
if (frame.type === "session" && frame.message.type !== "ping") requests.push(frame.message);
|
||||
}),
|
||||
);
|
||||
|
||||
try {
|
||||
const host = buildSeededHost({
|
||||
serverId,
|
||||
endpoint: `127.0.0.1:${daemon.port}`,
|
||||
nowIso: new Date().toISOString(),
|
||||
});
|
||||
await page.addInitScript(
|
||||
({ seededHost, preferences }) => {
|
||||
localStorage.setItem("@paseo:e2e", "1");
|
||||
localStorage.setItem("@paseo:daemon-registry", JSON.stringify([seededHost]));
|
||||
localStorage.setItem("@paseo:create-agent-preferences", JSON.stringify(preferences));
|
||||
},
|
||||
{ seededHost: host, preferences: buildCreateAgentPreferences() },
|
||||
);
|
||||
|
||||
await page.goto(buildAgentRoute(workspace.workspaceId, createdAgent.id, serverId));
|
||||
const updateHost = page.getByText("Update the host to use this version of Paseo.", {
|
||||
exact: true,
|
||||
});
|
||||
await expect(updateHost).toHaveCount(1);
|
||||
await expect(updateHost).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Manage host", exact: true })).toBeVisible();
|
||||
expect(requests).toEqual([]);
|
||||
await page.reload();
|
||||
await expect(updateHost).toHaveCount(1);
|
||||
await expect(updateHost).toBeVisible();
|
||||
expect(requests).toEqual([]);
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
await daemon.close();
|
||||
}
|
||||
});
|
||||
@@ -14,6 +14,8 @@ async function openExample(page: Page, example: string) {
|
||||
|
||||
async function closeExample(page: Page) {
|
||||
await page.getByRole("button", { name: "Close", exact: true }).last().click();
|
||||
// The launcher is visible behind a closing sheet; wait for dismissal before reopening.
|
||||
await expect(page.getByText(/^Modal example:/)).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "Open Form", exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { expect, test } from "../support/fixtures";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import {
|
||||
addFakeScheduleHostAndReload,
|
||||
buildFakeScheduleHostWorkspace,
|
||||
FAKE_HOST_MODEL_ID,
|
||||
FAKE_HOST_MODEL_LABEL,
|
||||
installFakeScheduleHost,
|
||||
} from "../support/helpers/schedule-fake-host";
|
||||
addScheduleHostAndReload,
|
||||
createScheduleHost,
|
||||
SECONDARY_MODEL_ID,
|
||||
SECONDARY_MODEL_LABEL,
|
||||
} from "../support/helpers/schedule-host";
|
||||
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
||||
import { expectSettled, expectStableHeight } from "../support/helpers/settled";
|
||||
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
||||
@@ -32,7 +31,11 @@ interface ScheduleSeedClient {
|
||||
scheduleDelete(input: { id: string }): Promise<{ error: string | null }>;
|
||||
}
|
||||
|
||||
async function seedMockSchedule(workspace: SeededWorkspace, name: string): Promise<string> {
|
||||
async function seedMockSchedule(
|
||||
workspace: SeededWorkspace,
|
||||
name: string,
|
||||
model = "ten-second-stream",
|
||||
): Promise<string> {
|
||||
const client = workspace.client as unknown as ScheduleSeedClient;
|
||||
const result = await client.scheduleCreate({
|
||||
prompt: "Say hello from the scheduled agent.",
|
||||
@@ -43,7 +46,7 @@ async function seedMockSchedule(workspace: SeededWorkspace, name: string): Promi
|
||||
config: {
|
||||
provider: "mock",
|
||||
cwd: workspace.repoPath,
|
||||
model: "ten-second-stream",
|
||||
model,
|
||||
modeId: "load-test",
|
||||
title: name,
|
||||
},
|
||||
@@ -66,42 +69,6 @@ async function deleteSeededSchedule(workspace: SeededWorkspace, id: string): Pro
|
||||
.catch(ignoreScheduleDeleteError);
|
||||
}
|
||||
|
||||
type FakeScheduleHostSchedule = NonNullable<
|
||||
Parameters<typeof installFakeScheduleHost>[0]["schedules"]
|
||||
>[number];
|
||||
|
||||
function buildFakeHostSchedule(input: {
|
||||
id: string;
|
||||
name: string;
|
||||
cwd: string;
|
||||
}): FakeScheduleHostSchedule {
|
||||
const now = "2026-07-01T00:00:00.000Z";
|
||||
return {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
prompt: "Run on the secondary host.",
|
||||
cadence: { type: "cron", expression: "0 9 * * *" },
|
||||
target: {
|
||||
type: "new-agent",
|
||||
config: {
|
||||
provider: "mock",
|
||||
cwd: input.cwd,
|
||||
model: FAKE_HOST_MODEL_ID,
|
||||
modeId: "load-test",
|
||||
title: input.name,
|
||||
},
|
||||
},
|
||||
status: "active",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
nextRunAt: now,
|
||||
lastRunAt: null,
|
||||
pausedAt: null,
|
||||
expiresAt: null,
|
||||
maxRuns: null,
|
||||
};
|
||||
}
|
||||
|
||||
test.describe("Schedules", () => {
|
||||
const cleanupTasks: Array<() => Promise<void>> = [];
|
||||
|
||||
@@ -154,34 +121,23 @@ test.describe("Schedules", () => {
|
||||
test("edit form hydrates a non-default host schedule after reload", async ({ page }) => {
|
||||
const workspace = await seedWorkspace({ repoPrefix: "schedule-host-b-hydration-" });
|
||||
cleanupTasks.push(() => workspace.cleanup());
|
||||
const fakeHost = await buildFakeScheduleHostWorkspace(workspace);
|
||||
const fakePort = String(59_000 + Math.floor(Math.random() * 900));
|
||||
const scheduleId = "fake-host-schedule";
|
||||
const scheduleName = "Secondary host schedule";
|
||||
|
||||
await installFakeScheduleHost({
|
||||
page,
|
||||
port: fakePort,
|
||||
serverId: fakeHost.serverId,
|
||||
workspace: fakeHost.workspace,
|
||||
project: fakeHost,
|
||||
schedules: [
|
||||
buildFakeHostSchedule({
|
||||
id: scheduleId,
|
||||
name: scheduleName,
|
||||
cwd: String(fakeHost.workspace.workspaceDirectory),
|
||||
}),
|
||||
],
|
||||
});
|
||||
const secondary = await createScheduleHost();
|
||||
cleanupTasks.push(() => secondary.cleanup());
|
||||
const scheduleId = await seedMockSchedule(
|
||||
secondary,
|
||||
"Secondary host schedule",
|
||||
SECONDARY_MODEL_ID,
|
||||
);
|
||||
cleanupTasks.push(() => deleteSeededSchedule(secondary, scheduleId));
|
||||
|
||||
await gotoAppShell(page);
|
||||
await waitForSidebarHydration(page);
|
||||
await page.goto(buildSchedulesRoute());
|
||||
await addFakeScheduleHostAndReload({
|
||||
await addScheduleHostAndReload({
|
||||
page,
|
||||
serverId: fakeHost.serverId,
|
||||
label: "Fake host",
|
||||
port: fakePort,
|
||||
serverId: secondary.serverId,
|
||||
label: secondary.label,
|
||||
port: secondary.port,
|
||||
});
|
||||
await page.reload();
|
||||
|
||||
@@ -197,14 +153,14 @@ test.describe("Schedules", () => {
|
||||
const modelTrigger = page.getByTestId("schedule-model-trigger");
|
||||
const modeTrigger = page.getByTestId("schedule-mode-trigger");
|
||||
|
||||
await expect(hostTrigger).toContainText("Fake host", { timeout: 30_000 });
|
||||
await expect(hostTrigger).toContainText(secondary.label, { timeout: 30_000 });
|
||||
await expect(hostTrigger).toBeDisabled();
|
||||
await expectSettled(hostTrigger);
|
||||
await expect(projectTrigger).toContainText(fakeHost.projectDisplayName, { timeout: 30_000 });
|
||||
await expect(projectTrigger).toContainText(secondary.projectDisplayName, { timeout: 30_000 });
|
||||
await expectSettled(projectTrigger);
|
||||
await expect(modelTrigger).toContainText(FAKE_HOST_MODEL_LABEL, { timeout: 30_000 });
|
||||
await expect(modelTrigger).toContainText(SECONDARY_MODEL_LABEL, { timeout: 30_000 });
|
||||
await expectSettled(modelTrigger);
|
||||
await expect(modeTrigger).toContainText("Load test", { timeout: 30_000 });
|
||||
await expect(modeTrigger).toContainText("Load Test", { timeout: 30_000 });
|
||||
await expectSettled(modeTrigger);
|
||||
await expect(page.getByTestId("cadence-mode")).toHaveCount(0);
|
||||
await expect(page.getByTestId("schedule-cadence-preset-trigger")).toContainText("Daily 9:00");
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { expect, test, type Page } from "../support/fixtures";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import {
|
||||
addFakeScheduleHostAndReload,
|
||||
buildFakeScheduleHostWorkspace,
|
||||
installFakeScheduleHost,
|
||||
} from "../support/helpers/schedule-fake-host";
|
||||
import { addScheduleHostAndReload, createScheduleHost } from "../support/helpers/schedule-host";
|
||||
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { projectEquivalenceViewKey } from "../support/helpers/project-view-key";
|
||||
@@ -242,20 +238,13 @@ test.describe("Schedules project target", () => {
|
||||
cadenceExpression: "0 9 * * *",
|
||||
});
|
||||
|
||||
const fakeHost = await buildFakeScheduleHostWorkspace(workspace);
|
||||
const fakePort = String(59_000 + Math.floor(Math.random() * 900));
|
||||
await installFakeScheduleHost({
|
||||
const secondary = await createScheduleHost();
|
||||
cleanupTasks.push(() => secondary.cleanup());
|
||||
await addScheduleHostAndReload({
|
||||
page,
|
||||
port: fakePort,
|
||||
serverId: fakeHost.serverId,
|
||||
workspace: fakeHost.workspace,
|
||||
project: fakeHost,
|
||||
});
|
||||
await addFakeScheduleHostAndReload({
|
||||
page,
|
||||
serverId: fakeHost.serverId,
|
||||
label: "Fake host",
|
||||
port: fakePort,
|
||||
serverId: secondary.serverId,
|
||||
label: secondary.label,
|
||||
port: secondary.port,
|
||||
});
|
||||
|
||||
const hostFilterTrigger = page.getByTestId("schedules-host-filter-trigger");
|
||||
@@ -274,25 +263,17 @@ test.describe("Schedules project target", () => {
|
||||
const workspace = await seedWorkspace({ repoPrefix: "schedule-project-host-model-" });
|
||||
cleanupTasks.push(() => workspace.cleanup());
|
||||
const serverId = getServerId();
|
||||
const fakeHost = await buildFakeScheduleHostWorkspace(workspace);
|
||||
const fakePort = String(59_000 + Math.floor(Math.random() * 900));
|
||||
|
||||
await installFakeScheduleHost({
|
||||
page,
|
||||
port: fakePort,
|
||||
serverId: fakeHost.serverId,
|
||||
workspace: fakeHost.workspace,
|
||||
project: fakeHost,
|
||||
});
|
||||
const secondary = await createScheduleHost();
|
||||
cleanupTasks.push(() => secondary.cleanup());
|
||||
|
||||
await gotoAppShell(page);
|
||||
await waitForSidebarHydration(page);
|
||||
await page.goto(buildSchedulesRoute());
|
||||
await addFakeScheduleHostAndReload({
|
||||
await addScheduleHostAndReload({
|
||||
page,
|
||||
serverId: fakeHost.serverId,
|
||||
label: "Fake host",
|
||||
port: fakePort,
|
||||
serverId: secondary.serverId,
|
||||
label: secondary.label,
|
||||
port: secondary.port,
|
||||
});
|
||||
await expect(page.getByTestId("schedules-empty")).toBeVisible({ timeout: 30_000 });
|
||||
await page.getByTestId("schedules-empty-new").click();
|
||||
@@ -334,8 +315,8 @@ test.describe("Schedules project target", () => {
|
||||
await expectSettled(modeTrigger);
|
||||
|
||||
await hostTrigger.click();
|
||||
await page.getByTestId(`schedule-host-option-${fakeHost.serverId}`).click();
|
||||
await expect(hostTrigger).toContainText("Fake host");
|
||||
await page.getByTestId(`schedule-host-option-${secondary.serverId}`).click();
|
||||
await expect(hostTrigger).toContainText(secondary.label);
|
||||
await expect(projectTrigger).toContainText(/select project/i);
|
||||
await expect(modelTrigger).toHaveCount(0);
|
||||
await expect(thinkingTrigger).toHaveCount(0);
|
||||
@@ -345,9 +326,9 @@ test.describe("Schedules project target", () => {
|
||||
|
||||
await projectTrigger.click();
|
||||
await page
|
||||
.getByTestId(`schedule-project-option-${projectEquivalenceViewKey(fakeHost.projectKey)}`)
|
||||
.getByTestId(`schedule-project-option-${projectEquivalenceViewKey(secondary.projectKey)}`)
|
||||
.click();
|
||||
await expect(projectTrigger).toContainText(fakeHost.projectDisplayName);
|
||||
await expect(projectTrigger).toContainText(secondary.projectDisplayName);
|
||||
await expectSettled(projectTrigger);
|
||||
await expect(modelTrigger).toContainText(/select model/i);
|
||||
await expectSettled(modelTrigger);
|
||||
@@ -355,7 +336,7 @@ test.describe("Schedules project target", () => {
|
||||
await expect(modeTrigger).toHaveCount(0);
|
||||
|
||||
await page.getByLabel("Schedule name").fill(`Cross host model ${Date.now()}`);
|
||||
await page.getByLabel("Prompt").fill("Run on the fake host project.");
|
||||
await page.getByLabel("Prompt").fill("Run on the secondary host project.");
|
||||
await expect(page.getByRole("button", { name: "Create schedule" })).toBeDisabled();
|
||||
});
|
||||
|
||||
|
||||
@@ -87,8 +87,7 @@ test.describe("Workspace setup streaming", () => {
|
||||
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).
|
||||
// The setup client owns progress before creating the workspace.
|
||||
const completed = waitForWorkspaceSetupProgress(
|
||||
client,
|
||||
(payload) =>
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
import { type Page } from "@playwright/test";
|
||||
import { buildSeededHost } from "./daemon-registry";
|
||||
import { wsRoutePatternForPort } from "./daemon-port";
|
||||
import { type SeededWorkspace } from "./seed-client";
|
||||
|
||||
const REGISTRY_KEY = "@paseo:daemon-registry";
|
||||
const SEED_NONCE_KEY = "@paseo:e2e-seed-nonce";
|
||||
const EXTRA_HOSTS_KEY = "@paseo:e2e-extra-hosts";
|
||||
export const FAKE_HOST_MODEL_ID = "fake-host-model";
|
||||
export const FAKE_HOST_MODEL_LABEL = "Fake host model";
|
||||
export const FAKE_HOST_PROJECT_DISPLAY_NAME = "Fake host project";
|
||||
|
||||
type WebSocketMessage = string | Buffer;
|
||||
type SessionRequest = Record<string, unknown> & { type?: string; requestId?: string };
|
||||
|
||||
export interface FakeScheduleHostWorkspace {
|
||||
serverId: string;
|
||||
projectId: string;
|
||||
projectKey: string;
|
||||
projectDisplayName: string;
|
||||
workspace: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface FakeScheduleSummary {
|
||||
id: string;
|
||||
name: string | null;
|
||||
prompt: string;
|
||||
cadence: { type: "cron"; expression: string };
|
||||
target: {
|
||||
type: "new-agent";
|
||||
config: {
|
||||
provider: "mock";
|
||||
cwd: string;
|
||||
model: string;
|
||||
modeId: string;
|
||||
title?: string;
|
||||
};
|
||||
};
|
||||
status: "active";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
nextRunAt: string | null;
|
||||
lastRunAt: string | null;
|
||||
pausedAt: string | null;
|
||||
expiresAt: string | null;
|
||||
maxRuns: number | null;
|
||||
}
|
||||
|
||||
function parseJson(message: WebSocketMessage): unknown {
|
||||
const raw = typeof message === "string" ? message : message.toString("utf8");
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildSessionMessage(type: string, payload: Record<string, unknown>) {
|
||||
return JSON.stringify({
|
||||
type: "session",
|
||||
message: {
|
||||
type,
|
||||
payload,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function buildFakeProviderEntries(nowIso: string) {
|
||||
return [
|
||||
{
|
||||
provider: "mock",
|
||||
label: "Mock",
|
||||
status: "ready",
|
||||
enabled: true,
|
||||
fetchedAt: nowIso,
|
||||
models: [
|
||||
{
|
||||
provider: "mock",
|
||||
id: FAKE_HOST_MODEL_ID,
|
||||
label: FAKE_HOST_MODEL_LABEL,
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
modes: [{ id: "load-test", label: "Load test" }],
|
||||
defaultModeId: "load-test",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function readSessionRequest(message: WebSocketMessage): SessionRequest | null {
|
||||
const parsed = parseJson(message);
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const envelope = parsed as { type?: string; message?: SessionRequest };
|
||||
if (envelope.type !== "session" || !envelope.message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return envelope.message;
|
||||
}
|
||||
|
||||
function getRequestId(request: SessionRequest): string {
|
||||
return typeof request.requestId === "string" ? request.requestId : "fake-request";
|
||||
}
|
||||
|
||||
export async function buildFakeScheduleHostWorkspace(
|
||||
workspace: SeededWorkspace,
|
||||
): Promise<FakeScheduleHostWorkspace> {
|
||||
const workspaceList = await workspace.client.fetchWorkspaces({
|
||||
filter: { projectId: workspace.projectId },
|
||||
});
|
||||
const baseWorkspace = workspaceList.entries.find((entry) => entry.id === workspace.workspaceId);
|
||||
if (!baseWorkspace) {
|
||||
throw new Error(`Failed to load seeded workspace descriptor ${workspace.workspaceId}`);
|
||||
}
|
||||
|
||||
const projectId = `${workspace.projectId}-fake-host`;
|
||||
const cwd = `${workspace.repoPath}-fake-host`;
|
||||
return {
|
||||
serverId: "schedule-fake-host",
|
||||
projectId,
|
||||
projectKey: workspace.projectKey,
|
||||
projectDisplayName: FAKE_HOST_PROJECT_DISPLAY_NAME,
|
||||
workspace: {
|
||||
...baseWorkspace,
|
||||
id: `${baseWorkspace.id}-fake-host`,
|
||||
projectId,
|
||||
projectDisplayName: FAKE_HOST_PROJECT_DISPLAY_NAME,
|
||||
projectRootPath: cwd,
|
||||
workspaceDirectory: cwd,
|
||||
name: FAKE_HOST_PROJECT_DISPLAY_NAME,
|
||||
project: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function installFakeScheduleHost(input: {
|
||||
page: Page;
|
||||
port: string;
|
||||
serverId: string;
|
||||
workspace: Record<string, unknown>;
|
||||
project: Pick<FakeScheduleHostWorkspace, "projectId" | "projectKey" | "projectDisplayName">;
|
||||
schedules?: FakeScheduleSummary[];
|
||||
}): Promise<void> {
|
||||
await input.page.routeWebSocket(wsRoutePatternForPort(input.port), (ws) => {
|
||||
ws.onMessage((message) => {
|
||||
const parsed = parseJson(message);
|
||||
if (parsed && typeof parsed === "object" && (parsed as { type?: string }).type === "hello") {
|
||||
ws.send(
|
||||
buildSessionMessage("status", {
|
||||
status: "server_info",
|
||||
serverId: input.serverId,
|
||||
hostname: "fake-schedule-host",
|
||||
version: "0.0.0-e2e",
|
||||
features: {
|
||||
providersSnapshot: true,
|
||||
workspaceMultiplicity: true,
|
||||
projectList: true,
|
||||
projectAdd: true,
|
||||
projectRemove: true,
|
||||
workspaceRecovery: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed && typeof parsed === "object" && (parsed as { type?: string }).type === "ping") {
|
||||
ws.send(JSON.stringify({ type: "pong" }));
|
||||
return;
|
||||
}
|
||||
|
||||
const request = readSessionRequest(message);
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = getRequestId(request);
|
||||
const now = Date.now();
|
||||
const nowIso = new Date(now).toISOString();
|
||||
switch (request.type) {
|
||||
case "ping":
|
||||
ws.send(
|
||||
buildSessionMessage("pong", {
|
||||
requestId,
|
||||
clientSentAt: typeof request.clientSentAt === "number" ? request.clientSentAt : now,
|
||||
serverReceivedAt: now,
|
||||
serverSentAt: now,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
case "fetch_workspaces_request":
|
||||
ws.send(
|
||||
buildSessionMessage("fetch_workspaces_response", {
|
||||
requestId,
|
||||
entries: [input.workspace],
|
||||
emptyProjects: [],
|
||||
pageInfo: { nextCursor: null, prevCursor: null, hasMore: false },
|
||||
}),
|
||||
);
|
||||
return;
|
||||
case "project.list.request":
|
||||
ws.send(
|
||||
buildSessionMessage("project.list.response", {
|
||||
requestId,
|
||||
projects: [
|
||||
{
|
||||
projectId: input.project.projectId,
|
||||
projectKey: input.project.projectKey,
|
||||
projectDisplayName: input.project.projectDisplayName,
|
||||
projectRootPath: String(input.workspace.projectRootPath),
|
||||
projectKind: "git",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
return;
|
||||
case "fetch_agents_request":
|
||||
ws.send(
|
||||
buildSessionMessage("fetch_agents_response", {
|
||||
requestId,
|
||||
entries: [],
|
||||
pageInfo: { nextCursor: null, prevCursor: null, hasMore: false },
|
||||
}),
|
||||
);
|
||||
return;
|
||||
case "get_providers_snapshot_request":
|
||||
ws.send(
|
||||
buildSessionMessage("get_providers_snapshot_response", {
|
||||
requestId,
|
||||
entries: buildFakeProviderEntries(nowIso),
|
||||
generatedAt: nowIso,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
case "refresh_providers_snapshot_request":
|
||||
ws.send(
|
||||
buildSessionMessage("refresh_providers_snapshot_response", {
|
||||
requestId,
|
||||
acknowledged: true,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
case "schedule/list":
|
||||
ws.send(
|
||||
buildSessionMessage("schedule/list/response", {
|
||||
requestId,
|
||||
schedules: input.schedules ?? [],
|
||||
error: null,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function addFakeScheduleHostAndReload(input: {
|
||||
page: Page;
|
||||
serverId: string;
|
||||
label: string;
|
||||
port: string;
|
||||
}): Promise<void> {
|
||||
const host = buildSeededHost({
|
||||
serverId: input.serverId,
|
||||
label: input.label,
|
||||
endpoint: `127.0.0.1:${input.port}`,
|
||||
nowIso: new Date().toISOString(),
|
||||
});
|
||||
|
||||
await input.page.evaluate(
|
||||
({ seededHost, keys }) => {
|
||||
const nonce = localStorage.getItem(keys.nonce);
|
||||
if (!nonce) {
|
||||
throw new Error("Expected the e2e seed nonce before overriding the host registry.");
|
||||
}
|
||||
const raw = localStorage.getItem(keys.registry);
|
||||
const registry: Array<{ serverId: string }> = raw ? JSON.parse(raw) : [];
|
||||
const nextRegistry = registry.filter((entry) => entry.serverId !== seededHost.serverId);
|
||||
nextRegistry.push(seededHost);
|
||||
localStorage.setItem(keys.registry, JSON.stringify(nextRegistry));
|
||||
|
||||
const rawExtraHosts = localStorage.getItem(keys.extraHosts);
|
||||
const extraHosts: Array<{ serverId: string }> = rawExtraHosts
|
||||
? JSON.parse(rawExtraHosts)
|
||||
: [];
|
||||
const nextExtraHosts = extraHosts.filter((entry) => entry.serverId !== seededHost.serverId);
|
||||
nextExtraHosts.push(seededHost);
|
||||
localStorage.setItem(keys.extraHosts, JSON.stringify(nextExtraHosts));
|
||||
},
|
||||
{
|
||||
seededHost: host,
|
||||
keys: {
|
||||
registry: REGISTRY_KEY,
|
||||
nonce: SEED_NONCE_KEY,
|
||||
extraHosts: EXTRA_HOSTS_KEY,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await input.page.reload();
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { type Page } from "@playwright/test";
|
||||
import { buildSeededHost } from "./daemon-registry";
|
||||
import { startIsolatedHostDaemon } from "./isolated-host-daemon";
|
||||
import { seedWorkspace, type SeededWorkspace } from "./seed-client";
|
||||
|
||||
const REGISTRY_KEY = "@paseo:daemon-registry";
|
||||
const SEED_NONCE_KEY = "@paseo:e2e-seed-nonce";
|
||||
const EXTRA_HOSTS_KEY = "@paseo:e2e-extra-hosts";
|
||||
export const SECONDARY_MODEL_ID = "one-minute-stream";
|
||||
export const SECONDARY_MODEL_LABEL = "One minute stream";
|
||||
|
||||
export async function createScheduleHost() {
|
||||
const daemon = await startIsolatedHostDaemon("schedule-secondary-host");
|
||||
let workspace: SeededWorkspace | undefined;
|
||||
try {
|
||||
workspace = await seedWorkspace({ repoPrefix: "schedule-secondary-", port: daemon.port });
|
||||
const seeded = workspace;
|
||||
await seeded.client.renameProject(seeded.projectId, "Secondary project");
|
||||
return {
|
||||
...seeded,
|
||||
projectDisplayName: "Secondary project",
|
||||
serverId: daemon.serverId,
|
||||
port: daemon.port,
|
||||
label: "Secondary host",
|
||||
cleanup: async () => {
|
||||
await seeded.cleanup();
|
||||
await daemon.close();
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
await workspace?.cleanup();
|
||||
await daemon.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addScheduleHostAndReload(input: {
|
||||
page: Page;
|
||||
serverId: string;
|
||||
label: string;
|
||||
port: number;
|
||||
}): Promise<void> {
|
||||
const host = buildSeededHost({
|
||||
serverId: input.serverId,
|
||||
label: input.label,
|
||||
endpoint: `127.0.0.1:${input.port}`,
|
||||
nowIso: new Date().toISOString(),
|
||||
});
|
||||
|
||||
await input.page.evaluate(
|
||||
({ seededHost, keys }) => {
|
||||
const nonce = localStorage.getItem(keys.nonce);
|
||||
if (!nonce) {
|
||||
throw new Error("Expected the e2e seed nonce before overriding the host registry.");
|
||||
}
|
||||
const raw = localStorage.getItem(keys.registry);
|
||||
const registry: Array<{ serverId: string }> = raw ? JSON.parse(raw) : [];
|
||||
const nextRegistry = registry.filter((entry) => entry.serverId !== seededHost.serverId);
|
||||
nextRegistry.push(seededHost);
|
||||
localStorage.setItem(keys.registry, JSON.stringify(nextRegistry));
|
||||
|
||||
const rawExtraHosts = localStorage.getItem(keys.extraHosts);
|
||||
const extraHosts: Array<{ serverId: string }> = rawExtraHosts
|
||||
? JSON.parse(rawExtraHosts)
|
||||
: [];
|
||||
const nextExtraHosts = extraHosts.filter((entry) => entry.serverId !== seededHost.serverId);
|
||||
nextExtraHosts.push(seededHost);
|
||||
localStorage.setItem(keys.extraHosts, JSON.stringify(nextExtraHosts));
|
||||
},
|
||||
{
|
||||
seededHost: host,
|
||||
keys: {
|
||||
registry: REGISTRY_KEY,
|
||||
nonce: SEED_NONCE_KEY,
|
||||
extraHosts: EXTRA_HOSTS_KEY,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await input.page.reload();
|
||||
}
|
||||
@@ -20,6 +20,7 @@ type WorkspaceSetupDaemonClient = Pick<
|
||||
| "fetchAgents"
|
||||
| "fetchWorkspaces"
|
||||
| "listTerminals"
|
||||
| "observeEvents"
|
||||
| "removeProject"
|
||||
| "subscribeRawMessages"
|
||||
>;
|
||||
@@ -35,6 +36,13 @@ export async function connectWorkspaceSetupClient(): Promise<WorkspaceSetupDaemo
|
||||
const client = await connectDaemonClient<WorkspaceSetupDaemonClient>({
|
||||
clientIdPrefix: "workspace-setup",
|
||||
});
|
||||
// Establish demand before a caller can launch setup. Client close releases it.
|
||||
try {
|
||||
await client.observeEvents(["workspace_setup_progress"]).ready;
|
||||
} catch (error) {
|
||||
await client.close();
|
||||
throw error;
|
||||
}
|
||||
return withProjectOwnership(client);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user