Merge origin/main into dev (second round)
Resolve 21 conflicts from latest main (provider profiles, Windows fixes, 0.1.55-rc.1). Fix all type errors and syntax issues from merge artifacts.
This commit is contained in:
@@ -117,7 +117,6 @@ export async function clickTerminal(page: Page): Promise<void> {
|
||||
await button.click();
|
||||
}
|
||||
|
||||
|
||||
// ─── Tab title assertions ──────────────────────────────────────────────────
|
||||
|
||||
/** Wait for any tab in the bar to display the given title text. */
|
||||
@@ -188,6 +187,8 @@ export async function sampleTabsDuringTransition(
|
||||
// ─── Workspace setup ───────────────────────────────────────────────────────
|
||||
|
||||
/** Create a temp git repo and return its path with a cleanup function. */
|
||||
export async function createWorkspace(prefix = "launcher-e2e-"): ReturnType<typeof createTempGitRepo> {
|
||||
export async function createWorkspace(
|
||||
prefix = "launcher-e2e-",
|
||||
): ReturnType<typeof createTempGitRepo> {
|
||||
return createTempGitRepo(prefix);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import type { DaemonClient as ServerDaemonClient } from "@server/client/daemon-client";
|
||||
import { decodeWorkspaceIdFromPathSegment } from "@/utils/host-routes";
|
||||
import { expectWorkspaceHeader, workspaceLabelFromPath } from "./workspace-ui";
|
||||
import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./node-ws-factory";
|
||||
|
||||
type NewWorkspaceDaemonClient = Pick<
|
||||
ServerDaemonClient,
|
||||
| "archivePaseoWorktree"
|
||||
| "archiveWorkspace"
|
||||
| "close"
|
||||
| "connect"
|
||||
| "createPaseoWorktree"
|
||||
| "openProject"
|
||||
>;
|
||||
|
||||
type NewWorkspaceDaemonClientConfig = {
|
||||
url: string;
|
||||
clientId: string;
|
||||
clientType: "cli";
|
||||
webSocketFactory?: NodeWebSocketFactory;
|
||||
};
|
||||
|
||||
type OpenProjectPayload = Awaited<ReturnType<NewWorkspaceDaemonClient["openProject"]>>;
|
||||
|
||||
export type OpenedProject = {
|
||||
workspaceId: string;
|
||||
projectKey: string;
|
||||
projectDisplayName: string;
|
||||
workspaceName: string;
|
||||
};
|
||||
|
||||
function getDaemonPort(): string {
|
||||
const daemonPort = process.env.E2E_DAEMON_PORT;
|
||||
if (!daemonPort) {
|
||||
throw new Error("E2E_DAEMON_PORT is not set.");
|
||||
}
|
||||
if (daemonPort === "6767") {
|
||||
throw new Error("E2E_DAEMON_PORT must not point at the developer daemon.");
|
||||
}
|
||||
return daemonPort;
|
||||
}
|
||||
|
||||
function getDaemonWsUrl(): string {
|
||||
return `ws://127.0.0.1:${getDaemonPort()}/ws`;
|
||||
}
|
||||
|
||||
async function loadDaemonClientConstructor(): Promise<
|
||||
new (
|
||||
config: NewWorkspaceDaemonClientConfig,
|
||||
) => NewWorkspaceDaemonClient
|
||||
> {
|
||||
const repoRoot = path.resolve(__dirname, "../../../../");
|
||||
const moduleUrl = pathToFileURL(
|
||||
path.join(repoRoot, "packages/server/dist/server/server/exports.js"),
|
||||
).href;
|
||||
const mod = (await import(moduleUrl)) as {
|
||||
DaemonClient: new (config: NewWorkspaceDaemonClientConfig) => NewWorkspaceDaemonClient;
|
||||
};
|
||||
return mod.DaemonClient;
|
||||
}
|
||||
|
||||
function requireWorkspace(payload: OpenProjectPayload) {
|
||||
if (payload.error) {
|
||||
throw new Error(payload.error);
|
||||
}
|
||||
if (!payload.workspace) {
|
||||
throw new Error("openProject returned no workspace.");
|
||||
}
|
||||
return payload.workspace;
|
||||
}
|
||||
|
||||
function parseWorkspaceIdFromPageUrl(page: Page, serverId: string): string | null {
|
||||
const pathname = new URL(page.url()).pathname;
|
||||
const match = pathname.match(
|
||||
new RegExp(`^/h/${serverId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/workspace/([^/?#]+)`),
|
||||
);
|
||||
if (!match?.[1]) {
|
||||
return null;
|
||||
}
|
||||
return decodeWorkspaceIdFromPathSegment(match[1]);
|
||||
}
|
||||
|
||||
function parseWorkspaceIdFromSidebarRowTestId(
|
||||
testId: string,
|
||||
input: { serverId: string; previousWorkspaceId: string },
|
||||
): string | null {
|
||||
const prefix = `sidebar-workspace-row-${input.serverId}:`;
|
||||
if (!testId.startsWith(prefix)) {
|
||||
return null;
|
||||
}
|
||||
const workspaceId = testId.slice(prefix.length).trim();
|
||||
if (!workspaceId || workspaceId === input.previousWorkspaceId) {
|
||||
return null;
|
||||
}
|
||||
return workspaceId;
|
||||
}
|
||||
|
||||
export async function connectNewWorkspaceDaemonClient(): Promise<NewWorkspaceDaemonClient> {
|
||||
const DaemonClient = await loadDaemonClientConstructor();
|
||||
const webSocketFactory = createNodeWebSocketFactory();
|
||||
const client = new DaemonClient({
|
||||
url: getDaemonWsUrl(),
|
||||
clientId: `app-e2e-new-workspace-${randomUUID()}`,
|
||||
clientType: "cli",
|
||||
webSocketFactory,
|
||||
});
|
||||
await client.connect();
|
||||
return client;
|
||||
}
|
||||
|
||||
export async function openProjectViaDaemon(
|
||||
client: NewWorkspaceDaemonClient,
|
||||
repoPath: string,
|
||||
): Promise<OpenedProject> {
|
||||
const workspace = requireWorkspace(await client.openProject(repoPath));
|
||||
return {
|
||||
workspaceId: workspace.id,
|
||||
projectKey: workspace.projectId,
|
||||
projectDisplayName: workspace.projectDisplayName,
|
||||
workspaceName: workspace.name,
|
||||
};
|
||||
}
|
||||
|
||||
export async function archiveWorkspaceFromDaemon(
|
||||
client: NewWorkspaceDaemonClient,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
const payload = await client.archivePaseoWorktree({ worktreePath: workspaceId });
|
||||
if (payload.error) {
|
||||
throw new Error(payload.error.message);
|
||||
}
|
||||
if (!payload.success) {
|
||||
throw new Error(`Failed to archive workspace: ${workspaceId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function archiveLocalWorkspaceFromDaemon(
|
||||
client: NewWorkspaceDaemonClient,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
const payload = await client.archiveWorkspace(workspaceId);
|
||||
if (payload.error) {
|
||||
throw new Error(payload.error);
|
||||
}
|
||||
if (!payload.archivedAt) {
|
||||
throw new Error(`Failed to archive workspace: ${workspaceId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createWorktreeViaDaemon(
|
||||
client: NewWorkspaceDaemonClient,
|
||||
input: { cwd: string; slug: string },
|
||||
): Promise<OpenedProject> {
|
||||
const payload = await client.createPaseoWorktree({
|
||||
cwd: input.cwd,
|
||||
worktreeSlug: input.slug,
|
||||
});
|
||||
const workspace = requireWorkspace(payload);
|
||||
return {
|
||||
workspaceId: workspace.id,
|
||||
projectKey: workspace.projectId,
|
||||
projectDisplayName: workspace.projectDisplayName,
|
||||
workspaceName: workspace.name,
|
||||
};
|
||||
}
|
||||
|
||||
export async function clickNewWorkspaceButton(
|
||||
page: Page,
|
||||
input: { projectKey: string; projectDisplayName: string },
|
||||
): Promise<void> {
|
||||
const projectRow = page.getByTestId(`sidebar-project-row-${input.projectKey}`).first();
|
||||
await expect(projectRow).toBeVisible({ timeout: 30_000 });
|
||||
await projectRow.hover();
|
||||
|
||||
const button = page.getByTestId(`sidebar-project-new-worktree-${input.projectKey}`).first();
|
||||
await expect(button).toBeVisible({ timeout: 30_000 });
|
||||
await button.click();
|
||||
}
|
||||
|
||||
export async function assertNewWorkspaceSidebarAndHeader(
|
||||
page: Page,
|
||||
input: { serverId: string; previousWorkspaceId: string; projectDisplayName: string },
|
||||
): Promise<{ workspaceId: string }> {
|
||||
let workspaceId: string | null = null;
|
||||
const sidebarWorkspaceRows = page.locator(
|
||||
`[data-testid^="sidebar-workspace-row-${input.serverId}:"]`,
|
||||
);
|
||||
const deadline = Date.now() + 30_000;
|
||||
while (Date.now() < deadline) {
|
||||
const sidebarRowTestIds = await sidebarWorkspaceRows.evaluateAll((elements) =>
|
||||
elements.map((element) => element.getAttribute("data-testid") ?? ""),
|
||||
);
|
||||
workspaceId =
|
||||
sidebarRowTestIds
|
||||
.map((testId) => parseWorkspaceIdFromSidebarRowTestId(testId, input))
|
||||
.find((id) => id !== null) ?? null;
|
||||
if (workspaceId) {
|
||||
break;
|
||||
}
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
|
||||
if (!workspaceId || workspaceId === input.previousWorkspaceId) {
|
||||
const sidebarWorkspaceRowIds = await sidebarWorkspaceRows.evaluateAll((elements) =>
|
||||
elements.map((element) => element.getAttribute("data-testid") ?? "<missing-testid>"),
|
||||
);
|
||||
throw new Error(
|
||||
[
|
||||
"Expected a newly created workspace to load.",
|
||||
`Current URL: ${page.url()}`,
|
||||
`Sidebar rows: ${sidebarWorkspaceRowIds.join(", ") || "<none>"}`,
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
const createdWorkspaceRow = page.getByTestId(
|
||||
`sidebar-workspace-row-${input.serverId}:${workspaceId}`,
|
||||
);
|
||||
await expect(createdWorkspaceRow.first()).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: workspaceLabelFromPath(workspaceId),
|
||||
subtitle: input.projectDisplayName,
|
||||
});
|
||||
|
||||
return { workspaceId };
|
||||
}
|
||||
@@ -8,9 +8,7 @@ import { buildHostWorkspaceRoute } from "../../src/utils/host-routes";
|
||||
export type TerminalPerfDaemonClient = {
|
||||
connect(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
openProject(
|
||||
cwd: string,
|
||||
): Promise<{
|
||||
openProject(cwd: string): Promise<{
|
||||
workspace: { id: number; name: string; projectRootPath: string } | null;
|
||||
error: string | null;
|
||||
}>;
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import {
|
||||
clickNewChat,
|
||||
clickTerminal,
|
||||
} from "./launcher";
|
||||
import { clickNewChat, clickTerminal } from "./launcher";
|
||||
import { setupDeterministicPrompt, waitForTerminalContent } from "./terminal-perf";
|
||||
|
||||
function terminalSurface(page: Page) {
|
||||
|
||||
@@ -10,9 +10,7 @@ import type { SessionOutboundMessage } from "@server/shared/messages";
|
||||
type WorkspaceSetupDaemonClient = {
|
||||
connect(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
openProject(
|
||||
cwd: string,
|
||||
): Promise<{
|
||||
openProject(cwd: string): Promise<{
|
||||
workspace: {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -21,9 +19,7 @@ type WorkspaceSetupDaemonClient = {
|
||||
} | null;
|
||||
error: string | null;
|
||||
}>;
|
||||
createPaseoWorktree(
|
||||
input: { cwd: string; worktreeSlug?: string },
|
||||
): Promise<{
|
||||
createPaseoWorktree(input: { cwd: string; worktreeSlug?: string }): Promise<{
|
||||
workspace: {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -45,15 +41,11 @@ type WorkspaceSetupDaemonClient = {
|
||||
agent: { id: string; cwd: string; workspaceId?: string | null };
|
||||
}>;
|
||||
}>;
|
||||
fetchAgent(
|
||||
agentId: string,
|
||||
): Promise<{
|
||||
fetchAgent(agentId: string): Promise<{
|
||||
agent: { id: string; cwd: string } | null;
|
||||
project: unknown | null;
|
||||
} | null>;
|
||||
listTerminals(
|
||||
cwd: string,
|
||||
): Promise<{
|
||||
listTerminals(cwd: string): Promise<{
|
||||
cwd?: string;
|
||||
terminals: Array<{ id: string; cwd: string; name: string }>;
|
||||
error?: string | null;
|
||||
@@ -77,7 +69,11 @@ function getDaemonWsUrl(): string {
|
||||
}
|
||||
|
||||
async function loadDaemonClientConstructor(): Promise<
|
||||
new (config: { url: string; clientId: string; clientType: "cli" }) => WorkspaceSetupDaemonClient
|
||||
new (config: {
|
||||
url: string;
|
||||
clientId: string;
|
||||
clientType: "cli";
|
||||
}) => WorkspaceSetupDaemonClient
|
||||
> {
|
||||
const repoRoot = path.resolve(process.cwd(), "../..");
|
||||
const moduleUrl = pathToFileURL(
|
||||
@@ -149,7 +145,9 @@ export async function createWorkspaceFromSidebar(page: Page, repoPath: string):
|
||||
await expect(button).toBeEnabled({ timeout: 30_000 });
|
||||
await button.click();
|
||||
await expect(page).toHaveURL(/\/new\?/, { timeout: 30_000 });
|
||||
await expect(page.getByRole("textbox", { name: "Message agent..." }).first()).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByRole("textbox", { name: "Message agent..." }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCurrentWorkspaceIdFromRoute(page: Page): Promise<string> {
|
||||
@@ -193,7 +191,10 @@ export async function createStandaloneTerminalFromWorkspaceSetup(page: Page): Pr
|
||||
.click();
|
||||
}
|
||||
|
||||
export async function waitForWorkspaceSetupDialogToClose(page: Page, timeoutMs = 45_000): Promise<void> {
|
||||
export async function waitForWorkspaceSetupDialogToClose(
|
||||
page: Page,
|
||||
timeoutMs = 45_000,
|
||||
): Promise<void> {
|
||||
const dialog = workspaceSetupDialog(page);
|
||||
|
||||
try {
|
||||
@@ -243,7 +244,9 @@ export async function expectSetupLogContains(page: Page, text: string): Promise<
|
||||
}
|
||||
|
||||
export async function expectNoSetupMessage(page: Page): Promise<void> {
|
||||
await expect(page.getByText("No setup commands ran for this workspace.", { exact: true })).toBeVisible({
|
||||
await expect(
|
||||
page.getByText("No setup commands ran for this workspace.", { exact: true }),
|
||||
).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
}
|
||||
@@ -276,7 +279,8 @@ export async function findWorktreeWorkspaceForProject(
|
||||
const workspace =
|
||||
payload.entries.find(
|
||||
(entry) =>
|
||||
entry.projectRootPath === normalizedRepoPath && entry.workspaceDirectory !== normalizedRepoPath,
|
||||
entry.projectRootPath === normalizedRepoPath &&
|
||||
entry.workspaceDirectory !== normalizedRepoPath,
|
||||
) ?? null;
|
||||
if (!workspace) {
|
||||
throw new Error(`Failed to find created worktree workspace for ${repoPath}`);
|
||||
|
||||
@@ -63,9 +63,7 @@ test.describe("Tab creation", () => {
|
||||
const countAfterFirst = await countTabsOfKind(page, "draft");
|
||||
|
||||
await pressNewTabShortcut(page);
|
||||
await expect
|
||||
.poll(() => countTabsOfKind(page, "draft"))
|
||||
.toBe(countAfterFirst + 1);
|
||||
await expect.poll(() => countTabsOfKind(page, "draft")).toBe(countAfterFirst + 1);
|
||||
});
|
||||
|
||||
test("clicking new agent tab creates a draft tab", async ({ page }) => {
|
||||
@@ -201,12 +199,7 @@ test.describe("Tab transitions (no flash)", () => {
|
||||
await gotoWorkspace(page, workspaceId);
|
||||
|
||||
// Sample tabs at high frequency across the transition
|
||||
const snapshots = await sampleTabsDuringTransition(
|
||||
page,
|
||||
() => clickNewChat(page),
|
||||
2_000,
|
||||
30,
|
||||
);
|
||||
const snapshots = await sampleTabsDuringTransition(page, () => clickNewChat(page), 2_000, 30);
|
||||
|
||||
// Every snapshot should have at least one tab — no blank/zero-tab frames
|
||||
for (const snapshot of snapshots) {
|
||||
@@ -246,12 +239,7 @@ test.describe("Tab transitions (no flash)", () => {
|
||||
|
||||
const composer = page.getByRole("textbox", { name: "Message agent..." }).first();
|
||||
|
||||
const elapsed = await measureTileTransition(
|
||||
page,
|
||||
() => clickNewChat(page),
|
||||
composer,
|
||||
10_000,
|
||||
);
|
||||
const elapsed = await measureTileTransition(page, () => clickNewChat(page), composer, 10_000);
|
||||
|
||||
// Draft creation is fully in-memory — should be fast
|
||||
// We use a generous budget here because CI can be slow, but the key assertion
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
|
||||
import { expect, test } from "./fixtures";
|
||||
import {
|
||||
archiveWorkspaceFromDaemon,
|
||||
archiveLocalWorkspaceFromDaemon,
|
||||
assertNewWorkspaceSidebarAndHeader,
|
||||
clickNewWorkspaceButton,
|
||||
connectNewWorkspaceDaemonClient,
|
||||
createWorktreeViaDaemon,
|
||||
openProjectViaDaemon,
|
||||
} from "./helpers/new-workspace";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
import {
|
||||
expectWorkspaceHeader,
|
||||
switchWorkspaceViaSidebar,
|
||||
workspaceLabelFromPath,
|
||||
} from "./helpers/workspace-ui";
|
||||
|
||||
test.describe("New workspace flow", () => {
|
||||
let client: Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>;
|
||||
const localWorkspaceIds = new Set<string>();
|
||||
const createdWorktreeIds = new Set<string>();
|
||||
|
||||
test.describe.configure({ timeout: 120_000 });
|
||||
|
||||
test.beforeEach(async () => {
|
||||
client = await connectNewWorkspaceDaemonClient();
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
if (client) {
|
||||
for (const workspaceId of createdWorktreeIds) {
|
||||
await archiveWorkspaceFromDaemon(client, workspaceId).catch(() => undefined);
|
||||
}
|
||||
for (const workspaceId of localWorkspaceIds) {
|
||||
await archiveLocalWorkspaceFromDaemon(client, workspaceId).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
createdWorktreeIds.clear();
|
||||
localWorkspaceIds.clear();
|
||||
await client?.close().catch(() => undefined);
|
||||
});
|
||||
|
||||
test("sidebar workspace navigation updates URL and header", async ({ page }) => {
|
||||
const serverId = process.env.E2E_SERVER_ID;
|
||||
if (!serverId) {
|
||||
throw new Error("E2E_SERVER_ID is not set.");
|
||||
}
|
||||
|
||||
const firstRepo = await createTempGitRepo("workspace-nav-a-");
|
||||
const secondRepo = await createTempGitRepo("workspace-nav-b-");
|
||||
|
||||
try {
|
||||
const firstWorkspace = await openProjectViaDaemon(client, firstRepo.path);
|
||||
const secondWorkspace = await openProjectViaDaemon(client, secondRepo.path);
|
||||
localWorkspaceIds.add(firstWorkspace.workspaceId);
|
||||
localWorkspaceIds.add(secondWorkspace.workspaceId);
|
||||
|
||||
await page.goto(buildHostWorkspaceRoute(serverId, firstWorkspace.workspaceId));
|
||||
await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, firstWorkspace.workspaceId));
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: firstWorkspace.workspaceName,
|
||||
subtitle: workspaceLabelFromPath(firstRepo.path),
|
||||
});
|
||||
|
||||
await switchWorkspaceViaSidebar({
|
||||
page,
|
||||
serverId,
|
||||
targetWorkspacePath: secondWorkspace.workspaceId,
|
||||
});
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: secondWorkspace.workspaceName,
|
||||
subtitle: workspaceLabelFromPath(secondRepo.path),
|
||||
});
|
||||
|
||||
await switchWorkspaceViaSidebar({
|
||||
page,
|
||||
serverId,
|
||||
targetWorkspacePath: firstWorkspace.workspaceId,
|
||||
});
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: firstWorkspace.workspaceName,
|
||||
subtitle: workspaceLabelFromPath(firstRepo.path),
|
||||
});
|
||||
} finally {
|
||||
await secondRepo.cleanup();
|
||||
await firstRepo.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("clicking new workspace redirects, renders header, shows sidebar row, and keeps one draft tab", async ({
|
||||
page,
|
||||
}) => {
|
||||
const serverId = process.env.E2E_SERVER_ID;
|
||||
if (!serverId) {
|
||||
throw new Error("E2E_SERVER_ID is not set.");
|
||||
}
|
||||
|
||||
const tempRepo = await createTempGitRepo("new-workspace-");
|
||||
|
||||
try {
|
||||
const openedProject = await openProjectViaDaemon(client, tempRepo.path);
|
||||
localWorkspaceIds.add(openedProject.workspaceId);
|
||||
|
||||
await page.goto(buildHostWorkspaceRoute(serverId, openedProject.workspaceId));
|
||||
await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, openedProject.workspaceId));
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: openedProject.workspaceName,
|
||||
subtitle: workspaceLabelFromPath(tempRepo.path),
|
||||
});
|
||||
|
||||
await clickNewWorkspaceButton(page, {
|
||||
projectKey: openedProject.projectKey,
|
||||
projectDisplayName: openedProject.projectDisplayName,
|
||||
});
|
||||
|
||||
const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, {
|
||||
serverId,
|
||||
previousWorkspaceId: openedProject.workspaceId,
|
||||
projectDisplayName: openedProject.projectDisplayName,
|
||||
});
|
||||
createdWorktreeIds.add(createdWorkspace.workspaceId);
|
||||
|
||||
expect(createdWorkspace.workspaceId).not.toBe(openedProject.workspaceId);
|
||||
await expect(page).toHaveURL(
|
||||
buildHostWorkspaceRoute(serverId, createdWorkspace.workspaceId),
|
||||
{
|
||||
timeout: 30_000,
|
||||
},
|
||||
);
|
||||
|
||||
const createdWorkspaceRow = page.getByTestId(
|
||||
`sidebar-workspace-row-${serverId}:${createdWorkspace.workspaceId}`,
|
||||
);
|
||||
await expect(createdWorkspaceRow).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: workspaceLabelFromPath(createdWorkspace.workspaceId),
|
||||
subtitle: openedProject.projectDisplayName,
|
||||
});
|
||||
|
||||
const draftTabs = page.locator('[data-testid^="workspace-tab-"]').filter({
|
||||
has: page.getByText("New Agent", { exact: true }),
|
||||
});
|
||||
await expect(draftTabs).toHaveCount(1, { timeout: 30_000 });
|
||||
|
||||
const composer = page.getByRole("textbox", { name: "Message agent..." });
|
||||
await expect(composer).toBeEditable({ timeout: 30_000 });
|
||||
} finally {
|
||||
await tempRepo.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -57,7 +57,10 @@ async function openProjectViaDaemon(
|
||||
};
|
||||
}
|
||||
|
||||
async function openWorkspaceFromSidebar(page: import("@playwright/test").Page, workspaceId: string) {
|
||||
async function openWorkspaceFromSidebar(
|
||||
page: import("@playwright/test").Page,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const row = page.getByTestId(getWorkspaceRowTestId(workspaceId));
|
||||
await expect(row).toBeVisible({ timeout: 30_000 });
|
||||
await row.click();
|
||||
@@ -65,10 +68,7 @@ async function openWorkspaceFromSidebar(page: import("@playwright/test").Page, w
|
||||
return row;
|
||||
}
|
||||
|
||||
async function waitForSidebarProject(
|
||||
page: import("@playwright/test").Page,
|
||||
projectName: string,
|
||||
) {
|
||||
async function waitForSidebarProject(page: import("@playwright/test").Page, projectName: string) {
|
||||
const row = page
|
||||
.getByRole("button", {
|
||||
name: new RegExp(escapeRegex(projectName), "i"),
|
||||
|
||||
@@ -2,14 +2,8 @@ import { execSync } from "node:child_process";
|
||||
import { realpathSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { expect, test } from "./fixtures";
|
||||
import {
|
||||
clickTerminal,
|
||||
waitForTabBar,
|
||||
} from "./helpers/launcher";
|
||||
import {
|
||||
setupDeterministicPrompt,
|
||||
waitForTerminalContent,
|
||||
} from "./helpers/terminal-perf";
|
||||
import { clickTerminal, waitForTabBar } from "./helpers/launcher";
|
||||
import { setupDeterministicPrompt, waitForTerminalContent } from "./helpers/terminal-perf";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
import {
|
||||
connectWorkspaceSetupClient,
|
||||
@@ -88,10 +82,13 @@ test.describe("Workspace cwd correctness", () => {
|
||||
try {
|
||||
await seedProjectForWorkspaceSetup(client, repo.path);
|
||||
|
||||
execSync(`git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(branchName)} main`, {
|
||||
cwd: repo.path,
|
||||
stdio: "ignore",
|
||||
});
|
||||
execSync(
|
||||
`git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(branchName)} main`,
|
||||
{
|
||||
cwd: repo.path,
|
||||
stdio: "ignore",
|
||||
},
|
||||
);
|
||||
worktreeCreated = true;
|
||||
|
||||
const workspaceResult = await client.openProject(worktreePath);
|
||||
@@ -132,5 +129,4 @@ test.describe("Workspace cwd correctness", () => {
|
||||
await repo.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -45,17 +45,21 @@ async function expectScriptInCard(page: Page, scriptName: string): Promise<void>
|
||||
/** Asserts the script status dot indicates "running". */
|
||||
async function expectScriptRunning(page: Page, scriptName: string): Promise<void> {
|
||||
const card = page.getByTestId("workspace-hover-card");
|
||||
await expect(
|
||||
card.getByTestId(`hover-card-script-status-${scriptName}`),
|
||||
).toHaveAttribute("aria-label", "Running", { timeout: 10_000 });
|
||||
await expect(card.getByTestId(`hover-card-script-status-${scriptName}`)).toHaveAttribute(
|
||||
"aria-label",
|
||||
"Running",
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
}
|
||||
|
||||
/** Asserts the script lifecycle is stopped. */
|
||||
async function expectScriptStopped(page: Page, scriptName: string): Promise<void> {
|
||||
const card = page.getByTestId("workspace-hover-card");
|
||||
await expect(
|
||||
card.getByTestId(`hover-card-script-status-${scriptName}`),
|
||||
).toHaveAttribute("aria-label", "Stopped", { timeout: 10_000 });
|
||||
await expect(card.getByTestId(`hover-card-script-status-${scriptName}`)).toHaveAttribute(
|
||||
"aria-label",
|
||||
"Stopped",
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
}
|
||||
|
||||
/** Asserts the script health label shown in the hover card. */
|
||||
@@ -117,7 +121,8 @@ test.describe("Workspace hover card", () => {
|
||||
// Wait for setup completion via daemon (setup snapshots are per-session)
|
||||
const completed = waitForWorkspaceSetupProgress(
|
||||
client,
|
||||
(payload) => payload.status === "completed" && payload.detail.log.includes("setup complete"),
|
||||
(payload) =>
|
||||
payload.status === "completed" && payload.detail.log.includes("setup complete"),
|
||||
);
|
||||
const workspace = await createWorkspaceThroughDaemon(client, {
|
||||
cwd: repo.path,
|
||||
@@ -182,7 +187,9 @@ test.describe("Workspace hover card", () => {
|
||||
}
|
||||
|
||||
await openHomeWithProject(page, repo.path);
|
||||
const wsRow = page.getByTestId(`sidebar-workspace-row-${getServerId()}:${workspace.workspace.id}`);
|
||||
const wsRow = page.getByTestId(
|
||||
`sidebar-workspace-row-${getServerId()}:${workspace.workspace.id}`,
|
||||
);
|
||||
await expect(wsRow).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await expectHoverCard(page, workspace.workspace.name);
|
||||
|
||||
@@ -107,10 +107,13 @@ test.describe("Workspace lifecycle", () => {
|
||||
try {
|
||||
await seedProjectForWorkspaceSetup(client, repo.path);
|
||||
|
||||
execSync(`git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(branchName)} main`, {
|
||||
cwd: repo.path,
|
||||
stdio: "ignore",
|
||||
});
|
||||
execSync(
|
||||
`git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(branchName)} main`,
|
||||
{
|
||||
cwd: repo.path,
|
||||
stdio: "ignore",
|
||||
},
|
||||
);
|
||||
worktreeCreated = true;
|
||||
|
||||
const workspaceResult = await client.openProject(worktreePath);
|
||||
@@ -154,10 +157,13 @@ test.describe("Workspace lifecycle", () => {
|
||||
try {
|
||||
await seedProjectForWorkspaceSetup(client, repo.path);
|
||||
|
||||
execSync(`git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(branchName)} main`, {
|
||||
cwd: repo.path,
|
||||
stdio: "ignore",
|
||||
});
|
||||
execSync(
|
||||
`git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(branchName)} main`,
|
||||
{
|
||||
cwd: repo.path,
|
||||
stdio: "ignore",
|
||||
},
|
||||
);
|
||||
worktreeCreated = true;
|
||||
|
||||
const workspaceResult = await client.openProject(worktreePath);
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { expect, test } from "./fixtures";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
import {
|
||||
clickTerminal,
|
||||
waitForTabBar,
|
||||
} from "./helpers/launcher";
|
||||
import { clickTerminal, waitForTabBar } from "./helpers/launcher";
|
||||
import {
|
||||
connectWorkspaceSetupClient,
|
||||
createWorkspaceThroughDaemon,
|
||||
@@ -82,15 +79,11 @@ test.describe("Workspace setup runtime authority", () => {
|
||||
|
||||
// Verify terminal is listed under the worktree directory, not the original repo
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await client.listTerminals(workspaceDir)).terminals.length > 0,
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.poll(async () => (await client.listTerminals(workspaceDir)).terminals.length > 0, {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.toBe(true);
|
||||
expect(
|
||||
(await client.listTerminals(repo.path)).terminals.length,
|
||||
).toBe(0);
|
||||
expect((await client.listTerminals(repo.path)).terminals.length).toBe(0);
|
||||
} finally {
|
||||
await client.close();
|
||||
await repo.cleanup();
|
||||
|
||||
@@ -36,7 +36,9 @@ test.describe("Workspace setup streaming", () => {
|
||||
const repo = await createTempGitRepo("setup-open-", {
|
||||
paseoConfig: {
|
||||
worktree: {
|
||||
setup: ["sh -c 'echo starting setup; for i in $(seq 1 30); do echo tick $i; sleep 1; done; echo setup complete'"],
|
||||
setup: [
|
||||
"sh -c 'echo starting setup; for i in $(seq 1 30); do echo tick $i; sleep 1; done; echo setup complete'",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -77,7 +79,8 @@ test.describe("Workspace setup streaming", () => {
|
||||
// so the browser session won't receive progress events).
|
||||
const completed = waitForWorkspaceSetupProgress(
|
||||
client,
|
||||
(payload) => payload.status === "completed" && payload.detail.log.includes("setup complete"),
|
||||
(payload) =>
|
||||
payload.status === "completed" && payload.detail.log.includes("setup complete"),
|
||||
);
|
||||
const workspace = await createWorkspaceThroughDaemon(client, {
|
||||
cwd: repo.path,
|
||||
@@ -138,7 +141,8 @@ test.describe("Workspace setup streaming", () => {
|
||||
);
|
||||
const completed = waitForWorkspaceSetupProgress(
|
||||
client,
|
||||
(payload) => payload.status === "completed" && payload.detail.log.includes("setup complete"),
|
||||
(payload) =>
|
||||
payload.status === "completed" && payload.detail.log.includes("setup complete"),
|
||||
);
|
||||
|
||||
await createWorkspaceThroughDaemon(client, {
|
||||
@@ -243,7 +247,8 @@ test.describe("Workspace setup streaming", () => {
|
||||
// Wait for setup completion via daemon (setup snapshots are per-session)
|
||||
const completed = waitForWorkspaceSetupProgress(
|
||||
client,
|
||||
(payload) => payload.status === "completed" && payload.detail.log.includes("setup complete"),
|
||||
(payload) =>
|
||||
payload.status === "completed" && payload.detail.log.includes("setup complete"),
|
||||
);
|
||||
const workspace = await createWorkspaceThroughDaemon(client, {
|
||||
cwd: repo.path,
|
||||
@@ -296,7 +301,8 @@ test.describe("Workspace setup streaming", () => {
|
||||
await seedProjectForWorkspaceSetup(client, repo.path);
|
||||
const completed = waitForWorkspaceSetupProgress(
|
||||
client,
|
||||
(payload) => payload.status === "completed" && payload.detail.log.includes("setup complete"),
|
||||
(payload) =>
|
||||
payload.status === "completed" && payload.detail.log.includes("setup complete"),
|
||||
);
|
||||
|
||||
const result = await client.createPaseoWorktree({
|
||||
|
||||
Reference in New Issue
Block a user