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;
};
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 {
const entries = parseKeyValueOutput(output, ":");
const info: ScheduledTaskInfo = {};
@ -388,7 +394,7 @@ async function resolveScheduledTaskGatewayListenerPids(port: number): Promise<nu
new Set(
diagnostics.listeners
.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(
diagnostics.listeners
.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) {
@ -496,7 +502,7 @@ async function resolveFallbackRuntime(env: GatewayServiceEnv): Promise<GatewaySe
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 {
status: diagnostics.status === "busy" ? "running" : "stopped",
...(listener?.pid ? { pid: listener.pid } : {}),

View File

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

View File

@ -251,7 +251,7 @@ describe("resolveGatewayRuntimeConfig", () => {
});
describe("HTTP security headers", () => {
it.each([
const cases = [
{
name: "resolves strict transport security headers from config",
strictTransportSecurity: " max-age=31536000; includeSubDomains ",
@ -267,7 +267,13 @@ describe("resolveGatewayRuntimeConfig", () => {
strictTransportSecurity: " ",
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({
cfg: {
gateway: {

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,8 @@ import {
resolveTimedInstallModeOptions,
} from "./install-mode-options.js";
type LoggerKey = "default" | "explicit";
describe("install mode option helpers", () => {
it.each([
{
@ -21,11 +23,15 @@ describe("install mode option helpers", () => {
params: { mode: "update" as const, 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 = {
default: { warn: (_message: string) => {} },
explicit: { warn: (_message: string) => {} },
};
} satisfies Record<LoggerKey, { warn: (_message: string) => void }>;
expect(
resolveInstallModeOptions(

View File

@ -462,7 +462,14 @@ describe("resolveSessionDeliveryTarget", () => {
expectedChannel: "none",
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({
name,
entry,

View File

@ -3,7 +3,7 @@ import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
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", () => {
let suiteRoot = "";
@ -214,7 +214,12 @@ describe("resolveProviderAuths key normalization", () => {
},
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 });
});

View File

@ -32,10 +32,11 @@ describe("provider usage fetch shared helpers", () => {
it("forwards request init and clears the timeout on success", async () => {
vi.useFakeTimers();
const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");
const fetchFn = vi.fn(
async (_url: string, init?: RequestInit) =>
const fetchFnMock = vi.fn(
async (_input: URL | RequestInfo, init?: RequestInit) =>
new Response(JSON.stringify({ aborted: init?.signal?.aborted ?? false }), { status: 200 }),
);
const fetchFn = fetchFnMock as typeof fetch;
const response = await fetchJson(
"https://example.com/usage",
@ -47,7 +48,7 @@ describe("provider usage fetch shared helpers", () => {
fetchFn,
);
expect(fetchFn).toHaveBeenCalledWith(
expect(fetchFnMock).toHaveBeenCalledWith(
"https://example.com/usage",
expect.objectContaining({
method: "POST",
@ -62,14 +63,15 @@ describe("provider usage fetch shared helpers", () => {
it("aborts timed out requests and clears the timer on rejection", async () => {
vi.useFakeTimers();
const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");
const fetchFn = vi.fn(
(_url: string, init?: RequestInit) =>
const fetchFnMock = vi.fn(
(_input: URL | RequestInfo, init?: RequestInit) =>
new Promise<Response>((_, reject) => {
init?.signal?.addEventListener("abort", () => reject(new Error("aborted by timeout")), {
once: true,
});
}),
);
const fetchFn = fetchFnMock as typeof fetch;
const request = fetchJson("https://example.com/usage", {}, 50, fetchFn);
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 { 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> {
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), prefix));
try {
@ -33,6 +38,20 @@ function mockStat(params: {
} 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", () => {
it("returns a path error for missing files", async () => {
await withTempDir("openclaw-safe-open-", async (root) => {
@ -115,15 +134,16 @@ describe("openVerifiedFileSync", () => {
closed.push(fd);
};
const closed: number[] = [];
const ioFs = {
const ioFs: SafeOpenSyncFs = {
constants: fs.constants,
lstatSync: (filePath: string) =>
filePath === "/real/file.txt"
lstatSync: mockLstatSync((filePath) =>
String(filePath) === "/real/file.txt"
? mockStat({ isFile: true, size: 1, dev: 1, ino: 1 })
: mockStat({ isFile: false }),
realpathSync: () => "/real/file.txt",
),
realpathSync: mockRealpathSync("/real/file.txt"),
openSync: () => 42,
fstatSync: () => mockStat({ isFile: true, size: 1, dev: 2, ino: 1 }),
fstatSync: mockFstatSync(mockStat({ isFile: true, size: 1, dev: 2, ino: 1 })),
closeSync,
};
@ -139,16 +159,16 @@ describe("openVerifiedFileSync", () => {
});
it("reports non-path filesystem failures as io errors", () => {
const ioFs = {
const ioFs: SafeOpenSyncFs = {
constants: fs.constants,
lstatSync: () => {
const err = new Error("permission denied") as NodeJS.ErrnoException;
err.code = "EACCES";
throw err;
},
realpathSync: () => "/real/file.txt",
realpathSync: mockRealpathSync("/real/file.txt"),
openSync: () => 42,
fstatSync: () => mockStat({ isFile: true }),
fstatSync: mockFstatSync(mockStat({ isFile: true })),
closeSync: () => {},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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