mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
perf: trim agent test runtime imports
This commit is contained in:
@@ -14,6 +14,42 @@ vi.mock("../plugins/provider-runtime.js", () => ({
|
||||
resolveExternalAuthProfilesWithPlugins: () => [],
|
||||
}));
|
||||
|
||||
vi.mock("./cli-credentials.js", () => ({
|
||||
readCodexCliCredentialsCached: () => {
|
||||
const codexHome = process.env.CODEX_HOME;
|
||||
if (!codexHome) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(path.join(codexHome, "auth.json"), "utf8")) as {
|
||||
tokens?: {
|
||||
access_token?: unknown;
|
||||
refresh_token?: unknown;
|
||||
account_id?: unknown;
|
||||
};
|
||||
};
|
||||
const access = raw.tokens?.access_token;
|
||||
const refresh = raw.tokens?.refresh_token;
|
||||
if (typeof access !== "string" || typeof refresh !== "string") {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access,
|
||||
refresh,
|
||||
expires: Date.now() + 60 * 60 * 1000,
|
||||
accountId: typeof raw.tokens?.account_id === "string" ? raw.tokens.account_id : undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
readMiniMaxCliCredentialsCached: () => null,
|
||||
resetCliCredentialCachesForTest: vi.fn(),
|
||||
writeCodexCliCredentials: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
describe("ensureAuthProfileStore", () => {
|
||||
function withTempAgentDir<T>(prefix: string, run: (agentDir: string) => T): T {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
|
||||
@@ -54,16 +54,21 @@ const loadPluginManifestRegistry = vi.hoisted(() =>
|
||||
})),
|
||||
);
|
||||
const resolveManifestContractOwnerPluginId = vi.hoisted(() => vi.fn<() => undefined>());
|
||||
const resolveProviderSyntheticAuthWithPlugin = vi.hoisted(() => vi.fn(() => undefined));
|
||||
|
||||
vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestContractOwnerPluginId,
|
||||
}));
|
||||
vi.mock("../plugins/provider-runtime.js", () => ({
|
||||
resolveProviderSyntheticAuthWithPlugin,
|
||||
}));
|
||||
|
||||
describe("provider auth aliases", () => {
|
||||
beforeEach(() => {
|
||||
loadPluginManifestRegistry.mockReset();
|
||||
loadPluginManifestRegistry.mockReturnValue(createFixtureProviderRegistry());
|
||||
resolveProviderSyntheticAuthWithPlugin.mockReset();
|
||||
});
|
||||
|
||||
it("shares manifest env vars across aliased providers", () => {
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
normalizePluginDiscoveryResult,
|
||||
resolvePluginDiscoveryProviders,
|
||||
runProviderCatalog,
|
||||
} from "../plugins/provider-discovery.js";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import type { ProviderConfig } from "./models-config.providers.js";
|
||||
|
||||
describe("Ollama auto-discovery", () => {
|
||||
let originalFetch: typeof globalThis.fetch;
|
||||
let ollamaCatalogProviderPromise: Promise<ProviderPlugin | undefined> | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
globalThis.fetch = originalFetch;
|
||||
delete process.env.OLLAMA_API_KEY;
|
||||
});
|
||||
|
||||
function createCleanProviderDiscoveryEnv(): NodeJS.ProcessEnv {
|
||||
const env = { ...process.env };
|
||||
delete env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
delete env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
|
||||
delete env.OPENCLAW_SKIP_PROVIDERS;
|
||||
delete env.OPENCLAW_SKIP_CHANNELS;
|
||||
delete env.OPENCLAW_SKIP_CRON;
|
||||
delete env.OPENCLAW_TEST_MINIMAL_GATEWAY;
|
||||
return env;
|
||||
}
|
||||
|
||||
function createCatalogLoadEnv(): NodeJS.ProcessEnv {
|
||||
originalFetch = globalThis.fetch;
|
||||
return {
|
||||
...createCleanProviderDiscoveryEnv(),
|
||||
OPENCLAW_TEST_ONLY_PROVIDER_PLUGIN_IDS: "ollama",
|
||||
VITEST: "1",
|
||||
NODE_ENV: "test",
|
||||
};
|
||||
}
|
||||
|
||||
function createDiscoveryRunEnv(): NodeJS.ProcessEnv {
|
||||
return {
|
||||
...createCleanProviderDiscoveryEnv(),
|
||||
OPENCLAW_TEST_ONLY_PROVIDER_PLUGIN_IDS: "ollama",
|
||||
VITEST: "",
|
||||
NODE_ENV: "development",
|
||||
};
|
||||
}
|
||||
|
||||
async function loadOllamaCatalogProvider(): Promise<ProviderPlugin | undefined> {
|
||||
ollamaCatalogProviderPromise ??= resolvePluginDiscoveryProviders({
|
||||
env: createCatalogLoadEnv(),
|
||||
onlyPluginIds: ["ollama"],
|
||||
}).then((providers) => providers.find((provider) => provider.id === "ollama"));
|
||||
return ollamaCatalogProviderPromise;
|
||||
}
|
||||
|
||||
async function runOllamaCatalog(params?: {
|
||||
explicitProviders?: Record<string, ProviderConfig>;
|
||||
}): Promise<ProviderConfig | undefined> {
|
||||
const provider = await loadOllamaCatalogProvider();
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
const env = createDiscoveryRunEnv();
|
||||
const config: OpenClawConfig | undefined = params?.explicitProviders
|
||||
? { models: { providers: params.explicitProviders } }
|
||||
: undefined;
|
||||
const result = await runProviderCatalog({
|
||||
provider,
|
||||
config: config ?? {},
|
||||
agentDir: mkdtempSync(join(tmpdir(), "openclaw-test-")),
|
||||
env,
|
||||
resolveProviderApiKey: () => ({
|
||||
apiKey: env.OLLAMA_API_KEY?.trim() ? env.OLLAMA_API_KEY : undefined,
|
||||
}),
|
||||
resolveProviderAuth: () => ({
|
||||
apiKey: env.OLLAMA_API_KEY?.trim() ? env.OLLAMA_API_KEY : undefined,
|
||||
mode: env.OLLAMA_API_KEY?.trim() ? "api_key" : "none",
|
||||
source: env.OLLAMA_API_KEY?.trim() ? "env" : "none",
|
||||
}),
|
||||
});
|
||||
return normalizePluginDiscoveryResult({ provider, result }).ollama as
|
||||
| ProviderConfig
|
||||
| undefined;
|
||||
}
|
||||
|
||||
function mockOllamaUnreachable() {
|
||||
globalThis.fetch = withFetchPreconnect(
|
||||
vi.fn().mockRejectedValue(new Error("connect ECONNREFUSED 127.0.0.1:11434")),
|
||||
) as typeof fetch;
|
||||
}
|
||||
|
||||
it("auto-registers ollama provider when models are discovered locally", async () => {
|
||||
globalThis.fetch = withFetchPreconnect(
|
||||
vi.fn().mockImplementation(async (url: string | URL) => {
|
||||
if (String(url).includes("/api/tags")) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
models: [{ name: "deepseek-r1:latest" }, { name: "llama3.3:latest" }],
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (String(url).includes("/api/show")) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ model_info: {} }),
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected fetch: ${url}`);
|
||||
}),
|
||||
) as typeof fetch;
|
||||
|
||||
const provider = await runOllamaCatalog();
|
||||
|
||||
expect(provider).toBeDefined();
|
||||
expect(provider?.apiKey).toBe("ollama-local");
|
||||
expect(provider?.api).toBe("ollama");
|
||||
expect(provider?.baseUrl).toBe("http://127.0.0.1:11434");
|
||||
expect(provider?.models).toHaveLength(2);
|
||||
expect(provider?.models?.[0]?.id).toBe("deepseek-r1:latest");
|
||||
expect(provider?.models?.[0]?.reasoning).toBe(true);
|
||||
expect(provider?.models?.[1]?.reasoning).toBe(false);
|
||||
});
|
||||
|
||||
it("does not warn when Ollama is unreachable and not explicitly configured", async () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
mockOllamaUnreachable();
|
||||
|
||||
const provider = await runOllamaCatalog();
|
||||
|
||||
expect(provider).toBeUndefined();
|
||||
const ollamaWarnings = warnSpy.mock.calls.filter(
|
||||
(args) => typeof args[0] === "string" && args[0].includes("Ollama"),
|
||||
);
|
||||
expect(ollamaWarnings).toHaveLength(0);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("warns when Ollama is unreachable and explicitly configured", async () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
mockOllamaUnreachable();
|
||||
|
||||
await runOllamaCatalog({
|
||||
explicitProviders: {
|
||||
ollama: {
|
||||
baseUrl: "http://127.0.0.1:11435/v1",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ollamaWarnings = warnSpy.mock.calls.filter(
|
||||
(args) => typeof args[0] === "string" && args[0].includes("Ollama"),
|
||||
);
|
||||
expect(ollamaWarnings.length).toBeGreaterThan(0);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -239,6 +239,91 @@ describe("Ollama provider", () => {
|
||||
expectDiscoveryCallCounts(fetchMock, { tags: 1, show: 2 });
|
||||
});
|
||||
|
||||
it("auto-registers ollama provider when models are discovered locally", async () => {
|
||||
await withoutAmbientOllamaEnv(async () => {
|
||||
enableDiscoveryEnv();
|
||||
const fetchMock = vi.fn(async (input: unknown) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("/api/tags")) {
|
||||
return tagsResponse(["deepseek-r1:latest", "llama3.3:latest"]);
|
||||
}
|
||||
if (url.endsWith("/api/show")) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ model_info: {} }),
|
||||
};
|
||||
}
|
||||
return notFoundJsonResponse();
|
||||
});
|
||||
vi.stubGlobal("fetch", withFetchPreconnect(fetchMock));
|
||||
|
||||
const provider = await runOllamaCatalog({
|
||||
env: { VITEST: "", NODE_ENV: "development" },
|
||||
});
|
||||
|
||||
expect(provider?.apiKey).toBe(OLLAMA_LOCAL_AUTH_MARKER);
|
||||
expect(provider?.api).toBe("ollama");
|
||||
expect(provider?.baseUrl).toBe("http://127.0.0.1:11434");
|
||||
expect(provider?.models).toHaveLength(2);
|
||||
expect(provider?.models?.[0]?.id).toBe("deepseek-r1:latest");
|
||||
expect(provider?.models?.[0]?.reasoning).toBe(true);
|
||||
expect(provider?.models?.[1]?.reasoning).toBe(false);
|
||||
expectDiscoveryCallCounts(fetchMock, { tags: 1, show: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
it("does not warn when Ollama is unreachable and not explicitly configured", async () => {
|
||||
await withoutAmbientOllamaEnv(async () => {
|
||||
enableDiscoveryEnv();
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("connect ECONNREFUSED 127.0.0.1:11434"));
|
||||
vi.stubGlobal("fetch", withFetchPreconnect(fetchMock));
|
||||
|
||||
const provider = await runOllamaCatalog({
|
||||
env: { VITEST: "", NODE_ENV: "development" },
|
||||
});
|
||||
|
||||
expect(provider).toBeUndefined();
|
||||
expect(
|
||||
warnSpy.mock.calls.filter(([message]) => String(message).includes("Ollama")),
|
||||
).toHaveLength(0);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
it("warns when Ollama is unreachable and explicitly configured", async () => {
|
||||
await withoutAmbientOllamaEnv(async () => {
|
||||
enableDiscoveryEnv();
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("connect ECONNREFUSED 127.0.0.1:11434"));
|
||||
vi.stubGlobal("fetch", withFetchPreconnect(fetchMock));
|
||||
|
||||
await runOllamaCatalog({
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
ollama: {
|
||||
baseUrl: "http://127.0.0.1:11435/v1",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: { VITEST: "", NODE_ENV: "development" },
|
||||
});
|
||||
|
||||
expect(
|
||||
warnSpy.mock.calls.filter(([message]) => String(message).includes("Ollama")).length,
|
||||
).toBeGreaterThan(0);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to default context window when /api/show fails", async () => {
|
||||
const agentDir = createAgentDir();
|
||||
enableDiscoveryEnv();
|
||||
|
||||
@@ -210,6 +210,15 @@ export async function loadCompactHooksHarness(): Promise<{
|
||||
maybeCompactAgentHarnessSession: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.doMock("../../plugins/provider-runtime.js", () => ({
|
||||
prepareProviderRuntimeAuth: vi.fn(async () => ({ resolvedApiKey: undefined })),
|
||||
resolveProviderSystemPromptContribution: vi.fn(() => undefined),
|
||||
resolveProviderTextTransforms: vi.fn(() => undefined),
|
||||
transformProviderSystemPrompt: vi.fn(
|
||||
(params: { systemPrompt?: string }) => params.systemPrompt,
|
||||
),
|
||||
}));
|
||||
|
||||
vi.doMock("../provider-stream.js", () => ({
|
||||
registerProviderStreamForModel: registerProviderStreamForModelMock,
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
drainSessionStoreLockQueuesForTest,
|
||||
resetSessionStoreLockRuntimeForTests,
|
||||
setSessionWriteLockAcquirerForTests,
|
||||
} from "../config/sessions.js";
|
||||
import {
|
||||
readCompactionCount,
|
||||
seedSessionStore,
|
||||
@@ -50,6 +55,17 @@ function createCompactionContext(params: {
|
||||
} as unknown as EmbeddedPiSubscribeContext;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setSessionWriteLockAcquirerForTests(async () => ({
|
||||
release: async () => {},
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetSessionStoreLockRuntimeForTests();
|
||||
await drainSessionStoreLockQueuesForTest();
|
||||
});
|
||||
|
||||
describe("reconcileSessionStoreCompactionCountAfterSuccess", () => {
|
||||
it("raises the stored compaction count to the observed value", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-compaction-reconcile-"));
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "../plugins/config-state.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import type { PluginOrigin } from "../plugins/plugin-origin.types.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import { normalizeProviderId } from "./provider-id.js";
|
||||
|
||||
export type ProviderAuthAliasLookupParams = {
|
||||
@@ -17,6 +17,8 @@ type ProviderAuthAliasCandidate = {
|
||||
target: string;
|
||||
};
|
||||
|
||||
type PluginEntriesConfig = NonNullable<NonNullable<OpenClawConfig["plugins"]>["entries"]>;
|
||||
|
||||
const PROVIDER_AUTH_ALIAS_ORIGIN_PRIORITY: Readonly<Record<PluginOrigin, number>> = {
|
||||
config: 0,
|
||||
bundled: 1,
|
||||
@@ -31,6 +33,56 @@ function resolveProviderAuthAliasOriginPriority(origin: PluginOrigin | undefined
|
||||
return PROVIDER_AUTH_ALIAS_ORIGIN_PRIORITY[origin] ?? Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
function normalizePluginConfigId(id: unknown): string {
|
||||
return normalizeOptionalLowercaseString(id) ?? "";
|
||||
}
|
||||
|
||||
function hasPluginId(list: unknown, pluginId: string): boolean {
|
||||
return Array.isArray(list) && list.some((entry) => normalizePluginConfigId(entry) === pluginId);
|
||||
}
|
||||
|
||||
function findPluginEntry(
|
||||
entries: PluginEntriesConfig | undefined,
|
||||
pluginId: string,
|
||||
): { enabled?: boolean } | undefined {
|
||||
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
|
||||
return undefined;
|
||||
}
|
||||
for (const [key, value] of Object.entries(entries)) {
|
||||
if (normalizePluginConfigId(key) !== pluginId) {
|
||||
continue;
|
||||
}
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as { enabled?: boolean })
|
||||
: {};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isWorkspacePluginTrustedForAuthAliases(
|
||||
plugin: PluginManifestRecord,
|
||||
config: OpenClawConfig | undefined,
|
||||
): boolean {
|
||||
const pluginsConfig = config?.plugins;
|
||||
if (pluginsConfig?.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pluginId = normalizePluginConfigId(plugin.id);
|
||||
if (!pluginId || hasPluginId(pluginsConfig?.deny, pluginId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const entry = findPluginEntry(pluginsConfig?.entries, pluginId);
|
||||
if (entry?.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
if (entry?.enabled === true || hasPluginId(pluginsConfig?.allow, pluginId)) {
|
||||
return true;
|
||||
}
|
||||
return normalizePluginConfigId(pluginsConfig?.slots?.contextEngine) === pluginId;
|
||||
}
|
||||
|
||||
function shouldUsePluginAuthAliases(
|
||||
plugin: PluginManifestRecord,
|
||||
params: ProviderAuthAliasLookupParams | undefined,
|
||||
@@ -38,13 +90,7 @@ function shouldUsePluginAuthAliases(
|
||||
if (plugin.origin !== "workspace" || params?.includeUntrustedWorkspacePlugins === true) {
|
||||
return true;
|
||||
}
|
||||
const normalizedConfig = normalizePluginsConfig(params?.config?.plugins);
|
||||
return resolveEffectiveEnableState({
|
||||
id: plugin.id,
|
||||
origin: plugin.origin,
|
||||
config: normalizedConfig,
|
||||
rootConfig: params?.config,
|
||||
}).enabled;
|
||||
return isWorkspacePluginTrustedForAuthAliases(plugin, params?.config);
|
||||
}
|
||||
|
||||
export function resolveProviderAuthAliasMap(
|
||||
|
||||
@@ -11,6 +11,7 @@ import { captureEnv } from "../test-utils/env.js";
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
announceSpy: vi.fn(async () => true),
|
||||
allowedRunIds: undefined as Set<string> | undefined,
|
||||
registryPath: undefined as string | undefined,
|
||||
}));
|
||||
const { announceSpy } = hoisted;
|
||||
@@ -46,10 +47,16 @@ vi.mock("./subagent-registry.store.js", async () => {
|
||||
runs: Map<string, import("./subagent-registry.types.js").SubagentRunRecord>,
|
||||
) => {
|
||||
const pathname = resolvePath();
|
||||
const persistedRuns = hoisted.allowedRunIds
|
||||
? new Map([...runs].filter(([runId]) => hoisted.allowedRunIds?.has(runId)))
|
||||
: runs;
|
||||
if (hoisted.allowedRunIds && persistedRuns.size === 0 && runs.size > 0) {
|
||||
return;
|
||||
}
|
||||
fsSync.mkdirSync(pathSync.dirname(pathname), { recursive: true });
|
||||
fsSync.writeFileSync(
|
||||
pathname,
|
||||
`${JSON.stringify({ version: 2, runs: Object.fromEntries(runs) }, null, 2)}\n`,
|
||||
`${JSON.stringify({ version: 2, runs: Object.fromEntries(persistedRuns) }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
},
|
||||
@@ -115,8 +122,16 @@ describe("subagent registry persistence resume", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
announceSpy.mockClear();
|
||||
vi.mocked(callGatewayModule.callGateway).mockReset();
|
||||
vi.mocked(callGatewayModule.callGateway).mockResolvedValue({
|
||||
status: "ok",
|
||||
startedAt: 111,
|
||||
endedAt: 222,
|
||||
});
|
||||
mod.__testing.setDepsForTest({
|
||||
callGateway: vi.mocked(callGatewayModule.callGateway),
|
||||
cleanupBrowserSessionsForLifecycleEnd: vi.fn(async () => {}),
|
||||
captureSubagentCompletionReply: vi.fn(async () => undefined),
|
||||
ensureContextEnginesInitialized: vi.fn(),
|
||||
ensureRuntimePluginsLoaded: vi.fn(),
|
||||
loadConfig: vi.fn(() => ({})),
|
||||
@@ -129,12 +144,6 @@ describe("subagent registry persistence resume", () => {
|
||||
})),
|
||||
});
|
||||
mod.resetSubagentRegistryForTests({ persist: false });
|
||||
vi.mocked(callGatewayModule.callGateway).mockReset();
|
||||
vi.mocked(callGatewayModule.callGateway).mockResolvedValue({
|
||||
status: "ok",
|
||||
startedAt: 111,
|
||||
endedAt: 222,
|
||||
});
|
||||
vi.mocked(agentEventsModule.onAgentEvent).mockReset();
|
||||
vi.mocked(agentEventsModule.onAgentEvent).mockReturnValue(() => undefined);
|
||||
});
|
||||
@@ -150,6 +159,7 @@ describe("subagent registry persistence resume", () => {
|
||||
tempStateDir = null;
|
||||
}
|
||||
hoisted.registryPath = undefined;
|
||||
hoisted.allowedRunIds = undefined;
|
||||
envSnapshot.restore();
|
||||
});
|
||||
|
||||
@@ -158,6 +168,7 @@ describe("subagent registry persistence resume", () => {
|
||||
process.env.OPENCLAW_STATE_DIR = tempStateDir;
|
||||
const registryPath = path.join(tempStateDir, "subagents", "runs.json");
|
||||
hoisted.registryPath = registryPath;
|
||||
hoisted.allowedRunIds = new Set(["run-1"]);
|
||||
|
||||
let releaseInitialWait:
|
||||
| ((value: { status: "ok"; startedAt: number; endedAt: number }) => void)
|
||||
|
||||
@@ -181,6 +181,17 @@ describe("subagent registry persistence", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
__testing.setDepsForTest({
|
||||
cleanupBrowserSessionsForLifecycleEnd: vi.fn(async () => {}),
|
||||
ensureContextEnginesInitialized: vi.fn(),
|
||||
ensureRuntimePluginsLoaded: vi.fn(),
|
||||
loadConfig: vi.fn(() => ({})),
|
||||
resolveAgentTimeoutMs: vi.fn(() => 100),
|
||||
resolveContextEngine: vi.fn(async () => ({
|
||||
info: { id: "test", name: "Test", version: "0.0.1" },
|
||||
ingest: vi.fn(async () => ({ ingested: false })),
|
||||
assemble: vi.fn(async ({ messages }) => ({ messages, estimatedTokens: 0 })),
|
||||
compact: vi.fn(async () => ({ ok: false, compacted: false })),
|
||||
})),
|
||||
runSubagentAnnounceFlow: announceSpy,
|
||||
});
|
||||
vi.mocked(callGateway).mockReset();
|
||||
@@ -362,11 +373,7 @@ describe("subagent registry persistence", () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-subagent-"));
|
||||
process.env.OPENCLAW_STATE_DIR = tempStateDir;
|
||||
|
||||
vi.mocked(callGateway).mockResolvedValue({
|
||||
status: "ok",
|
||||
startedAt: 111,
|
||||
endedAt: 222,
|
||||
});
|
||||
vi.mocked(callGateway).mockImplementationOnce(async () => await new Promise(() => {}));
|
||||
|
||||
registerSubagentRun({
|
||||
runId: " run-live ",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createSubagentSpawnTestConfig,
|
||||
loadSubagentSpawnModuleForTest,
|
||||
@@ -14,7 +14,15 @@ let configOverride: Record<string, unknown> = {
|
||||
...createSubagentSpawnTestConfig(),
|
||||
};
|
||||
let workspaceDirOverride = "";
|
||||
let resetSubagentRegistryForTests: typeof import("./subagent-registry.js").resetSubagentRegistryForTests;
|
||||
let subagentSpawnModule: Awaited<ReturnType<typeof loadSubagentSpawnModuleForTest>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
subagentSpawnModule = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock,
|
||||
loadConfig: () => configOverride,
|
||||
workspaceDir: workspaceDirOverride || os.tmpdir(),
|
||||
});
|
||||
});
|
||||
|
||||
// --- decodeStrictBase64 ---
|
||||
|
||||
@@ -22,11 +30,7 @@ describe("decodeStrictBase64", () => {
|
||||
const maxBytes = 1024;
|
||||
|
||||
it("valid base64 returns buffer with correct bytes", async () => {
|
||||
const { decodeStrictBase64 } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock,
|
||||
loadConfig: () => configOverride,
|
||||
workspaceDir: workspaceDirOverride || os.tmpdir(),
|
||||
});
|
||||
const { decodeStrictBase64 } = subagentSpawnModule;
|
||||
const input = "hello world";
|
||||
const encoded = Buffer.from(input).toString("base64");
|
||||
const result = decodeStrictBase64(encoded, maxBytes);
|
||||
@@ -35,69 +39,41 @@ describe("decodeStrictBase64", () => {
|
||||
});
|
||||
|
||||
it("empty string returns null", async () => {
|
||||
const { decodeStrictBase64 } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock,
|
||||
loadConfig: () => configOverride,
|
||||
workspaceDir: workspaceDirOverride || os.tmpdir(),
|
||||
});
|
||||
const { decodeStrictBase64 } = subagentSpawnModule;
|
||||
expect(decodeStrictBase64("", maxBytes)).toBeNull();
|
||||
});
|
||||
|
||||
it("bad padding (length % 4 !== 0) returns null", async () => {
|
||||
const { decodeStrictBase64 } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock,
|
||||
loadConfig: () => configOverride,
|
||||
workspaceDir: workspaceDirOverride || os.tmpdir(),
|
||||
});
|
||||
const { decodeStrictBase64 } = subagentSpawnModule;
|
||||
expect(decodeStrictBase64("abc", maxBytes)).toBeNull();
|
||||
});
|
||||
|
||||
it("non-base64 chars returns null", async () => {
|
||||
const { decodeStrictBase64 } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock,
|
||||
loadConfig: () => configOverride,
|
||||
workspaceDir: workspaceDirOverride || os.tmpdir(),
|
||||
});
|
||||
const { decodeStrictBase64 } = subagentSpawnModule;
|
||||
expect(decodeStrictBase64("!@#$", maxBytes)).toBeNull();
|
||||
});
|
||||
|
||||
it("whitespace-only returns null (empty after strip)", async () => {
|
||||
const { decodeStrictBase64 } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock,
|
||||
loadConfig: () => configOverride,
|
||||
workspaceDir: workspaceDirOverride || os.tmpdir(),
|
||||
});
|
||||
const { decodeStrictBase64 } = subagentSpawnModule;
|
||||
expect(decodeStrictBase64(" ", maxBytes)).toBeNull();
|
||||
});
|
||||
|
||||
it("pre-decode oversize guard: encoded string > maxEncodedBytes * 2 returns null", async () => {
|
||||
const { decodeStrictBase64 } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock,
|
||||
loadConfig: () => configOverride,
|
||||
workspaceDir: workspaceDirOverride || os.tmpdir(),
|
||||
});
|
||||
const { decodeStrictBase64 } = subagentSpawnModule;
|
||||
// maxEncodedBytes = ceil(1024/3)*4 = 1368; *2 = 2736
|
||||
const oversized = "A".repeat(2737);
|
||||
expect(decodeStrictBase64(oversized, maxBytes)).toBeNull();
|
||||
});
|
||||
|
||||
it("decoded byteLength exceeds maxDecodedBytes returns null", async () => {
|
||||
const { decodeStrictBase64 } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock,
|
||||
loadConfig: () => configOverride,
|
||||
workspaceDir: workspaceDirOverride || os.tmpdir(),
|
||||
});
|
||||
const { decodeStrictBase64 } = subagentSpawnModule;
|
||||
const bigBuf = Buffer.alloc(1025, 0x42);
|
||||
const encoded = bigBuf.toString("base64");
|
||||
expect(decodeStrictBase64(encoded, maxBytes)).toBeNull();
|
||||
});
|
||||
|
||||
it("valid base64 at exact boundary returns Buffer", async () => {
|
||||
const { decodeStrictBase64 } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock,
|
||||
loadConfig: () => configOverride,
|
||||
workspaceDir: workspaceDirOverride || os.tmpdir(),
|
||||
});
|
||||
const { decodeStrictBase64 } = subagentSpawnModule;
|
||||
const exactBuf = Buffer.alloc(1024, 0x41);
|
||||
const encoded = exactBuf.toString("base64");
|
||||
const result = decodeStrictBase64(encoded, maxBytes);
|
||||
@@ -114,12 +90,7 @@ describe("spawnSubagentDirect filename validation", () => {
|
||||
path.join(os.tmpdir(), `openclaw-subagent-attachments-${process.pid}-${Date.now()}-`),
|
||||
);
|
||||
configOverride = createSubagentSpawnTestConfig(workspaceDirOverride);
|
||||
({ resetSubagentRegistryForTests } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock,
|
||||
loadConfig: () => configOverride,
|
||||
workspaceDir: workspaceDirOverride,
|
||||
}));
|
||||
resetSubagentRegistryForTests();
|
||||
subagentSpawnModule.resetSubagentRegistryForTests();
|
||||
callGatewayMock.mockClear();
|
||||
setupAcceptedSubagentGatewayMock(callGatewayMock);
|
||||
});
|
||||
@@ -141,11 +112,7 @@ describe("spawnSubagentDirect filename validation", () => {
|
||||
const validContent = Buffer.from("hello").toString("base64");
|
||||
|
||||
async function spawnWithName(name: string) {
|
||||
const { spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock,
|
||||
loadConfig: () => configOverride,
|
||||
workspaceDir: workspaceDirOverride,
|
||||
});
|
||||
const { spawnSubagentDirect } = subagentSpawnModule;
|
||||
return spawnSubagentDirect(
|
||||
{
|
||||
task: "test",
|
||||
@@ -180,11 +147,7 @@ describe("spawnSubagentDirect filename validation", () => {
|
||||
});
|
||||
|
||||
it("duplicate name returns attachments_duplicate_name", async () => {
|
||||
const { spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock,
|
||||
loadConfig: () => configOverride,
|
||||
workspaceDir: workspaceDirOverride,
|
||||
});
|
||||
const { spawnSubagentDirect } = subagentSpawnModule;
|
||||
const result = await spawnSubagentDirect(
|
||||
{
|
||||
task: "test",
|
||||
@@ -219,11 +182,7 @@ describe("spawnSubagentDirect filename validation", () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
const { spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({
|
||||
callGatewayMock,
|
||||
loadConfig: () => configOverride,
|
||||
workspaceDir: workspaceDirOverride,
|
||||
});
|
||||
const { spawnSubagentDirect } = subagentSpawnModule;
|
||||
const result = await spawnSubagentDirect(
|
||||
{
|
||||
task: "test",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import sharp from "sharp";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { infoMock, warnMock } = vi.hoisted(() => ({
|
||||
infoMock: vi.fn(),
|
||||
@@ -25,8 +25,8 @@ vi.mock("../logging/subsystem.js", () => {
|
||||
import { sanitizeContentBlocksImages } from "./tool-images.js";
|
||||
|
||||
async function createLargePng(): Promise<Buffer> {
|
||||
const width = 2400;
|
||||
const height = 680;
|
||||
const width = 2001;
|
||||
const height = 8;
|
||||
const raw = Buffer.alloc(width * height * 3, 0x7f);
|
||||
return await sharp(raw, {
|
||||
raw: { width, height, channels: 3 },
|
||||
@@ -36,13 +36,18 @@ async function createLargePng(): Promise<Buffer> {
|
||||
}
|
||||
|
||||
describe("tool-images log context", () => {
|
||||
let png: Buffer;
|
||||
|
||||
beforeAll(async () => {
|
||||
png = await createLargePng();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
infoMock.mockClear();
|
||||
warnMock.mockClear();
|
||||
});
|
||||
|
||||
it("includes filename from MEDIA text", async () => {
|
||||
const png = await createLargePng();
|
||||
const blocks = [
|
||||
{ type: "text" as const, text: "MEDIA:/tmp/snapshots/camera-front.png" },
|
||||
{ type: "image" as const, data: png.toString("base64"), mimeType: "image/png" },
|
||||
@@ -53,7 +58,6 @@ describe("tool-images log context", () => {
|
||||
});
|
||||
|
||||
it("includes filename from read label", async () => {
|
||||
const png = await createLargePng();
|
||||
const blocks = [
|
||||
{ type: "image" as const, data: png.toString("base64"), mimeType: "image/png" },
|
||||
];
|
||||
|
||||
@@ -172,7 +172,23 @@ function createFalEditProvider(params?: {
|
||||
|
||||
describe("createImageGenerateTool", () => {
|
||||
beforeAll(async () => {
|
||||
vi.doUnmock("../../secrets/provider-env-vars.js");
|
||||
vi.doMock("../../secrets/provider-env-vars.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../secrets/provider-env-vars.js")>(
|
||||
"../../secrets/provider-env-vars.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
getProviderEnvVars: (providerId: string) => {
|
||||
if (providerId === "google") {
|
||||
return ["GEMINI_API_KEY", "GOOGLE_API_KEY"];
|
||||
}
|
||||
if (providerId === "openai") {
|
||||
return ["OPENAI_API_KEY"];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
});
|
||||
imageGenerationRuntime = await import("../../image-generation/runtime.js");
|
||||
imageOps = await import("../../media/image-ops.js");
|
||||
mediaStore = await import("../../media/store.js");
|
||||
|
||||
@@ -287,6 +287,7 @@ describe("message tool secret scoping", () => {
|
||||
currentChannelProvider: "discord",
|
||||
agentAccountId: "ops",
|
||||
loadConfig: mocks.loadConfig as never,
|
||||
getScopedChannelsCommandSecretTargets: mocks.getScopedChannelsCommandSecretTargets as never,
|
||||
resolveCommandSecretRefsViaGateway: mocks.resolveCommandSecretRefsViaGateway as never,
|
||||
runMessageAction: mocks.runMessageAction as never,
|
||||
});
|
||||
|
||||
@@ -398,6 +398,7 @@ type MessageToolOptions = {
|
||||
sessionId?: string;
|
||||
config?: OpenClawConfig;
|
||||
loadConfig?: () => OpenClawConfig;
|
||||
getScopedChannelsCommandSecretTargets?: typeof getScopedChannelsCommandSecretTargets;
|
||||
resolveCommandSecretRefsViaGateway?: typeof resolveCommandSecretRefsViaGateway;
|
||||
runMessageAction?: typeof runMessageAction;
|
||||
currentChannelId?: string;
|
||||
@@ -616,6 +617,8 @@ function appendMessageToolReadHint(
|
||||
|
||||
export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
|
||||
const loadConfigForTool = options?.loadConfig ?? loadConfig;
|
||||
const getScopedSecretTargetsForTool =
|
||||
options?.getScopedChannelsCommandSecretTargets ?? getScopedChannelsCommandSecretTargets;
|
||||
const resolveSecretRefsForTool =
|
||||
options?.resolveCommandSecretRefsViaGateway ?? resolveCommandSecretRefsViaGateway;
|
||||
const runMessageActionForTool = options?.runMessageAction ?? runMessageAction;
|
||||
@@ -693,7 +696,7 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
|
||||
accountId: params.accountId,
|
||||
fallbackAccountId: agentAccountId,
|
||||
});
|
||||
const scopedTargets = getScopedChannelsCommandSecretTargets({
|
||||
const scopedTargets = getScopedSecretTargetsForTool({
|
||||
config: loadedRaw,
|
||||
channel: scope.channel,
|
||||
accountId: scope.accountId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getAcpSessionManager } from "../../acp/control-plane/manager.js";
|
||||
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
import { abortEmbeddedPiRun } from "../../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun } from "../../agents/pi-embedded-runner/runs.js";
|
||||
import {
|
||||
getLatestSubagentRunByChildSessionKey,
|
||||
listSubagentRunsForController,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-bu
|
||||
import { estimateMessagesTokens } from "../../agents/compaction.js";
|
||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||
import { isCliProvider } from "../../agents/model-selection.js";
|
||||
import { compactEmbeddedPiSession, runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
|
||||
import { resolveSandboxConfigForAgent, resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||
import {
|
||||
derivePromptTokens,
|
||||
@@ -45,10 +44,26 @@ import { refreshQueuedFollowupSession, type FollowupRun } from "./queue.js";
|
||||
import type { ReplyOperation } from "./reply-run-registry.js";
|
||||
import { incrementCompactionCount } from "./session-updates.js";
|
||||
|
||||
async function compactEmbeddedPiSessionDefault(
|
||||
...args: Parameters<typeof import("../../agents/pi-embedded.js").compactEmbeddedPiSession>
|
||||
): Promise<
|
||||
Awaited<ReturnType<typeof import("../../agents/pi-embedded.js").compactEmbeddedPiSession>>
|
||||
> {
|
||||
const { compactEmbeddedPiSession } = await import("../../agents/pi-embedded.js");
|
||||
return await compactEmbeddedPiSession(...args);
|
||||
}
|
||||
|
||||
async function runEmbeddedPiAgentDefault(
|
||||
...args: Parameters<typeof import("../../agents/pi-embedded.js").runEmbeddedPiAgent>
|
||||
): Promise<Awaited<ReturnType<typeof import("../../agents/pi-embedded.js").runEmbeddedPiAgent>>> {
|
||||
const { runEmbeddedPiAgent } = await import("../../agents/pi-embedded.js");
|
||||
return await runEmbeddedPiAgent(...args);
|
||||
}
|
||||
|
||||
const memoryDeps = {
|
||||
compactEmbeddedPiSession,
|
||||
compactEmbeddedPiSession: compactEmbeddedPiSessionDefault,
|
||||
runWithModelFallback,
|
||||
runEmbeddedPiAgent,
|
||||
runEmbeddedPiAgent: runEmbeddedPiAgentDefault,
|
||||
registerAgentRunContext,
|
||||
refreshQueuedFollowupSession,
|
||||
incrementCompactionCount,
|
||||
@@ -60,8 +75,8 @@ const memoryDeps = {
|
||||
export function setAgentRunnerMemoryTestDeps(overrides?: Partial<typeof memoryDeps>): void {
|
||||
Object.assign(memoryDeps, {
|
||||
runWithModelFallback,
|
||||
compactEmbeddedPiSession,
|
||||
runEmbeddedPiAgent,
|
||||
compactEmbeddedPiSession: compactEmbeddedPiSessionDefault,
|
||||
runEmbeddedPiAgent: runEmbeddedPiAgentDefault,
|
||||
registerAgentRunContext,
|
||||
refreshQueuedFollowupSession,
|
||||
incrementCompactionCount,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { resolveContextTokensForModel } from "../../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import { resolveModelAuthMode } from "../../agents/model-auth.js";
|
||||
import { isCliProvider } from "../../agents/model-selection.js";
|
||||
import { queueEmbeddedPiMessage } from "../../agents/pi-embedded.js";
|
||||
import { queueEmbeddedPiMessage } from "../../agents/pi-embedded-runner/runs.js";
|
||||
import { hasNonzeroUsage } from "../../agents/usage.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
|
||||
@@ -90,6 +90,13 @@ vi.mock("../../acp/runtime/session-meta.js", () => ({
|
||||
resolveSessionStorePathForAcp: (args: unknown) => hoisted.resolveSessionStorePathForAcpMock(args),
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/acp-spawn.js", () => ({
|
||||
resolveAcpSpawnRuntimePolicyError: (params: { cfg?: OpenClawConfig }) =>
|
||||
params.cfg?.agents?.defaults?.sandbox?.mode === "all"
|
||||
? 'Sandboxed sessions cannot spawn ACP sessions because runtime="acp" runs on the host. Use runtime="subagent" from sandboxed sessions.'
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
vi.mock("../../config/sessions.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../config/sessions.js")>(
|
||||
"../../config/sessions.js",
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { requireGatewayClientScopeForInternalChannel } from "./command-gates.js";
|
||||
import {
|
||||
handleAcpDoctorAction,
|
||||
handleAcpInstallAction,
|
||||
handleAcpSessionsAction,
|
||||
} from "./commands-acp/diagnostics.js";
|
||||
import {
|
||||
handleAcpCancelAction,
|
||||
handleAcpCloseAction,
|
||||
handleAcpSpawnAction,
|
||||
handleAcpSteerAction,
|
||||
} from "./commands-acp/lifecycle.js";
|
||||
import {
|
||||
handleAcpCwdAction,
|
||||
handleAcpModelAction,
|
||||
handleAcpPermissionsAction,
|
||||
handleAcpResetOptionsAction,
|
||||
handleAcpSetAction,
|
||||
handleAcpSetModeAction,
|
||||
handleAcpStatusAction,
|
||||
handleAcpTimeoutAction,
|
||||
} from "./commands-acp/runtime-options.js";
|
||||
import {
|
||||
COMMAND,
|
||||
type AcpAction,
|
||||
@@ -39,23 +18,57 @@ type AcpActionHandler = (
|
||||
tokens: string[],
|
||||
) => Promise<CommandHandlerResult>;
|
||||
|
||||
const ACP_ACTION_HANDLERS: Record<Exclude<AcpAction, "help">, AcpActionHandler> = {
|
||||
spawn: handleAcpSpawnAction,
|
||||
cancel: handleAcpCancelAction,
|
||||
steer: handleAcpSteerAction,
|
||||
close: handleAcpCloseAction,
|
||||
status: handleAcpStatusAction,
|
||||
"set-mode": handleAcpSetModeAction,
|
||||
set: handleAcpSetAction,
|
||||
cwd: handleAcpCwdAction,
|
||||
permissions: handleAcpPermissionsAction,
|
||||
timeout: handleAcpTimeoutAction,
|
||||
model: handleAcpModelAction,
|
||||
"reset-options": handleAcpResetOptionsAction,
|
||||
doctor: handleAcpDoctorAction,
|
||||
install: async (params, tokens) => handleAcpInstallAction(params, tokens),
|
||||
sessions: async (params, tokens) => handleAcpSessionsAction(params, tokens),
|
||||
};
|
||||
let lifecycleHandlersPromise: Promise<typeof import("./commands-acp/lifecycle.js")> | undefined;
|
||||
let runtimeOptionHandlersPromise:
|
||||
| Promise<typeof import("./commands-acp/runtime-options.js")>
|
||||
| undefined;
|
||||
let diagnosticHandlersPromise: Promise<typeof import("./commands-acp/diagnostics.js")> | undefined;
|
||||
|
||||
async function loadAcpActionHandler(action: Exclude<AcpAction, "help">): Promise<AcpActionHandler> {
|
||||
if (action === "spawn" || action === "cancel" || action === "steer" || action === "close") {
|
||||
lifecycleHandlersPromise ??= import("./commands-acp/lifecycle.js");
|
||||
const handlers = await lifecycleHandlersPromise;
|
||||
return {
|
||||
spawn: handlers.handleAcpSpawnAction,
|
||||
cancel: handlers.handleAcpCancelAction,
|
||||
steer: handlers.handleAcpSteerAction,
|
||||
close: handlers.handleAcpCloseAction,
|
||||
}[action];
|
||||
}
|
||||
|
||||
if (
|
||||
action === "status" ||
|
||||
action === "set-mode" ||
|
||||
action === "set" ||
|
||||
action === "cwd" ||
|
||||
action === "permissions" ||
|
||||
action === "timeout" ||
|
||||
action === "model" ||
|
||||
action === "reset-options"
|
||||
) {
|
||||
runtimeOptionHandlersPromise ??= import("./commands-acp/runtime-options.js");
|
||||
const handlers = await runtimeOptionHandlersPromise;
|
||||
return {
|
||||
status: handlers.handleAcpStatusAction,
|
||||
"set-mode": handlers.handleAcpSetModeAction,
|
||||
set: handlers.handleAcpSetAction,
|
||||
cwd: handlers.handleAcpCwdAction,
|
||||
permissions: handlers.handleAcpPermissionsAction,
|
||||
timeout: handlers.handleAcpTimeoutAction,
|
||||
model: handlers.handleAcpModelAction,
|
||||
"reset-options": handlers.handleAcpResetOptionsAction,
|
||||
}[action];
|
||||
}
|
||||
|
||||
diagnosticHandlersPromise ??= import("./commands-acp/diagnostics.js");
|
||||
const handlers = await diagnosticHandlersPromise;
|
||||
const diagnosticHandlers: Record<"doctor" | "install" | "sessions", AcpActionHandler> = {
|
||||
doctor: handlers.handleAcpDoctorAction,
|
||||
install: async (params, tokens) => handlers.handleAcpInstallAction(params, tokens),
|
||||
sessions: async (params, tokens) => handlers.handleAcpSessionsAction(params, tokens),
|
||||
};
|
||||
return diagnosticHandlers[action];
|
||||
}
|
||||
|
||||
const ACP_MUTATING_ACTIONS = new Set<AcpAction>([
|
||||
"spawn",
|
||||
@@ -105,6 +118,6 @@ export const handleAcpCommand: CommandHandler = async (params, allowTextCommands
|
||||
}
|
||||
}
|
||||
|
||||
const handler = ACP_ACTION_HANDLERS[action];
|
||||
return handler ? await handler(params, tokens) : stopWithText(resolveAcpHelpText());
|
||||
const handler = await loadAcpActionHandler(action);
|
||||
return await handler(params, tokens);
|
||||
};
|
||||
|
||||
@@ -12,12 +12,12 @@ import {
|
||||
} from "../../../shared/string-coerce.js";
|
||||
import type { CommandHandlerResult, HandleCommandsParams } from "../commands-types.js";
|
||||
import { resolveAcpCommandBindingContext } from "./context.js";
|
||||
import { resolveAcpInstallCommandHint } from "./install-hints.js";
|
||||
import {
|
||||
ACP_DOCTOR_USAGE,
|
||||
ACP_INSTALL_USAGE,
|
||||
ACP_SESSIONS_USAGE,
|
||||
formatAcpCapabilitiesText,
|
||||
resolveAcpInstallCommandHint,
|
||||
stopWithText,
|
||||
} from "./shared.js";
|
||||
import { resolveBoundAcpThreadSessionKey } from "./targets.js";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import { resolveBundledPluginWorkspaceSourcePath } from "../../../plugins/bundled-plugin-metadata.js";
|
||||
import { resolveBundledPluginInstallCommandHint } from "../../../plugins/bundled-sources.js";
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
@@ -16,11 +15,8 @@ export function resolveAcpInstallCommandHint(cfg: OpenClawConfig): string {
|
||||
const workspaceDir = process.cwd();
|
||||
const backendId = normalizeOptionalLowercaseString(cfg.acp?.backend) ?? "acpx";
|
||||
if (backendId === "acpx") {
|
||||
const workspaceLocalPath = resolveBundledPluginWorkspaceSourcePath({
|
||||
rootDir: workspaceDir,
|
||||
pluginId: backendId,
|
||||
});
|
||||
if (workspaceLocalPath && existsSync(workspaceLocalPath)) {
|
||||
const workspaceLocalPath = path.join(workspaceDir, "extensions", "acpx");
|
||||
if (existsSync(workspaceLocalPath)) {
|
||||
return `openclaw plugins install ${workspaceLocalPath}`;
|
||||
}
|
||||
const bundledInstallHint = resolveBundledPluginInstallCommandHint({
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from "../../../shared/string-coerce.js";
|
||||
import type { CommandHandlerResult, HandleCommandsParams } from "../commands-types.js";
|
||||
import { resolveAcpCommandChannel, resolveAcpCommandThreadId } from "./context.js";
|
||||
export { resolveAcpInstallCommandHint } from "./install-hints.js";
|
||||
|
||||
export const COMMAND = "/acp";
|
||||
export const ACP_SPAWN_USAGE =
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { abortEmbeddedPiRun } from "../../agents/pi-embedded.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
|
||||
@@ -27,6 +26,11 @@ type AbortTarget = {
|
||||
sessionId?: string;
|
||||
};
|
||||
|
||||
async function abortEmbeddedPiRunForSession(sessionId: string): Promise<void> {
|
||||
const { abortEmbeddedPiRun } = await import("../../agents/pi-embedded-runner/runs.js");
|
||||
abortEmbeddedPiRun(sessionId);
|
||||
}
|
||||
|
||||
function resolveAbortTarget(params: {
|
||||
ctx: { CommandTargetSessionKey?: string | null };
|
||||
sessionKey?: string;
|
||||
@@ -90,7 +94,7 @@ async function applyAbortTarget(params: {
|
||||
replyRunRegistry.abort(abortTarget.key);
|
||||
}
|
||||
if (abortTarget.sessionId) {
|
||||
abortEmbeddedPiRun(abortTarget.sessionId);
|
||||
await abortEmbeddedPiRunForSession(abortTarget.sessionId);
|
||||
}
|
||||
|
||||
const persisted = await persistAbortTargetEntry({
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveSessionAgentIds } from "../../agents/agent-scope.js";
|
||||
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
|
||||
import { createOpenClawCodingTools } from "../../agents/pi-tools.js";
|
||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
|
||||
import { resolveCommandsSystemPromptBundle } from "./commands-system-prompt.js";
|
||||
import type { HandleCommandsParams } from "./commands-types.js";
|
||||
|
||||
const { createOpenClawCodingToolsMock } = vi.hoisted(() => ({
|
||||
@@ -49,6 +52,14 @@ vi.mock("../../agents/system-prompt.js", () => ({
|
||||
buildAgentSystemPrompt: vi.fn(() => "system prompt"),
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/pi-tools.js", () => ({
|
||||
createOpenClawCodingTools: createOpenClawCodingToolsMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../tts/tts.js", () => ({
|
||||
buildTtsSystemPromptHint: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/skills-remote.js", () => ({
|
||||
getRemoteSkillEligibility: vi.fn(() => false),
|
||||
}));
|
||||
@@ -103,24 +114,16 @@ function makeParams(): HandleCommandsParams {
|
||||
}
|
||||
|
||||
describe("resolveCommandsSystemPromptBundle", () => {
|
||||
beforeEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
vi.resetModules();
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
createOpenClawCodingToolsMock.mockClear();
|
||||
createOpenClawCodingToolsMock.mockReturnValue([]);
|
||||
const piTools = await import("../../agents/pi-tools.js");
|
||||
vi.spyOn(piTools, "createOpenClawCodingTools").mockImplementation(
|
||||
createOpenClawCodingToolsMock,
|
||||
);
|
||||
const ttsRuntime = await import("../../tts/tts.js");
|
||||
vi.spyOn(ttsRuntime, "buildTtsSystemPromptHint").mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it("opts command tool builds into gateway subagent binding", async () => {
|
||||
const { resolveCommandsSystemPromptBundle } = await import("./commands-system-prompt.js");
|
||||
await resolveCommandsSystemPromptBundle(makeParams());
|
||||
|
||||
expect(createOpenClawCodingToolsMock).toHaveBeenCalledWith(
|
||||
expect(vi.mocked(createOpenClawCodingTools)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
allowGatewaySubagentBinding: true,
|
||||
sessionKey: "agent:main:default",
|
||||
@@ -139,7 +142,6 @@ describe("resolveCommandsSystemPromptBundle", () => {
|
||||
params.ctx.SessionKey = "agent:main:telegram:slash-session";
|
||||
params.sessionKey = "agent:main:telegram:direct:target-session";
|
||||
|
||||
const { resolveCommandsSystemPromptBundle } = await import("./commands-system-prompt.js");
|
||||
await resolveCommandsSystemPromptBundle(params);
|
||||
|
||||
expect(vi.mocked(resolveSandboxRuntimeStatus)).toHaveBeenCalledWith({
|
||||
@@ -157,10 +159,9 @@ describe("resolveCommandsSystemPromptBundle", () => {
|
||||
defaultAgentId: "main",
|
||||
});
|
||||
|
||||
const { resolveCommandsSystemPromptBundle } = await import("./commands-system-prompt.js");
|
||||
await resolveCommandsSystemPromptBundle(params);
|
||||
|
||||
expect(createOpenClawCodingToolsMock).toHaveBeenCalledWith(
|
||||
expect(vi.mocked(createOpenClawCodingTools)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "target",
|
||||
sessionKey: "agent:target:telegram:direct:target-session",
|
||||
@@ -190,7 +191,6 @@ describe("resolveCommandsSystemPromptBundle", () => {
|
||||
} as HandleCommandsParams["sessionStore"];
|
||||
params.sessionKey = "agent:target:telegram:direct:target-session";
|
||||
|
||||
const { resolveCommandsSystemPromptBundle } = await import("./commands-system-prompt.js");
|
||||
await resolveCommandsSystemPromptBundle(params);
|
||||
|
||||
expect(vi.mocked(resolveBootstrapContextForRun)).toHaveBeenCalledWith(
|
||||
@@ -198,7 +198,7 @@ describe("resolveCommandsSystemPromptBundle", () => {
|
||||
sessionId: "target-session",
|
||||
}),
|
||||
);
|
||||
expect(createOpenClawCodingToolsMock).toHaveBeenCalledWith(
|
||||
expect(vi.mocked(createOpenClawCodingTools)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
groupId: "target-group",
|
||||
groupChannel: "#target",
|
||||
@@ -209,11 +209,7 @@ describe("resolveCommandsSystemPromptBundle", () => {
|
||||
});
|
||||
|
||||
it("uses the resolved session key and forwards full-access block reasons", async () => {
|
||||
const { resolveCommandsSystemPromptBundle } = await import("./commands-system-prompt.js");
|
||||
const sandboxRuntime = await import("../../agents/sandbox.js");
|
||||
const systemPromptRuntime = await import("../../agents/system-prompt.js");
|
||||
|
||||
vi.mocked(sandboxRuntime.resolveSandboxRuntimeStatus).mockImplementation(({ sessionKey }) => {
|
||||
vi.mocked(resolveSandboxRuntimeStatus).mockImplementation(({ sessionKey }) => {
|
||||
expect(sessionKey).toBe("agent:target:default");
|
||||
return { sandboxed: true, mode: "workspace-write" } as never;
|
||||
});
|
||||
@@ -229,7 +225,7 @@ describe("resolveCommandsSystemPromptBundle", () => {
|
||||
|
||||
await resolveCommandsSystemPromptBundle(params);
|
||||
|
||||
expect(vi.mocked(systemPromptRuntime.buildAgentSystemPrompt)).toHaveBeenCalledWith(
|
||||
expect(vi.mocked(buildAgentSystemPrompt)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sandboxInfo: expect.objectContaining({
|
||||
enabled: true,
|
||||
|
||||
@@ -43,21 +43,18 @@ const channelPluginMocks = vi.hoisted(() => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../tts/tts.js", () => ({
|
||||
vi.mock("./dispatch-acp-tts.runtime.js", () => ({
|
||||
maybeApplyTtsToPayload: (params: unknown) => ttsMocks.maybeApplyTtsToPayload(params),
|
||||
}));
|
||||
|
||||
vi.mock("./route-reply.js", () => ({
|
||||
vi.mock("./route-reply.runtime.js", () => ({
|
||||
routeReply: (params: unknown) => deliveryMocks.routeReply(params),
|
||||
}));
|
||||
|
||||
vi.mock("../../channels/plugins/index.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../channels/plugins/index.js")>();
|
||||
return {
|
||||
...actual,
|
||||
getChannelPlugin: (channelId: string) => channelPluginMocks.getChannelPlugin(channelId),
|
||||
};
|
||||
});
|
||||
vi.mock("../../channels/plugins/index.js", () => ({
|
||||
getChannelPlugin: (channelId: string) => channelPluginMocks.getChannelPlugin(channelId),
|
||||
normalizeChannelId: (channelId?: string | null) => channelId?.trim().toLowerCase() || null,
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/outbound/message-action-runner.js", () => ({
|
||||
runMessageAction: (params: unknown) => deliveryMocks.runMessageAction(params),
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { SessionBindingRecord } from "../../infra/outbound/session-binding-
|
||||
import type { MediaUnderstandingSkipError } from "../../media-understanding/errors.js";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
import { resolveAcpAttachments } from "./dispatch-acp-attachments.js";
|
||||
import { tryDispatchAcpReply } from "./dispatch-acp.js";
|
||||
import type { ReplyDispatcher } from "./reply-dispatcher.js";
|
||||
import { buildTestCtx } from "./test-ctx.js";
|
||||
import { createAcpSessionMeta, createAcpTestConfig } from "./test-fixtures/acp-runtime.js";
|
||||
@@ -79,10 +80,88 @@ const bindingServiceMocks = vi.hoisted(() => ({
|
||||
unbind: vi.fn<(input: unknown) => Promise<SessionBindingRecord[]>>(async () => []),
|
||||
}));
|
||||
|
||||
vi.mock("./dispatch-acp-manager.runtime.js", () => ({
|
||||
getAcpSessionManager: () => managerMocks,
|
||||
getSessionBindingService: () => ({
|
||||
listBySession: (targetSessionKey: string) =>
|
||||
bindingServiceMocks.listBySession(targetSessionKey),
|
||||
unbind: (input: unknown) => bindingServiceMocks.unbind(input),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../acp/policy.js", () => ({
|
||||
resolveAcpDispatchPolicyError: (cfg: OpenClawConfig) =>
|
||||
policyMocks.resolveAcpDispatchPolicyError(cfg),
|
||||
resolveAcpAgentPolicyError: (cfg: OpenClawConfig, agent: string) =>
|
||||
policyMocks.resolveAcpAgentPolicyError(cfg, agent),
|
||||
}));
|
||||
|
||||
vi.mock("./route-reply.runtime.js", () => ({
|
||||
routeReply: (params: unknown) => routeMocks.routeReply(params),
|
||||
}));
|
||||
|
||||
vi.mock("../../channels/plugins/index.js", () => ({
|
||||
getChannelPlugin: (channelId: string) => channelPluginMocks.getChannelPlugin(channelId),
|
||||
getLoadedChannelPlugin: (channelId: string) => channelPluginMocks.getChannelPlugin(channelId),
|
||||
normalizeChannelId: (channelId?: string | null) => channelId?.trim().toLowerCase() || null,
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/outbound/message-action-runner.js", () => ({
|
||||
runMessageAction: (params: unknown) => messageActionMocks.runMessageAction(params),
|
||||
}));
|
||||
|
||||
vi.mock("./dispatch-acp-tts.runtime.js", () => ({
|
||||
maybeApplyTtsToPayload: (params: unknown) => ttsMocks.maybeApplyTtsToPayload(params),
|
||||
}));
|
||||
|
||||
vi.mock("../../tts/status-config.js", () => ({
|
||||
resolveStatusTtsSnapshot: () => ({
|
||||
autoMode: "always",
|
||||
provider: "auto",
|
||||
maxLength: 1500,
|
||||
summarize: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("./dispatch-acp-media.runtime.js", () => ({
|
||||
applyMediaUnderstanding: (params: unknown) =>
|
||||
mediaUnderstandingMocks.applyMediaUnderstanding(params),
|
||||
isMediaUnderstandingSkipError: (error: unknown): error is MediaUnderstandingSkipError =>
|
||||
error instanceof Error && error.name === "MediaUnderstandingSkipError",
|
||||
normalizeAttachments: (ctx: { MediaPath?: string; MediaType?: string }) =>
|
||||
ctx.MediaPath
|
||||
? [
|
||||
{
|
||||
path: ctx.MediaPath,
|
||||
mime: ctx.MediaType,
|
||||
index: 0,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
resolveMediaAttachmentLocalRoots: (params: {
|
||||
cfg: { channels?: Record<string, { attachmentRoots?: string[] } | undefined> };
|
||||
ctx: { Provider?: string; Surface?: string };
|
||||
}) => {
|
||||
const channel = params.ctx.Provider ?? params.ctx.Surface ?? "";
|
||||
return params.cfg.channels?.[channel]?.attachmentRoots ?? [];
|
||||
},
|
||||
MediaAttachmentCache: class {
|
||||
async getBuffer(): Promise<never> {
|
||||
const error = new Error("outside allowed roots");
|
||||
error.name = "MediaUnderstandingSkipError";
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./dispatch-acp-session.runtime.js", () => ({
|
||||
readAcpSessionEntry: (params: { sessionKey: string; cfg?: OpenClawConfig }) =>
|
||||
sessionMetaMocks.readAcpSessionEntry(params),
|
||||
}));
|
||||
|
||||
const sessionKey = "agent:codex-acp:session-1";
|
||||
const originalFetch = globalThis.fetch;
|
||||
type MockTtsReply = Awaited<ReturnType<typeof ttsMocks.maybeApplyTtsToPayload>>;
|
||||
let tryDispatchAcpReply: typeof import("./dispatch-acp.js").tryDispatchAcpReply;
|
||||
|
||||
function createDispatcher(): {
|
||||
dispatcher: ReplyDispatcher;
|
||||
@@ -246,68 +325,7 @@ function expectRoutedPayload(callIndex: number, payload: Partial<MockTtsReply>)
|
||||
}
|
||||
|
||||
describe("tryDispatchAcpReply", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("../../acp/control-plane/manager.js", () => ({
|
||||
getAcpSessionManager: () => managerMocks,
|
||||
}));
|
||||
vi.doMock("../../acp/policy.js", () => ({
|
||||
resolveAcpDispatchPolicyError: (cfg: OpenClawConfig) =>
|
||||
policyMocks.resolveAcpDispatchPolicyError(cfg),
|
||||
resolveAcpAgentPolicyError: (cfg: OpenClawConfig, agent: string) =>
|
||||
policyMocks.resolveAcpAgentPolicyError(cfg, agent),
|
||||
}));
|
||||
vi.doMock("./route-reply.js", () => ({
|
||||
routeReply: (params: unknown) => routeMocks.routeReply(params),
|
||||
}));
|
||||
vi.doMock("../../channels/plugins/index.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../channels/plugins/index.js")>();
|
||||
return {
|
||||
...actual,
|
||||
getChannelPlugin: (channelId: string) => channelPluginMocks.getChannelPlugin(channelId),
|
||||
};
|
||||
});
|
||||
vi.doMock("../../infra/outbound/message-action-runner.js", () => ({
|
||||
runMessageAction: (params: unknown) => messageActionMocks.runMessageAction(params),
|
||||
}));
|
||||
vi.doMock("../../tts/tts.js", () => ({
|
||||
maybeApplyTtsToPayload: (params: unknown) => ttsMocks.maybeApplyTtsToPayload(params),
|
||||
resolveTtsConfig: (cfg: OpenClawConfig) => ttsMocks.resolveTtsConfig(cfg),
|
||||
}));
|
||||
vi.doMock("../../tts/tts.runtime.js", () => ({
|
||||
maybeApplyTtsToPayload: (params: unknown) => ttsMocks.maybeApplyTtsToPayload(params),
|
||||
}));
|
||||
vi.doMock("../../tts/status-config.js", () => ({
|
||||
resolveStatusTtsSnapshot: () => ({
|
||||
autoMode: "always",
|
||||
provider: "auto",
|
||||
maxLength: 1500,
|
||||
summarize: true,
|
||||
}),
|
||||
}));
|
||||
vi.doMock("./dispatch-acp-tts.runtime.js", () => ({
|
||||
maybeApplyTtsToPayload: (params: unknown) => ttsMocks.maybeApplyTtsToPayload(params),
|
||||
}));
|
||||
vi.doMock("../../media-understanding/apply.js", () => ({
|
||||
applyMediaUnderstanding: (params: unknown) =>
|
||||
mediaUnderstandingMocks.applyMediaUnderstanding(params),
|
||||
}));
|
||||
vi.doMock("../../acp/runtime/session-meta.js", () => ({
|
||||
readAcpSessionEntry: (params: { sessionKey: string; cfg?: OpenClawConfig }) =>
|
||||
sessionMetaMocks.readAcpSessionEntry(params),
|
||||
}));
|
||||
vi.doMock("./dispatch-acp-session.runtime.js", () => ({
|
||||
readAcpSessionEntry: (params: { sessionKey: string; cfg?: OpenClawConfig }) =>
|
||||
sessionMetaMocks.readAcpSessionEntry(params),
|
||||
}));
|
||||
vi.doMock("../../infra/outbound/session-binding-service.js", () => ({
|
||||
getSessionBindingService: () => ({
|
||||
listBySession: (targetSessionKey: string) =>
|
||||
bindingServiceMocks.listBySession(targetSessionKey),
|
||||
unbind: (input: unknown) => bindingServiceMocks.unbind(input),
|
||||
}),
|
||||
}));
|
||||
({ tryDispatchAcpReply } = await import("./dispatch-acp.js"));
|
||||
beforeEach(() => {
|
||||
managerMocks.resolveSession.mockReset();
|
||||
managerMocks.runTurn.mockReset();
|
||||
managerMocks.runTurn.mockImplementation(
|
||||
@@ -747,12 +765,8 @@ describe("tryDispatchAcpReply", () => {
|
||||
it("does not unbind valid bindings on generic ACP runTurn init failure", async () => {
|
||||
setReadyAcpResolution();
|
||||
// Match the post-reset module instance so dispatch-acp preserves the ACP error code.
|
||||
const { AcpRuntimeError: FreshAcpRuntimeError } = await import("../../acp/runtime/errors.js");
|
||||
managerMocks.runTurn.mockRejectedValueOnce(
|
||||
new FreshAcpRuntimeError(
|
||||
"ACP_SESSION_INIT_FAILED",
|
||||
"Could not initialize ACP session runtime.",
|
||||
),
|
||||
new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "Could not initialize ACP session runtime."),
|
||||
);
|
||||
const { dispatcher } = createDispatcher();
|
||||
|
||||
@@ -778,9 +792,8 @@ describe("tryDispatchAcpReply", () => {
|
||||
sessionKey: canonicalSessionKey,
|
||||
meta: createAcpSessionMeta(),
|
||||
});
|
||||
const { AcpRuntimeError: FreshAcpRuntimeError } = await import("../../acp/runtime/errors.js");
|
||||
managerMocks.runTurn.mockRejectedValueOnce(
|
||||
new FreshAcpRuntimeError(
|
||||
new AcpRuntimeError(
|
||||
"ACP_SESSION_INIT_FAILED",
|
||||
`ACP metadata is missing for ${canonicalSessionKey}. Recreate this ACP session with /acp spawn and rebind the thread.`,
|
||||
),
|
||||
|
||||
@@ -3,40 +3,38 @@ import type { SkillCommandSpec } from "../../agents/skills.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
import { clearInlineDirectives } from "./get-reply-directives-utils.js";
|
||||
import { handleInlineActions } from "./get-reply-inline-actions.js";
|
||||
import { stripInlineStatus } from "./reply-inline.js";
|
||||
import { buildTestCtx } from "./test-ctx.js";
|
||||
import type { TypingController } from "./typing.js";
|
||||
|
||||
const handleCommandsMock = vi.fn();
|
||||
const getChannelPluginMock = vi.fn();
|
||||
const createOpenClawToolsMock = vi.fn();
|
||||
const buildStatusReplyMock = vi.fn();
|
||||
const { buildStatusReplyMock, createOpenClawToolsMock, getChannelPluginMock, handleCommandsMock } =
|
||||
vi.hoisted(() => ({
|
||||
buildStatusReplyMock: vi.fn(),
|
||||
createOpenClawToolsMock: vi.fn(),
|
||||
getChannelPluginMock: vi.fn(),
|
||||
handleCommandsMock: vi.fn(),
|
||||
}));
|
||||
|
||||
let handleInlineActions: typeof import("./get-reply-inline-actions.js").handleInlineActions;
|
||||
type HandleInlineActionsInput = Parameters<
|
||||
typeof import("./get-reply-inline-actions.js").handleInlineActions
|
||||
>[0];
|
||||
|
||||
async function loadFreshInlineActionsModuleForTest() {
|
||||
vi.resetModules();
|
||||
vi.doMock("./commands.runtime.js", () => ({
|
||||
handleCommands: (...args: unknown[]) => handleCommandsMock(...args),
|
||||
buildStatusReply: (...args: unknown[]) => buildStatusReplyMock(...args),
|
||||
}));
|
||||
vi.doMock("../../agents/openclaw-tools.runtime.js", () => ({
|
||||
createOpenClawTools: (...args: unknown[]) => createOpenClawToolsMock(...args),
|
||||
}));
|
||||
vi.doMock("../../channels/plugins/index.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../channels/plugins/index.js")>(
|
||||
"../../channels/plugins/index.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
|
||||
};
|
||||
});
|
||||
({ handleInlineActions } = await import("./get-reply-inline-actions.js"));
|
||||
}
|
||||
vi.mock("./commands.runtime.js", () => ({
|
||||
handleCommands: (...args: unknown[]) => handleCommandsMock(...args),
|
||||
buildStatusReply: (...args: unknown[]) => buildStatusReplyMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/openclaw-tools.runtime.js", () => ({
|
||||
createOpenClawTools: (...args: unknown[]) => createOpenClawToolsMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../channels/plugins/index.js", () => ({
|
||||
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
|
||||
getLoadedChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
|
||||
listChannelPlugins: () => [],
|
||||
normalizeChannelId: (value?: string) => value?.trim().toLowerCase() || null,
|
||||
}));
|
||||
|
||||
const createTypingController = (): TypingController => ({
|
||||
onReplyStart: async () => {},
|
||||
@@ -119,7 +117,7 @@ async function expectInlineActionSkipped(params: {
|
||||
}
|
||||
|
||||
describe("handleInlineActions", () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
handleCommandsMock.mockReset();
|
||||
handleCommandsMock.mockResolvedValue({ shouldContinue: true, reply: undefined });
|
||||
getChannelPluginMock.mockReset();
|
||||
@@ -134,7 +132,6 @@ describe("handleInlineActions", () => {
|
||||
? { mentions: { stripPatterns: () => ["<@!?\\d+>"] } }
|
||||
: undefined,
|
||||
);
|
||||
await loadFreshInlineActionsModuleForTest();
|
||||
});
|
||||
|
||||
it("skips whatsapp replies when config is empty and From !== To", async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import {
|
||||
@@ -18,13 +18,10 @@ const mocks = vi.hoisted(() => ({
|
||||
resolveReplyDirectives: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/workspace.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../agents/workspace.js")>();
|
||||
return {
|
||||
...actual,
|
||||
ensureAgentWorkspace: (...args: unknown[]) => mocks.ensureAgentWorkspace(...args),
|
||||
};
|
||||
});
|
||||
vi.mock("../../agents/workspace.js", () => ({
|
||||
DEFAULT_AGENT_WORKSPACE_DIR: "/tmp/openclaw-workspace",
|
||||
ensureAgentWorkspace: (...args: unknown[]) => mocks.ensureAgentWorkspace(...args),
|
||||
}));
|
||||
vi.mock("./directive-handling.defaults.js", () => ({
|
||||
resolveDefaultModel: vi.fn(() => ({
|
||||
defaultProvider: "openai",
|
||||
@@ -38,13 +35,9 @@ vi.mock("./get-reply-directives.js", () => ({
|
||||
vi.mock("./get-reply-inline-actions.js", () => ({
|
||||
handleInlineActions: vi.fn(async () => ({ kind: "reply", reply: { text: "ok" } })),
|
||||
}));
|
||||
vi.mock("./session.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./session.js")>();
|
||||
return {
|
||||
...actual,
|
||||
initSessionState: (...args: unknown[]) => mocks.initSessionState(...args),
|
||||
};
|
||||
});
|
||||
vi.mock("./session.js", () => ({
|
||||
initSessionState: (...args: unknown[]) => mocks.initSessionState(...args),
|
||||
}));
|
||||
|
||||
let getReplyFromConfig: typeof import("./get-reply.js").getReplyFromConfig;
|
||||
let loadConfigMock: typeof import("../../config/config.js").loadConfig;
|
||||
@@ -74,8 +67,11 @@ function buildCtx(overrides: Partial<MsgContext> = {}): MsgContext {
|
||||
}
|
||||
|
||||
describe("getReplyFromConfig fast test bootstrap", () => {
|
||||
beforeEach(async () => {
|
||||
beforeAll(async () => {
|
||||
await loadGetReplyRuntimeForTest();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
|
||||
mocks.ensureAgentWorkspace.mockReset();
|
||||
mocks.initSessionState.mockReset();
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "../../config/sessions/reset.js";
|
||||
import { resolveAndPersistSessionFile } from "../../config/sessions/session-file.js";
|
||||
import { resolveSessionKey } from "../../config/sessions/session-key.js";
|
||||
import { resolveMaintenanceConfigFromInput } from "../../config/sessions/store-maintenance.js";
|
||||
import { loadSessionStore, updateSessionStore } from "../../config/sessions/store.js";
|
||||
import {
|
||||
DEFAULT_RESET_TRIGGERS,
|
||||
@@ -241,6 +242,7 @@ export async function initSessionState(params: {
|
||||
? { ...ctx, SessionKey: targetSessionKey }
|
||||
: ctx;
|
||||
const sessionCfg = cfg.session;
|
||||
const maintenanceConfig = resolveMaintenanceConfigFromInput(sessionCfg?.maintenance);
|
||||
const mainKey = normalizeMainKey(sessionCfg?.mainKey);
|
||||
const agentId = resolveSessionAgentId({
|
||||
sessionKey: sessionCtxForState.SessionKey,
|
||||
@@ -631,6 +633,7 @@ export async function initSessionState(params: {
|
||||
sessionsDir: path.dirname(storePath),
|
||||
fallbackSessionFile,
|
||||
activeSessionKey: sessionKey,
|
||||
maintenanceConfig,
|
||||
});
|
||||
sessionEntry = resolvedSessionFile.sessionEntry;
|
||||
if (isNewSession) {
|
||||
@@ -661,6 +664,7 @@ export async function initSessionState(params: {
|
||||
},
|
||||
{
|
||||
activeSessionKey: sessionKey,
|
||||
maintenanceConfig,
|
||||
onWarn: (warning) =>
|
||||
deliverSessionMaintenanceWarning({
|
||||
cfg,
|
||||
|
||||
Reference in New Issue
Block a user