diff --git a/extensions/acpx/src/acpx-runtime.d.ts b/extensions/acpx/src/acpx-runtime.d.ts deleted file mode 100644 index aff0bdfd66c..00000000000 --- a/extensions/acpx/src/acpx-runtime.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -declare module "acpx/dist/runtime.js" { - import type { - AcpRuntimeCapabilities, - AcpRuntimeDoctorReport, - AcpRuntimeEvent, - AcpRuntimeHandle, - AcpRuntimeStatus, - } from "../../../src/acp/runtime/types.js"; - - export const ACPX_BACKEND_ID: string; - export type { AcpRuntimeDoctorReport, AcpRuntimeEvent, AcpRuntimeHandle, AcpRuntimeStatus }; - - export type AcpSessionRecord = { - name?: string; - [key: string]: unknown; - }; - - export type AcpSessionStore = { - load: (sessionId: string) => Promise; - save: (record: AcpSessionRecord) => Promise; - }; - - export type AcpAgentRegistry = { - resolve: (agentId: string) => string; - list: () => string[]; - }; - - export type AcpRuntimeOptions = { - cwd: string; - sessionStore: AcpSessionStore; - agentRegistry: AcpAgentRegistry; - permissionMode?: string; - [key: string]: unknown; - }; - - export class AcpxRuntime { - constructor(options: AcpRuntimeOptions, testOptions?: unknown); - isHealthy(): boolean; - probeAvailability(): Promise; - doctor(): Promise; - ensureSession(input: unknown): Promise; - runTurn(input: unknown): AsyncIterable; - getCapabilities(input?: { handle?: AcpRuntimeHandle }): AcpRuntimeCapabilities; - getStatus(input: unknown): Promise; - setMode(input: unknown): Promise; - setConfigOption(input: unknown): Promise; - cancel(input: unknown): Promise; - close(input: unknown): Promise; - } - - export function createAcpRuntime(...args: unknown[]): AcpxRuntime; - export function createAgentRegistry(...args: unknown[]): AcpAgentRegistry; - export function createFileSessionStore(...args: unknown[]): AcpSessionStore; - export function decodeAcpxRuntimeHandleState(...args: unknown[]): unknown; - export function encodeAcpxRuntimeHandleState(...args: unknown[]): unknown; -} diff --git a/src/cron/isolated-agent.direct-delivery-core-channels.test.ts b/src/cron/isolated-agent.direct-delivery-core-channels.test.ts index f46b089e0b9..8f216ddd568 100644 --- a/src/cron/isolated-agent.direct-delivery-core-channels.test.ts +++ b/src/cron/isolated-agent.direct-delivery-core-channels.test.ts @@ -135,7 +135,7 @@ const whatsappResolveTarget = createWhatsAppTestPlugin().outbound?.resolveTarget describe("runCronIsolatedAgentTurn core-channel direct delivery", () => { beforeEach(() => { - setupIsolatedAgentTurnMocks(); + setupIsolatedAgentTurnMocks({ fast: true }); setActivePluginRegistry( createTestRegistry([ { diff --git a/src/cron/isolated-agent.model-formatting.test.ts b/src/cron/isolated-agent.model-formatting.test.ts index 4546c688dda..43051c5b249 100644 --- a/src/cron/isolated-agent.model-formatting.test.ts +++ b/src/cron/isolated-agent.model-formatting.test.ts @@ -14,9 +14,20 @@ const { normalizeProviderIdMock: vi.fn((value: unknown) => typeof value === "string" && value.trim() ? value.trim().toLowerCase() : "", ), - normalizeModelSelectionMock: vi.fn((value: unknown) => - typeof value === "string" && value.trim() ? value.trim() : undefined, - ), + normalizeModelSelectionMock: vi.fn((value: unknown) => { + if (typeof value === "string" && value.trim()) { + return value.trim(); + } + if ( + value && + typeof value === "object" && + typeof (value as { primary?: unknown }).primary === "string" && + (value as { primary: string }).primary.trim() + ) { + return (value as { primary: string }).primary.trim(); + } + return undefined; + }), resolveAllowedModelRefMock: vi.fn(), resolveConfiguredModelRefMock: vi.fn(), resolveHooksGmailModelMock: vi.fn(), @@ -437,5 +448,77 @@ describe("cron model formatting and precedence edge cases", () => { { provider: "anthropic", model: "claude-sonnet-4-6" }, ); }); + + it("uses agents.defaults.subagents.model when set", async () => { + await expectSelectedModel( + { + cfg: { + agents: { + defaults: { + model: "anthropic/claude-sonnet-4-6", + subagents: { model: "ollama/llama3.2:3b" }, + }, + }, + }, + }, + { provider: "ollama", model: "llama3.2:3b" }, + ); + }); + + it("supports subagents.model with {primary} object format", async () => { + await expectSelectedModel( + { + cfg: { + agents: { + defaults: { + model: "anthropic/claude-sonnet-4-6", + subagents: { model: { primary: "google/gemini-2.5-flash" } }, + }, + }, + }, + }, + { provider: "google", model: "gemini-2.5-flash" }, + ); + }); + + it("job payload model override takes precedence over subagents.model", async () => { + await expectSelectedModel( + { + cfg: { + agents: { + defaults: { + model: "anthropic/claude-sonnet-4-6", + subagents: { model: "ollama/llama3.2:3b" }, + }, + }, + }, + payload: { + kind: "agentTurn", + message: DEFAULT_MESSAGE, + model: "openai/gpt-4o", + }, + }, + { provider: "openai", model: "gpt-4o" }, + ); + }); + + it("prefers the agent model over agents.defaults.subagents.model", async () => { + await expectSelectedModel( + { + cfg: { + agents: { + defaults: { + model: "anthropic/claude-sonnet-4-6", + subagents: { model: "ollama/llama3.2:3b" }, + }, + }, + }, + agentConfigOverride: { + model: { primary: "anthropic/claude-opus-4-6" }, + }, + }, + { provider: "anthropic", model: "claude-opus-4-6" }, + ); + }); }); }); diff --git a/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.ts b/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.ts index 63a518b461b..8c6a4b4fabe 100644 --- a/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.ts +++ b/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.ts @@ -276,7 +276,7 @@ async function assertExplicitTelegramTargetDelivery(params: { describe("runCronIsolatedAgentTurn", () => { beforeEach(() => { vi.spyOn(modelSelection, "resolveThinkingDefault").mockReturnValue("off"); - setupIsolatedAgentTurnMocks(); + setupIsolatedAgentTurnMocks({ fast: true }); }); it("delivers explicit targets with direct text", async () => { diff --git a/src/cron/isolated-agent.subagent-model.test.ts b/src/cron/isolated-agent.subagent-model.test.ts deleted file mode 100644 index c47da6ee6e6..00000000000 --- a/src/cron/isolated-agent.subagent-model.test.ts +++ /dev/null @@ -1,220 +0,0 @@ -import "./isolated-agent.mocks.js"; -import fs from "node:fs/promises"; -import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { withTempHome as withTempHomeHelper } from "../../test/helpers/temp-home.js"; -import { loadModelCatalog } from "../agents/model-catalog.js"; -import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; -import type { CliDeps } from "../cli/deps.js"; -import type { OpenClawConfig } from "../config/config.js"; -import { runCronIsolatedAgentTurn } from "./isolated-agent.js"; -import type { CronJob } from "./types.js"; - -async function withTempHome(fn: (home: string) => Promise): Promise { - return withTempHomeHelper(fn, { prefix: "openclaw-cron-submodel-" }); -} - -async function writeSessionStore(home: string) { - const dir = path.join(home, ".openclaw", "sessions"); - await fs.mkdir(dir, { recursive: true }); - const storePath = path.join(dir, "sessions.json"); - await fs.writeFile( - storePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "main-session", - updatedAt: Date.now(), - lastProvider: "webchat", - lastTo: "", - }, - }, - null, - 2, - ), - "utf-8", - ); - return storePath; -} - -function makeCfg( - home: string, - storePath: string, - overrides: Partial = {}, -): OpenClawConfig { - const base: OpenClawConfig = { - agents: { - defaults: { - model: "anthropic/claude-sonnet-4-6", - workspace: path.join(home, "openclaw"), - }, - }, - session: { store: storePath, mainKey: "main" }, - } as OpenClawConfig; - return { ...base, ...overrides }; -} - -function makeDeps(): CliDeps { - return { - sendMessageWhatsApp: vi.fn(), - sendMessageTelegram: vi.fn(), - sendMessageDiscord: vi.fn(), - sendMessageSlack: vi.fn(), - sendMessageSignal: vi.fn(), - sendMessageIMessage: vi.fn(), - }; -} - -function makeJob(): CronJob { - const now = Date.now(); - return { - id: "job-sub", - name: "subagent-model-job", - enabled: true, - createdAtMs: now, - updatedAtMs: now, - schedule: { kind: "every", everyMs: 60_000 }, - sessionTarget: "isolated", - wakeMode: "now", - payload: { kind: "agentTurn", message: "do work" }, - state: {}, - }; -} - -function mockEmbeddedAgent() { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ - payloads: [{ text: "ok" }], - meta: { - durationMs: 5, - agentMeta: { sessionId: "s", provider: "p", model: "m" }, - }, - }); -} - -async function runSubagentModelCase(params: { - home: string; - cfgOverrides?: Partial; - jobModelOverride?: string; - agentId?: string; -}) { - const storePath = await writeSessionStore(params.home); - mockEmbeddedAgent(); - const job = makeJob(); - if (params.jobModelOverride) { - job.payload = { kind: "agentTurn", message: "do work", model: params.jobModelOverride }; - } - if (params.agentId) { - job.agentId = params.agentId; - } - - await runCronIsolatedAgentTurn({ - cfg: makeCfg(params.home, storePath, params.cfgOverrides), - deps: makeDeps(), - job, - message: "do work", - sessionKey: "cron:job-sub", - lane: "cron", - }); - - return vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]; -} - -describe("runCronIsolatedAgentTurn: subagent model resolution (#11461)", () => { - beforeEach(() => { - vi.mocked(runEmbeddedPiAgent).mockReset(); - vi.mocked(loadModelCatalog).mockResolvedValue([]); - }); - - it.each([ - { - name: "uses agents.defaults.subagents.model when set", - cfgOverrides: { - agents: { - defaults: { - model: "anthropic/claude-sonnet-4-6", - subagents: { model: "ollama/llama3.2:3b" }, - }, - }, - } satisfies Partial, - expectedProvider: "ollama", - expectedModel: "llama3.2:3b", - }, - { - name: "falls back to main model when subagents.model is unset", - cfgOverrides: undefined, - expectedProvider: "anthropic", - expectedModel: "claude-sonnet-4-6", - }, - { - name: "supports subagents.model with {primary} object format", - cfgOverrides: { - agents: { - defaults: { - model: "anthropic/claude-sonnet-4-6", - subagents: { model: { primary: "google/gemini-2.5-flash" } }, - }, - }, - } satisfies Partial, - expectedProvider: "google", - expectedModel: "gemini-2.5-flash", - }, - ])("$name", async ({ cfgOverrides, expectedProvider, expectedModel }) => { - await withTempHome(async (home) => { - const resolvedCfg = - cfgOverrides === undefined - ? undefined - : ({ - agents: { - defaults: { - ...cfgOverrides.agents?.defaults, - workspace: path.join(home, "openclaw"), - }, - }, - } satisfies Partial); - const call = await runSubagentModelCase({ home, cfgOverrides: resolvedCfg }); - expect(call?.provider).toBe(expectedProvider); - expect(call?.model).toBe(expectedModel); - }); - }); - - it("explicit job model override takes precedence over subagents.model", async () => { - await withTempHome(async (home) => { - const call = await runSubagentModelCase({ - home, - cfgOverrides: { - agents: { - defaults: { - model: "anthropic/claude-sonnet-4-6", - workspace: path.join(home, "openclaw"), - subagents: { model: "ollama/llama3.2:3b" }, - }, - }, - }, - jobModelOverride: "openai/gpt-4o", - }); - expect(call?.provider).toBe("openai"); - expect(call?.model).toBe("gpt-4o"); - }); - }); - - it("prefers the agent model over agents.defaults.subagents.model", async () => { - await withTempHome(async (home) => { - const call = await runSubagentModelCase({ - home, - agentId: "research", - cfgOverrides: { - agents: { - defaults: { - model: "anthropic/claude-sonnet-4-6", - workspace: path.join(home, "openclaw"), - subagents: { model: "ollama/llama3.2:3b" }, - }, - list: [{ id: "research", model: { primary: "anthropic/claude-opus-4-6" } }], - }, - }, - }); - expect(call?.provider).toBe("anthropic"); - expect(call?.model).toBe("claude-opus-4-6"); - }); - }); -}); diff --git a/test/vitest-scoped-config.test.ts b/test/vitest-scoped-config.test.ts index a80006488d8..d6ce32ccb60 100644 --- a/test/vitest-scoped-config.test.ts +++ b/test/vitest-scoped-config.test.ts @@ -203,7 +203,6 @@ describe("scoped vitest configs", () => { defaultAutoReplyTopLevelConfig, defaultAutoReplyReplyConfig, defaultToolingConfig, - defaultUiConfig, ]) { expect(config.test?.pool).toBe("threads"); expect(config.test?.isolate).toBe(false); @@ -215,6 +214,10 @@ describe("scoped vitest configs", () => { expect(config.test?.isolate).toBe(false); expect(config.test?.runner).toBe("./test/non-isolated-runner.ts"); } + + expect(defaultUiConfig.test?.pool).toBe("threads"); + expect(defaultUiConfig.test?.isolate).toBe(true); + expect(defaultUiConfig.test?.runner).toBeUndefined(); }); it("keeps the process lane off the openclaw runtime setup", () => {