From f08f58f5f9525d3f532eda1df9082f1831241e2f Mon Sep 17 00:00:00 2001 From: Thomas Mustier <6326440+tmustier@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:27:43 +0100 Subject: [PATCH] fix(coding-agent): run coding-agent tests offline by default (#7031) --- packages/coding-agent/test/git-update.test.ts | 2 + .../coding-agent/test/model-registry.test.ts | 2 +- .../test/package-command-paths.test.ts | 2 + .../coding-agent/test/package-manager.test.ts | 37 ++++++------------- packages/coding-agent/test/radius.test.ts | 4 +- .../test/stdout-cleanliness.test.ts | 7 +++- .../coding-agent/test/test-network-env.ts | 6 +++ .../coding-agent/test/version-check.test.ts | 13 +++---- packages/coding-agent/vitest.config.ts | 3 ++ 9 files changed, 41 insertions(+), 35 deletions(-) create mode 100644 packages/coding-agent/test/test-network-env.ts diff --git a/packages/coding-agent/test/git-update.test.ts b/packages/coding-agent/test/git-update.test.ts index 1508adcdc..7b2a8f7e4 100644 --- a/packages/coding-agent/test/git-update.test.ts +++ b/packages/coding-agent/test/git-update.test.ts @@ -13,6 +13,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { DefaultPackageManager } from "../src/core/package-manager.ts"; import { SettingsManager } from "../src/core/settings-manager.ts"; +import { allowNetwork } from "./test-network-env.ts"; // Helper to run git commands in a directory function git(args: string[], cwd: string): string { @@ -78,6 +79,7 @@ describe("DefaultPackageManager git update", () => { const gitSource = "git:github.com/test/extension"; beforeEach(() => { + allowNetwork(); tempDir = join(tmpdir(), `git-update-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); mkdirSync(tempDir, { recursive: true }); remoteDir = join(tempDir, "remote"); diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 70a8fe9e8..ef4b133b9 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -652,7 +652,7 @@ describe("ModelRegistry", () => { expect(anthropicModels.some((m) => m.id === "claude-custom")).toBe(false); expect(anthropicModels.some((m) => m.id === "claude-custom-2")).toBe(true); expect(anthropicModels.some((m) => m.id.includes("claude"))).toBe(true); - }, 60_000); + }); test("removing custom models from models.json keeps built-in provider models", async () => { writeModelsJson({ diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts index eb6972bf0..c7b11c8cc 100644 --- a/packages/coding-agent/test/package-command-paths.test.ts +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -10,6 +10,7 @@ import { ProjectTrustStore } from "../src/core/trust-manager.ts"; import { main } from "../src/main.ts"; import { ConfigSelectorComponent } from "../src/modes/interactive/components/config-selector.ts"; import { handlePackageCommand } from "../src/package-manager-cli.ts"; +import { allowNetwork } from "./test-network-env.ts"; describe("package commands", () => { let tempDir: string; @@ -51,6 +52,7 @@ describe("package commands", () => { } beforeEach(() => { + allowNetwork(); tempDir = join(tmpdir(), `pi-package-commands-${Date.now()}-${Math.random().toString(36).slice(2)}`); agentDir = join(tempDir, "agent"); projectDir = join(tempDir, "project"); diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index a4451e190..68cc0ec77 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -1101,42 +1101,29 @@ Content`, it("should emit progress events on install attempt", async () => { const events: ProgressEvent[] = []; packageManager.setProgressCallback((event) => events.push(event)); + vi.spyOn(packageManager as unknown as PackageManagerInternals, "runCommand").mockRejectedValue( + new Error("simulated npm install failure"), + ); - // Use public install method which emits progress events - try { - await packageManager.install("npm:nonexistent-package@1.0.0"); - } catch { - // Expected to fail - package doesn't exist - } + await expect(packageManager.install("npm:nonexistent-package@1.0.0")).rejects.toThrow( + "simulated npm install failure", + ); - // Should have emitted start event before failure expect(events.some((e) => e.type === "start" && e.action === "install")).toBe(true); - // Should have emitted error event expect(events.some((e) => e.type === "error")).toBe(true); }); it("should recognize github URLs without git: prefix", async () => { const events: ProgressEvent[] = []; packageManager.setProgressCallback((event) => events.push(event)); - const previousGitTerminalPrompt = process.env.GIT_TERMINAL_PROMPT; - process.env.GIT_TERMINAL_PROMPT = "0"; + const runCommand = vi + .spyOn(packageManager as unknown as PackageManagerInternals, "runCommand") + .mockRejectedValue(new Error("simulated git clone failure")); + const source = "https://github.com/nonexistent/repo"; - try { - // This should be parsed as a git source, not throw "unsupported" - try { - await packageManager.install("https://github.com/nonexistent/repo"); - } catch { - // Expected to fail - repo doesn't exist - } - } finally { - if (previousGitTerminalPrompt === undefined) { - delete process.env.GIT_TERMINAL_PROMPT; - } else { - process.env.GIT_TERMINAL_PROMPT = previousGitTerminalPrompt; - } - } + await expect(packageManager.install(source)).rejects.toThrow("simulated git clone failure"); - // Should have attempted clone, not thrown unsupported error + expect(runCommand).toHaveBeenCalledWith("git", ["clone", source, expect.any(String)]); expect(events.some((e) => e.type === "start" && e.action === "install")).toBe(true); }); diff --git a/packages/coding-agent/test/radius.test.ts b/packages/coding-agent/test/radius.test.ts index 8a7f5965a..630bcecf2 100644 --- a/packages/coding-agent/test/radius.test.ts +++ b/packages/coding-agent/test/radius.test.ts @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.ts"; import { ModelRuntime } from "../src/core/model-runtime.ts"; import { RADIUS_PROVIDER_ID } from "../src/core/radius.ts"; +import { allowNetwork } from "./test-network-env.ts"; function radiusOAuthCredential(gatewayBaseUrl: string) { return { @@ -37,6 +38,7 @@ function radiusConfig(baseUrl: string) { let tempDir: string; beforeEach(() => { + allowNetwork(); tempDir = join(tmpdir(), `pi-test-radius-${Date.now()}-${Math.random().toString(36).slice(2)}`); mkdirSync(tempDir, { recursive: true }); }); @@ -92,7 +94,7 @@ describe("Radius provider", () => { }); it("does not refresh catalogs over the network by default", async () => { - const fetchSpy = vi.spyOn(globalThis, "fetch"); + const fetchSpy = vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("unexpected catalog fetch")); const runtime = await ModelRuntime.create({ credentials: AuthStorage.inMemory({ [RADIUS_PROVIDER_ID]: radiusOAuthCredential("https://radius.example.com/v1"), diff --git a/packages/coding-agent/test/stdout-cleanliness.test.ts b/packages/coding-agent/test/stdout-cleanliness.test.ts index f1b31eb3f..83abde7c9 100644 --- a/packages/coding-agent/test/stdout-cleanliness.test.ts +++ b/packages/coding-agent/test/stdout-cleanliness.test.ts @@ -2,13 +2,18 @@ import { spawn } from "node:child_process"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { ENV_AGENT_DIR } from "../src/config.ts"; +import { allowNetwork } from "./test-network-env.ts"; const cliPath = resolve(__dirname, "../src/cli.ts"); const tempDirs: string[] = []; +beforeEach(() => { + allowNetwork(); +}); + afterEach(() => { for (const dir of tempDirs.splice(0)) { rmSync(dir, { recursive: true, force: true }); diff --git a/packages/coding-agent/test/test-network-env.ts b/packages/coding-agent/test/test-network-env.ts new file mode 100644 index 000000000..35515f345 --- /dev/null +++ b/packages/coding-agent/test/test-network-env.ts @@ -0,0 +1,6 @@ +import { vi } from "vitest"; + +/** Enable network code paths for tests that replace external I/O with local fixtures or mocks. */ +export function allowNetwork(): void { + vi.stubEnv("PI_OFFLINE", undefined); +} diff --git a/packages/coding-agent/test/version-check.test.ts b/packages/coding-agent/test/version-check.test.ts index 6e2f0ac4c..c06ee12cb 100644 --- a/packages/coding-agent/test/version-check.test.ts +++ b/packages/coding-agent/test/version-check.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { checkForNewPiVersion, comparePackageVersions, @@ -6,9 +6,13 @@ import { getLatestPiVersion, isNewerPackageVersion, } from "../src/utils/version-check.ts"; +import { allowNetwork } from "./test-network-env.ts"; const originalSkipVersionCheck = process.env.PI_SKIP_VERSION_CHECK; -const originalOffline = process.env.PI_OFFLINE; + +beforeEach(() => { + allowNetwork(); +}); afterEach(() => { vi.unstubAllGlobals(); @@ -17,11 +21,6 @@ afterEach(() => { } else { process.env.PI_SKIP_VERSION_CHECK = originalSkipVersionCheck; } - if (originalOffline === undefined) { - delete process.env.PI_OFFLINE; - } else { - process.env.PI_OFFLINE = originalOffline; - } }); describe("version checks", () => { diff --git a/packages/coding-agent/vitest.config.ts b/packages/coding-agent/vitest.config.ts index dbd8f34ac..19057ab89 100644 --- a/packages/coding-agent/vitest.config.ts +++ b/packages/coding-agent/vitest.config.ts @@ -8,6 +8,9 @@ export default mergeConfig( globals: true, environment: "node", testTimeout: 30000, + // Tests run offline by default; opt in with allowNetwork() from test/test-network-env.ts. + env: { PI_OFFLINE: "1" }, + unstubEnvs: true, reporters: process.env.GITHUB_ACTIONS ? ["dot", "github-actions"] : ["dot"], silent: "passed-only", server: {