mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 09:42:53 +00:00
fix(workspaces): restore clean runtime test paths
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"dev": "./scripts/dev.sh",
|
||||
"predev:win": "npm run dev:prerequisites",
|
||||
"dev:win": "powershell ./scripts/dev.ps1",
|
||||
"pretest:e2e:renderer": "npm --prefix ../.. run build:workspace-runtime-fixture",
|
||||
"test:e2e:renderer": "cross-env E2E_DESKTOP_RUNTIME=1 playwright test --config=playwright.config.ts --project=desktop",
|
||||
"test:e2e:browser-tabs": "npm run build:main && node ./e2e/browser-tabs.e2e.mjs",
|
||||
"verify:electron-cdp": "node ./scripts/verify-electron-cdp.mjs",
|
||||
|
||||
@@ -19,6 +19,7 @@ import { describe, expect, test } from "vitest";
|
||||
const desktopDir = path.resolve(import.meta.dirname, "..");
|
||||
const repoRoot = path.resolve(desktopDir, "../..");
|
||||
const execFileAsync = promisify(execFile);
|
||||
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const prerequisite = "npm run dev:prerequisites";
|
||||
|
||||
describe("desktop dev build contract", () => {
|
||||
@@ -38,6 +39,14 @@ describe("desktop dev build contract", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("builds the private runtime fixture before renderer acceptance", async () => {
|
||||
const packageJson = JSON.parse(await readFile(path.join(desktopDir, "package.json"), "utf8"));
|
||||
|
||||
expect(packageJson.scripts["pretest:e2e:renderer"]).toBe(
|
||||
"npm --prefix ../.. run build:workspace-runtime-fixture",
|
||||
);
|
||||
});
|
||||
|
||||
test.each(["missing", "stale"])(
|
||||
"the public prerequisite repairs %s server artifacts in an isolated checkout",
|
||||
async (initialState) => {
|
||||
@@ -118,7 +127,7 @@ async function createIsolatedBuildCheckout() {
|
||||
|
||||
async function runPrerequisite(isolatedRoot) {
|
||||
try {
|
||||
await execFileAsync("npm", ["run", "dev:prerequisites", "--workspace=@getpaseo/desktop"], {
|
||||
await execFileAsync(npmCommand, ["run", "dev:prerequisites", "--workspace=@getpaseo/desktop"], {
|
||||
cwd: isolatedRoot,
|
||||
timeout: 60_000,
|
||||
});
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"speech:download": "tsx scripts/download-speech-models.ts",
|
||||
"speech:transcribe:local": "tsx scripts/transcribe-local-wav.ts",
|
||||
"test": "npm run test:unit && npm run test:integration",
|
||||
"pretest:unit": "npm --prefix ../.. run build:workspace-runtime-fixture",
|
||||
"test:unit": "vitest run --fileParallelism --exclude \"**/*.e2e.test.ts\"",
|
||||
"test:integration": "vitest run --maxWorkers=1 src/server/daemon-e2e/models.e2e.test.ts src/server/daemon-e2e/live-preferences.e2e.test.ts src/server/agent/model-catalog.e2e.test.ts && vitest run --maxWorkers=1 src/server/session.create-agent-worktree-autoarchive.e2e.test.ts -t \"creates a worktree and auto-archives both\"",
|
||||
"test:hub-cli-contract": "vitest run --maxWorkers=1 src/server/hub/hub-cli-contract.test.ts",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>({}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"clean": "node ../../scripts/clean-package-dist.mjs",
|
||||
"build": "tsc -p tsconfig.json --incremental false && node -e \"const fs=require('node:fs');fs.copyFileSync('src/executable.mjs','dist/executable.mjs');fs.chmodSync('dist/executable.mjs',0o755)\"",
|
||||
"build:clean": "npm run clean && npm run build",
|
||||
"prepack": "npm run build:clean",
|
||||
"test": "npm run build && vitest run test/workspace-helper.posix.test.ts --bail=1",
|
||||
"test:standalone": "node --test test/standalone.test.mjs",
|
||||
"typecheck": "tsgo -p tsconfig.json --noEmit"
|
||||
|
||||
@@ -7,6 +7,7 @@ import assert from "node:assert/strict";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const run = promisify(execFile);
|
||||
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const packageRoot = path.resolve(import.meta.dirname, "..");
|
||||
|
||||
test("packed helper works from a fresh project without monorepo source fallback", async (t) => {
|
||||
@@ -15,10 +16,10 @@ test("packed helper works from a fresh project without monorepo source fallback"
|
||||
const packDirectory = path.join(root, "packs");
|
||||
const project = path.join(root, "project");
|
||||
await Promise.all([mkdir(packDirectory), mkdir(project)]);
|
||||
await run("npm", ["run", "build"], { cwd: packageRoot });
|
||||
await run(npmCommand, ["run", "build"], { cwd: packageRoot });
|
||||
const packed = JSON.parse(
|
||||
(
|
||||
await run("npm", ["pack", "--json", "--pack-destination", packDirectory], {
|
||||
await run(npmCommand, ["pack", "--json", "--pack-destination", packDirectory], {
|
||||
cwd: packageRoot,
|
||||
})
|
||||
).stdout,
|
||||
@@ -30,7 +31,7 @@ test("packed helper works from a fresh project without monorepo source fallback"
|
||||
dependencies: { "@getpaseo/workspace-helper": `file:${path.join(packDirectory, packed)}` },
|
||||
}),
|
||||
);
|
||||
await run("npm", ["install", "--ignore-scripts"], { cwd: project });
|
||||
await run(npmCommand, ["install", "--ignore-scripts"], { cwd: project });
|
||||
await writeFile(path.join(project, "notes.txt"), "before\n");
|
||||
|
||||
const imported = JSON.parse(
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"clean": "node ../../scripts/clean-package-dist.mjs",
|
||||
"build": "tsc -p tsconfig.json --incremental false",
|
||||
"build:clean": "npm run clean && npm run build",
|
||||
"prepack": "npm run build:clean",
|
||||
"test": "npm run build && tsc -p tsconfig.test.json && node --test test/*.test.mjs",
|
||||
"typecheck": "tsgo -p tsconfig.json --noEmit"
|
||||
},
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from "../dist/index.js";
|
||||
|
||||
const run = promisify(execFile);
|
||||
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const examples = JSON.parse(await readFile(path.join(packageRoot, "examples/v1.json"), "utf8"));
|
||||
|
||||
@@ -521,7 +522,7 @@ test("every export condition resolves from a packed standalone install", async (
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const pack = JSON.parse(
|
||||
(
|
||||
await run("npm", ["pack", "--json", "--ignore-scripts", "--pack-destination", root], {
|
||||
await run(npmCommand, ["pack", "--json", "--ignore-scripts", "--pack-destination", root], {
|
||||
cwd: packageRoot,
|
||||
})
|
||||
).stdout,
|
||||
@@ -529,7 +530,7 @@ test("every export condition resolves from a packed standalone install", async (
|
||||
const archive = path.join(root, pack[0].filename);
|
||||
const installRoot = path.join(root, "standalone");
|
||||
await mkdir(installRoot);
|
||||
await run("npm", ["install", "--ignore-scripts", "--no-package-lock", archive], {
|
||||
await run(npmCommand, ["install", "--ignore-scripts", "--no-package-lock", archive], {
|
||||
cwd: installRoot,
|
||||
});
|
||||
const installed = path.join(installRoot, "node_modules/@getpaseo/workspace-runtime-contract");
|
||||
|
||||
@@ -56,6 +56,58 @@ test("build:server:clean constructs public prerequisites without runtime impleme
|
||||
await assert.rejects(access(path.join(isolatedRoot, "runtimes")));
|
||||
});
|
||||
|
||||
test("public runtime packages construct complete tarballs from source", async (t) => {
|
||||
const isolatedRoot = await mkdtemp(path.join(os.tmpdir(), "paseo-runtime-packages-"));
|
||||
t.after(() => rm(isolatedRoot, { recursive: true, force: true }));
|
||||
|
||||
await copyBuildCheckout(isolatedRoot);
|
||||
await createNodeModulesOverlay(isolatedRoot);
|
||||
await createPackageNodeModulesOverlays(isolatedRoot);
|
||||
|
||||
for (const relativePath of [
|
||||
"packages/workspace-runtime-contract/dist",
|
||||
"packages/workspace-helper/dist",
|
||||
]) {
|
||||
await assert.rejects(access(path.join(isolatedRoot, relativePath)), relativePath);
|
||||
}
|
||||
|
||||
const packDirectory = path.join(isolatedRoot, "packs");
|
||||
await mkdir(packDirectory);
|
||||
const packages = [
|
||||
{
|
||||
workspace: "@getpaseo/workspace-runtime-contract",
|
||||
files: ["dist/index.js", "dist/index.d.ts"],
|
||||
},
|
||||
{
|
||||
workspace: "@getpaseo/workspace-helper",
|
||||
files: ["dist/index.js", "dist/index.d.ts", "dist/executable.mjs"],
|
||||
executable: "dist/executable.mjs",
|
||||
},
|
||||
];
|
||||
|
||||
for (const expected of packages) {
|
||||
const packed = JSON.parse(
|
||||
(
|
||||
await run(
|
||||
"npm",
|
||||
[
|
||||
"pack",
|
||||
"--json",
|
||||
`--workspace=${expected.workspace}`,
|
||||
"--pack-destination",
|
||||
packDirectory,
|
||||
],
|
||||
{ cwd: isolatedRoot, timeout: 120_000 },
|
||||
)
|
||||
).stdout,
|
||||
)[0];
|
||||
const files = new Map(packed.files.map((file) => [file.path, file]));
|
||||
for (const expectedFile of expected.files) assert.ok(files.has(expectedFile), expectedFile);
|
||||
if (expected.executable) assert.equal(files.get(expected.executable)?.mode, 0o755);
|
||||
await access(path.join(packDirectory, packed.filename));
|
||||
}
|
||||
});
|
||||
|
||||
async function copyBuildCheckout(isolatedRoot) {
|
||||
await Promise.all([
|
||||
...["package.json", "package-lock.json", "tsconfig.base.json", "tsconfig.json"].map((file) =>
|
||||
|
||||
Reference in New Issue
Block a user