From 02fe0c840e52f99df487ab881ddeeecdde6e5463 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Feb 2026 15:53:26 +0000 Subject: [PATCH] perf(test): remove resetModules from auth/models/subagent suites --- src/agents/model-auth.e2e.test.ts | 50 ++------------- ...thub-copilot-provider-token-is.e2e.test.ts | 60 ++++++++---------- ...t-baseurl-token-exchange-fails.e2e.test.ts | 51 ++++++++------- ...g-provider-apikey-from-env-var.e2e.test.ts | 12 +--- ...iting-models-json-no-env-token.e2e.test.ts | 19 +----- ...hub-copilot-profile-env-tokens.e2e.test.ts | 62 +++++++++---------- .../subagent-registry.persistence.e2e.test.ts | 45 ++++++-------- src/agents/subagent-registry.store.ts | 4 +- src/agents/subagent-registry.ts | 6 +- 9 files changed, 118 insertions(+), 191 deletions(-) diff --git a/src/agents/model-auth.e2e.test.ts b/src/agents/model-auth.e2e.test.ts index 1924ebf318..7385f18ee3 100644 --- a/src/agents/model-auth.e2e.test.ts +++ b/src/agents/model-auth.e2e.test.ts @@ -2,7 +2,9 @@ import type { Api, Model } from "@mariozechner/pi-ai"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; +import { ensureAuthProfileStore } from "./auth-profiles.js"; +import { getApiKeyForModel, resolveApiKeyForProvider, resolveEnvApiKey } from "./model-auth.js"; const oauthFixture = { access: "access-token", @@ -31,10 +33,6 @@ describe("getApiKeyForModel", () => { "utf8", ); - vi.resetModules(); - const { ensureAuthProfileStore } = await import("./auth-profiles.js"); - const { getApiKeyForModel } = await import("./model-auth.js"); - const model = { id: "codex-mini-latest", provider: "openai-codex", @@ -131,9 +129,6 @@ describe("getApiKeyForModel", () => { "utf8", ); - vi.resetModules(); - const { resolveApiKeyForProvider } = await import("./model-auth.js"); - let error: unknown = null; try { await resolveApiKeyForProvider({ provider: "openai" }); @@ -174,9 +169,6 @@ describe("getApiKeyForModel", () => { delete process.env.ZAI_API_KEY; delete process.env.Z_AI_API_KEY; - vi.resetModules(); - const { resolveApiKeyForProvider } = await import("./model-auth.js"); - let error: unknown = null; try { await resolveApiKeyForProvider({ @@ -210,9 +202,6 @@ describe("getApiKeyForModel", () => { delete process.env.ZAI_API_KEY; process.env.Z_AI_API_KEY = "zai-test-key"; - vi.resetModules(); - const { resolveApiKeyForProvider } = await import("./model-auth.js"); - const resolved = await resolveApiKeyForProvider({ provider: "zai", store: { version: 1, profiles: {} }, @@ -239,9 +228,6 @@ describe("getApiKeyForModel", () => { try { process.env.SYNTHETIC_API_KEY = "synthetic-test-key"; - vi.resetModules(); - const { resolveApiKeyForProvider } = await import("./model-auth.js"); - const resolved = await resolveApiKeyForProvider({ provider: "synthetic", store: { version: 1, profiles: {} }, @@ -263,9 +249,6 @@ describe("getApiKeyForModel", () => { try { process.env.QIANFAN_API_KEY = "qianfan-test-key"; - vi.resetModules(); - const { resolveApiKeyForProvider } = await import("./model-auth.js"); - const resolved = await resolveApiKeyForProvider({ provider: "qianfan", store: { version: 1, profiles: {} }, @@ -287,9 +270,6 @@ describe("getApiKeyForModel", () => { try { process.env.AI_GATEWAY_API_KEY = "gateway-test-key"; - vi.resetModules(); - const { resolveApiKeyForProvider } = await import("./model-auth.js"); - const resolved = await resolveApiKeyForProvider({ provider: "vercel-ai-gateway", store: { version: 1, profiles: {} }, @@ -319,9 +299,6 @@ describe("getApiKeyForModel", () => { process.env.AWS_SECRET_ACCESS_KEY = "secret-key"; process.env.AWS_PROFILE = "profile"; - vi.resetModules(); - const { resolveApiKeyForProvider } = await import("./model-auth.js"); - const resolved = await resolveApiKeyForProvider({ provider: "amazon-bedrock", store: { version: 1, profiles: {} }, @@ -380,9 +357,6 @@ describe("getApiKeyForModel", () => { process.env.AWS_SECRET_ACCESS_KEY = "secret-key"; process.env.AWS_PROFILE = "profile"; - vi.resetModules(); - const { resolveApiKeyForProvider } = await import("./model-auth.js"); - const resolved = await resolveApiKeyForProvider({ provider: "amazon-bedrock", store: { version: 1, profiles: {} }, @@ -441,9 +415,6 @@ describe("getApiKeyForModel", () => { delete process.env.AWS_SECRET_ACCESS_KEY; process.env.AWS_PROFILE = "profile"; - vi.resetModules(); - const { resolveApiKeyForProvider } = await import("./model-auth.js"); - const resolved = await resolveApiKeyForProvider({ provider: "amazon-bedrock", store: { version: 1, profiles: {} }, @@ -494,9 +465,6 @@ describe("getApiKeyForModel", () => { try { process.env.VOYAGE_API_KEY = "voyage-test-key"; - vi.resetModules(); - const { resolveApiKeyForProvider } = await import("./model-auth.js"); - const resolved = await resolveApiKeyForProvider({ provider: "voyage", store: { version: 1, profiles: {} }, @@ -518,9 +486,6 @@ describe("getApiKeyForModel", () => { try { process.env.ANTHROPIC_API_KEY = "sk-ant-test-\r\nkey"; - vi.resetModules(); - const { resolveEnvApiKey } = await import("./model-auth.js"); - const resolved = resolveEnvApiKey("anthropic"); expect(resolved?.apiKey).toBe("sk-ant-test-key"); expect(resolved?.source).toContain("ANTHROPIC_API_KEY"); @@ -539,8 +504,7 @@ describe("getApiKeyForModel", () => { try { delete process.env.HF_TOKEN; process.env.HUGGINGFACE_HUB_TOKEN = "hf_hub_xyz"; - vi.resetModules(); - const { resolveEnvApiKey } = await import("./model-auth.js"); + const resolved = resolveEnvApiKey("huggingface"); expect(resolved?.apiKey).toBe("hf_hub_xyz"); expect(resolved?.source).toContain("HUGGINGFACE_HUB_TOKEN"); @@ -564,8 +528,7 @@ describe("getApiKeyForModel", () => { try { process.env.HUGGINGFACE_HUB_TOKEN = "hf_hub_first"; process.env.HF_TOKEN = "hf_second"; - vi.resetModules(); - const { resolveEnvApiKey } = await import("./model-auth.js"); + const resolved = resolveEnvApiKey("huggingface"); expect(resolved?.apiKey).toBe("hf_hub_first"); expect(resolved?.source).toContain("HUGGINGFACE_HUB_TOKEN"); @@ -589,8 +552,7 @@ describe("getApiKeyForModel", () => { try { delete process.env.HUGGINGFACE_HUB_TOKEN; process.env.HF_TOKEN = "hf_abc123"; - vi.resetModules(); - const { resolveEnvApiKey } = await import("./model-auth.js"); + const resolved = resolveEnvApiKey("huggingface"); expect(resolved?.apiKey).toBe("hf_abc123"); expect(resolved?.source).toContain("HF_TOKEN"); diff --git a/src/agents/models-config.auto-injects-github-copilot-provider-token-is.e2e.test.ts b/src/agents/models-config.auto-injects-github-copilot-provider-token-is.e2e.test.ts index 199ba0ca89..a2f93b7961 100644 --- a/src/agents/models-config.auto-injects-github-copilot-provider-token-is.e2e.test.ts +++ b/src/agents/models-config.auto-injects-github-copilot-provider-token-is.e2e.test.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; +import { ensureOpenClawModelsJson } from "./models-config.js"; async function withTempHome(fn: (home: string) => Promise): Promise { return withTempHomeBase(fn, { prefix: "openclaw-models-" }); @@ -34,6 +35,7 @@ const _MODELS_CONFIG: OpenClawConfig = { describe("models-config", () => { let previousHome: string | undefined; + const originalFetch = globalThis.fetch; beforeEach(() => { previousHome = process.env.HOME; @@ -41,28 +43,26 @@ describe("models-config", () => { afterEach(() => { process.env.HOME = previousHome; + if (originalFetch) { + globalThis.fetch = originalFetch; + } }); it("auto-injects github-copilot provider when token is present", async () => { await withTempHome(async (home) => { const previous = process.env.COPILOT_GITHUB_TOKEN; process.env.COPILOT_GITHUB_TOKEN = "gh-token"; + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ + token: "copilot-token;proxy-ep=proxy.copilot.example", + expires_at: Math.floor(Date.now() / 1000) + 3600, + }), + }); + globalThis.fetch = fetchMock as unknown as typeof fetch; try { - vi.resetModules(); - - vi.doMock("../providers/github-copilot-token.js", () => ({ - DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com", - resolveCopilotApiToken: vi.fn().mockResolvedValue({ - token: "copilot", - expiresAt: Date.now() + 60 * 60 * 1000, - source: "mock", - baseUrl: "https://api.copilot.example", - }), - })); - - const { ensureOpenClawModelsJson } = await import("./models-config.js"); - const agentDir = path.join(home, "agent-default-base-url"); await ensureOpenClawModelsJson({ models: { providers: {} } }, agentDir); @@ -78,6 +78,7 @@ describe("models-config", () => { } }); }); + it("prefers COPILOT_GITHUB_TOKEN over GH_TOKEN and GITHUB_TOKEN", async () => { await withTempHome(async () => { const previous = process.env.COPILOT_GITHUB_TOKEN; @@ -87,28 +88,21 @@ describe("models-config", () => { process.env.GH_TOKEN = "gh-token"; process.env.GITHUB_TOKEN = "github-token"; + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ + token: "copilot-token;proxy-ep=proxy.copilot.example", + expires_at: Math.floor(Date.now() / 1000) + 3600, + }), + }); + globalThis.fetch = fetchMock as unknown as typeof fetch; + try { - vi.resetModules(); - - const resolveCopilotApiToken = vi.fn().mockResolvedValue({ - token: "copilot", - expiresAt: Date.now() + 60 * 60 * 1000, - source: "mock", - baseUrl: "https://api.copilot.example", - }); - - vi.doMock("../providers/github-copilot-token.js", () => ({ - DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com", - resolveCopilotApiToken, - })); - - const { ensureOpenClawModelsJson } = await import("./models-config.js"); - await ensureOpenClawModelsJson({ models: { providers: {} } }); - expect(resolveCopilotApiToken).toHaveBeenCalledWith( - expect.objectContaining({ githubToken: "copilot-token" }), - ); + const [, opts] = fetchMock.mock.calls[0] as [string, { headers?: Record }]; + expect(opts?.headers?.Authorization).toBe("Bearer copilot-token"); } finally { process.env.COPILOT_GITHUB_TOKEN = previous; process.env.GH_TOKEN = previousGh; diff --git a/src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.e2e.test.ts b/src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.e2e.test.ts index 6f5371c509..6c011e28cc 100644 --- a/src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.e2e.test.ts +++ b/src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.e2e.test.ts @@ -3,6 +3,8 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; +import { DEFAULT_COPILOT_API_BASE_URL } from "../providers/github-copilot-token.js"; +import { ensureOpenClawModelsJson } from "./models-config.js"; async function withTempHome(fn: (home: string) => Promise): Promise { return withTempHomeBase(fn, { prefix: "openclaw-models-" }); @@ -34,6 +36,7 @@ const _MODELS_CONFIG: OpenClawConfig = { describe("models-config", () => { let previousHome: string | undefined; + const originalFetch = globalThis.fetch; beforeEach(() => { previousHome = process.env.HOME; @@ -41,38 +44,38 @@ describe("models-config", () => { afterEach(() => { process.env.HOME = previousHome; + if (originalFetch) { + globalThis.fetch = originalFetch; + } }); it("falls back to default baseUrl when token exchange fails", async () => { await withTempHome(async () => { const previous = process.env.COPILOT_GITHUB_TOKEN; process.env.COPILOT_GITHUB_TOKEN = "gh-token"; + const fetchMock = vi.fn().mockResolvedValue({ + ok: false, + status: 500, + json: async () => ({ message: "boom" }), + }); + globalThis.fetch = fetchMock as unknown as typeof fetch; try { - vi.resetModules(); - - vi.doMock("../providers/github-copilot-token.js", () => ({ - DEFAULT_COPILOT_API_BASE_URL: "https://api.default.test", - resolveCopilotApiToken: vi.fn().mockRejectedValue(new Error("boom")), - })); - - const { ensureOpenClawModelsJson } = await import("./models-config.js"); - const { resolveOpenClawAgentDir } = await import("./agent-paths.js"); - await ensureOpenClawModelsJson({ models: { providers: {} } }); - const agentDir = resolveOpenClawAgentDir(); + const agentDir = path.join(process.env.HOME ?? "", ".openclaw", "agents", "main", "agent"); const raw = await fs.readFile(path.join(agentDir, "models.json"), "utf8"); const parsed = JSON.parse(raw) as { providers: Record; }; - expect(parsed.providers["github-copilot"]?.baseUrl).toBe("https://api.default.test"); + expect(parsed.providers["github-copilot"]?.baseUrl).toBe(DEFAULT_COPILOT_API_BASE_URL); } finally { process.env.COPILOT_GITHUB_TOKEN = previous; } }); }); + it("uses agentDir override auth profiles for copilot injection", async () => { await withTempHome(async (home) => { const previous = process.env.COPILOT_GITHUB_TOKEN; @@ -82,9 +85,17 @@ describe("models-config", () => { delete process.env.GH_TOKEN; delete process.env.GITHUB_TOKEN; - try { - vi.resetModules(); + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ + token: "copilot-token;proxy-ep=proxy.copilot.example", + expires_at: Math.floor(Date.now() / 1000) + 3600, + }), + }); + globalThis.fetch = fetchMock as unknown as typeof fetch; + try { const agentDir = path.join(home, "agent-override"); await fs.mkdir(agentDir, { recursive: true }); await fs.writeFile( @@ -105,18 +116,6 @@ describe("models-config", () => { ), ); - vi.doMock("../providers/github-copilot-token.js", () => ({ - DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com", - resolveCopilotApiToken: vi.fn().mockResolvedValue({ - token: "copilot", - expiresAt: Date.now() + 60 * 60 * 1000, - source: "mock", - baseUrl: "https://api.copilot.example", - }), - })); - - const { ensureOpenClawModelsJson } = await import("./models-config.js"); - await ensureOpenClawModelsJson({ models: { providers: {} } }, agentDir); const raw = await fs.readFile(path.join(agentDir, "models.json"), "utf8"); diff --git a/src/agents/models-config.fills-missing-provider-apikey-from-env-var.e2e.test.ts b/src/agents/models-config.fills-missing-provider-apikey-from-env-var.e2e.test.ts index cafc01a4eb..58761e115e 100644 --- a/src/agents/models-config.fills-missing-provider-apikey-from-env-var.e2e.test.ts +++ b/src/agents/models-config.fills-missing-provider-apikey-from-env-var.e2e.test.ts @@ -1,8 +1,10 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; +import { resolveOpenClawAgentDir } from "./agent-paths.js"; +import { ensureOpenClawModelsJson } from "./models-config.js"; async function withTempHome(fn: (home: string) => Promise): Promise { return withTempHomeBase(fn, { prefix: "openclaw-models-" }); @@ -45,13 +47,9 @@ describe("models-config", () => { it("fills missing provider.apiKey from env var name when models exist", async () => { await withTempHome(async () => { - vi.resetModules(); const prevKey = process.env.MINIMAX_API_KEY; process.env.MINIMAX_API_KEY = "sk-minimax-test"; try { - const { ensureOpenClawModelsJson } = await import("./models-config.js"); - const { resolveOpenClawAgentDir } = await import("./agent-paths.js"); - const cfg: OpenClawConfig = { models: { providers: { @@ -95,10 +93,6 @@ describe("models-config", () => { }); it("merges providers by default", async () => { await withTempHome(async () => { - vi.resetModules(); - const { ensureOpenClawModelsJson } = await import("./models-config.js"); - const { resolveOpenClawAgentDir } = await import("./agent-paths.js"); - const agentDir = resolveOpenClawAgentDir(); await fs.mkdir(agentDir, { recursive: true }); await fs.writeFile( diff --git a/src/agents/models-config.skips-writing-models-json-no-env-token.e2e.test.ts b/src/agents/models-config.skips-writing-models-json-no-env-token.e2e.test.ts index 671a814a80..05d4e62cb7 100644 --- a/src/agents/models-config.skips-writing-models-json-no-env-token.e2e.test.ts +++ b/src/agents/models-config.skips-writing-models-json-no-env-token.e2e.test.ts @@ -1,8 +1,10 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; +import { resolveOpenClawAgentDir } from "./agent-paths.js"; +import { ensureOpenClawModelsJson } from "./models-config.js"; async function withTempHome(fn: (home: string) => Promise): Promise { return withTempHomeBase(fn, { prefix: "openclaw-models-" }); @@ -65,9 +67,6 @@ describe("models-config", () => { delete process.env.XIAOMI_API_KEY; try { - vi.resetModules(); - const { ensureOpenClawModelsJson } = await import("./models-config.js"); - const agentDir = path.join(home, "agent-empty"); const result = await ensureOpenClawModelsJson( { @@ -129,10 +128,6 @@ describe("models-config", () => { }); it("writes models.json for configured providers", async () => { await withTempHome(async () => { - vi.resetModules(); - const { ensureOpenClawModelsJson } = await import("./models-config.js"); - const { resolveOpenClawAgentDir } = await import("./agent-paths.js"); - await ensureOpenClawModelsJson(MODELS_CONFIG); const modelPath = path.join(resolveOpenClawAgentDir(), "models.json"); @@ -146,13 +141,9 @@ describe("models-config", () => { }); it("adds minimax provider when MINIMAX_API_KEY is set", async () => { await withTempHome(async () => { - vi.resetModules(); const prevKey = process.env.MINIMAX_API_KEY; process.env.MINIMAX_API_KEY = "sk-minimax-test"; try { - const { ensureOpenClawModelsJson } = await import("./models-config.js"); - const { resolveOpenClawAgentDir } = await import("./agent-paths.js"); - await ensureOpenClawModelsJson({}); const modelPath = path.join(resolveOpenClawAgentDir(), "models.json"); @@ -183,13 +174,9 @@ describe("models-config", () => { }); it("adds synthetic provider when SYNTHETIC_API_KEY is set", async () => { await withTempHome(async () => { - vi.resetModules(); const prevKey = process.env.SYNTHETIC_API_KEY; process.env.SYNTHETIC_API_KEY = "sk-synthetic-test"; try { - const { ensureOpenClawModelsJson } = await import("./models-config.js"); - const { resolveOpenClawAgentDir } = await import("./agent-paths.js"); - await ensureOpenClawModelsJson({}); const modelPath = path.join(resolveOpenClawAgentDir(), "models.json"); diff --git a/src/agents/models-config.uses-first-github-copilot-profile-env-tokens.e2e.test.ts b/src/agents/models-config.uses-first-github-copilot-profile-env-tokens.e2e.test.ts index 3e321dc0b1..b06214e022 100644 --- a/src/agents/models-config.uses-first-github-copilot-profile-env-tokens.e2e.test.ts +++ b/src/agents/models-config.uses-first-github-copilot-profile-env-tokens.e2e.test.ts @@ -3,6 +3,8 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; +import { resolveOpenClawAgentDir } from "./agent-paths.js"; +import { ensureOpenClawModelsJson } from "./models-config.js"; async function withTempHome(fn: (home: string) => Promise): Promise { return withTempHomeBase(fn, { prefix: "openclaw-models-" }); @@ -34,6 +36,7 @@ const _MODELS_CONFIG: OpenClawConfig = { describe("models-config", () => { let previousHome: string | undefined; + const originalFetch = globalThis.fetch; beforeEach(() => { previousHome = process.env.HOME; @@ -41,6 +44,9 @@ describe("models-config", () => { afterEach(() => { process.env.HOME = previousHome; + if (originalFetch) { + globalThis.fetch = originalFetch; + } }); it("uses the first github-copilot profile when env tokens are missing", async () => { @@ -52,9 +58,17 @@ describe("models-config", () => { delete process.env.GH_TOKEN; delete process.env.GITHUB_TOKEN; - try { - vi.resetModules(); + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ + token: "copilot-token;proxy-ep=proxy.copilot.example", + expires_at: Math.floor(Date.now() / 1000) + 3600, + }), + }); + globalThis.fetch = fetchMock as unknown as typeof fetch; + try { const agentDir = path.join(home, "agent-profiles"); await fs.mkdir(agentDir, { recursive: true }); await fs.writeFile( @@ -80,25 +94,10 @@ describe("models-config", () => { ), ); - const resolveCopilotApiToken = vi.fn().mockResolvedValue({ - token: "copilot", - expiresAt: Date.now() + 60 * 60 * 1000, - source: "mock", - baseUrl: "https://api.copilot.example", - }); - - vi.doMock("../providers/github-copilot-token.js", () => ({ - DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com", - resolveCopilotApiToken, - })); - - const { ensureOpenClawModelsJson } = await import("./models-config.js"); - await ensureOpenClawModelsJson({ models: { providers: {} } }, agentDir); - expect(resolveCopilotApiToken).toHaveBeenCalledWith( - expect.objectContaining({ githubToken: "alpha-token" }), - ); + const [, opts] = fetchMock.mock.calls[0] as [string, { headers?: Record }]; + expect(opts?.headers?.Authorization).toBe("Bearer alpha-token"); } finally { if (previous === undefined) { delete process.env.COPILOT_GITHUB_TOKEN; @@ -118,27 +117,22 @@ describe("models-config", () => { } }); }); + it("does not override explicit github-copilot provider config", async () => { await withTempHome(async () => { const previous = process.env.COPILOT_GITHUB_TOKEN; process.env.COPILOT_GITHUB_TOKEN = "gh-token"; + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ + token: "copilot-token;proxy-ep=proxy.copilot.example", + expires_at: Math.floor(Date.now() / 1000) + 3600, + }), + }); + globalThis.fetch = fetchMock as unknown as typeof fetch; try { - vi.resetModules(); - - vi.doMock("../providers/github-copilot-token.js", () => ({ - DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com", - resolveCopilotApiToken: vi.fn().mockResolvedValue({ - token: "copilot", - expiresAt: Date.now() + 60 * 60 * 1000, - source: "mock", - baseUrl: "https://api.copilot.example", - }), - })); - - const { ensureOpenClawModelsJson } = await import("./models-config.js"); - const { resolveOpenClawAgentDir } = await import("./agent-paths.js"); - await ensureOpenClawModelsJson({ models: { providers: { diff --git a/src/agents/subagent-registry.persistence.e2e.test.ts b/src/agents/subagent-registry.persistence.e2e.test.ts index 6b7aa4f473..0f8a6d4fc1 100644 --- a/src/agents/subagent-registry.persistence.e2e.test.ts +++ b/src/agents/subagent-registry.persistence.e2e.test.ts @@ -2,6 +2,12 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { + initSubagentRegistry, + registerSubagentRun, + resetSubagentRegistryForTests, +} from "./subagent-registry.js"; +import { loadSubagentRegistryFromDisk } from "./subagent-registry.store.js"; const noop = () => {}; @@ -28,7 +34,7 @@ describe("subagent registry persistence", () => { afterEach(async () => { announceSpy.mockClear(); - vi.resetModules(); + resetSubagentRegistryForTests({ persist: false }); if (tempStateDir) { await fs.rm(tempStateDir, { recursive: true, force: true }); tempStateDir = null; @@ -44,10 +50,7 @@ describe("subagent registry persistence", () => { tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-subagent-")); process.env.OPENCLAW_STATE_DIR = tempStateDir; - vi.resetModules(); - const mod1 = await import("./subagent-registry.js"); - - mod1.registerSubagentRun({ + registerSubagentRun({ runId: "run-1", childSessionKey: "agent:main:subagent:test", requesterSessionKey: "agent:main:main", @@ -76,9 +79,8 @@ describe("subagent registry persistence", () => { // Simulate a process restart: module re-import should load persisted runs // and trigger the announce flow once the run resolves. - vi.resetModules(); - const mod2 = await import("./subagent-registry.js"); - mod2.initSubagentRegistry(); + resetSubagentRegistryForTests({ persist: false }); + initSubagentRegistry(); // allow queued async wait/cleanup to execute await new Promise((r) => setTimeout(r, 0)); @@ -125,9 +127,8 @@ describe("subagent registry persistence", () => { await fs.mkdir(path.dirname(registryPath), { recursive: true }); await fs.writeFile(registryPath, `${JSON.stringify(persisted)}\n`, "utf8"); - vi.resetModules(); - const mod = await import("./subagent-registry.js"); - mod.initSubagentRegistry(); + resetSubagentRegistryForTests({ persist: false }); + initSubagentRegistry(); await new Promise((r) => setTimeout(r, 0)); @@ -168,8 +169,6 @@ describe("subagent registry persistence", () => { await fs.mkdir(path.dirname(registryPath), { recursive: true }); await fs.writeFile(registryPath, `${JSON.stringify(persisted)}\n`, "utf8"); - vi.resetModules(); - const { loadSubagentRegistryFromDisk } = await import("./subagent-registry.store.js"); const runs = loadSubagentRegistryFromDisk(); const entry = runs.get("run-legacy"); expect(entry?.cleanupHandled).toBe(true); @@ -206,9 +205,8 @@ describe("subagent registry persistence", () => { await fs.writeFile(registryPath, `${JSON.stringify(persisted)}\n`, "utf8"); announceSpy.mockResolvedValueOnce(false); - vi.resetModules(); - const mod1 = await import("./subagent-registry.js"); - mod1.initSubagentRegistry(); + resetSubagentRegistryForTests({ persist: false }); + initSubagentRegistry(); await new Promise((r) => setTimeout(r, 0)); expect(announceSpy).toHaveBeenCalledTimes(1); @@ -219,9 +217,8 @@ describe("subagent registry persistence", () => { expect(afterFirst.runs["run-3"].cleanupCompletedAt).toBeUndefined(); announceSpy.mockResolvedValueOnce(true); - vi.resetModules(); - const mod2 = await import("./subagent-registry.js"); - mod2.initSubagentRegistry(); + resetSubagentRegistryForTests({ persist: false }); + initSubagentRegistry(); await new Promise((r) => setTimeout(r, 0)); expect(announceSpy).toHaveBeenCalledTimes(2); @@ -256,9 +253,8 @@ describe("subagent registry persistence", () => { await fs.writeFile(registryPath, `${JSON.stringify(persisted)}\n`, "utf8"); announceSpy.mockResolvedValueOnce(false); - vi.resetModules(); - const mod1 = await import("./subagent-registry.js"); - mod1.initSubagentRegistry(); + resetSubagentRegistryForTests({ persist: false }); + initSubagentRegistry(); await new Promise((r) => setTimeout(r, 0)); expect(announceSpy).toHaveBeenCalledTimes(1); @@ -268,9 +264,8 @@ describe("subagent registry persistence", () => { expect(afterFirst.runs["run-4"]?.cleanupHandled).toBe(false); announceSpy.mockResolvedValueOnce(true); - vi.resetModules(); - const mod2 = await import("./subagent-registry.js"); - mod2.initSubagentRegistry(); + resetSubagentRegistryForTests({ persist: false }); + initSubagentRegistry(); await new Promise((r) => setTimeout(r, 0)); expect(announceSpy).toHaveBeenCalledTimes(2); diff --git a/src/agents/subagent-registry.store.ts b/src/agents/subagent-registry.store.ts index 510268e522..ad82ce132a 100644 --- a/src/agents/subagent-registry.store.ts +++ b/src/agents/subagent-registry.store.ts @@ -1,6 +1,6 @@ import path from "node:path"; import type { SubagentRunRecord } from "./subagent-registry.js"; -import { STATE_DIR } from "../config/paths.js"; +import { resolveStateDir } from "../config/paths.js"; import { loadJsonFile, saveJsonFile } from "../infra/json-file.js"; import { normalizeDeliveryContext } from "../utils/delivery-context.js"; @@ -30,7 +30,7 @@ type LegacySubagentRunRecord = PersistedSubagentRunRecord & { }; export function resolveSubagentRegistryPath(): string { - return path.join(STATE_DIR, "subagents", "runs.json"); + return path.join(resolveStateDir(), "subagents", "runs.json"); } export function loadSubagentRegistryFromDisk(): Map { diff --git a/src/agents/subagent-registry.ts b/src/agents/subagent-registry.ts index 3b090e3061..8eadf55141 100644 --- a/src/agents/subagent-registry.ts +++ b/src/agents/subagent-registry.ts @@ -395,7 +395,7 @@ async function waitForSubagentCompletion(runId: string, waitTimeoutMs: number) { } } -export function resetSubagentRegistryForTests() { +export function resetSubagentRegistryForTests(opts?: { persist?: boolean }) { subagentRuns.clear(); resumedRuns.clear(); stopSweeper(); @@ -405,7 +405,9 @@ export function resetSubagentRegistryForTests() { listenerStop = null; } listenerStarted = false; - persistSubagentRuns(); + if (opts?.persist !== false) { + persistSubagentRuns(); + } } export function addSubagentRunForTests(entry: SubagentRunRecord) {