Fix type-test harness issues from session routing and mock typing

This commit is contained in:
Vincent Koc
2026-03-02 00:02:55 -08:00
parent 0b8142f706
commit e6cf0bce5e
6 changed files with 146 additions and 51 deletions

View File

@@ -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,

View File

@@ -1,7 +1,10 @@
import { describe, expect, it, vi } from "vitest";
import type { MockInstance } from "vitest";
import { createTypingCallbacks } from "./typing.js";
type TypingCallbackOverrides = Partial<Parameters<typeof createTypingCallbacks>[0]>;
type TypingHarnessStart = ReturnType<typeof vi.fn<() => Promise<void>>>;
type TypingHarnessError = ReturnType<typeof vi.fn<(err: unknown) => void>>;
const flushMicrotasks = async () => {
await Promise.resolve();
await Promise.resolve();
@@ -16,17 +19,25 @@ async function withFakeTimers(run: () => Promise<void>) {
}
}
function createTypingHarness(overrides: Partial<Parameters<typeof createTypingCallbacks>[0]> = {}) {
const start = (overrides.start ?? vi.fn().mockResolvedValue(undefined)) as MockInstance<
[],
Promise<void>
>;
const stop = (overrides.stop ?? vi.fn().mockResolvedValue(undefined)) as MockInstance<
[],
Promise<void>
>;
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<void>>(async () => {});
const stop: TypingHarnessStart = vi.fn<() => Promise<void>>(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,

View File

@@ -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<ChannelOnboardingAdapter>,
patch: ChannelOnboardingAdapterPatch,
): () => void {
const adapter = getChannelOnboardingAdapter(channel);
if (!adapter) {
throw new Error(`missing onboarding adapter for ${channel}`);
}
const keys = Object.keys(patch) as Array<keyof ChannelOnboardingAdapter>;
const previous: Partial<ChannelOnboardingAdapter> = {};
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;
}
};
}

View File

@@ -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,

View File

@@ -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<HandleSystemRunInvokeOptions["runCommand"]>;
type MockedRunViaMacAppExecHost = Mock<HandleSystemRunInvokeOptions["runViaMacAppExecHost"]>;
type MockedSendInvokeResult = Mock<HandleSystemRunInvokeOptions["sendInvokeResult"]>;
type MockedSendExecFinishedEvent = Mock<HandleSystemRunInvokeOptions["sendExecFinishedEvent"]>;
type MockedSendNodeEvent = Mock<HandleSystemRunInvokeOptions["sendNodeEvent"]>;
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<typeof vi.fn>,
sendInvokeResult: MockedSendInvokeResult,
params?: { payloadContains?: string },
) {
expect(sendInvokeResult).toHaveBeenCalledWith(
@@ -49,7 +55,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
}
function expectInvokeErrorMessage(
sendInvokeResult: ReturnType<typeof vi.fn>,
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<typeof vi.fn>;
sendInvokeResult: ReturnType<typeof vi.fn>;
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<typeof vi.fn>;
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<Array<{ name: string; resolvedPath: string }>>;
}) {
const runCommand =
params.runCommand ??
(vi.fn(async (_command: string[], _cwd?: string, _env?: Record<string, string>) =>
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<HandleSystemRunInvokeOptions["runCommand"]>(
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<HandleSystemRunInvokeOptions["sendNodeEvent"]>(
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 () => {

View File

@@ -76,16 +76,30 @@ async function runPinCase(input: PinCase = {}): Promise<void> {
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,
},
{