chore: Fix types in tests 33/N.

This commit is contained in:
cpojer
2026-02-17 15:45:36 +09:00
parent f44b58fd58
commit 49bd9f75f4
21 changed files with 148 additions and 42 deletions

22
scripts/run-node.d.mts Normal file
View File

@@ -0,0 +1,22 @@
export const runNodeWatchedPaths: string[];
export function runNodeMain(params?: {
spawn?: (
cmd: string,
args: string[],
options: unknown,
) => {
on: (
event: "exit",
cb: (code: number | null, signal: string | null) => void,
) => void | undefined;
};
spawnSync?: unknown;
fs?: unknown;
stderr?: { write: (value: string) => void };
execPath?: string;
cwd?: string;
args?: string[];
env?: NodeJS.ProcessEnv;
platform?: NodeJS.Platform;
}): Promise<number>;

14
scripts/watch-node.d.mts Normal file
View File

@@ -0,0 +1,14 @@
export function runWatchMain(params?: {
spawn?: (
cmd: string,
args: string[],
options: unknown,
) => {
on: (event: "exit", cb: (code: number | null, signal: string | null) => void) => void;
};
process?: NodeJS.Process;
cwd?: string;
args?: string[];
env?: NodeJS.ProcessEnv;
now?: () => number;
}): Promise<number>;

View File

