mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
fix: stabilize agent and config isolation
This commit is contained in:
@@ -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}`,
|
||||
|
||||
@@ -25,5 +25,6 @@ export const MODELS_JSON_STATE = (() => {
|
||||
})();
|
||||
|
||||
export function resetModelsJsonReadyCacheForTest(): void {
|
||||
MODELS_JSON_STATE.writeLocks.clear();
|
||||
MODELS_JSON_STATE.readyCache.clear();
|
||||
}
|
||||
|
||||
@@ -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<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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, {
|
||||
|
||||
@@ -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<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user