mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-05 20:38:22 +00:00
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.
This commit is contained in:
+12
-2
@@ -165,17 +165,27 @@ type PiConfig = {
|
|||||||
providers?: Record<string, PiProviderConfig>;
|
providers?: Record<string, PiProviderConfig>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PiIntegrationDeps = {
|
||||||
|
callDaemon: typeof callDaemon;
|
||||||
|
getDaemonBaseUrl: typeof getDaemonBaseUrl;
|
||||||
|
};
|
||||||
|
|
||||||
export async function installPiIntegration(
|
export async function installPiIntegration(
|
||||||
config: RoutstrdConfig,
|
config: RoutstrdConfig,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
integrationConfig: IntegrationConfig,
|
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<PiIntegrationDeps> = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { name, configPath } = integrationConfig;
|
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("\nInstalling routstr models in pi models.json...");
|
||||||
console.log(`Using API key for ${name}`);
|
console.log(`Using API key for ${name}`);
|
||||||
|
|
||||||
const baseUrl = `${getDaemonBaseUrl(config)}/v1`;
|
const baseUrl = `${getDaemonBaseUrlFn(config)}/v1`;
|
||||||
|
|
||||||
let piConfig: PiConfig = {};
|
let piConfig: PiConfig = {};
|
||||||
|
|
||||||
@@ -197,7 +207,7 @@ export async function installPiIntegration(
|
|||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
mkdirSync(dirname(configPath), { recursive: true });
|
mkdirSync(dirname(configPath), { recursive: true });
|
||||||
|
|
||||||
const data = await callDaemon("/models");
|
const data = await callDaemonFn("/models");
|
||||||
const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
|
const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
|
||||||
|
|
||||||
if (models.length === 0) {
|
if (models.length === 0) {
|
||||||
|
|||||||
@@ -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 { mkdtempSync, readFileSync } from "fs";
|
||||||
import { tmpdir } from "os";
|
import { tmpdir } from "os";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
@@ -10,12 +10,16 @@ import {
|
|||||||
buildPiModelEntry,
|
buildPiModelEntry,
|
||||||
deriveThinkingFields,
|
deriveThinkingFields,
|
||||||
installPiIntegration,
|
installPiIntegration,
|
||||||
|
type PiIntegrationDeps,
|
||||||
type PiModelEntry,
|
type PiModelEntry,
|
||||||
} from "../../src/integrations/pi";
|
} from "../../src/integrations/pi";
|
||||||
import type { RoutstrdConfig } from "../../src/utils/config";
|
import type { RoutstrdConfig } from "../../src/utils/config";
|
||||||
|
|
||||||
// Install the mock before importing modules that pull it in transitively.
|
// Injected I/O instead of mock.module: bun's mock.module overrides leak
|
||||||
mock.module("../../src/utils/daemon-client", () => ({
|
// 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<PiIntegrationDeps> = {
|
||||||
callDaemon: async () => ({
|
callDaemon: async () => ({
|
||||||
output: {
|
output: {
|
||||||
models: [
|
models: [
|
||||||
@@ -36,7 +40,7 @@ mock.module("../../src/utils/daemon-client", () => ({
|
|||||||
}),
|
}),
|
||||||
getDaemonBaseUrl: (config: RoutstrdConfig) =>
|
getDaemonBaseUrl: (config: RoutstrdConfig) =>
|
||||||
`http://127.0.0.1:${config.port}`,
|
`http://127.0.0.1:${config.port}`,
|
||||||
}));
|
};
|
||||||
|
|
||||||
const CONFIG: RoutstrdConfig = { port: 8008 } as RoutstrdConfig;
|
const CONFIG: RoutstrdConfig = { port: 8008 } as RoutstrdConfig;
|
||||||
|
|
||||||
@@ -55,7 +59,7 @@ describe("installPiIntegration", () => {
|
|||||||
it("pins supportsDeveloperRole=false for deepseek* models", async () => {
|
it("pins supportsDeveloperRole=false for deepseek* models", async () => {
|
||||||
const dir = mkdtempSync(join(tmpdir(), "pi-models-"));
|
const dir = mkdtempSync(join(tmpdir(), "pi-models-"));
|
||||||
const configPath = join(dir, "models.json");
|
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 provider = await readProvider(configPath);
|
||||||
const deepseek = provider.models.find((m) => m.id === "deepseek-v4.1-flash");
|
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 () => {
|
it("leaves non-deepseek models without a compat block", async () => {
|
||||||
const dir = mkdtempSync(join(tmpdir(), "pi-models-"));
|
const dir = mkdtempSync(join(tmpdir(), "pi-models-"));
|
||||||
const configPath = join(dir, "models.json");
|
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 provider = await readProvider(configPath);
|
||||||
const glm = provider.models.find((m) => m.id === "glm-5.3");
|
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 () => {
|
it("preserves user compat keys on deepseek* models while pinning the role", async () => {
|
||||||
const dir = mkdtempSync(join(tmpdir(), "pi-models-"));
|
const dir = mkdtempSync(join(tmpdir(), "pi-models-"));
|
||||||
const configPath = join(dir, "models.json");
|
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.
|
// Simulate a user-curated refresh: seed reasoning/compat, run again.
|
||||||
const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as {
|
const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as {
|
||||||
@@ -89,7 +93,7 @@ describe("installPiIntegration", () => {
|
|||||||
const { writeFileSync } = await import("fs");
|
const { writeFileSync } = await import("fs");
|
||||||
writeFileSync(configPath, JSON.stringify(parsed));
|
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 provider = await readProvider(configPath);
|
||||||
const updated = provider.models.find((m) => m.id === "deepseek-v4.1-flash");
|
const updated = provider.models.find((m) => m.id === "deepseek-v4.1-flash");
|
||||||
expect(updated?.compat).toEqual({
|
expect(updated?.compat).toEqual({
|
||||||
@@ -102,7 +106,7 @@ describe("installPiIntegration", () => {
|
|||||||
it("preserves compat untouched for non-deepseek models across refreshes", async () => {
|
it("preserves compat untouched for non-deepseek models across refreshes", async () => {
|
||||||
const dir = mkdtempSync(join(tmpdir(), "pi-models-"));
|
const dir = mkdtempSync(join(tmpdir(), "pi-models-"));
|
||||||
const configPath = join(dir, "models.json");
|
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 {
|
const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as {
|
||||||
providers: Record<string, { models: Array<Record<string, unknown>> }>;
|
providers: Record<string, { models: Array<Record<string, unknown>> }>;
|
||||||
@@ -112,7 +116,7 @@ describe("installPiIntegration", () => {
|
|||||||
const { writeFileSync } = await import("fs");
|
const { writeFileSync } = await import("fs");
|
||||||
writeFileSync(configPath, JSON.stringify(parsed));
|
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 provider = await readProvider(configPath);
|
||||||
const updated = provider.models.find((m) => m.id === "glm-5.3");
|
const updated = provider.models.find((m) => m.id === "glm-5.3");
|
||||||
expect(updated?.compat).toEqual({ supportsDeveloperRole: true });
|
expect(updated?.compat).toEqual({ supportsDeveloperRole: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user