fix(workspaces): restore clean runtime test paths

This commit is contained in:
Mohamed Boudra
2026-08-14 14:11:12 +02:00
parent accd2d7587
commit ff83ca4bdc
13 changed files with 171 additions and 25 deletions
@@ -1891,6 +1891,62 @@ test("selected workspace availability fails closed without probing or launching
}
});
test("host-placed workspaces require host provider availability before create and resume", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-host-workspace-provider-"));
const availabilityScopes: Array<FetchCatalogOptions | undefined> = [];
let createCalls = 0;
let resumeCalls = 0;
class UnavailableHostClient extends TestAgentClient {
override async isAvailable(options?: FetchCatalogOptions): Promise<boolean> {
availabilityScopes.push(options);
return false;
}
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
createCalls += 1;
return new TestAgentSession(config);
}
override async resumeSession(
handle: AgentPersistenceHandle,
overrides?: Partial<AgentSessionConfig>,
): Promise<AgentSession> {
resumeCalls += 1;
return await super.resumeSession(handle, overrides);
}
}
const manager = new AgentManager({
clients: { codex: new UnavailableHostClient() },
logger,
resolveProviderWorkspace: async () => null,
});
const handle: AgentPersistenceHandle = {
provider: "codex",
sessionId: "host-workspace-session",
metadata: { cwd: workdir },
};
try {
await expect(
manager.createAgent({ provider: "codex", cwd: workdir }, undefined, {
workspaceId: "local-workspace",
}),
).rejects.toThrow("Provider 'codex' is not available");
await expect(
manager.resumeAgentFromPersistence(handle, undefined, undefined, {
workspaceId: "local-workspace",
}),
).rejects.toThrow("Provider 'codex' is not available");
expect(availabilityScopes.length).toBeGreaterThanOrEqual(2);
expect(availabilityScopes.every((scope) => scope === undefined)).toBe(true);
expect(createCalls).toBe(0);
expect(resumeCalls).toBe(0);
} finally {
rmSync(workdir, { recursive: true, force: true });
}
});
test("selected workspace without a bound capability never checks host availability", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-missing-workspace-capability-"));
let hostAvailabilityCalls = 0;
@@ -1141,9 +1141,7 @@ export class AgentManager {
options.workspaceId,
);
this.requireEnabledProvider(storedConfig.provider);
const client = options.workspaceId
? this.requireClient(storedConfig.provider)
: await this.requireAvailableClient({ provider: storedConfig.provider });
const client = this.requireClient(storedConfig.provider);
const launchContext = await this.buildLaunchContext(
resolvedAgentId,
client,
@@ -1151,7 +1149,7 @@ export class AgentManager {
options?.env,
options.workspaceId,
);
await this.requireWorkspaceProviderAvailable(
await this.requireProviderAvailableForLaunch(
client,
storedConfig.provider,
storedConfig.cwd,
@@ -1232,11 +1230,6 @@ export class AgentManager {
);
const client = this.requireClient(handle.provider);
if (!options?.workspaceId && !(await client.isAvailable())) {
throw new Error(
`Provider '${handle.provider}' is not available. Please ensure the CLI is installed.`,
);
}
const launchContext = await this.buildLaunchContext(
resolvedAgentId,
client,
@@ -1244,7 +1237,7 @@ export class AgentManager {
undefined,
options?.workspaceId,
);
await this.requireWorkspaceProviderAvailable(
await this.requireProviderAvailableForLaunch(
client,
storedConfig.provider,
storedConfig.cwd,
@@ -1307,7 +1300,7 @@ export class AgentManager {
undefined,
input.workspaceId,
);
await this.requireWorkspaceProviderAvailable(
await this.requireProviderAvailableForLaunch(
client,
input.provider,
storedConfig.cwd,
@@ -4687,15 +4680,20 @@ export class AgentManager {
return launchContext.paseoTools ? stripInternalPaseoMcpServer(launchConfig) : launchConfig;
}
private async requireWorkspaceProviderAvailable(
private async requireProviderAvailableForLaunch(
client: AgentClient,
provider: AgentProvider,
cwd: string,
workspaceId: string | undefined,
launchContext: AgentLaunchContext,
): Promise<void> {
if (!workspaceId) return;
if (!launchContext.workspace) return;
if (!launchContext.workspace) {
await this.requireAvailableClient({ provider });
return;
}
if (!workspaceId) {
throw new Error(`Workspace-scoped provider '${provider}' is missing a workspace ID.`);
}
const available = await client.isAvailable({
scope: "workspace",
cwd,
+10 -3
View File
@@ -51,6 +51,8 @@ import {
import type { CheckDetails, ForgeService } from "../services/forge-service.js";
import type { GitHubPullRequestStatusFacts } from "../services/github-facts.js";
import { bindWorkspaceGitService } from "./test-utils/workspace-git-service-stub.js";
import { createStub } from "./test-utils/class-mocks.js";
import type { WorkspaceRuntimeService } from "./workspace-runtime/index.js";
interface SessionHandlerInternals {
interruptAgentIfRunning(agentId: string): Promise<void>;
@@ -304,6 +306,7 @@ interface SessionForTestOptions {
getProjectSlug?: ReturnType<typeof vi.fn>;
};
workspaceRegistry?: { get: ReturnType<typeof vi.fn> };
workspaceRuntime?: WorkspaceRuntimeService;
projectRegistry?: Partial<SessionOptions["projectRegistry"]>;
terminalManager?: SessionOptions["terminalManager"];
serviceProxy?: SessionOptions["serviceProxy"];
@@ -477,6 +480,7 @@ function createSessionForTest(options: SessionForTestOptions = {}): Session {
get: vi.fn(),
list: vi.fn().mockResolvedValue([]),
},
workspaceRuntime: options.workspaceRuntime,
scheduleService: asScheduleService(),
checkoutDiffManager: asCheckoutDiffManager(checkoutDiffManager),
github: asGitHubService(github),
@@ -1832,7 +1836,7 @@ describe("session provider refresh cwd routing", () => {
test("get_providers_snapshot_request forwards cwd to the provider authority", async () => {
const messages: unknown[] = [];
const workspaceCwd = resolvePath("/tmp/session-provider-snapshot");
const { manager: providerSnapshotManager, getSnapshot } = createProviderSnapshotManagerStub();
const { manager: providerSnapshotManager, readSnapshot } = createProviderSnapshotManagerStub();
const session = createSessionForTest({ messages, providerSnapshotManager });
await session.handleMessage({
@@ -1841,7 +1845,7 @@ describe("session provider refresh cwd routing", () => {
requestId: "snapshot-workspace",
});
expect(getSnapshot).toHaveBeenCalledWith(workspaceCwd);
expect(readSnapshot).toHaveBeenCalledWith({ cwd: workspaceCwd });
});
test("preserves legacy model and mode list requests without cwd as global", async () => {
@@ -4328,7 +4332,10 @@ describe("session stash mutation handling", () => {
describe("session paseo worktree creation handling", () => {
test("forces workspace git refreshes for the source repo and created worktree", async () => {
const workspaceGitService = { getSnapshot: vi.fn().mockResolvedValue({}) };
const session = createSessionForTest({ workspaceGitService });
const session = createSessionForTest({
workspaceGitService,
workspaceRuntime: createStub<WorkspaceRuntimeService>({}),
});
paseoWorktreeServiceMocks.createPaseoWorktree.mockResolvedValue({
repoRoot: "/tmp/repo",
worktree: {
@@ -148,7 +148,10 @@ export function findByType<T extends SessionOutboundMessage["type"]>(
// ---------------------------------------------------------------------------
export interface ProviderSnapshotManagerSpies {
getSnapshot: ReturnType<typeof vi.fn<[cwd?: string], ProviderSnapshotEntry[]>>;
getSnapshot: ReturnType<
typeof vi.fn<[cwd?: string, workspaceId?: string], ProviderSnapshotEntry[]>
>;
readSnapshot: ReturnType<typeof vi.fn<[unknown], Promise<ProviderSnapshotEntry[]>>>;
refreshSnapshotForCwd: ReturnType<typeof vi.fn<[unknown], Promise<void>>>;
refreshSettingsSnapshot: ReturnType<typeof vi.fn<[unknown], Promise<void>>>;
warmUpSnapshotForCwd: ReturnType<typeof vi.fn<[unknown], Promise<void>>>;
@@ -175,7 +178,13 @@ export interface ProviderSnapshotManagerSpies {
export function createProviderSnapshotManagerStub(): {
manager: ProviderSnapshotManager;
} & ProviderSnapshotManagerSpies {
const getSnapshot = vi.fn<[cwd?: string], ProviderSnapshotEntry[]>(() => []);
const getSnapshot = vi.fn<[cwd?: string, workspaceId?: string], ProviderSnapshotEntry[]>(
() => [],
);
const readSnapshot = vi.fn<[unknown], Promise<ProviderSnapshotEntry[]>>(async (options) => {
const { cwd, workspaceId } = options as { cwd?: string | null; workspaceId?: string };
return getSnapshot(cwd ?? undefined, workspaceId);
});
const refreshSnapshotForCwd = vi.fn<[unknown], Promise<void>>(async () => {});
const refreshSettingsSnapshot = vi.fn<[unknown], Promise<void>>(async () => {});
const warmUpSnapshotForCwd = vi.fn<[unknown], Promise<void>>(async () => {});
@@ -218,6 +227,7 @@ export function createProviderSnapshotManagerStub(): {
const destroy = vi.fn<[], void>();
const stub = {
getSnapshot,
readSnapshot,
refreshSnapshotForCwd,
refreshSettingsSnapshot,
warmUpSnapshotForCwd,
@@ -244,6 +254,7 @@ export function createProviderSnapshotManagerStub(): {
return {
manager,
getSnapshot,
readSnapshot,
refreshSnapshotForCwd,
refreshSettingsSnapshot,
warmUpSnapshotForCwd,
@@ -17,6 +17,7 @@ import {
TerminalStreamOpcode,
} from "@getpaseo/protocol/terminal-stream-protocol";
import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities";
import type { WorkspaceRuntimeService } from "./workspace-runtime/index.js";
type SocketListener = (...args: unknown[]) => void;
@@ -293,6 +294,12 @@ function createServer(options?: {
undefined,
undefined,
createProviderSnapshotManagerStub().manager,
undefined,
undefined,
undefined,
undefined,
undefined,
createStub<WorkspaceRuntimeService>({}),
);
}