diff --git a/src/auto-reply/commands-registry.test.ts b/src/auto-reply/commands-registry.test.ts index 6fd72a9f9..a38bb5a10 100644 --- a/src/auto-reply/commands-registry.test.ts +++ b/src/auto-reply/commands-registry.test.ts @@ -255,8 +255,12 @@ describe("commands registry args", () => { }); it("resolves function-based choices with a default provider/model context", () => { - let seen: { provider: string; model: string; commandKey: string; argName: string } | null = - null; + let seen: { + provider?: string; + model?: string; + commandKey: string; + argName: string; + } | null = null; const command: ChatCommandDefinition = { key: "think", @@ -284,10 +288,16 @@ describe("commands registry args", () => { { label: "low", value: "low" }, { label: "high", value: "high" }, ]); - expect(seen?.commandKey).toBe("think"); - expect(seen?.argName).toBe("level"); - expect(seen?.provider).toBeTruthy(); - expect(seen?.model).toBeTruthy(); + const seenChoice = seen as { + provider?: string; + model?: string; + commandKey: string; + argName: string; + } | null; + expect(seenChoice?.commandKey).toBe("think"); + expect(seenChoice?.argName).toBe("level"); + expect(seenChoice?.provider).toBeTruthy(); + expect(seenChoice?.model).toBeTruthy(); }); it("does not show menus when args were provided as raw text only", () => { diff --git a/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.e2e.test.ts b/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.e2e.test.ts index 206fce686..9d0321bfb 100644 --- a/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.e2e.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.e2e.test.ts @@ -1,6 +1,7 @@ import "./reply.directive.directive-behavior.e2e-mocks.js"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; import { installDirectiveBehaviorE2EHooks, loadModelCatalog, @@ -18,7 +19,7 @@ function makeThinkConfig(home: string) { }, }, session: { store: path.join(home, "sessions.json") }, - } as const; + } as unknown as OpenClawConfig; } function makeWhatsAppConfig(home: string) { @@ -31,7 +32,7 @@ function makeWhatsAppConfig(home: string) { }, channels: { whatsapp: { allowFrom: ["*"] } }, session: { store: path.join(home, "sessions.json") }, - } as const; + } as unknown as OpenClawConfig; } async function runReplyToCurrentCase(home: string, text: string) { diff --git a/src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.e2e.test.ts b/src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.e2e.test.ts index 333675728..d73b1c151 100644 --- a/src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.e2e.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.e2e.test.ts @@ -1,6 +1,7 @@ import "./reply.directive.directive-behavior.e2e-mocks.js"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; import { assertModelSelection, installDirectiveBehaviorE2EHooks, @@ -9,6 +10,18 @@ import { } from "./reply.directive.directive-behavior.e2e-harness.js"; import { getReplyFromConfig } from "./reply.js"; +function makeModelDefinition(id: string, name: string) { + return { + id, + name, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200_000, + maxTokens: 8192, + }; +} + function makeMoonshotConfig(home: string, storePath: string) { return { agents: { @@ -28,12 +41,12 @@ function makeMoonshotConfig(home: string, storePath: string) { baseUrl: "https://api.moonshot.ai/v1", apiKey: "sk-test", api: "openai-completions", - models: [{ id: "kimi-k2-0905-preview", name: "Kimi K2" }], + models: [makeModelDefinition("kimi-k2-0905-preview", "Kimi K2")], }, }, }, session: { store: storePath }, - }; + } as unknown as OpenClawConfig; } describe("directive behavior", () => { @@ -129,18 +142,18 @@ describe("directive behavior", () => { baseUrl: "https://api.minimax.io/anthropic", apiKey: "sk-test", api: "anthropic-messages", - models: [{ id: "MiniMax-M2.1", name: "MiniMax M2.1" }], + models: [makeModelDefinition("MiniMax-M2.1", "MiniMax M2.1")], }, lmstudio: { baseUrl: "http://127.0.0.1:1234/v1", apiKey: "lmstudio", api: "openai-responses", - models: [{ id: "minimax-m2.1-gs32", name: "MiniMax M2.1 GS32" }], + models: [makeModelDefinition("minimax-m2.1-gs32", "MiniMax M2.1 GS32")], }, }, }, session: { store: storePath }, - }, + } as unknown as OpenClawConfig, ); assertModelSelection(storePath); @@ -173,17 +186,14 @@ describe("directive behavior", () => { apiKey: "sk-test", api: "anthropic-messages", models: [ - { id: "MiniMax-M2.1", name: "MiniMax M2.1" }, - { - id: "MiniMax-M2.1-lightning", - name: "MiniMax M2.1 Lightning", - }, + makeModelDefinition("MiniMax-M2.1", "MiniMax M2.1"), + makeModelDefinition("MiniMax-M2.1-lightning", "MiniMax M2.1 Lightning"), ], }, }, }, session: { store: storePath }, - }, + } as unknown as OpenClawConfig, ); assertModelSelection(storePath); diff --git a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts index cae6dadfd..c2514485a 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import { join } from "node:path"; import { beforeAll, describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; import { loadSessionStore } from "../config/sessions.js"; import { getAbortEmbeddedPiRunMock, @@ -23,10 +24,14 @@ describe("trigger handling", () => { it("targets the active session for native /stop", async () => { await withTempHome(async (home) => { const cfg = makeCfg(home); + const storePath = cfg.session?.store; + if (!storePath) { + throw new Error("missing session store path"); + } const targetSessionKey = "agent:main:telegram:group:123"; const targetSessionId = "session-target"; await fs.writeFile( - cfg.session!.store, + storePath, JSON.stringify( { [targetSessionKey]: { @@ -85,7 +90,7 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("⚙️ Agent was aborted."); expect(getAbortEmbeddedPiRunMock()).toHaveBeenCalledWith(targetSessionId); - const store = loadSessionStore(cfg.session!.store); + const store = loadSessionStore(storePath); expect(store[targetSessionKey]?.abortedLastRun).toBe(true); expect(getFollowupQueueDepth(targetSessionKey)).toBe(0); }); @@ -93,12 +98,16 @@ describe("trigger handling", () => { it("applies native /model to the target session", async () => { await withTempHome(async (home) => { const cfg = makeCfg(home); + const storePath = cfg.session?.store; + if (!storePath) { + throw new Error("missing session store path"); + } const slashSessionKey = "telegram:slash:111"; const targetSessionKey = MAIN_SESSION_KEY; // Seed the target session to ensure the native command mutates it. await fs.writeFile( - cfg.session!.store, + storePath, JSON.stringify( { [targetSessionKey]: { @@ -131,7 +140,7 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("Model set to openai/gpt-4.1-mini"); - const store = loadSessionStore(cfg.session!.store); + const store = loadSessionStore(storePath); expect(store[targetSessionKey]?.providerOverride).toBe("openai"); expect(store[targetSessionKey]?.modelOverride).toBe("gpt-4.1-mini"); expect(store[slashSessionKey]).toBeUndefined(); @@ -183,7 +192,7 @@ describe("trigger handling", () => { }, }, session: { store: join(home, "sessions.json") }, - }; + } as unknown as OpenClawConfig; const res = await getReplyFromConfig( { diff --git a/src/auto-reply/reply/abort.test.ts b/src/auto-reply/reply/abort.test.ts index b9e5993f2..d8f8a6d57 100644 --- a/src/auto-reply/reply/abort.test.ts +++ b/src/auto-reply/reply/abort.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; +import type { SubagentRunRecord } from "../../agents/subagent-registry.js"; import type { OpenClawConfig } from "../../config/config.js"; import { getAbortMemory, @@ -28,7 +29,9 @@ const commandQueueMocks = vi.hoisted(() => ({ vi.mock("../../process/command-queue.js", () => commandQueueMocks); const subagentRegistryMocks = vi.hoisted(() => ({ - listSubagentRunsForRequester: vi.fn(() => []), + listSubagentRunsForRequester: vi.fn<(requesterSessionKey: string) => SubagentRunRecord[]>( + () => [], + ), markSubagentRunTerminated: vi.fn(() => 1), })); diff --git a/src/auto-reply/reply/agent-runner.runreplyagent.test.ts b/src/auto-reply/reply/agent-runner.runreplyagent.test.ts index bf6560e5b..7ad7e165d 100644 --- a/src/auto-reply/reply/agent-runner.runreplyagent.test.ts +++ b/src/auto-reply/reply/agent-runner.runreplyagent.test.ts @@ -233,7 +233,7 @@ async function runReplyAgentWithBase(params: { baseRun: ReturnType; storePath: string; sessionKey: string; - sessionEntry: Record; + sessionEntry: SessionEntry; commandBody: string; typingMode?: "instant"; }): Promise { @@ -303,7 +303,7 @@ describe("runReplyAgent typing (heartbeat)", () => { persistStore: boolean; }) { const storePath = path.join(params.stateDir, "sessions", "sessions.json"); - const sessionEntry = { sessionId: params.sessionId, updatedAt: Date.now() }; + const sessionEntry: SessionEntry = { sessionId: params.sessionId, updatedAt: Date.now() }; const sessionStore = { main: sessionEntry }; await fs.mkdir(path.dirname(storePath), { recursive: true }); @@ -490,7 +490,7 @@ describe("runReplyAgent typing (heartbeat)", () => { it("announces auto-compaction in verbose mode and tracks count", async () => { await withTempStateDir(async (stateDir) => { const storePath = path.join(stateDir, "sessions", "sessions.json"); - const sessionEntry = { sessionId: "session", updatedAt: Date.now() }; + const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now() }; const sessionStore = { main: sessionEntry }; state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { @@ -549,6 +549,9 @@ describe("runReplyAgent typing (heartbeat)", () => { expect(payload).toMatchObject({ text: expect.stringContaining("Context limit exceeded during compaction"), }); + if (!payload) { + throw new Error("expected payload"); + } expect(payload.text?.toLowerCase()).toContain("reset"); expect(sessionStore.main.sessionId).not.toBe(sessionId); @@ -594,6 +597,9 @@ describe("runReplyAgent typing (heartbeat)", () => { expect(payload).toMatchObject({ text: expect.stringContaining("Context limit exceeded"), }); + if (!payload) { + throw new Error("expected payload"); + } expect(payload.text?.toLowerCase()).toContain("reset"); expect(sessionStore.main.sessionId).not.toBe(sessionId); @@ -638,6 +644,9 @@ describe("runReplyAgent typing (heartbeat)", () => { expect(payload).toMatchObject({ text: expect.stringContaining("Message ordering conflict"), }); + if (!payload) { + throw new Error("expected payload"); + } expect(payload.text?.toLowerCase()).toContain("reset"); expect(sessionStore.main.sessionId).not.toBe(sessionId); await expect(fs.access(transcriptPath)).rejects.toBeDefined(); diff --git a/src/auto-reply/reply/followup-runner.test.ts b/src/auto-reply/reply/followup-runner.test.ts index d0860cc20..4d2cf45d2 100644 --- a/src/auto-reply/reply/followup-runner.test.ts +++ b/src/auto-reply/reply/followup-runner.test.ts @@ -128,7 +128,8 @@ describe("createFollowupRunner compaction", () => { await runner(queued); expect(onBlockReply).toHaveBeenCalled(); - expect(onBlockReply.mock.calls[0][0].text).toContain("Auto-compaction complete"); + const firstCall = (onBlockReply.mock.calls as unknown as Array>)[0]; + expect(firstCall?.[0]?.text).toContain("Auto-compaction complete"); expect(sessionStore.main.compactionCount).toBe(1); }); diff --git a/src/auto-reply/reply/route-reply.test.ts b/src/auto-reply/reply/route-reply.test.ts index 541fb0aef..c4df75dd4 100644 --- a/src/auto-reply/reply/route-reply.test.ts +++ b/src/auto-reply/reply/route-reply.test.ts @@ -16,7 +16,7 @@ import { SILENT_REPLY_TOKEN } from "../tokens.js"; const mocks = vi.hoisted(() => ({ sendMessageDiscord: vi.fn(async () => ({ messageId: "m1", channelId: "c1" })), sendMessageIMessage: vi.fn(async () => ({ messageId: "ok" })), - sendMessageMSTeams: vi.fn(async () => ({ + sendMessageMSTeams: vi.fn(async (_params: unknown) => ({ messageId: "m1", conversationId: "c1", })), diff --git a/src/auto-reply/reply/session.test.ts b/src/auto-reply/reply/session.test.ts index b17f570d1..2791cba49 100644 --- a/src/auto-reply/reply/session.test.ts +++ b/src/auto-reply/reply/session.test.ts @@ -5,6 +5,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } import { buildModelAliasIndex } from "../../agents/model-selection.js"; import type { OpenClawConfig } from "../../config/config.js"; import { saveSessionStore } from "../../config/sessions.js"; +import type { SessionEntry } from "../../config/sessions.js"; import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.ts"; import { enqueueSystemEvent, resetSystemEventsForTest } from "../../infra/system-events.js"; import { applyResetModelOverride } from "./session-reset-model.js"; @@ -866,11 +867,11 @@ describe("applyResetModelOverride", () => { it("selects a model hint and strips it from the body", async () => { const cfg = {} as OpenClawConfig; const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: "openai" }); - const sessionEntry = { + const sessionEntry: SessionEntry = { sessionId: "s1", updatedAt: Date.now(), }; - const sessionStore = { "agent:main:dm:1": sessionEntry }; + const sessionStore: Record = { "agent:main:dm:1": sessionEntry }; const sessionCtx = { BodyStripped: "minimax summarize" }; const ctx = { ChatType: "direct" }; @@ -896,14 +897,14 @@ describe("applyResetModelOverride", () => { it("clears auth profile overrides when reset applies a model", async () => { const cfg = {} as OpenClawConfig; const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: "openai" }); - const sessionEntry = { + const sessionEntry: SessionEntry = { sessionId: "s1", updatedAt: Date.now(), authProfileOverride: "anthropic:default", authProfileOverrideSource: "user", authProfileOverrideCompactionCount: 2, }; - const sessionStore = { "agent:main:dm:1": sessionEntry }; + const sessionStore: Record = { "agent:main:dm:1": sessionEntry }; const sessionCtx = { BodyStripped: "minimax summarize" }; const ctx = { ChatType: "direct" }; @@ -929,11 +930,11 @@ describe("applyResetModelOverride", () => { it("skips when resetTriggered is false", async () => { const cfg = {} as OpenClawConfig; const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: "openai" }); - const sessionEntry = { + const sessionEntry: SessionEntry = { sessionId: "s1", updatedAt: Date.now(), }; - const sessionStore = { "agent:main:dm:1": sessionEntry }; + const sessionStore: Record = { "agent:main:dm:1": sessionEntry }; const sessionCtx = { BodyStripped: "minimax summarize" }; const ctx = { ChatType: "direct" }; diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index ad094713f..f8bf9da24 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -48,7 +48,7 @@ describe("buildStatusMessage", () => { }, }, }, - } as OpenClawConfig, + } as unknown as OpenClawConfig, agent: { model: "anthropic/pi:opus", contextTokens: 32_000, @@ -99,7 +99,7 @@ describe("buildStatusMessage", () => { { id: "discord", sandbox: { mode: "all" } }, ], }, - } as OpenClawConfig, + } as unknown as OpenClawConfig, agent: {}, sessionKey: "agent:discord:discord:channel:1456350065223270435", sessionScope: "per-sender", @@ -314,7 +314,7 @@ describe("buildStatusMessage", () => { }, }, }, - } as OpenClawConfig, + } as unknown as OpenClawConfig, agent: { model: "anthropic/claude-opus-4-5" }, sessionEntry: { sessionId: "c1", updatedAt: 0, inputTokens: 10 }, sessionKey: "agent:main:main", @@ -491,7 +491,7 @@ describe("buildCommandsMessage", () => { it("lists commands with aliases and hints", () => { const text = buildCommandsMessage({ commands: { config: false, debug: false }, - } as OpenClawConfig); + } as unknown as OpenClawConfig); expect(text).toContain("ℹ️ Slash commands"); expect(text).toContain("Status"); expect(text).toContain("/commands - List all slash commands."); @@ -506,7 +506,7 @@ describe("buildCommandsMessage", () => { const text = buildCommandsMessage( { commands: { config: false, debug: false }, - } as OpenClawConfig, + } as unknown as OpenClawConfig, [ { name: "demo_skill", @@ -523,7 +523,7 @@ describe("buildHelpMessage", () => { it("hides config/debug when disabled", () => { const text = buildHelpMessage({ commands: { config: false, debug: false }, - } as OpenClawConfig); + } as unknown as OpenClawConfig); expect(text).toContain("Skills"); expect(text).toContain("/skill [input]"); expect(text).not.toContain("/config"); @@ -536,7 +536,7 @@ describe("buildCommandsMessagePaginated", () => { const result = buildCommandsMessagePaginated( { commands: { config: false, debug: false }, - } as OpenClawConfig, + } as unknown as OpenClawConfig, undefined, { surface: "telegram", page: 1 }, ); @@ -552,7 +552,7 @@ describe("buildCommandsMessagePaginated", () => { const result = buildCommandsMessagePaginated( { commands: { config: false, debug: false }, - } as OpenClawConfig, + } as unknown as OpenClawConfig, undefined, { surface: "telegram", page: 99 }, ); diff --git a/src/process/child-process-bridge.test.ts b/src/process/child-process-bridge.test.ts index 9a8c2f507..8d90eeee7 100644 --- a/src/process/child-process-bridge.test.ts +++ b/src/process/child-process-bridge.test.ts @@ -86,7 +86,7 @@ describe("attachChildProcessBridge", () => { if (!addedSigterm) { throw new Error("expected SIGTERM listener"); } - addedSigterm(); + addedSigterm("SIGTERM"); await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error("timeout waiting for child exit")), 10_000); diff --git a/src/process/command-queue.test.ts b/src/process/command-queue.test.ts index 79b8389a8..aac751b6e 100644 --- a/src/process/command-queue.test.ts +++ b/src/process/command-queue.test.ts @@ -98,7 +98,7 @@ describe("command queue", () => { await Promise.all([first, second]); expect(waited).not.toBeNull(); - expect(waited as number).toBeGreaterThanOrEqual(5); + expect(waited as unknown as number).toBeGreaterThanOrEqual(5); expect(queuedAhead).toBe(0); }); diff --git a/src/signal/monitor/event-handler.inbound-contract.test.ts b/src/signal/monitor/event-handler.inbound-contract.test.ts index 554281c0e..51df113f6 100644 --- a/src/signal/monitor/event-handler.inbound-contract.test.ts +++ b/src/signal/monitor/event-handler.inbound-contract.test.ts @@ -45,9 +45,10 @@ describe("signal createSignalEventHandler inbound contract", () => { expect(capturedCtx).toBeTruthy(); expectInboundContextContract(capturedCtx!); + const contextWithBody = capturedCtx as unknown as { Body?: string }; // Sender should appear as prefix in group messages (no redundant [from:] suffix) - expect(String(capturedCtx?.Body ?? "")).toContain("Alice"); - expect(String(capturedCtx?.Body ?? "")).toMatch(/Alice.*:/); - expect(String(capturedCtx?.Body ?? "")).not.toContain("[from:"); + expect(String(contextWithBody.Body ?? "")).toContain("Alice"); + expect(String(contextWithBody.Body ?? "")).toMatch(/Alice.*:/); + expect(String(contextWithBody.Body ?? "")).not.toContain("[from:"); }); }); diff --git a/src/signal/monitor/event-handler.mention-gating.test.ts b/src/signal/monitor/event-handler.mention-gating.test.ts index 6a38799c8..6aacdcf07 100644 --- a/src/signal/monitor/event-handler.mention-gating.test.ts +++ b/src/signal/monitor/event-handler.mention-gating.test.ts @@ -4,13 +4,17 @@ import type { MsgContext } from "../../auto-reply/templating.js"; import type { OpenClawConfig } from "../../config/types.js"; import { createBaseSignalEventHandlerDeps } from "./event-handler.test-harness.js"; -type SignalMsgContext = MsgContext & { +type SignalMsgContext = Pick & { Body?: string; WasMentioned?: boolean; }; let capturedCtx: SignalMsgContext | undefined; +function getCapturedCtx() { + return capturedCtx as SignalMsgContext; +} + vi.mock("../../auto-reply/dispatch.js", async (importOriginal) => { const actual = await importOriginal(); return buildDispatchInboundCaptureMock(actual, (ctx) => { @@ -113,7 +117,7 @@ describe("signal mention gating", () => { await handler(makeGroupEvent({ message: "hey @bot what's up" })); expect(capturedCtx).toBeTruthy(); - expect(capturedCtx?.WasMentioned).toBe(true); + expect(getCapturedCtx()?.WasMentioned).toBe(true); }); it("sets WasMentioned=false for group messages without mention when requireMention is off", async () => { @@ -126,7 +130,7 @@ describe("signal mention gating", () => { await handler(makeGroupEvent({ message: "hello everyone" })); expect(capturedCtx).toBeTruthy(); - expect(capturedCtx?.WasMentioned).toBe(false); + expect(getCapturedCtx()?.WasMentioned).toBe(false); }); it("records pending history for skipped group messages", async () => { @@ -187,7 +191,7 @@ describe("signal mention gating", () => { ); expect(capturedCtx).toBeTruthy(); - const body = String(capturedCtx?.Body ?? ""); + const body = String(getCapturedCtx()?.Body ?? ""); expect(body).toContain("@123e4567 hi @+15550002222"); expect(body).not.toContain(placeholder); }); @@ -212,8 +216,8 @@ describe("signal mention gating", () => { ); expect(capturedCtx).toBeTruthy(); - expect(String(capturedCtx?.Body ?? "")).toContain("@123e4567"); - expect(capturedCtx?.WasMentioned).toBe(true); + expect(String(getCapturedCtx()?.Body ?? "")).toContain("@123e4567"); + expect(getCapturedCtx()?.WasMentioned).toBe(true); }); });