diff --git a/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts b/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts index c4ed1da0293..be6bd5b1c20 100644 --- a/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts +++ b/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts @@ -44,12 +44,12 @@ function createMergeConfigProvider() { return { baseUrl: "https://config.example/v1", apiKey: "CONFIG_KEY", - api: "openai-responses", + api: "openai-responses" as const, models: [ { id: "config-model", name: "Config model", - input: ["text"], + input: ["text"] as Array<"text" | "image">, reasoning: false, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 8192, diff --git a/src/channels/typing.test.ts b/src/channels/typing.test.ts index 07db9a95135..3c398b2b01c 100644 --- a/src/channels/typing.test.ts +++ b/src/channels/typing.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it, vi } from "vitest"; -import type { MockInstance } from "vitest"; import { createTypingCallbacks } from "./typing.js"; +type TypingCallbackOverrides = Partial[0]>; +type TypingHarnessStart = ReturnType Promise>>; +type TypingHarnessError = ReturnType void>>; + const flushMicrotasks = async () => { await Promise.resolve(); await Promise.resolve(); @@ -16,17 +19,25 @@ async function withFakeTimers(run: () => Promise) { } } -function createTypingHarness(overrides: Partial[0]> = {}) { - const start = (overrides.start ?? vi.fn().mockResolvedValue(undefined)) as MockInstance< - [], - Promise - >; - const stop = (overrides.stop ?? vi.fn().mockResolvedValue(undefined)) as MockInstance< - [], - Promise - >; - const onStartError = (overrides.onStartError ?? vi.fn()) as MockInstance<[unknown], void>; - const onStopError = (overrides.onStopError ?? vi.fn()) as MockInstance<[unknown], void>; +function createTypingHarness(overrides: TypingCallbackOverrides = {}) { + const start: TypingHarnessStart = vi.fn<() => Promise>(async () => {}); + const stop: TypingHarnessStart = vi.fn<() => Promise>(async () => {}); + const onStartError: TypingHarnessError = vi.fn<(err: unknown) => void>(); + const onStopError: TypingHarnessError = vi.fn<(err: unknown) => void>(); + + if (overrides.start) { + start.mockImplementation(overrides.start); + } + if (overrides.stop) { + stop.mockImplementation(overrides.stop); + } + if (overrides.onStartError) { + onStartError.mockImplementation(overrides.onStartError); + } + if (overrides.onStopError) { + onStopError.mockImplementation(overrides.onStopError); + } + const callbacks = createTypingCallbacks({ start, stop, diff --git a/src/commands/channel-test-helpers.ts b/src/commands/channel-test-helpers.ts index c93a5feb526..2814f6bb5bd 100644 --- a/src/commands/channel-test-helpers.ts +++ b/src/commands/channel-test-helpers.ts @@ -10,6 +10,20 @@ import type { ChannelChoice } from "./onboard-types.js"; import { getChannelOnboardingAdapter } from "./onboarding/registry.js"; import type { ChannelOnboardingAdapter } from "./onboarding/types.js"; +type ChannelOnboardingAdapterPatch = Partial< + Pick< + ChannelOnboardingAdapter, + "configure" | "configureInteractive" | "configureWhenConfigured" | "getStatus" + > +>; + +type PatchedOnboardingAdapterFields = { + configure?: ChannelOnboardingAdapter["configure"]; + configureInteractive?: ChannelOnboardingAdapter["configureInteractive"]; + configureWhenConfigured?: ChannelOnboardingAdapter["configureWhenConfigured"]; + getStatus?: ChannelOnboardingAdapter["getStatus"]; +}; + export function setDefaultChannelPluginRegistryForTests(): void { const channels = [ { pluginId: "discord", plugin: discordPlugin, source: "test" }, @@ -24,21 +38,44 @@ export function setDefaultChannelPluginRegistryForTests(): void { export function patchChannelOnboardingAdapter( channel: ChannelChoice, - patch: Partial, + patch: ChannelOnboardingAdapterPatch, ): () => void { const adapter = getChannelOnboardingAdapter(channel); if (!adapter) { throw new Error(`missing onboarding adapter for ${channel}`); } - const keys = Object.keys(patch) as Array; - const previous: Partial = {}; - for (const key of keys) { - previous[key] = adapter[key]; - adapter[key] = patch[key] as ChannelOnboardingAdapter[typeof key]; + + const previous: PatchedOnboardingAdapterFields = {}; + + if (Object.prototype.hasOwnProperty.call(patch, "getStatus")) { + previous.getStatus = adapter.getStatus; + adapter.getStatus = patch.getStatus ?? adapter.getStatus; } + if (Object.prototype.hasOwnProperty.call(patch, "configure")) { + previous.configure = adapter.configure; + adapter.configure = patch.configure ?? adapter.configure; + } + if (Object.prototype.hasOwnProperty.call(patch, "configureInteractive")) { + previous.configureInteractive = adapter.configureInteractive; + adapter.configureInteractive = patch.configureInteractive; + } + if (Object.prototype.hasOwnProperty.call(patch, "configureWhenConfigured")) { + previous.configureWhenConfigured = adapter.configureWhenConfigured; + adapter.configureWhenConfigured = patch.configureWhenConfigured; + } + return () => { - for (const key of keys) { - adapter[key] = previous[key] as ChannelOnboardingAdapter[typeof key]; + if (Object.prototype.hasOwnProperty.call(patch, "getStatus")) { + adapter.getStatus = previous.getStatus!; + } + if (Object.prototype.hasOwnProperty.call(patch, "configure")) { + adapter.configure = previous.configure!; + } + if (Object.prototype.hasOwnProperty.call(patch, "configureInteractive")) { + adapter.configureInteractive = previous.configureInteractive; + } + if (Object.prototype.hasOwnProperty.call(patch, "configureWhenConfigured")) { + adapter.configureWhenConfigured = previous.configureWhenConfigured; } }; } diff --git a/src/commands/onboard-custom.test.ts b/src/commands/onboard-custom.test.ts index bcfcebe42b7..374f188dc62 100644 --- a/src/commands/onboard-custom.test.ts +++ b/src/commands/onboard-custom.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { CONTEXT_WINDOW_HARD_MIN_TOKENS } from "../agents/context-window-guard.js"; +import type { OpenClawConfig } from "../config/config.js"; import { defaultRuntime } from "../runtime.js"; import { applyCustomApiConfig, diff --git a/src/node-host/invoke-system-run.test.ts b/src/node-host/invoke-system-run.test.ts index a199c023dd4..03e1d0c10f4 100644 --- a/src/node-host/invoke-system-run.test.ts +++ b/src/node-host/invoke-system-run.test.ts @@ -1,12 +1,18 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, type Mock, vi } from "vitest"; import { saveExecApprovals } from "../infra/exec-approvals.js"; import type { ExecHostResponse } from "../infra/exec-host.js"; import { handleSystemRunInvoke, formatSystemRunAllowlistMissMessage } from "./invoke-system-run.js"; import type { HandleSystemRunInvokeOptions } from "./invoke-system-run.js"; +type MockedRunCommand = Mock; +type MockedRunViaMacAppExecHost = Mock; +type MockedSendInvokeResult = Mock; +type MockedSendExecFinishedEvent = Mock; +type MockedSendNodeEvent = Mock; + describe("formatSystemRunAllowlistMissMessage", () => { it("returns legacy allowlist miss message by default", () => { expect(formatSystemRunAllowlistMissMessage()).toBe("SYSTEM_RUN_DENIED: allowlist miss"); @@ -35,7 +41,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { } function expectInvokeOk( - sendInvokeResult: ReturnType, + sendInvokeResult: MockedSendInvokeResult, params?: { payloadContains?: string }, ) { expect(sendInvokeResult).toHaveBeenCalledWith( @@ -49,7 +55,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { } function expectInvokeErrorMessage( - sendInvokeResult: ReturnType, + sendInvokeResult: MockedSendInvokeResult, params: { message: string; exact?: boolean }, ) { expect(sendInvokeResult).toHaveBeenCalledWith( @@ -63,8 +69,8 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { } function expectApprovalRequiredDenied(params: { - sendNodeEvent: ReturnType; - sendInvokeResult: ReturnType; + sendNodeEvent: MockedSendNodeEvent; + sendInvokeResult: MockedSendInvokeResult; }) { expect(params.sendNodeEvent).toHaveBeenCalledWith( expect.anything(), @@ -126,7 +132,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { } function expectCommandPinnedToCanonicalPath(params: { - runCommand: ReturnType; + runCommand: MockedRunCommand; expected: string; commandTail: string[]; cwd?: string; @@ -153,24 +159,44 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { sendExecFinishedEvent?: HandleSystemRunInvokeOptions["sendExecFinishedEvent"]; sendNodeEvent?: HandleSystemRunInvokeOptions["sendNodeEvent"]; skillBinsCurrent?: () => Promise>; - }) { - const runCommand = - params.runCommand ?? - (vi.fn(async (_command: string[], _cwd?: string, _env?: Record) => - createLocalRunResult(), - ) as HandleSystemRunInvokeOptions["runCommand"]); - const runViaMacAppExecHost = - params.runViaMacAppExecHost ?? - (vi.fn(async () => params.runViaResponse ?? null) as HandleSystemRunInvokeOptions["runViaMacAppExecHost"]); - const sendInvokeResult = - params.sendInvokeResult ?? - (vi.fn(async () => {}) as HandleSystemRunInvokeOptions["sendInvokeResult"]); - const sendExecFinishedEvent = - params.sendExecFinishedEvent ?? - (vi.fn(async () => {}) as HandleSystemRunInvokeOptions["sendExecFinishedEvent"]); - const sendNodeEvent = - params.sendNodeEvent ?? - (vi.fn(async () => {}) as HandleSystemRunInvokeOptions["sendNodeEvent"]); + }): Promise<{ + runCommand: MockedRunCommand; + runViaMacAppExecHost: MockedRunViaMacAppExecHost; + sendInvokeResult: MockedSendInvokeResult; + sendNodeEvent: MockedSendNodeEvent; + sendExecFinishedEvent: MockedSendExecFinishedEvent; + }> { + const runCommand: MockedRunCommand = vi.fn( + async () => createLocalRunResult(), + ); + const runViaMacAppExecHost: MockedRunViaMacAppExecHost = vi.fn< + HandleSystemRunInvokeOptions["runViaMacAppExecHost"] + >(async () => params.runViaResponse ?? null); + const sendInvokeResult: MockedSendInvokeResult = vi.fn< + HandleSystemRunInvokeOptions["sendInvokeResult"] + >(async () => {}); + const sendNodeEvent: MockedSendNodeEvent = vi.fn( + async () => {}, + ); + const sendExecFinishedEvent: MockedSendExecFinishedEvent = vi.fn< + HandleSystemRunInvokeOptions["sendExecFinishedEvent"] + >(async () => {}); + + if (params.runCommand !== undefined) { + runCommand.mockImplementation(params.runCommand); + } + if (params.runViaMacAppExecHost !== undefined) { + runViaMacAppExecHost.mockImplementation(params.runViaMacAppExecHost); + } + if (params.sendInvokeResult !== undefined) { + sendInvokeResult.mockImplementation(params.sendInvokeResult); + } + if (params.sendNodeEvent !== undefined) { + sendNodeEvent.mockImplementation(params.sendNodeEvent); + } + if (params.sendExecFinishedEvent !== undefined) { + sendExecFinishedEvent.mockImplementation(params.sendExecFinishedEvent); + } await handleSystemRunInvoke({ client: {} as never, @@ -198,7 +224,13 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { preferMacAppExecHost: params.preferMacAppExecHost, }); - return { runCommand, runViaMacAppExecHost, sendInvokeResult, sendExecFinishedEvent }; + return { + runCommand, + runViaMacAppExecHost, + sendInvokeResult, + sendNodeEvent, + sendExecFinishedEvent, + }; } it("uses local execution by default when mac app exec host preference is disabled", async () => { diff --git a/src/slack/monitor/events/pins.test.ts b/src/slack/monitor/events/pins.test.ts index bb51bed2a76..352b7d03a2b 100644 --- a/src/slack/monitor/events/pins.test.ts +++ b/src/slack/monitor/events/pins.test.ts @@ -76,16 +76,30 @@ async function runPinCase(input: PinCase = {}): Promise { describe("registerSlackPinEvents", () => { const cases: Array<{ name: string; args: PinCase; expectedCalls: number }> = [ - { name: "enqueues DM pin system events when dmPolicy is open", args: { overrides: { dmPolicy: "open" } }, expectedCalls: 1 }, - { name: "blocks DM pin system events when dmPolicy is disabled", args: { overrides: { dmPolicy: "disabled" } }, expectedCalls: 0 }, + { + name: "enqueues DM pin system events when dmPolicy is open", + args: { overrides: { dmPolicy: "open" } }, + expectedCalls: 1, + }, + { + name: "blocks DM pin system events when dmPolicy is disabled", + args: { overrides: { dmPolicy: "disabled" } }, + expectedCalls: 0, + }, { name: "blocks DM pin system events for unauthorized senders in allowlist mode", - args: { overrides: { dmPolicy: "allowlist", allowFrom: ["U2"] }, event: makePinEvent({ user: "U1" }) }, + args: { + overrides: { dmPolicy: "allowlist", allowFrom: ["U2"] }, + event: makePinEvent({ user: "U1" }), + }, expectedCalls: 0, }, { name: "allows DM pin system events for authorized senders in allowlist mode", - args: { overrides: { dmPolicy: "allowlist", allowFrom: ["U1"] }, event: makePinEvent({ user: "U1" }) }, + args: { + overrides: { dmPolicy: "allowlist", allowFrom: ["U1"] }, + event: makePinEvent({ user: "U1" }), + }, expectedCalls: 1, }, {