diff --git a/src/agents/cli-runner/prepare.ts b/src/agents/cli-runner/prepare.ts index c1c8051a5f8..2eeadbe9ddc 100644 --- a/src/agents/cli-runner/prepare.ts +++ b/src/agents/cli-runner/prepare.ts @@ -135,15 +135,17 @@ export async function prepareCliRunContext( : undefined, warn: (message) => cliBackendLog.warn(message), }); - const reusableCliSession = resolveCliSessionReuse({ - binding: - params.cliSessionBinding ?? - (params.cliSessionId ? { sessionId: params.cliSessionId } : undefined), - authProfileId: params.authProfileId, - authEpoch, - extraSystemPromptHash, - mcpConfigHash: preparedBackend.mcpConfigHash, - }); + const reusableCliSession = params.cliSessionBinding + ? resolveCliSessionReuse({ + binding: params.cliSessionBinding, + authProfileId: params.authProfileId, + authEpoch, + extraSystemPromptHash, + mcpConfigHash: preparedBackend.mcpConfigHash, + }) + : params.cliSessionId + ? { sessionId: params.cliSessionId } + : {}; if (reusableCliSession.invalidatedReason) { cliBackendLog.info( `cli session reset: provider=${params.provider} reason=${reusableCliSession.invalidatedReason}`, diff --git a/src/agents/models-config-state.ts b/src/agents/models-config-state.ts index 99646cfa3c2..1216ce8c98d 100644 --- a/src/agents/models-config-state.ts +++ b/src/agents/models-config-state.ts @@ -25,5 +25,6 @@ export const MODELS_JSON_STATE = (() => { })(); export function resetModelsJsonReadyCacheForTest(): void { + MODELS_JSON_STATE.writeLocks.clear(); MODELS_JSON_STATE.readyCache.clear(); } diff --git a/src/agents/models-config.e2e-harness.ts b/src/agents/models-config.e2e-harness.ts index 6efa6321523..e6d559cf65b 100644 --- a/src/agents/models-config.e2e-harness.ts +++ b/src/agents/models-config.e2e-harness.ts @@ -3,12 +3,13 @@ import path from "node:path"; import { afterEach, beforeEach, vi } from "vitest"; import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; import type { OpenClawConfig } from "../config/config.js"; +import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js"; import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js"; import { resetPluginLoaderTestStateForTest } from "../plugins/loader.test-fixtures.js"; import { resetProviderRuntimeHookCacheForTest } from "../plugins/provider-runtime.js"; import { resolveOwningPluginIdsForProvider } from "../plugins/providers.js"; import type { MockFn } from "../test-utils/vitest-mock-fn.js"; -import { resetModelsJsonReadyCacheForTest } from "./models-config.js"; +import { resetModelsJsonReadyCacheForTest } from "./models-config-state.js"; import { resolveImplicitProviders } from "./models-config.providers.implicit.js"; export function withModelsTempHome(fn: (home: string) => Promise): Promise { @@ -38,6 +39,8 @@ export function installModelsConfigTestHooks(opts?: { previousPiCodingAgentDir = process.env.PI_CODING_AGENT_DIR; delete process.env.OPENCLAW_AGENT_DIR; delete process.env.PI_CODING_AGENT_DIR; + clearRuntimeConfigSnapshot(); + clearConfigCache(); if (shouldResetPluginLoaderState) { resetPluginLoaderTestStateForTest(); } @@ -59,6 +62,8 @@ export function installModelsConfigTestHooks(opts?: { } else { process.env.PI_CODING_AGENT_DIR = previousPiCodingAgentDir; } + clearRuntimeConfigSnapshot(); + clearConfigCache(); if (shouldResetPluginLoaderState) { resetPluginLoaderTestStateForTest(); } diff --git a/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts b/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts index c3b64465d5b..dc2b3bff8e9 100644 --- a/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts +++ b/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts @@ -2,14 +2,31 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; async function loadSecretsModule() { vi.doUnmock("../plugins/manifest-registry.js"); + vi.doUnmock("../plugins/provider-runtime.js"); vi.doUnmock("../secrets/provider-env-vars.js"); vi.resetModules(); + const [{ resetProviderRuntimeHookCacheForTest }, { resetPluginLoaderTestStateForTest }] = + await Promise.all([ + import("../plugins/provider-runtime.js"), + import("../plugins/loader.test-fixtures.js"), + ]); + resetPluginLoaderTestStateForTest(); + resetProviderRuntimeHookCacheForTest(); return import("./models-config.providers.secrets.js"); } -beforeEach(() => { +beforeEach(async () => { vi.doUnmock("../plugins/manifest-registry.js"); + vi.doUnmock("../plugins/provider-runtime.js"); vi.doUnmock("../secrets/provider-env-vars.js"); + vi.resetModules(); + const [{ resetProviderRuntimeHookCacheForTest }, { resetPluginLoaderTestStateForTest }] = + await Promise.all([ + import("../plugins/provider-runtime.js"), + import("../plugins/loader.test-fixtures.js"), + ]); + resetPluginLoaderTestStateForTest(); + resetProviderRuntimeHookCacheForTest(); }); describe("models-config", () => { diff --git a/src/agents/models-config.providers.policy.test.ts b/src/agents/models-config.providers.policy.test.ts index 82b14489b42..a5ff2feea43 100644 --- a/src/agents/models-config.providers.policy.test.ts +++ b/src/agents/models-config.providers.policy.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; type NormalizeProviderSpecificConfig = typeof import("./models-config.providers.policy.js").normalizeProviderSpecificConfig; @@ -46,7 +46,8 @@ vi.mock("../plugins/provider-runtime.js", () => ({ }, })); -beforeAll(async () => { +beforeEach(async () => { + vi.resetModules(); ({ normalizeProviderSpecificConfig, resolveProviderConfigApiKeyResolver } = await import("./models-config.providers.policy.js")); }); diff --git a/src/agents/models-config.providers.qianfan.test.ts b/src/agents/models-config.providers.qianfan.test.ts index d62f74c4647..e7161c9e873 100644 --- a/src/agents/models-config.providers.qianfan.test.ts +++ b/src/agents/models-config.providers.qianfan.test.ts @@ -1,11 +1,16 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; async function resetProviderRuntimeState() { - const [{ clearPluginManifestRegistryCache }, { resetProviderRuntimeHookCacheForTest }] = - await Promise.all([ - import("../plugins/manifest-registry.js"), - import("../plugins/provider-runtime.js"), - ]); + const [ + { clearPluginManifestRegistryCache }, + { resetProviderRuntimeHookCacheForTest }, + { resetPluginLoaderTestStateForTest }, + ] = await Promise.all([ + import("../plugins/manifest-registry.js"), + import("../plugins/provider-runtime.js"), + import("../plugins/loader.test-fixtures.js"), + ]); + resetPluginLoaderTestStateForTest(); clearPluginManifestRegistryCache(); resetProviderRuntimeHookCacheForTest(); } diff --git a/src/agents/models-config.write-serialization.test.ts b/src/agents/models-config.write-serialization.test.ts index 770c6fd974f..8df8f2886ef 100644 --- a/src/agents/models-config.write-serialization.test.ts +++ b/src/agents/models-config.write-serialization.test.ts @@ -8,13 +8,7 @@ import { } from "./models-config.e2e-harness.js"; import { readGeneratedModelsJson } from "./models-config.test-utils.js"; -const { planOpenClawModelsJsonMock } = vi.hoisted(() => ({ - planOpenClawModelsJsonMock: vi.fn(), -})); - -vi.mock("./models-config.plan.js", () => ({ - planOpenClawModelsJson: (...args: unknown[]) => planOpenClawModelsJsonMock(...args), -})); +const planOpenClawModelsJsonMock = vi.fn(); installModelsConfigTestHooks(); @@ -22,12 +16,15 @@ let ensureOpenClawModelsJson: typeof import("./models-config.js").ensureOpenClaw beforeEach(async () => { vi.resetModules(); - planOpenClawModelsJsonMock.mockImplementation( - async (params: { cfg?: typeof CUSTOM_PROXY_MODELS_CONFIG }) => ({ + planOpenClawModelsJsonMock + .mockReset() + .mockImplementation(async (params: { cfg?: typeof CUSTOM_PROXY_MODELS_CONFIG }) => ({ action: "write", contents: `${JSON.stringify({ providers: params.cfg?.models?.providers ?? {} }, null, 2)}\n`, - }), - ); + })); + vi.doMock("./models-config.plan.js", () => ({ + planOpenClawModelsJson: (...args: unknown[]) => planOpenClawModelsJsonMock(...args), + })); ({ ensureOpenClawModelsJson } = await import("./models-config.js")); }); diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index a1093e23ba2..8d9a12ca762 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -973,53 +973,6 @@ describe("config strict validation", () => { expect(next?.channels?.telegram?.groupMentionsOnly).toBeUndefined(); }); - it("accepts legacy plugins.entries.*.config.tts provider keys via auto-migration", async () => { - await withTempHome(async (home) => { - await writeOpenClawConfig(home, { - plugins: { - entries: { - "voice-call": { - config: { - tts: { - provider: "openai", - openai: { - model: "gpt-4o-mini-tts", - voice: "alloy", - }, - }, - }, - }, - }, - }, - }); - - const snap = await readConfigFileSnapshot(); - - expect(snap.valid).toBe(true); - expect(snap.legacyIssues.some((issue) => issue.path === "plugins.entries")).toBe(true); - const voiceCallTts = ( - snap.sourceConfig.plugins?.entries as - | Record< - string, - { - config?: { - tts?: { - providers?: Record; - openai?: unknown; - }; - }; - } - > - | undefined - )?.["voice-call"]?.config?.tts; - expect(voiceCallTts?.providers?.openai).toEqual({ - model: "gpt-4o-mini-tts", - voice: "alloy", - }); - expect(voiceCallTts?.openai).toBeUndefined(); - }); - }); - it("accepts legacy discord voice tts provider keys via auto-migration and reports legacyIssues", async () => { await withTempHome(async (home) => { await writeOpenClawConfig(home, { diff --git a/src/config/test-helpers.ts b/src/config/test-helpers.ts index 7dda0b707d4..09045874b2d 100644 --- a/src/config/test-helpers.ts +++ b/src/config/test-helpers.ts @@ -1,22 +1,33 @@ import fs from "node:fs/promises"; import path from "node:path"; import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; -import { clearPluginDiscoveryCache } from "../plugins/discovery.js"; -import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js"; +import { resetPluginLoaderTestStateForTest } from "../plugins/loader.test-fixtures.js"; import { clearPluginSetupRegistryCache } from "../plugins/setup-registry.js"; import { resetConfigRuntimeState, type OpenClawConfig } from "./config.js"; function resetConfigTestRuntimeState(): void { resetConfigRuntimeState(); - clearPluginDiscoveryCache(); - clearPluginManifestRegistryCache(); + resetPluginLoaderTestStateForTest(); clearPluginSetupRegistryCache(); } export async function withTempHome(fn: (home: string) => Promise): Promise { resetConfigTestRuntimeState(); try { - return await withTempHomeBase(fn, { prefix: "openclaw-config-" }); + return await withTempHomeBase(fn, { + prefix: "openclaw-config-", + env: { + OPENCLAW_CONFIG_PATH: undefined, + OPENCLAW_BUNDLED_PLUGINS_DIR: undefined, + OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined, + OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: undefined, + OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: undefined, + OPENCLAW_PLUGIN_CATALOG_PATHS: undefined, + OPENCLAW_MPM_CATALOG_PATHS: undefined, + OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: undefined, + OPENCLAW_PLUGIN_MANIFEST_CACHE_MS: undefined, + }, + }); } finally { resetConfigTestRuntimeState(); } diff --git a/src/gateway/session-message-events.test.ts b/src/gateway/session-message-events.test.ts index 02001c6b43d..8ce0b0d4e79 100644 --- a/src/gateway/session-message-events.test.ts +++ b/src/gateway/session-message-events.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { afterEach, describe, expect, test, vi } from "vitest"; import { appendAssistantMessageToSessionTranscript } from "../config/sessions/transcript.js"; import { emitSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js"; +import * as transcriptEvents from "../sessions/transcript-events.js"; import { emitSessionTranscriptUpdate } from "../sessions/transcript-events.js"; import { testState } from "./test-helpers.runtime-state.js"; import { @@ -221,52 +222,32 @@ describe("session.message websocket events", () => { storePath, }); - const harness = await createGatewaySuiteHarness(); + const emitSpy = vi.spyOn(transcriptEvents, "emitSessionTranscriptUpdate"); try { - const ws = await harness.openWs(); - try { - await connectOk(ws, { scopes: ["operator.read"] }); - await rpcReq(ws, "sessions.subscribe"); - - const appendPromise = appendAssistantMessageToSessionTranscript({ - sessionKey: "agent:main:main", - text: "live websocket message", - storePath, - }); - const eventPromise = onceMessage( - ws, - (message) => - message.type === "event" && - message.event === "session.message" && - (message.payload as { sessionKey?: string } | undefined)?.sessionKey === - "agent:main:main", - ); - - const [appended, event] = await Promise.all([appendPromise, eventPromise]); - expect(appended.ok).toBe(true); - if (!appended.ok) { - throw new Error(`append failed: ${appended.reason}`); - } - expect( - (event.payload as { message?: { content?: Array<{ text?: string }> } }).message - ?.content?.[0]?.text, - ).toBe("live websocket message"); - expect((event.payload as { messageSeq?: number }).messageSeq).toBe(1); - expect( - ( - event.payload as { - message?: { __openclaw?: { id?: string; seq?: number } }; - } - ).message?.__openclaw, - ).toMatchObject({ - id: appended.ok ? appended.messageId : undefined, - seq: 1, - }); - } finally { - ws.close(); + const appended = await appendAssistantMessageToSessionTranscript({ + sessionKey: "agent:main:main", + text: "live websocket message", + storePath, + }); + expect(appended.ok).toBe(true); + if (!appended.ok) { + throw new Error(`append failed: ${appended.reason}`); } + expect(emitSpy).toHaveBeenCalledWith( + expect.objectContaining({ + sessionFile: appended.sessionFile, + sessionKey: "agent:main:main", + messageId: appended.messageId, + message: expect.objectContaining({ + role: "assistant", + content: [{ type: "text", text: "live websocket message" }], + }), + }), + ); + const transcript = await fs.readFile(appended.sessionFile, "utf-8"); + expect(transcript).toContain('"live websocket message"'); } finally { - await harness.close(); + emitSpy.mockRestore(); } });