Files
paseo/packages/app/e2e/browser/creation-old-daemon.spec.ts
Mohamed Boudra 3a3ccd5385 feat(app): open workspace chats immediately
Reserve the final workspace, agent, and initial message identities before preparation so the normal agent panel can own the conversation throughout creation. Keep follow-ups queued until the first prompt starts and hand failures back to the existing workspace form or failed agent draft.
2026-10-26 09:50:00 +01:00

237 lines
9.2 KiB
TypeScript

/* eslint-disable no-empty-pattern -- Playwright reads fixture dependencies from destructured parameters. */
import { randomUUID } from "node:crypto";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import { metroTest, expect } from "../support/fixtures";
import { connectDaemonClient } from "../support/helpers/daemon-client-loader";
import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon";
import { withProjectOwnership } from "../support/helpers/project-ownership";
import { createTempGitRepo } from "../support/helpers/workspace";
import type { Page } from "@playwright/test";
import { buildHostWorkspaceRoute } from "../../src/utils/host-routes";
import { buildSeededHost, buildCreateAgentPreferences } from "../support/helpers/daemon-registry";
import { seedWorkspace } from "../support/helpers/seed-client";
import {
openNewWorkspaceComposer,
selectWorkspaceIsolation,
loadSessionMessageReaders,
} from "../support/helpers/new-workspace";
import { fillComposerDraft } from "../support/helpers/composer";
import { pressSubmitBeforeTheNextRender } from "../support/helpers/creation";
import { createAgentTabFromMenu } from "../support/helpers/workspace-tabs";
for (const version of ["0.2.5", "0.7.2", "0.8.0"]) {
let daemon: Awaited<ReturnType<typeof startIsolatedHostDaemon>>;
const test = metroTest.extend<{
client: DaemonClient;
gate: Awaited<ReturnType<typeof holdLegacyAgentCreation>>;
repo: Awaited<ReturnType<typeof createTempGitRepo>>;
project: Awaited<ReturnType<typeof seedWorkspace>>;
}>({
gate: async ({ page }, provide) => {
const gate = await holdLegacyAgentCreation(page, daemon.port);
await provide(gate);
gate.release();
},
client: async ({}, provide) => {
const client = withProjectOwnership(
await connectDaemonClient<DaemonClient>({
clientIdPrefix: "legacy-creation",
port: daemon.port,
}),
);
await provide(client);
await client.close();
},
repo: async ({}, provide) => {
const repo = await createTempGitRepo("legacy-creation-");
await provide(repo);
await repo.cleanup();
},
project: async ({}, provide) => {
const project = await seedWorkspace({ repoPrefix: "legacy-app-", port: daemon.port });
await provide(project);
await project.cleanup();
},
});
test.describe(`published daemon ${version}`, () => {
test.beforeAll(async () => {
test.setTimeout(120_000);
daemon = await startIsolatedHostDaemon(`srv_creation_${randomUUID()}`, {
publishedVersion: version,
});
});
test.afterAll(async () => {
await daemon?.close();
});
for (const kind of ["workspace", "agent"] as const) {
test(`current client creates a keyed ${kind} and initial prompt on a published old daemon`, async ({
repo,
client,
}) => {
const prompt = `Create ${kind}: emit 1 coalesced agent stream updates`;
const features = client.getLastServerInfoMessage()?.features;
expect(features?.creationLifecycle).not.toBe(true);
expect(features?.workspaceRequestReceipts).not.toBe(true);
expect(features?.agentRequestReceipts === true).toBe(version === "0.8.0");
const create = await prepareCreation(client, kind, repo.path, prompt);
const [agent, duplicate] = await Promise.all([create(), create()]);
expect(duplicate.id).toBe(agent.id);
expect(agent.title).toBe(prompt);
await client.waitForFinish(agent.id, 20_000);
const agents = await client.fetchAgents();
expect(agents.entries).toHaveLength(1);
expect(agents.entries[0]!.agent.title).toBe(prompt);
const timeline = await client.fetchAgentTimeline(agent.id);
expect(
timeline.entries.filter(
({ item }) => item.type === "user_message" && item.text === prompt,
),
).toHaveLength(1);
});
}
test("current app requires a host update for chat workspaces and preserves existing-workspace agent creation", async ({
page,
project,
client,
gate,
}) => {
await openOldHost(page, project.workspaceId);
await openNewWorkspaceComposer(page, project);
await selectWorkspaceIsolation(page, "worktree");
const prompt = "Create this workspace: emit 1 coalesced agent stream updates";
await fillComposerDraft(page, prompt);
await pressSubmitBeforeTheNextRender(page, "Create");
await expect(
page
.getByText("Update this host to create a workspace with an agent.", { exact: true })
.first(),
).toBeVisible();
await expect(page).toHaveURL(/\/new\?/);
expect((await client.fetchAgents()).entries).toHaveLength(0);
expect(
(await client.fetchWorkspaces()).entries.filter(
(workspace) => workspace.projectId === project.projectId,
),
).toHaveLength(1);
await page.goto(buildHostWorkspaceRoute(daemon.serverId, project.workspaceId));
await createAgentTabFromMenu(page);
const secondPrompt = "Create another agent: emit 1 coalesced agent stream updates";
await fillComposerDraft(page, secondPrompt);
await pressSubmitBeforeTheNextRender(page, "Send message");
await gate.waitForRequest();
gate.release();
await gate.waitForCreated();
await expect.poll(async () => (await client.fetchAgents()).entries.length).toBe(1);
const agent = (await client.fetchAgents()).entries[0]!.agent;
expect(agent.workspaceId).toBe(project.workspaceId);
await client.waitForFinish(agent.id, 20_000);
await expectPromptOnce(client, agent.id, secondPrompt);
await expect(
page.getByTestId(`workspace-tab-agent_${agent.id}`).filter({ visible: true }),
).toHaveText(secondPrompt);
});
async function expectPromptOnce(client: DaemonClient, agentId: string, text: string) {
const timeline = await client.fetchAgentTimeline(agentId);
expect(
timeline.entries.filter(({ item }) => item.type === "user_message" && item.text === text),
).toHaveLength(1);
}
async function openOldHost(page: Page, workspaceId: string) {
const host = buildSeededHost({
serverId: daemon.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(buildHostWorkspaceRoute(daemon.serverId, workspaceId));
}
async function prepareCreation(
client: DaemonClient,
kind: "workspace" | "agent",
cwd: string,
prompt: string,
) {
const input = { provider: "mock", cwd, initialPrompt: prompt };
if (kind === "workspace")
return async () => {
const result = await client.createWorkspace({
idempotencyKey: "workspace-intent",
source: { kind: "worktree", cwd, worktreeSlug: "created-once" },
agent: input,
});
expect(result.error).toBeNull();
expect(result.workspace?.workspaceDirectory).not.toBe(cwd);
return result.agent!;
};
const workspace = await client.createWorkspace({
idempotencyKey: "empty-workspace",
source: { kind: "directory", path: cwd },
});
return () =>
client.createAgent({
...input,
workspaceId: workspace.workspace!.id,
idempotencyKey: "agent-intent",
});
}
});
}
async function holdLegacyAgentCreation(page: Page, port: number) {
const readers = await loadSessionMessageReaders();
const arrived = Promise.withResolvers<void>();
let created = Promise.withResolvers<void>();
let createRequestId: string | undefined;
let release = () => {};
let holding = true;
await page.routeWebSocket(new RegExp(`:${port}/ws`), (browser) => {
const server = browser.connectToServer();
browser.onMessage((frame) => {
const message = readers.client(frame);
if (message?.type === "create_agent_request") createRequestId = message.requestId;
if (holding && message?.type === "create_agent_request") {
holding = false;
release = () => server.send(frame);
arrived.resolve();
} else server.send(frame);
});
server.onMessage((frame) => {
const message = readers.server(frame);
if (
message?.type === "status" &&
message.payload.status === "agent_created" &&
message.payload.requestId === createRequestId
) {
created.resolve();
}
browser.send(frame);
});
});
return {
waitForRequest: () => arrived.promise,
async waitForCreated() {
// Agent-list membership includes initialization, before the initial prompt starts.
await created.promise;
created = Promise.withResolvers<void>();
},
release() {
const send = release;
release = () => {};
send();
},
};
}