@@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({
logDebug: vi.fn(),
}));
const { createService, shutdown, registerUnhandledRejectionHandler, logWarn, logDebug } = mocks;
const getLoggerInfo = vi.fn();
const asString = (value: unknown, fallback: string) =>
typeof value === "string" && value.trim() ? value : fallback;
@@ -81,7 +82,7 @@ describe("gateway bonjour advertiser", () => {
beforeEach(() => {
vi.spyOn(logging, "getLogger").mockReturnValue({
info: (...args: unknown[]) => getLoggerInfo(...args),
});
} as unknown as ReturnType<typeof logging.getLogger>);
});
afterEach(() => {

View File

@@ -42,7 +42,9 @@ function createForwarder(params: {
const deliver = params.deliver ?? vi.fn().mockResolvedValue([]);
const forwarder = createExecApprovalForwarder({
getConfig: () => params.cfg,
deliver,
deliver: deliver as unknown as NonNullable<
NonNullable<Parameters<typeof createExecApprovalForwarder>[0]>["deliver"]
>,
nowMs: () => 1000,
resolveSessionTarget: params.resolveSessionTarget ?? (() => null),
});

View File

@@ -693,7 +693,7 @@ describe("exec approvals node host allowlist check", () => {
describe("exec approvals default agent migration", () => {
it("migrates legacy default agent entries to main", () => {
const file = {
const file: ExecApprovalsFile = {
version: 1,
agents: {
default: { allowlist: [{ pattern: "/bin/legacy" }] },
@@ -706,7 +706,7 @@ describe("exec approvals default agent migration", () => {
});
it("prefers main agent settings when both main and default exist", () => {
const file = {
const file: ExecApprovalsFile = {
version: 1,
agents: {
main: { ask: "always", allowlist: [{ pattern: "/bin/main" }] },

View File

@@ -92,7 +92,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
): Promise<{
result: Awaited<ReturnType<typeof runHeartbeatOnce>>;
sendTelegram: ReturnType<typeof vi.fn>;
getReplySpy: ReturnType<typeof vi.spyOn<typeof replyModule, "getReplyFromConfig">>;
getReplySpy: ReturnType<typeof vi.fn>;
}> => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), tmpPrefix));
const sendTelegram = vi.fn().mockResolvedValue({

View File

@@ -744,7 +744,7 @@ describe("runHeartbeatOnce", () => {
const forcedSessionKey = buildAgentPeerSessionKey({
agentId,
channel: "whatsapp",
peerKind: "dm",
peerKind: "direct",
peerId: "+15559990000",
});

View File

@@ -4,7 +4,7 @@ import { startHeartbeatRunner } from "./heartbeat-runner.js";
import { requestHeartbeatNow, resetHeartbeatWakeStateForTests } from "./heartbeat-wake.js";
describe("startHeartbeatRunner", () => {
function startDefaultRunner(runOnce: (typeof startHeartbeatRunner)[0]["runOnce"]) {
function startDefaultRunner(runOnce: Parameters<typeof startHeartbeatRunner>[0]["runOnce"]) {
return startHeartbeatRunner({
cfg: {
agents: { defaults: { heartbeat: { every: "30m" } } },

View File

@@ -75,13 +75,13 @@ describe("heartbeat transcript pruning", () => {
});
// Run heartbeat
const cfg: OpenClawConfig = {
const cfg = {
version: 1,
model: "test-model",
agent: { workspace: tmpDir },
sessionStore: storePath,
channels: { telegram: { showOk: true, showAlerts: true } },
};
channels: { telegram: {} },
} as unknown as OpenClawConfig;
await runHeartbeatOnce({
agentId: undefined,
@@ -123,13 +123,13 @@ describe("heartbeat transcript pruning", () => {
});
// Run heartbeat
const cfg: OpenClawConfig = {
const cfg = {
version: 1,
model: "test-model",
agent: { workspace: tmpDir },
sessionStore: storePath,
channels: { telegram: { showOk: true, showAlerts: true } },
};
channels: { telegram: {} },
} as unknown as OpenClawConfig;
await runHeartbeatOnce({
agentId: undefined,

View File

@@ -13,7 +13,9 @@ describe("heartbeat-wake", () => {
initialReason: string;
expectedRetryReason: string;
}) {
setHeartbeatWakeHandler(params.handler);
setHeartbeatWakeHandler(
params.handler as unknown as Parameters<typeof setHeartbeatWakeHandler>[0],
);
requestHeartbeatNow({ reason: params.initialReason, coalesceMs: 0 });
await vi.advanceTimersByTimeAsync(1);

View File

@@ -9,6 +9,8 @@ function redirectResponse(location: string): Response {
}
describe("fetchWithSsrFGuard hardening", () => {
type LookupFn = NonNullable<Parameters<typeof fetchWithSsrFGuard>[0]["lookupFn"]>;
it("blocks private IP literal URLs before fetch", async () => {
const fetchImpl = vi.fn();
await expect(
@@ -21,7 +23,9 @@ describe("fetchWithSsrFGuard hardening", () => {
});
it("blocks redirect chains that hop to private hosts", async () => {
const lookupFn = vi.fn(async () => [{ address: "93.184.216.34", family: 4 }]);
const lookupFn = vi.fn(async () => [
{ address: "93.184.216.34", family: 4 },
]) as unknown as LookupFn;
const fetchImpl = vi.fn().mockResolvedValueOnce(redirectResponse("http://127.0.0.1:6379/"));
await expect(
@@ -47,7 +51,9 @@ describe("fetchWithSsrFGuard hardening", () => {
});
it("allows wildcard allowlisted hosts", async () => {
const lookupFn = vi.fn(async () => [{ address: "93.184.216.34", family: 4 }]);
const lookupFn = vi.fn(async () => [
{ address: "93.184.216.34", family: 4 },
]) as unknown as LookupFn;
const fetchImpl = vi.fn(async () => new Response("ok", { status: 200 }));
const result = await fetchWithSsrFGuard({
url: "https://img.assets.example.com/pic.png",

View File

@@ -19,6 +19,8 @@ describe("agent delivery helpers", () => {
it("builds a delivery plan from session delivery context", () => {
const plan = resolveAgentDeliveryPlan({
sessionEntry: {
sessionId: "s1",
updatedAt: 1,
deliveryContext: { channel: "whatsapp", to: "+1555", accountId: "work" },
},
requestedChannel: "last",
@@ -36,6 +38,8 @@ describe("agent delivery helpers", () => {
it("resolves fallback targets when no explicit destination is provided", () => {
const plan = resolveAgentDeliveryPlan({
sessionEntry: {
sessionId: "s2",
updatedAt: 2,
deliveryContext: { channel: "whatsapp" },
},
requestedChannel: "last",
@@ -58,6 +62,8 @@ describe("agent delivery helpers", () => {
it("skips outbound target resolution when explicit target validation is disabled", () => {
const plan = resolveAgentDeliveryPlan({
sessionEntry: {
sessionId: "s3",
updatedAt: 3,
deliveryContext: { channel: "whatsapp", to: "+1555" },
},
requestedChannel: "last",

View File

@@ -54,7 +54,9 @@ const whatsappChunkConfig: OpenClawConfig = {
};
async function deliverWhatsAppPayload(params: {
sendWhatsApp: ReturnType<typeof vi.fn>;
sendWhatsApp: NonNullable<
NonNullable<Parameters<typeof deliverOutboundPayloads>[0]["deps"]>["sendWhatsApp"]
>;
payload: { text: string; mediaUrl?: string };
cfg?: OpenClawConfig;
}) {
@@ -517,7 +519,7 @@ describe("deliverOutboundPayloads", () => {
});
it("emits message_sent success for text-only deliveries", async () => {
hookMocks.runner.hasHooks.mockImplementation((name: string) => name === "message_sent");
hookMocks.runner.hasHooks.mockReturnValue(true);
const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
await deliverOutboundPayloads({
@@ -537,7 +539,7 @@ describe("deliverOutboundPayloads", () => {
});
it("emits message_sent success for sendPayload deliveries", async () => {
hookMocks.runner.hasHooks.mockImplementation((name: string) => name === "message_sent");
hookMocks.runner.hasHooks.mockReturnValue(true);
const sendPayload = vi.fn().mockResolvedValue({ channel: "matrix", messageId: "mx-1" });
const sendText = vi.fn();
const sendMedia = vi.fn();
@@ -570,7 +572,7 @@ describe("deliverOutboundPayloads", () => {
});
it("emits message_sent failure when delivery errors", async () => {
hookMocks.runner.hasHooks.mockImplementation((name: string) => name === "message_sent");
hookMocks.runner.hasHooks.mockReturnValue(true);
const sendWhatsApp = vi.fn().mockRejectedValue(new Error("downstream failed"));
await expect(

View File

@@ -558,6 +558,9 @@ describe("runMessageAction sandboxed media validation", () => {
});
expect(result.kind).toBe("send");
if (result.kind !== "send") {
throw new Error("expected send result");
}
expect(result.sendResult?.mediaUrl).toBe(path.join(sandboxDir, "data", "file.txt"));
});
});
@@ -575,6 +578,9 @@ describe("runMessageAction sandboxed media validation", () => {
});
expect(result.kind).toBe("send");
if (result.kind !== "send") {
throw new Error("expected send result");
}
expect(result.sendResult?.mediaUrl).toBe(path.join(sandboxDir, "data", "note.ogg"));
});
});

View File

@@ -446,7 +446,7 @@ describe("DirectoryCache", () => {
describe("buildOutboundResultEnvelope", () => {
it("flattens delivery-only payloads by default", () => {
const delivery: OutboundDeliveryJson = {
provider: "whatsapp",
channel: "whatsapp",
via: "gateway",
to: "+1",
messageId: "m1",
@@ -468,7 +468,7 @@ describe("buildOutboundResultEnvelope", () => {
it("includes delivery when payloads are present", () => {
const delivery: OutboundDeliveryJson = {
provider: "telegram",
channel: "telegram",
via: "direct",
to: "123",
messageId: "m2",
@@ -489,7 +489,7 @@ describe("buildOutboundResultEnvelope", () => {
it("can keep delivery wrapped when requested", () => {
const delivery: OutboundDeliveryJson = {
provider: "discord",
channel: "discord",
via: "gateway",
to: "channel:C1",
messageId: "m3",

View File

@@ -31,7 +31,7 @@ describe("resolveMessagingTarget (directory fallback)", () => {
});
it("uses live directory fallback and caches the result", async () => {
const entry: ChannelDirectoryEntry = { id: "123456789", name: "support" };
const entry: ChannelDirectoryEntry = { kind: "group", id: "123456789", name: "support" };
mocks.listGroups.mockResolvedValue([]);
mocks.listGroupsLive.mockResolvedValue([entry]);

View File

@@ -22,7 +22,7 @@ const describeUnix = process.platform === "win32" ? describe.skip : describe;
describe("ports helpers", () => {
it("ensurePortAvailable rejects when port busy", async () => {
const server = net.createServer();
await new Promise((resolve) => server.listen(0, resolve));
await new Promise<void>((resolve) => server.listen(0, () => resolve()));
const port = (server.address() as net.AddressInfo).port;
await expect(ensurePortAvailable(port)).rejects.toBeInstanceOf(PortInUseError);
await new Promise<void>((resolve) => server.close(() => resolve()));

38
src/infra/scripts-modules.d.ts vendored Normal file
View File

@@ -0,0 +1,38 @@
declare module "../../scripts/run-node.mjs" {
export const runNodeWatchedPaths: string[];
export function runNodeMain(params?: {
spawn?: (
cmd: string,
args: string[],
options: unknown,
) => {
on: (
event: "exit",
cb: (code: number | null, signal: string | null) => void,
) => void | undefined;
};
spawnSync?: unknown;
fs?: unknown;
stderr?: { write: (value: string) => void };
execPath?: string;
cwd?: string;
args?: string[];
env?: NodeJS.ProcessEnv;
platform?: NodeJS.Platform;
}): Promise<number>;
}
declare module "../../scripts/watch-node.mjs" {
export function runWatchMain(params?: {
spawn?: (
cmd: string,
args: string[],
options: unknown,
) => { on: (event: "exit", cb: (code: number | null, signal: string | null) => void) => void };
process?: NodeJS.Process;
cwd?: string;
args?: string[];
env?: NodeJS.ProcessEnv;
now?: () => number;
}): Promise<number>;
}

View File

@@ -1,4 +1,4 @@
import { spawn } from "node:child_process";
import { spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
import { EventEmitter } from "node:events";
import { describe, expect, it, vi } from "vitest";
@@ -64,13 +64,15 @@ describe("ssh-config", () => {
});
it("returns null when ssh -G fails", async () => {
spawnMock.mockImplementationOnce(() => {
const { child } = createMockSpawnChild();
process.nextTick(() => {
child.emit("exit", 1);
});
return child;
});
spawnMock.mockImplementationOnce(
(_command: string, _args: readonly string[], _options: SpawnOptions): ChildProcess => {
const { child } = createMockSpawnChild();
process.nextTick(() => {
child.emit("exit", 1);
});
return child as unknown as ChildProcess;
},
);
const { resolveSshConfig } = await import("./ssh-config.js");
const config = await resolveSshConfig({ user: "me", host: "bad-host", port: 22 });

View File

@@ -2,13 +2,15 @@ import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { POSIX_OPENCLAW_TMP_DIR, resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir.js";
type TmpDirOptions = NonNullable<Parameters<typeof resolvePreferredOpenClawTmpDir>[0]>;
function fallbackTmp(uid = 501) {
return path.join("/var/fallback", `openclaw-${uid}`);
}
function resolveWithMocks(params: {
lstatSync: ReturnType<typeof vi.fn>;
accessSync?: ReturnType<typeof vi.fn>;
lstatSync: NonNullable<TmpDirOptions["lstatSync"]>;
accessSync?: NonNullable<TmpDirOptions["accessSync"]>;
uid?: number;
tmpdirPath?: string;
}) {
@@ -28,7 +30,7 @@ function resolveWithMocks(params: {
describe("resolvePreferredOpenClawTmpDir", () => {
it("prefers /tmp/openclaw when it already exists and is writable", () => {
const lstatSync = vi.fn(() => ({
const lstatSync: NonNullable<TmpDirOptions["lstatSync"]> = vi.fn(() => ({
isDirectory: () => true,
isSymbolicLink: () => false,
uid: 501,
@@ -43,26 +45,28 @@ describe("resolvePreferredOpenClawTmpDir", () => {
});
it("prefers /tmp/openclaw when it does not exist but /tmp is writable", () => {
const lstatSync = vi.fn(() => {
const lstatSyncMock = vi.fn<NonNullable<TmpDirOptions["lstatSync"]>>(() => {
const err = new Error("missing") as Error & { code?: string };
err.code = "ENOENT";
throw err;
});
// second lstat call (after mkdir) should succeed
lstatSync.mockImplementationOnce(() => {
lstatSyncMock.mockImplementationOnce(() => {
const err = new Error("missing") as Error & { code?: string };
err.code = "ENOENT";
throw err;
});
lstatSync.mockImplementationOnce(() => ({
lstatSyncMock.mockImplementationOnce(() => ({
isDirectory: () => true,
isSymbolicLink: () => false,
uid: 501,
mode: 0o40700,
}));
const { resolved, accessSync, mkdirSync, tmpdir } = resolveWithMocks({ lstatSync });
const { resolved, accessSync, mkdirSync, tmpdir } = resolveWithMocks({
lstatSync: lstatSyncMock,
});
expect(resolved).toBe(POSIX_OPENCLAW_TMP_DIR);
expect(accessSync).toHaveBeenCalledWith("/tmp", expect.any(Number));
@@ -76,7 +80,7 @@ describe("resolvePreferredOpenClawTmpDir", () => {
isSymbolicLink: () => false,
uid: 501,
mode: 0o100644,
}));
})) as unknown as ReturnType<typeof vi.fn> & NonNullable<TmpDirOptions["lstatSync"]>;
const { resolved, tmpdir } = resolveWithMocks({ lstatSync });
expect(resolved).toBe(fallbackTmp());

View File

@@ -16,10 +16,11 @@ describe("installUnhandledRejectionHandler - fatal detection", () => {
beforeEach(() => {
exitCalls = [];
vi.spyOn(process, "exit").mockImplementation((code: string | number | null | undefined) => {
vi.spyOn(process, "exit").mockImplementation((code?: string | number | null): never => {
if (code !== undefined && code !== null) {
exitCalls.push(code);
}
throw new Error("process.exit");
});
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});