test(app): lift new-workspace daemon lookups into shared helpers

The navigation guard tests polled for the created workspace and then
fetched it a second time, with a throw in between to narrow away a null
the poll had already ruled out. `waitForCreatedWorkspace` polls once and
returns the entry, so both tests read as user-level flows again.

Moves the agent and terminal counters alongside it, since they answer
the same question — what did the daemon actually end up with — for any
test that creates a workspace without navigating to it.
This commit is contained in:
Christoph Leiter
2026-09-09 18:35:07 +02:00
committed by Mohamed Boudra
parent 003b6139f0
commit 7e6ad30f9b
2 changed files with 52 additions and 44 deletions
@@ -5,9 +5,12 @@ import {
archiveLocalWorkspaceFromDaemon,
archiveWorkspaceFromDaemon,
connectNewWorkspaceDaemonClient,
countWorkspaceAgents,
countWorkspaceTerminals,
delayBrowserWorkspaceCreatedResponse,
openNewWorkspaceComposer,
openProjectViaDaemon,
waitForCreatedWorkspace,
} from "../support/helpers/new-workspace";
import { createTempGitRepo } from "../support/helpers/workspace";
import { expectAppRoute } from "../support/helpers/route-assertions";
@@ -41,30 +44,6 @@ const TERMINAL_PROFILE: TerminalProfile = {
args: ["-c", 'echo captured: "$0"; sleep 120', "{{{prompt}}}"],
};
async function findUnknownWorkspace(
client: Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>,
knownWorkspaceIds: ReadonlySet<string>,
) {
const payload = await client.fetchWorkspaces();
return payload.entries.find((candidate) => !knownWorkspaceIds.has(candidate.id)) ?? null;
}
async function countWorkspaceAgents(
client: Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>,
workspaceId: string,
): Promise<number> {
const payload = await client.fetchAgents({ filter: { includeArchived: true } });
return payload.entries.filter((entry) => entry.agent.workspaceId === workspaceId).length;
}
async function countWorkspaceTerminals(
client: Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>,
workspaceId: string,
): Promise<number> {
const payload = await client.listTerminals(undefined, undefined, { workspaceId });
return payload.terminals.length;
}
test.describe("New workspace navigation guard", () => {
let client: Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>;
const localWorkspaceIds = new Set<string>();
@@ -138,16 +117,7 @@ test.describe("New workspace navigation guard", () => {
// Wait for the agent the background path is responsible for creating, then prove the app
// never followed it. Polling the daemon rather than the URL keeps this ordering explicit.
await expect
.poll(async () => (await findUnknownWorkspace(client, knownWorkspaceIds))?.id ?? null, {
timeout: 60_000,
})
.not.toBeNull();
const createdWorkspace = await findUnknownWorkspace(client, knownWorkspaceIds);
if (!createdWorkspace) {
throw new Error("Created workspace disappeared from the daemon");
}
const createdWorkspace = await waitForCreatedWorkspace(client, knownWorkspaceIds);
createdWorktreeDirectories.add(createdWorkspace.workspaceDirectory);
await expect
@@ -217,16 +187,7 @@ test.describe("New workspace navigation guard", () => {
});
createDelay.release();
await expect
.poll(async () => (await findUnknownWorkspace(client, knownWorkspaceIds))?.id ?? null, {
timeout: 60_000,
})
.not.toBeNull();
const createdWorkspace = await findUnknownWorkspace(client, knownWorkspaceIds);
if (!createdWorkspace) {
throw new Error("Created workspace disappeared from the daemon");
}
const createdWorkspace = await waitForCreatedWorkspace(client, knownWorkspaceIds);
createdWorktreeDirectories.add(createdWorkspace.workspaceDirectory);
// Wait for the terminal, not just the workspace. The launch path spawns the terminal and
@@ -689,3 +689,50 @@ export async function delayBrowserWorkspaceCreatedResponse(
waitForCreateRequest: () => createRequestSeen,
};
}
type WorkspaceEntry = Awaited<
ReturnType<NewWorkspaceDaemonClient["fetchWorkspaces"]>
>["entries"][number];
/**
* Waits for a workspace the caller did not already know about, and returns it. Tests that create a
* workspace without navigating to it have no route to assert on, so the daemon's own list is the
* signal that creation finished.
*/
export async function waitForCreatedWorkspace(
client: NewWorkspaceDaemonClient,
knownWorkspaceIds: ReadonlySet<string>,
options?: { timeout?: number },
): Promise<WorkspaceEntry> {
let created: WorkspaceEntry | undefined;
await expect
.poll(
async () => {
const payload = await client.fetchWorkspaces();
created = payload.entries.find((entry) => !knownWorkspaceIds.has(entry.id));
return created !== undefined;
},
{ timeout: options?.timeout ?? 60_000 },
)
.toBe(true);
if (!created) {
throw new Error("Workspace list reported a new workspace but it could not be read back");
}
return created;
}
export async function countWorkspaceAgents(
client: NewWorkspaceDaemonClient,
workspaceId: string,
): Promise<number> {
const payload = await client.fetchAgents({ filter: { includeArchived: true } });
return payload.entries.filter((entry) => entry.agent.workspaceId === workspaceId).length;
}
export async function countWorkspaceTerminals(
client: NewWorkspaceDaemonClient,
workspaceId: string,
): Promise<number> {
const payload = await client.listTerminals(undefined, undefined, { workspaceId });
return payload.terminals.length;
}