test: fix CI type regressions

This commit is contained in:
Peter Steinberger 2026-03-13 19:53:34 +00:00
parent d17490ff54
commit 60d308cff0
18 changed files with 138 additions and 57 deletions

View File

@ -161,6 +161,12 @@ export type ScheduledTaskInfo = {
lastRunResult?: string; lastRunResult?: string;
}; };
function hasListenerPid<T extends { pid?: number | null }>(
listener: T,
): listener is T & { pid: number } {
return typeof listener.pid === "number";
}
export function parseSchtasksQuery(output: string): ScheduledTaskInfo { export function parseSchtasksQuery(output: string): ScheduledTaskInfo {
const entries = parseKeyValueOutput(output, ":"); const entries = parseKeyValueOutput(output, ":");
const info: ScheduledTaskInfo = {}; const info: ScheduledTaskInfo = {};
@ -388,7 +394,7 @@ async function resolveScheduledTaskGatewayListenerPids(port: number): Promise<nu
new Set( new Set(
diagnostics.listeners diagnostics.listeners
.map((listener) => listener.pid) .map((listener) => listener.pid)
.filter((pid): pid is number => Number.isFinite(pid) && pid > 0), .filter((pid): pid is number => typeof pid === "number" && Number.isFinite(pid) && pid > 0),
), ),
); );
} }
@ -472,7 +478,7 @@ async function terminateBusyPortListeners(port: number): Promise<number[]> {
new Set( new Set(
diagnostics.listeners diagnostics.listeners
.map((listener) => listener.pid) .map((listener) => listener.pid)
.filter((pid): pid is number => Number.isFinite(pid) && pid > 0), .filter((pid): pid is number => typeof pid === "number" && Number.isFinite(pid) && pid > 0),
), ),
); );
for (const pid of pids) { for (const pid of pids) {
@ -496,7 +502,7 @@ async function resolveFallbackRuntime(env: GatewayServiceEnv): Promise<GatewaySe
detail: `Startup-folder login item installed; could not inspect port ${port}.`, detail: `Startup-folder login item installed; could not inspect port ${port}.`,
}; };
} }
const listener = diagnostics.listeners.find((item) => typeof item.pid === "number"); const listener = diagnostics.listeners.find(hasListenerPid);
return { return {
status: diagnostics.status === "busy" ? "running" : "stopped", status: diagnostics.status === "busy" ? "running" : "stopped",
...(listener?.pid ? { pid: listener.pid } : {}), ...(listener?.pid ? { pid: listener.pid } : {}),

View File

@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { DedupeEntry } from "../server-shared.js";
import { import {
__testing, __testing,
readTerminalSnapshotFromGatewayDedupe, readTerminalSnapshotFromGatewayDedupe,
@ -8,7 +9,7 @@ import {
describe("agent wait dedupe helper", () => { describe("agent wait dedupe helper", () => {
function setRunEntry(params: { function setRunEntry(params: {
dedupe: Map<unknown, unknown>; dedupe: Map<string, DedupeEntry>;
kind: "agent" | "chat"; kind: "agent" | "chat";
runId: string; runId: string;
ts?: number; ts?: number;

View File

@ -251,7 +251,7 @@ describe("resolveGatewayRuntimeConfig", () => {
}); });
describe("HTTP security headers", () => { describe("HTTP security headers", () => {
it.each([ const cases = [
{ {
name: "resolves strict transport security headers from config", name: "resolves strict transport security headers from config",
strictTransportSecurity: " max-age=31536000; includeSubDomains ", strictTransportSecurity: " max-age=31536000; includeSubDomains ",
@ -267,7 +267,13 @@ describe("resolveGatewayRuntimeConfig", () => {
strictTransportSecurity: " ", strictTransportSecurity: " ",
expected: undefined, expected: undefined,
}, },
])("$name", async ({ strictTransportSecurity, expected }) => { ] satisfies ReadonlyArray<{
name: string;
strictTransportSecurity: string | false;
expected: string | undefined;
}>;
it.each(cases)("$name", async ({ strictTransportSecurity, expected }) => {
const result = await resolveGatewayRuntimeConfig({ const result = await resolveGatewayRuntimeConfig({
cfg: { cfg: {
gateway: { gateway: {

View File

@ -40,6 +40,7 @@ type TalkConfigPayload = {
ui?: { seamColor?: string }; ui?: { seamColor?: string };
}; };
}; };
type TalkConfig = NonNullable<NonNullable<TalkConfigPayload["config"]>["talk"]>;
const TALK_CONFIG_DEVICE_PATH = path.join( const TALK_CONFIG_DEVICE_PATH = path.join(
os.tmpdir(), os.tmpdir(),
`openclaw-talk-config-device-${process.pid}.json`, `openclaw-talk-config-device-${process.pid}.json`,
@ -95,7 +96,7 @@ async function fetchTalkConfig(
} }
function expectElevenLabsTalkConfig( function expectElevenLabsTalkConfig(
talk: TalkConfigPayload["config"] extends { talk?: infer T } ? T : never, talk: TalkConfig | undefined,
expected: { expected: {
voiceId?: string; voiceId?: string;
apiKey?: string | SecretRef; apiKey?: string | SecretRef;

View File

@ -91,13 +91,13 @@ describe("agent-events sequencing", () => {
isControlUiVisible: true, isControlUiVisible: true,
}); });
registerAgentRunContext("run-ctx", { registerAgentRunContext("run-ctx", {
verboseLevel: "high", verboseLevel: "full",
isHeartbeat: true, isHeartbeat: true,
}); });
expect(getAgentRunContext("run-ctx")).toEqual({ expect(getAgentRunContext("run-ctx")).toEqual({
sessionKey: "session-main", sessionKey: "session-main",
verboseLevel: "high", verboseLevel: "full",
isHeartbeat: true, isHeartbeat: true,
isControlUiVisible: true, isControlUiVisible: true,
}); });

View File

@ -32,7 +32,10 @@ describe("error helpers", () => {
child.cause = root; child.cause = root;
expect( expect(
collectErrorGraphCandidates(root, (current) => [current.cause, ...(current.errors ?? [])]), collectErrorGraphCandidates(root, (current) => [
current.cause,
...((current as { errors?: unknown[] }).errors ?? []),
]),
).toEqual([root, child, leaf]); ).toEqual([root, child, leaf]);
expect(collectErrorGraphCandidates(null)).toEqual([]); expect(collectErrorGraphCandidates(null)).toEqual([]);
}); });

View File

@ -192,7 +192,7 @@ describe("format-datetime", () => {
formatToParts: () => { formatToParts: () => {
throw new Error("boom"); throw new Error("boom");
}, },
} as Intl.DateTimeFormat; } as unknown as Intl.DateTimeFormat;
} }
vi.spyOn(Intl, "DateTimeFormat").mockImplementation( vi.spyOn(Intl, "DateTimeFormat").mockImplementation(

View File

@ -4,6 +4,8 @@ import {
resolveTimedInstallModeOptions, resolveTimedInstallModeOptions,
} from "./install-mode-options.js"; } from "./install-mode-options.js";
type LoggerKey = "default" | "explicit";
describe("install mode option helpers", () => { describe("install mode option helpers", () => {
it.each([ it.each([
{ {
@ -21,11 +23,15 @@ describe("install mode option helpers", () => {
params: { mode: "update" as const, dryRun: false }, params: { mode: "update" as const, dryRun: false },
expected: { loggerKey: "default", mode: "update", dryRun: false }, expected: { loggerKey: "default", mode: "update", dryRun: false },
}, },
])("$name", ({ params, expected }) => { ] satisfies Array<{
name: string;
params: { loggerKey?: LoggerKey; mode?: "install" | "update"; dryRun?: boolean };
expected: { loggerKey: LoggerKey; mode: "install" | "update"; dryRun: boolean };
}>)("$name", ({ params, expected }) => {
const loggers = { const loggers = {
default: { warn: (_message: string) => {} }, default: { warn: (_message: string) => {} },
explicit: { warn: (_message: string) => {} }, explicit: { warn: (_message: string) => {} },
}; } satisfies Record<LoggerKey, { warn: (_message: string) => void }>;
expect( expect(
resolveInstallModeOptions( resolveInstallModeOptions(

View File

@ -462,7 +462,14 @@ describe("resolveSessionDeliveryTarget", () => {
expectedChannel: "none", expectedChannel: "none",
expectedReason: "dm-blocked", expectedReason: "dm-blocked",
}, },
])("$name", ({ name, entry, directPolicy, expectedChannel, expectedTo, expectedReason }) => { ] satisfies Array<{
name: string;
entry: NonNullable<Parameters<typeof resolveHeartbeatDeliveryTarget>[0]["entry"]>;
directPolicy?: "allow" | "block";
expectedChannel: string;
expectedTo?: string;
expectedReason?: string;
}>)("$name", ({ name, entry, directPolicy, expectedChannel, expectedTo, expectedReason }) => {
expectHeartbeatTarget({ expectHeartbeatTarget({
name, name,
entry, entry,

View File

@ -3,7 +3,7 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { NON_ENV_SECRETREF_MARKER } from "../agents/model-auth-markers.js"; import { NON_ENV_SECRETREF_MARKER } from "../agents/model-auth-markers.js";
import { resolveProviderAuths } from "./provider-usage.auth.js"; import { resolveProviderAuths, type ProviderAuth } from "./provider-usage.auth.js";
describe("resolveProviderAuths key normalization", () => { describe("resolveProviderAuths key normalization", () => {
let suiteRoot = ""; let suiteRoot = "";
@ -214,7 +214,12 @@ describe("resolveProviderAuths key normalization", () => {
}, },
expected: [{ provider: "minimax", token: "code-plan-key" }], expected: [{ provider: "minimax", token: "code-plan-key" }],
}, },
])("$name", async ({ providers, env, expected }) => { ] satisfies Array<{
name: string;
providers: readonly Parameters<typeof resolveProviderAuths>[0]["providers"][number][];
env: Record<string, string | undefined>;
expected: ProviderAuth[];
}>)("$name", async ({ providers, env, expected }) => {
await expectResolvedAuthsFromSuiteHome({ providers: [...providers], env, expected }); await expectResolvedAuthsFromSuiteHome({ providers: [...providers], env, expected });
}); });

View File

@ -32,10 +32,11 @@ describe("provider usage fetch shared helpers", () => {
it("forwards request init and clears the timeout on success", async () => { it("forwards request init and clears the timeout on success", async () => {
vi.useFakeTimers(); vi.useFakeTimers();
const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout"); const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");
const fetchFn = vi.fn( const fetchFnMock = vi.fn(
async (_url: string, init?: RequestInit) => async (_input: URL | RequestInfo, init?: RequestInit) =>
new Response(JSON.stringify({ aborted: init?.signal?.aborted ?? false }), { status: 200 }), new Response(JSON.stringify({ aborted: init?.signal?.aborted ?? false }), { status: 200 }),
); );
const fetchFn = fetchFnMock as typeof fetch;
const response = await fetchJson( const response = await fetchJson(
"https://example.com/usage", "https://example.com/usage",
@ -47,7 +48,7 @@ describe("provider usage fetch shared helpers", () => {
fetchFn, fetchFn,
); );
expect(fetchFn).toHaveBeenCalledWith( expect(fetchFnMock).toHaveBeenCalledWith(
"https://example.com/usage", "https://example.com/usage",
expect.objectContaining({ expect.objectContaining({
method: "POST", method: "POST",
@ -62,14 +63,15 @@ describe("provider usage fetch shared helpers", () => {
it("aborts timed out requests and clears the timer on rejection", async () => { it("aborts timed out requests and clears the timer on rejection", async () => {
vi.useFakeTimers(); vi.useFakeTimers();
const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout"); const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");
const fetchFn = vi.fn( const fetchFnMock = vi.fn(
(_url: string, init?: RequestInit) => (_input: URL | RequestInfo, init?: RequestInit) =>
new Promise<Response>((_, reject) => { new Promise<Response>((_, reject) => {
init?.signal?.addEventListener("abort", () => reject(new Error("aborted by timeout")), { init?.signal?.addEventListener("abort", () => reject(new Error("aborted by timeout")), {
once: true, once: true,
}); });
}), }),
); );
const fetchFn = fetchFnMock as typeof fetch;
const request = fetchJson("https://example.com/usage", {}, 50, fetchFn); const request = fetchJson("https://example.com/usage", {}, 50, fetchFn);
const rejection = expect(request).rejects.toThrow("aborted by timeout"); const rejection = expect(request).rejects.toThrow("aborted by timeout");

View File

@ -5,6 +5,11 @@ import path from "node:path";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { openVerifiedFileSync } from "./safe-open-sync.js"; import { openVerifiedFileSync } from "./safe-open-sync.js";
type SafeOpenSyncFs = NonNullable<Parameters<typeof openVerifiedFileSync>[0]["ioFs"]>;
type SafeOpenSyncLstatSync = SafeOpenSyncFs["lstatSync"];
type SafeOpenSyncRealpathSync = SafeOpenSyncFs["realpathSync"];
type SafeOpenSyncFstatSync = SafeOpenSyncFs["fstatSync"];
async function withTempDir<T>(prefix: string, run: (dir: string) => Promise<T>): Promise<T> { async function withTempDir<T>(prefix: string, run: (dir: string) => Promise<T>): Promise<T> {
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), prefix)); const dir = await fsp.mkdtemp(path.join(os.tmpdir(), prefix));
try { try {
@ -33,6 +38,20 @@ function mockStat(params: {
} as unknown as fs.Stats; } as unknown as fs.Stats;
} }
function mockRealpathSync(result: string): SafeOpenSyncRealpathSync {
const resolvePath = ((_: fs.PathLike) => result) as SafeOpenSyncRealpathSync;
resolvePath.native = ((_: fs.PathLike) => result) as typeof resolvePath.native;
return resolvePath;
}
function mockLstatSync(read: (filePath: fs.PathLike) => fs.Stats): SafeOpenSyncLstatSync {
return ((filePath: fs.PathLike) => read(filePath)) as unknown as SafeOpenSyncLstatSync;
}
function mockFstatSync(stat: fs.Stats): SafeOpenSyncFstatSync {
return ((_: number) => stat) as unknown as SafeOpenSyncFstatSync;
}
describe("openVerifiedFileSync", () => { describe("openVerifiedFileSync", () => {
it("returns a path error for missing files", async () => { it("returns a path error for missing files", async () => {
await withTempDir("openclaw-safe-open-", async (root) => { await withTempDir("openclaw-safe-open-", async (root) => {
@ -115,15 +134,16 @@ describe("openVerifiedFileSync", () => {
closed.push(fd); closed.push(fd);
}; };
const closed: number[] = []; const closed: number[] = [];
const ioFs = { const ioFs: SafeOpenSyncFs = {
constants: fs.constants, constants: fs.constants,
lstatSync: (filePath: string) => lstatSync: mockLstatSync((filePath) =>
filePath === "/real/file.txt" String(filePath) === "/real/file.txt"
? mockStat({ isFile: true, size: 1, dev: 1, ino: 1 }) ? mockStat({ isFile: true, size: 1, dev: 1, ino: 1 })
: mockStat({ isFile: false }), : mockStat({ isFile: false }),
realpathSync: () => "/real/file.txt", ),
realpathSync: mockRealpathSync("/real/file.txt"),
openSync: () => 42, openSync: () => 42,
fstatSync: () => mockStat({ isFile: true, size: 1, dev: 2, ino: 1 }), fstatSync: mockFstatSync(mockStat({ isFile: true, size: 1, dev: 2, ino: 1 })),
closeSync, closeSync,
}; };
@ -139,16 +159,16 @@ describe("openVerifiedFileSync", () => {
}); });
it("reports non-path filesystem failures as io errors", () => { it("reports non-path filesystem failures as io errors", () => {
const ioFs = { const ioFs: SafeOpenSyncFs = {
constants: fs.constants, constants: fs.constants,
lstatSync: () => { lstatSync: () => {
const err = new Error("permission denied") as NodeJS.ErrnoException; const err = new Error("permission denied") as NodeJS.ErrnoException;
err.code = "EACCES"; err.code = "EACCES";
throw err; throw err;
}, },
realpathSync: () => "/real/file.txt", realpathSync: mockRealpathSync("/real/file.txt"),
openSync: () => 42, openSync: () => 42,
fstatSync: () => mockStat({ isFile: true }), fstatSync: mockFstatSync(mockStat({ isFile: true })),
closeSync: () => {}, closeSync: () => {},
}; };

View File

@ -7,6 +7,8 @@ import {
normalizeUpdateChannel, normalizeUpdateChannel,
resolveEffectiveUpdateChannel, resolveEffectiveUpdateChannel,
resolveUpdateChannelDisplay, resolveUpdateChannelDisplay,
type UpdateChannel,
type UpdateChannelSource,
} from "./update-channels.js"; } from "./update-channels.js";
describe("update-channels tag detection", () => { describe("update-channels tag detection", () => {
@ -32,9 +34,12 @@ describe("normalizeUpdateChannel", () => {
{ value: " nightly ", expected: null }, { value: " nightly ", expected: null },
{ value: null, expected: null }, { value: null, expected: null },
{ value: undefined, expected: null }, { value: undefined, expected: null },
])("normalizes %j", ({ value, expected }) => { ] satisfies Array<{ value: string | null | undefined; expected: UpdateChannel | null }>)(
expect(normalizeUpdateChannel(value)).toBe(expected); "normalizes %j",
}); ({ value, expected }) => {
expect(normalizeUpdateChannel(value)).toBe(expected);
},
);
}); });
describe("channelToNpmTag", () => { describe("channelToNpmTag", () => {
@ -42,9 +47,12 @@ describe("channelToNpmTag", () => {
{ channel: "stable", expected: "latest" }, { channel: "stable", expected: "latest" },
{ channel: "beta", expected: "beta" }, { channel: "beta", expected: "beta" },
{ channel: "dev", expected: "dev" }, { channel: "dev", expected: "dev" },
])("maps $channel to $expected", ({ channel, expected }) => { ] satisfies Array<{ channel: UpdateChannel; expected: string }>)(
expect(channelToNpmTag(channel)).toBe(expected); "maps $channel to $expected",
}); ({ channel, expected }) => {
expect(channelToNpmTag(channel)).toBe(expected);
},
);
}); });
describe("resolveEffectiveUpdateChannel", () => { describe("resolveEffectiveUpdateChannel", () => {
@ -100,7 +108,11 @@ describe("resolveEffectiveUpdateChannel", () => {
params: { installKind: "unknown" as const }, params: { installKind: "unknown" as const },
expected: { channel: "stable", source: "default" }, expected: { channel: "stable", source: "default" },
}, },
])("$name", ({ params, expected }) => { ] satisfies Array<{
name: string;
params: Parameters<typeof resolveEffectiveUpdateChannel>[0];
expected: { channel: UpdateChannel; source: UpdateChannelSource };
}>)("$name", ({ params, expected }) => {
expect(resolveEffectiveUpdateChannel(params)).toEqual(expected); expect(resolveEffectiveUpdateChannel(params)).toEqual(expected);
}); });
}); });
@ -145,7 +157,11 @@ describe("formatUpdateChannelLabel", () => {
params: { channel: "stable", source: "default" as const }, params: { channel: "stable", source: "default" as const },
expected: "stable (default)", expected: "stable (default)",
}, },
])("$name", ({ params, expected }) => { ] satisfies Array<{
name: string;
params: Parameters<typeof formatUpdateChannelLabel>[0];
expected: string;
}>)("$name", ({ params, expected }) => {
expect(formatUpdateChannelLabel(params)).toBe(expected); expect(formatUpdateChannelLabel(params)).toBe(expected);
}); });
}); });

View File

@ -7,19 +7,20 @@ import {
normalizeWideAreaDomain, normalizeWideAreaDomain,
renderWideAreaGatewayZoneText, renderWideAreaGatewayZoneText,
resolveWideAreaDiscoveryDomain, resolveWideAreaDiscoveryDomain,
type WideAreaGatewayZoneOpts,
writeWideAreaGatewayZone, writeWideAreaGatewayZone,
} from "./widearea-dns.js"; } from "./widearea-dns.js";
const baseZoneOpts = { const baseZoneOpts: WideAreaGatewayZoneOpts = {
domain: "openclaw.internal.", domain: "openclaw.internal.",
gatewayPort: 18789, gatewayPort: 18789,
displayName: "Mac Studio (OpenClaw)", displayName: "Mac Studio (OpenClaw)",
tailnetIPv4: "100.123.224.76", tailnetIPv4: "100.123.224.76",
hostLabel: "studio-london", hostLabel: "studio-london",
instanceLabel: "studio-london", instanceLabel: "studio-london",
} as const; };
function makeZoneOpts(overrides: Partial<typeof baseZoneOpts> = {}) { function makeZoneOpts(overrides: Partial<WideAreaGatewayZoneOpts> = {}): WideAreaGatewayZoneOpts {
return { ...baseZoneOpts, ...overrides }; return { ...baseZoneOpts, ...overrides };
} }

View File

@ -678,7 +678,7 @@ describe("handleLineWebhookEvents", () => {
it("skips group messages by default when requireMention is not configured", async () => { it("skips group messages by default when requireMention is not configured", async () => {
const processMessage = vi.fn(); const processMessage = vi.fn();
const event = createTestMessageEvent({ const event = createTestMessageEvent({
message: { id: "m-default-skip", type: "text", text: "hi there" }, message: { id: "m-default-skip", type: "text", text: "hi there", quoteToken: "q-default" },
source: { type: "group", groupId: "group-default", userId: "user-default" }, source: { type: "group", groupId: "group-default", userId: "user-default" },
webhookEventId: "evt-default-skip", webhookEventId: "evt-default-skip",
}); });
@ -702,7 +702,7 @@ describe("handleLineWebhookEvents", () => {
import("../auto-reply/reply/history.js").HistoryEntry[] import("../auto-reply/reply/history.js").HistoryEntry[]
>(); >();
const event = createTestMessageEvent({ const event = createTestMessageEvent({
message: { id: "m-hist-1", type: "text", text: "hello history" }, message: { id: "m-hist-1", type: "text", text: "hello history", quoteToken: "q-hist-1" },
timestamp: 1700000000000, timestamp: 1700000000000,
source: { type: "group", groupId: "group-hist-1", userId: "user-hist" }, source: { type: "group", groupId: "group-hist-1", userId: "user-hist" },
webhookEventId: "evt-hist-1", webhookEventId: "evt-hist-1",
@ -730,7 +730,7 @@ describe("handleLineWebhookEvents", () => {
it("skips group messages without mention when requireMention is set", async () => { it("skips group messages without mention when requireMention is set", async () => {
const processMessage = vi.fn(); const processMessage = vi.fn();
const event = createTestMessageEvent({ const event = createTestMessageEvent({
message: { id: "m-mention-1", type: "text", text: "hi there" }, message: { id: "m-mention-1", type: "text", text: "hi there", quoteToken: "q-mention-1" },
source: { type: "group", groupId: "group-mention", userId: "user-mention" }, source: { type: "group", groupId: "group-mention", userId: "user-mention" },
webhookEventId: "evt-mention-1", webhookEventId: "evt-mention-1",
}); });
@ -808,7 +808,7 @@ describe("handleLineWebhookEvents", () => {
it("does not apply requireMention gating to DM messages", async () => { it("does not apply requireMention gating to DM messages", async () => {
const processMessage = vi.fn(); const processMessage = vi.fn();
const event = createTestMessageEvent({ const event = createTestMessageEvent({
message: { id: "m-mention-dm", type: "text", text: "hi" }, message: { id: "m-mention-dm", type: "text", text: "hi", quoteToken: "q-mention-dm" },
source: { type: "user", userId: "user-dm" }, source: { type: "user", userId: "user-dm" },
webhookEventId: "evt-mention-dm", webhookEventId: "evt-mention-dm",
}); });
@ -830,7 +830,12 @@ describe("handleLineWebhookEvents", () => {
const processMessage = vi.fn(); const processMessage = vi.fn();
// Image message -- LINE only carries mention metadata on text messages. // Image message -- LINE only carries mention metadata on text messages.
const event = createTestMessageEvent({ const event = createTestMessageEvent({
message: { id: "m-mention-img", type: "image", contentProvider: { type: "line" } }, message: {
id: "m-mention-img",
type: "image",
contentProvider: { type: "line" },
quoteToken: "q-mention-img",
},
source: { type: "group", groupId: "group-1", userId: "user-img" }, source: { type: "group", groupId: "group-1", userId: "user-img" },
webhookEventId: "evt-mention-img", webhookEventId: "evt-mention-img",
}); });

View File

@ -1488,7 +1488,7 @@ describe("loadOpenClawPlugins", () => {
load: { paths: [plugin.file] }, load: { paths: [plugin.file] },
}, },
}, },
} as const; };
loadOpenClawPlugins(options); loadOpenClawPlugins(options);
loadOpenClawPlugins(options); loadOpenClawPlugins(options);

View File

@ -1,4 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import type { TelegramNetworkConfig } from "../config/types.telegram.js";
import { import {
resetTelegramNetworkConfigStateForTests, resetTelegramNetworkConfigStateForTests,
resolveTelegramAutoSelectFamilyDecision, resolveTelegramAutoSelectFamilyDecision,
@ -157,7 +158,9 @@ describe("resolveTelegramDnsResultOrderDecision", () => {
}, },
{ {
name: "normalizes trimmed config values", name: "normalizes trimmed config values",
network: { dnsResultOrder: " Verbatim " }, network: { dnsResultOrder: " Verbatim " } as TelegramNetworkConfig & {
dnsResultOrder: string;
},
nodeMajor: 20, nodeMajor: 20,
expected: { value: "verbatim", source: "config" }, expected: { value: "verbatim", source: "config" },
}, },
@ -171,11 +174,17 @@ describe("resolveTelegramDnsResultOrderDecision", () => {
{ {
name: "ignores invalid env and config values before applying Node 22 default", name: "ignores invalid env and config values before applying Node 22 default",
env: { OPENCLAW_TELEGRAM_DNS_RESULT_ORDER: "bogus" }, env: { OPENCLAW_TELEGRAM_DNS_RESULT_ORDER: "bogus" },
network: { dnsResultOrder: "invalid" }, network: { dnsResultOrder: "invalid" } as TelegramNetworkConfig & { dnsResultOrder: string },
nodeMajor: 22, nodeMajor: 22,
expected: { value: "ipv4first", source: "default-node22" }, expected: { value: "ipv4first", source: "default-node22" },
}, },
])("$name", ({ env, network, nodeMajor, expected }) => { ] satisfies Array<{
name: string;
env?: NodeJS.ProcessEnv;
network?: TelegramNetworkConfig | (TelegramNetworkConfig & { dnsResultOrder: string });
nodeMajor: number;
expected: ReturnType<typeof resolveTelegramDnsResultOrderDecision>;
}>)("$name", ({ env, network, nodeMajor, expected }) => {
const decision = resolveTelegramDnsResultOrderDecision({ const decision = resolveTelegramDnsResultOrderDecision({
env, env,
network, network,

View File

@ -28,14 +28,7 @@ export function expectSingleNpmInstallIgnoreScriptsCall(params: {
throw new Error("expected npm install call"); throw new Error("expected npm install call");
} }
const [argv, opts] = first; const [argv, opts] = first;
expect(argv).toEqual([ expect(argv).toEqual(["npm", "install", "--omit=dev", "--silent", "--ignore-scripts"]);
"npm",
"install",
"--omit=dev",
"--omit=peer",
"--silent",
"--ignore-scripts",
]);
expect(opts?.cwd).toBeTruthy(); expect(opts?.cwd).toBeTruthy();
const cwd = String(opts?.cwd); const cwd = String(opts?.cwd);
const expectedTargetDir = params.expectedTargetDir; const expectedTargetDir = params.expectedTargetDir;