From bc1dbeb9985bbb215076c0e0a09f38022e500159 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:58:19 +0300 Subject: [PATCH] fix(tests): replace leaking mock.module in pi tests with injected deps CI run 35723560562 failed 3 tests in tests/utils/daemon-client.test.ts: getDaemonBaseUrl returned the default http://127.0.0.1:8008 even for an explicit daemonUrl. Root cause: PR #111 added mock.module over src/utils/daemon-client in tests/integrations/pi.test.ts. Bun's mock.module overrides persist for the rest of the run in the same worker, so when worker scheduling runs pi.test.ts after daemon-client.ts has been imported (cached) but before tests/utils/daemon-client.test.ts, the mocked getDaemonBaseUrl (host/daemonUrl-ignoring) replaces the real one. Deterministic on CI's Linux ordering, non-reproducible locally. mock.restore() does not revert mock.module, so fix at the seam instead: installPiIntegration now accepts optional { callDaemon, getDaemonBaseUrl } deps (defaulting to the real implementations) and pi.test.ts injects the mock. No mock.module remains for src/utils/daemon-client, eliminating the cross-file leak entirely. --- src/integrations/pi.ts | 14 ++++++++++++-- tests/integrations/pi.test.ts | 24 ++++++++++++++---------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/integrations/pi.ts b/src/integrations/pi.ts index 40f23a7..4fb0ea0 100644 --- a/src/integrations/pi.ts +++ b/src/integrations/pi.ts @@ -165,17 +165,27 @@ type PiConfig = { providers?: Record; }; +export type PiIntegrationDeps = { + callDaemon: typeof callDaemon; + getDaemonBaseUrl: typeof getDaemonBaseUrl; +}; + export async function installPiIntegration( config: RoutstrdConfig, apiKey: string, integrationConfig: IntegrationConfig, + // Injectable I/O so tests don't need mock.module, whose overrides leak + // across test files for the rest of the run under bun's runner. + deps: Partial = {}, ): Promise { const { name, configPath } = integrationConfig; + const callDaemonFn = deps.callDaemon ?? callDaemon; + const getDaemonBaseUrlFn = deps.getDaemonBaseUrl ?? getDaemonBaseUrl; console.log("\nInstalling routstr models in pi models.json..."); console.log(`Using API key for ${name}`); - const baseUrl = `${getDaemonBaseUrl(config)}/v1`; + const baseUrl = `${getDaemonBaseUrlFn(config)}/v1`; let piConfig: PiConfig = {}; @@ -197,7 +207,7 @@ export async function installPiIntegration( // Ensure directory exists mkdirSync(dirname(configPath), { recursive: true }); - const data = await callDaemon("/models"); + const data = await callDaemonFn("/models"); const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || []; if (models.length === 0) { diff --git a/tests/integrations/pi.test.ts b/tests/integrations/pi.test.ts index 88495e8..32a4240 100644 --- a/tests/integrations/pi.test.ts +++ b/tests/integrations/pi.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, mock } from "bun:test"; +import { describe, expect, it } from "bun:test"; import { mkdtempSync, readFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; @@ -10,12 +10,16 @@ import { buildPiModelEntry, deriveThinkingFields, installPiIntegration, + type PiIntegrationDeps, type PiModelEntry, } from "../../src/integrations/pi"; import type { RoutstrdConfig } from "../../src/utils/config"; -// Install the mock before importing modules that pull it in transitively. -mock.module("../../src/utils/daemon-client", () => ({ +// Injected I/O instead of mock.module: bun's mock.module overrides leak +// across test files for the rest of the run and made tests/utils/ +// daemon-client.test.ts fail non-deterministically depending on worker +// scheduling (see CI run 35723560562). +const MOCK_DEPS: Partial = { callDaemon: async () => ({ output: { models: [ @@ -36,7 +40,7 @@ mock.module("../../src/utils/daemon-client", () => ({ }), getDaemonBaseUrl: (config: RoutstrdConfig) => `http://127.0.0.1:${config.port}`, -})); +}; const CONFIG: RoutstrdConfig = { port: 8008 } as RoutstrdConfig; @@ -55,7 +59,7 @@ describe("installPiIntegration", () => { it("pins supportsDeveloperRole=false for deepseek* models", async () => { const dir = mkdtempSync(join(tmpdir(), "pi-models-")); const configPath = join(dir, "models.json"); - await installPiIntegration(CONFIG, "key", makeIntegration(configPath)); + await installPiIntegration(CONFIG, "key", makeIntegration(configPath), MOCK_DEPS); const provider = await readProvider(configPath); const deepseek = provider.models.find((m) => m.id === "deepseek-v4.1-flash"); @@ -65,7 +69,7 @@ describe("installPiIntegration", () => { it("leaves non-deepseek models without a compat block", async () => { const dir = mkdtempSync(join(tmpdir(), "pi-models-")); const configPath = join(dir, "models.json"); - await installPiIntegration(CONFIG, "key", makeIntegration(configPath)); + await installPiIntegration(CONFIG, "key", makeIntegration(configPath), MOCK_DEPS); const provider = await readProvider(configPath); const glm = provider.models.find((m) => m.id === "glm-5.3"); @@ -75,7 +79,7 @@ describe("installPiIntegration", () => { it("preserves user compat keys on deepseek* models while pinning the role", async () => { const dir = mkdtempSync(join(tmpdir(), "pi-models-")); const configPath = join(dir, "models.json"); - await installPiIntegration(CONFIG, "key", makeIntegration(configPath)); + await installPiIntegration(CONFIG, "key", makeIntegration(configPath), MOCK_DEPS); // Simulate a user-curated refresh: seed reasoning/compat, run again. const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as { @@ -89,7 +93,7 @@ describe("installPiIntegration", () => { const { writeFileSync } = await import("fs"); writeFileSync(configPath, JSON.stringify(parsed)); - await installPiIntegration(CONFIG, "key", makeIntegration(configPath)); + await installPiIntegration(CONFIG, "key", makeIntegration(configPath), MOCK_DEPS); const provider = await readProvider(configPath); const updated = provider.models.find((m) => m.id === "deepseek-v4.1-flash"); expect(updated?.compat).toEqual({ @@ -102,7 +106,7 @@ describe("installPiIntegration", () => { it("preserves compat untouched for non-deepseek models across refreshes", async () => { const dir = mkdtempSync(join(tmpdir(), "pi-models-")); const configPath = join(dir, "models.json"); - await installPiIntegration(CONFIG, "key", makeIntegration(configPath)); + await installPiIntegration(CONFIG, "key", makeIntegration(configPath), MOCK_DEPS); const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as { providers: Record> }>; @@ -112,7 +116,7 @@ describe("installPiIntegration", () => { const { writeFileSync } = await import("fs"); writeFileSync(configPath, JSON.stringify(parsed)); - await installPiIntegration(CONFIG, "key", makeIntegration(configPath)); + await installPiIntegration(CONFIG, "key", makeIntegration(configPath), MOCK_DEPS); const provider = await readProvider(configPath); const updated = provider.models.find((m) => m.id === "glm-5.3"); expect(updated?.compat).toEqual({ supportsDeveloperRole: true });