refactor(test): consolidate infra unit tests

This commit is contained in:
Peter Steinberger
2026-02-07 08:27:56 +00:00
parent 8fce7dc9b6
commit b8a5f94f25
17 changed files with 478 additions and 490 deletions

View File

@@ -1,33 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import type { runExec } from "../process/exec.js";
import type { RuntimeEnv } from "../runtime.js";
import { ensureBinary } from "./binaries.js";
describe("ensureBinary", () => {
it("passes through when binary exists", async () => {
const exec: typeof runExec = vi.fn().mockResolvedValue({
stdout: "",
stderr: "",
});
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
await ensureBinary("node", exec, runtime);
expect(exec).toHaveBeenCalledWith("which", ["node"]);
});
it("logs and exits when missing", async () => {
const exec: typeof runExec = vi.fn().mockRejectedValue(new Error("missing"));
const error = vi.fn();
const exit = vi.fn(() => {
throw new Error("exit");
});
await expect(ensureBinary("ghost", exec, { log: vi.fn(), error, exit })).rejects.toThrow(
"exit",
);
expect(error).toHaveBeenCalledWith("Missing required binary: ghost. Please install it.");
expect(exit).toHaveBeenCalledWith(1);
});
});

View File

@@ -1,50 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
getChannelActivity,
recordChannelActivity,
resetChannelActivityForTest,
} from "./channel-activity.js";
describe("channel activity", () => {
beforeEach(() => {
resetChannelActivityForTest();
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-01-08T00:00:00Z"));
});
afterEach(() => {
vi.useRealTimers();
});
it("records inbound/outbound separately", () => {
recordChannelActivity({ channel: "telegram", direction: "inbound" });
vi.advanceTimersByTime(1000);
recordChannelActivity({ channel: "telegram", direction: "outbound" });
const res = getChannelActivity({ channel: "telegram" });
expect(res.inboundAt).toBe(1767830400000);
expect(res.outboundAt).toBe(1767830401000);
});
it("isolates accounts", () => {
recordChannelActivity({
channel: "whatsapp",
accountId: "a",
direction: "inbound",
at: 1,
});
recordChannelActivity({
channel: "whatsapp",
accountId: "b",
direction: "inbound",
at: 2,
});
expect(getChannelActivity({ channel: "whatsapp", accountId: "a" })).toEqual({
inboundAt: 1,
outboundAt: null,
});
expect(getChannelActivity({ channel: "whatsapp", accountId: "b" })).toEqual({
inboundAt: 2,
outboundAt: null,
});
});
});

View File

@@ -1,33 +0,0 @@
import { describe, expect, it } from "vitest";
import { createDedupeCache } from "./dedupe.js";
describe("createDedupeCache", () => {
it("marks duplicates within TTL", () => {
const cache = createDedupeCache({ ttlMs: 1000, maxSize: 10 });
expect(cache.check("a", 100)).toBe(false);
expect(cache.check("a", 500)).toBe(true);
});
it("expires entries after TTL", () => {
const cache = createDedupeCache({ ttlMs: 1000, maxSize: 10 });
expect(cache.check("a", 100)).toBe(false);
expect(cache.check("a", 1501)).toBe(false);
});
it("evicts oldest entries when over max size", () => {
const cache = createDedupeCache({ ttlMs: 10_000, maxSize: 2 });
expect(cache.check("a", 100)).toBe(false);
expect(cache.check("b", 200)).toBe(false);
expect(cache.check("c", 300)).toBe(false);
expect(cache.check("a", 400)).toBe(false);
});
it("prunes expired entries even when refreshed keys are older in insertion order", () => {
const cache = createDedupeCache({ ttlMs: 100, maxSize: 10 });
expect(cache.check("a", 0)).toBe(false);
expect(cache.check("b", 50)).toBe(false);
expect(cache.check("a", 120)).toBe(false);
expect(cache.check("c", 200)).toBe(false);
expect(cache.size()).toBe(2);
});
});

View File

@@ -1,54 +0,0 @@
import { describe, expect, test } from "vitest";
import {
emitDiagnosticEvent,
onDiagnosticEvent,
resetDiagnosticEventsForTest,
} from "./diagnostic-events.js";
describe("diagnostic-events", () => {
test("emits monotonic seq", async () => {
resetDiagnosticEventsForTest();
const seqs: number[] = [];
const stop = onDiagnosticEvent((evt) => seqs.push(evt.seq));
emitDiagnosticEvent({
type: "model.usage",
usage: { total: 1 },
});
emitDiagnosticEvent({
type: "model.usage",
usage: { total: 2 },
});
stop();
expect(seqs).toEqual([1, 2]);
});
test("emits message-flow events", async () => {
resetDiagnosticEventsForTest();
const types: string[] = [];
const stop = onDiagnosticEvent((evt) => types.push(evt.type));
emitDiagnosticEvent({
type: "webhook.received",
channel: "telegram",
updateType: "telegram-post",
});
emitDiagnosticEvent({
type: "message.queued",
channel: "telegram",
source: "telegram",
queueDepth: 1,
});
emitDiagnosticEvent({
type: "session.state",
state: "processing",
reason: "run_started",
});
stop();
expect(types).toEqual(["webhook.received", "message.queued", "session.state"]);
});
});

View File

@@ -1,30 +0,0 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { isDiagnosticFlagEnabled, resolveDiagnosticFlags } from "./diagnostic-flags.js";
describe("diagnostic flags", () => {
it("merges config + env flags", () => {
const cfg = {
diagnostics: { flags: ["telegram.http", "cache.*"] },
} as OpenClawConfig;
const env = {
OPENCLAW_DIAGNOSTICS: "foo,bar",
} as NodeJS.ProcessEnv;
const flags = resolveDiagnosticFlags(cfg, env);
expect(flags).toEqual(expect.arrayContaining(["telegram.http", "cache.*", "foo", "bar"]));
expect(isDiagnosticFlagEnabled("telegram.http", cfg, env)).toBe(true);
expect(isDiagnosticFlagEnabled("cache.hit", cfg, env)).toBe(true);
expect(isDiagnosticFlagEnabled("foo", cfg, env)).toBe(true);
});
it("treats env true as wildcard", () => {
const env = { OPENCLAW_DIAGNOSTICS: "1" } as NodeJS.ProcessEnv;
expect(isDiagnosticFlagEnabled("anything.here", undefined, env)).toBe(true);
});
it("treats env false as disabled", () => {
const env = { OPENCLAW_DIAGNOSTICS: "0" } as NodeJS.ProcessEnv;
expect(isDiagnosticFlagEnabled("telegram.http", undefined, env)).toBe(false);
});
});

View File

@@ -0,0 +1,131 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { isDiagnosticFlagEnabled, resolveDiagnosticFlags } from "./diagnostic-flags.js";
import { isMainModule } from "./is-main.js";
import { buildNodeShellCommand } from "./node-shell.js";
import { parseSshTarget } from "./ssh-tunnel.js";
describe("infra parsing", () => {
describe("diagnostic flags", () => {
it("merges config + env flags", () => {
const cfg = {
diagnostics: { flags: ["telegram.http", "cache.*"] },
} as OpenClawConfig;
const env = {
OPENCLAW_DIAGNOSTICS: "foo,bar",
} as NodeJS.ProcessEnv;
const flags = resolveDiagnosticFlags(cfg, env);
expect(flags).toEqual(expect.arrayContaining(["telegram.http", "cache.*", "foo", "bar"]));
expect(isDiagnosticFlagEnabled("telegram.http", cfg, env)).toBe(true);
expect(isDiagnosticFlagEnabled("cache.hit", cfg, env)).toBe(true);
expect(isDiagnosticFlagEnabled("foo", cfg, env)).toBe(true);
});
it("treats env true as wildcard", () => {
const env = { OPENCLAW_DIAGNOSTICS: "1" } as NodeJS.ProcessEnv;
expect(isDiagnosticFlagEnabled("anything.here", undefined, env)).toBe(true);
});
it("treats env false as disabled", () => {
const env = { OPENCLAW_DIAGNOSTICS: "0" } as NodeJS.ProcessEnv;
expect(isDiagnosticFlagEnabled("telegram.http", undefined, env)).toBe(false);
});
});
describe("isMainModule", () => {
it("returns true when argv[1] matches current file", () => {
expect(
isMainModule({
currentFile: "/repo/dist/index.js",
argv: ["node", "/repo/dist/index.js"],
cwd: "/repo",
env: {},
}),
).toBe(true);
});
it("returns true under PM2 when pm_exec_path matches current file", () => {
expect(
isMainModule({
currentFile: "/repo/dist/index.js",
argv: ["node", "/pm2/lib/ProcessContainerFork.js"],
cwd: "/repo",
env: { pm_exec_path: "/repo/dist/index.js", pm_id: "0" },
}),
).toBe(true);
});
it("returns false when running under PM2 but this module is imported", () => {
expect(
isMainModule({
currentFile: "/repo/node_modules/openclaw/dist/index.js",
argv: ["node", "/repo/app.js"],
cwd: "/repo",
env: { pm_exec_path: "/repo/app.js", pm_id: "0" },
}),
).toBe(false);
});
});
describe("buildNodeShellCommand", () => {
it("uses cmd.exe for win32", () => {
expect(buildNodeShellCommand("echo hi", "win32")).toEqual([
"cmd.exe",
"/d",
"/s",
"/c",
"echo hi",
]);
});
it("uses cmd.exe for windows labels", () => {
expect(buildNodeShellCommand("echo hi", "windows")).toEqual([
"cmd.exe",
"/d",
"/s",
"/c",
"echo hi",
]);
expect(buildNodeShellCommand("echo hi", "Windows 11")).toEqual([
"cmd.exe",
"/d",
"/s",
"/c",
"echo hi",
]);
});
it("uses /bin/sh for darwin", () => {
expect(buildNodeShellCommand("echo hi", "darwin")).toEqual(["/bin/sh", "-lc", "echo hi"]);
});
it("uses /bin/sh when platform missing", () => {
expect(buildNodeShellCommand("echo hi")).toEqual(["/bin/sh", "-lc", "echo hi"]);
});
});
describe("parseSshTarget", () => {
it("parses user@host:port targets", () => {
expect(parseSshTarget("me@example.com:2222")).toEqual({
user: "me",
host: "example.com",
port: 2222,
});
});
it("parses host-only targets with default port", () => {
expect(parseSshTarget("example.com")).toEqual({
user: undefined,
host: "example.com",
port: 22,
});
});
it("rejects hostnames that start with '-'", () => {
expect(parseSshTarget("-V")).toBeNull();
expect(parseSshTarget("me@-badhost")).toBeNull();
expect(parseSshTarget("-oProxyCommand=echo")).toBeNull();
});
});
});

View File

@@ -0,0 +1,163 @@
import os from "node:os";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { runExec } from "../process/exec.js";
import type { RuntimeEnv } from "../runtime.js";
import { ensureBinary } from "./binaries.js";
import {
__testing,
consumeGatewaySigusr1RestartAuthorization,
isGatewaySigusr1RestartExternallyAllowed,
scheduleGatewaySigusr1Restart,
setGatewaySigusr1RestartPolicy,
} from "./restart.js";
import { createTelegramRetryRunner } from "./retry-policy.js";
import { getShellPathFromLoginShell, resetShellPathCacheForTests } from "./shell-env.js";
import { listTailnetAddresses } from "./tailnet.js";
describe("infra runtime", () => {
describe("ensureBinary", () => {
it("passes through when binary exists", async () => {
const exec: typeof runExec = vi.fn().mockResolvedValue({
stdout: "",
stderr: "",
});
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
await ensureBinary("node", exec, runtime);
expect(exec).toHaveBeenCalledWith("which", ["node"]);
});
it("logs and exits when missing", async () => {
const exec: typeof runExec = vi.fn().mockRejectedValue(new Error("missing"));
const error = vi.fn();
const exit = vi.fn(() => {
throw new Error("exit");
});
await expect(ensureBinary("ghost", exec, { log: vi.fn(), error, exit })).rejects.toThrow(
"exit",
);
expect(error).toHaveBeenCalledWith("Missing required binary: ghost. Please install it.");
expect(exit).toHaveBeenCalledWith(1);
});
});
describe("createTelegramRetryRunner", () => {
afterEach(() => {
vi.useRealTimers();
});
it("retries when custom shouldRetry matches non-telegram error", async () => {
vi.useFakeTimers();
const runner = createTelegramRetryRunner({
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
shouldRetry: (err) => err instanceof Error && err.message === "boom",
});
const fn = vi.fn().mockRejectedValueOnce(new Error("boom")).mockResolvedValue("ok");
const promise = runner(fn, "request");
await vi.runAllTimersAsync();
await expect(promise).resolves.toBe("ok");
expect(fn).toHaveBeenCalledTimes(2);
});
});
describe("restart authorization", () => {
beforeEach(() => {
__testing.resetSigusr1State();
vi.useFakeTimers();
vi.spyOn(process, "kill").mockImplementation(() => true);
});
afterEach(async () => {
await vi.runOnlyPendingTimersAsync();
vi.useRealTimers();
vi.restoreAllMocks();
__testing.resetSigusr1State();
});
it("consumes a scheduled authorization once", async () => {
expect(consumeGatewaySigusr1RestartAuthorization()).toBe(false);
scheduleGatewaySigusr1Restart({ delayMs: 0 });
expect(consumeGatewaySigusr1RestartAuthorization()).toBe(true);
expect(consumeGatewaySigusr1RestartAuthorization()).toBe(false);
await vi.runAllTimersAsync();
});
it("tracks external restart policy", () => {
expect(isGatewaySigusr1RestartExternallyAllowed()).toBe(false);
setGatewaySigusr1RestartPolicy({ allowExternal: true });
expect(isGatewaySigusr1RestartExternallyAllowed()).toBe(true);
});
});
describe("getShellPathFromLoginShell", () => {
afterEach(() => resetShellPathCacheForTests());
it("returns PATH from login shell env", () => {
if (process.platform === "win32") {
return;
}
const exec = vi
.fn()
.mockReturnValue(Buffer.from("PATH=/custom/bin\0HOME=/home/user\0", "utf-8"));
const result = getShellPathFromLoginShell({ env: { SHELL: "/bin/sh" }, exec });
expect(result).toBe("/custom/bin");
});
it("caches the value", () => {
if (process.platform === "win32") {
return;
}
const exec = vi.fn().mockReturnValue(Buffer.from("PATH=/custom/bin\0", "utf-8"));
const env = { SHELL: "/bin/sh" } as NodeJS.ProcessEnv;
expect(getShellPathFromLoginShell({ env, exec })).toBe("/custom/bin");
expect(getShellPathFromLoginShell({ env, exec })).toBe("/custom/bin");
expect(exec).toHaveBeenCalledTimes(1);
});
it("returns null on exec failure", () => {
if (process.platform === "win32") {
return;
}
const exec = vi.fn(() => {
throw new Error("boom");
});
const result = getShellPathFromLoginShell({ env: { SHELL: "/bin/sh" }, exec });
expect(result).toBeNull();
});
});
describe("tailnet address detection", () => {
it("detects tailscale IPv4 and IPv6 addresses", () => {
vi.spyOn(os, "networkInterfaces").mockReturnValue({
lo0: [{ address: "127.0.0.1", family: "IPv4", internal: true, netmask: "" }],
utun9: [
{
address: "100.123.224.76",
family: "IPv4",
internal: false,
netmask: "",
},
{
address: "fd7a:115c:a1e0::8801:e04c",
family: "IPv6",
internal: false,
netmask: "",
},
],
// oxlint-disable-next-line typescript/no-explicit-any
} as any);
const out = listTailnetAddresses();
expect(out.ipv4).toEqual(["100.123.224.76"]);
expect(out.ipv6).toEqual(["fd7a:115c:a1e0::8801:e04c"]);
});
});
});

View File

@@ -0,0 +1,184 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
getChannelActivity,
recordChannelActivity,
resetChannelActivityForTest,
} from "./channel-activity.js";
import { createDedupeCache } from "./dedupe.js";
import {
emitDiagnosticEvent,
onDiagnosticEvent,
resetDiagnosticEventsForTest,
} from "./diagnostic-events.js";
import { readSessionStoreJson5 } from "./state-migrations.fs.js";
import {
defaultVoiceWakeTriggers,
loadVoiceWakeConfig,
setVoiceWakeTriggers,
} from "./voicewake.js";
describe("infra store", () => {
describe("state migrations fs", () => {
it("treats array session stores as invalid", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-store-"));
const storePath = path.join(dir, "sessions.json");
await fs.writeFile(storePath, "[]", "utf-8");
const result = readSessionStoreJson5(storePath);
expect(result.ok).toBe(false);
expect(result.store).toEqual({});
});
});
describe("voicewake store", () => {
it("returns defaults when missing", async () => {
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-voicewake-"));
const cfg = await loadVoiceWakeConfig(baseDir);
expect(cfg.triggers).toEqual(defaultVoiceWakeTriggers());
expect(cfg.updatedAtMs).toBe(0);
});
it("sanitizes and persists triggers", async () => {
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-voicewake-"));
const saved = await setVoiceWakeTriggers([" hi ", "", " there "], baseDir);
expect(saved.triggers).toEqual(["hi", "there"]);
expect(saved.updatedAtMs).toBeGreaterThan(0);
const loaded = await loadVoiceWakeConfig(baseDir);
expect(loaded.triggers).toEqual(["hi", "there"]);
expect(loaded.updatedAtMs).toBeGreaterThan(0);
});
it("falls back to defaults when triggers empty", async () => {
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-voicewake-"));
const saved = await setVoiceWakeTriggers(["", " "], baseDir);
expect(saved.triggers).toEqual(defaultVoiceWakeTriggers());
});
});
describe("diagnostic-events", () => {
it("emits monotonic seq", async () => {
resetDiagnosticEventsForTest();
const seqs: number[] = [];
const stop = onDiagnosticEvent((evt) => seqs.push(evt.seq));
emitDiagnosticEvent({
type: "model.usage",
usage: { total: 1 },
});
emitDiagnosticEvent({
type: "model.usage",
usage: { total: 2 },
});
stop();
expect(seqs).toEqual([1, 2]);
});
it("emits message-flow events", async () => {
resetDiagnosticEventsForTest();
const types: string[] = [];
const stop = onDiagnosticEvent((evt) => types.push(evt.type));
emitDiagnosticEvent({
type: "webhook.received",
channel: "telegram",
updateType: "telegram-post",
});
emitDiagnosticEvent({
type: "message.queued",
channel: "telegram",
source: "telegram",
queueDepth: 1,
});
emitDiagnosticEvent({
type: "session.state",
state: "processing",
reason: "run_started",
});
stop();
expect(types).toEqual(["webhook.received", "message.queued", "session.state"]);
});
});
describe("channel activity", () => {
beforeEach(() => {
resetChannelActivityForTest();
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-01-08T00:00:00Z"));
});
afterEach(() => {
vi.useRealTimers();
});
it("records inbound/outbound separately", () => {
recordChannelActivity({ channel: "telegram", direction: "inbound" });
vi.advanceTimersByTime(1000);
recordChannelActivity({ channel: "telegram", direction: "outbound" });
const res = getChannelActivity({ channel: "telegram" });
expect(res.inboundAt).toBe(1767830400000);
expect(res.outboundAt).toBe(1767830401000);
});
it("isolates accounts", () => {
recordChannelActivity({
channel: "whatsapp",
accountId: "a",
direction: "inbound",
at: 1,
});
recordChannelActivity({
channel: "whatsapp",
accountId: "b",
direction: "inbound",
at: 2,
});
expect(getChannelActivity({ channel: "whatsapp", accountId: "a" })).toEqual({
inboundAt: 1,
outboundAt: null,
});
expect(getChannelActivity({ channel: "whatsapp", accountId: "b" })).toEqual({
inboundAt: 2,
outboundAt: null,
});
});
});
describe("createDedupeCache", () => {
it("marks duplicates within TTL", () => {
const cache = createDedupeCache({ ttlMs: 1000, maxSize: 10 });
expect(cache.check("a", 100)).toBe(false);
expect(cache.check("a", 500)).toBe(true);
});
it("expires entries after TTL", () => {
const cache = createDedupeCache({ ttlMs: 1000, maxSize: 10 });
expect(cache.check("a", 100)).toBe(false);
expect(cache.check("a", 1501)).toBe(false);
});
it("evicts oldest entries when over max size", () => {
const cache = createDedupeCache({ ttlMs: 10_000, maxSize: 2 });
expect(cache.check("a", 100)).toBe(false);
expect(cache.check("b", 200)).toBe(false);
expect(cache.check("c", 300)).toBe(false);
expect(cache.check("a", 400)).toBe(false);
});
it("prunes expired entries even when refreshed keys are older in insertion order", () => {
const cache = createDedupeCache({ ttlMs: 100, maxSize: 10 });
expect(cache.check("a", 0)).toBe(false);
expect(cache.check("b", 50)).toBe(false);
expect(cache.check("a", 120)).toBe(false);
expect(cache.check("c", 200)).toBe(false);
expect(cache.size()).toBe(2);
});
});
});

View File

@@ -1,37 +0,0 @@
import { describe, expect, it } from "vitest";
import { isMainModule } from "./is-main.js";
describe("isMainModule", () => {
it("returns true when argv[1] matches current file", () => {
expect(
isMainModule({
currentFile: "/repo/dist/index.js",
argv: ["node", "/repo/dist/index.js"],
cwd: "/repo",
env: {},
}),
).toBe(true);
});
it("returns true under PM2 when pm_exec_path matches current file", () => {
expect(
isMainModule({
currentFile: "/repo/dist/index.js",
argv: ["node", "/pm2/lib/ProcessContainerFork.js"],
cwd: "/repo",
env: { pm_exec_path: "/repo/dist/index.js", pm_id: "0" },
}),
).toBe(true);
});
it("returns false when running under PM2 but this module is imported", () => {
expect(
isMainModule({
currentFile: "/repo/node_modules/openclaw/dist/index.js",
argv: ["node", "/repo/app.js"],
cwd: "/repo",
env: { pm_exec_path: "/repo/app.js", pm_id: "0" },
}),
).toBe(false);
});
});

View File

@@ -1,39 +0,0 @@
import { describe, expect, it } from "vitest";
import { buildNodeShellCommand } from "./node-shell.js";
describe("buildNodeShellCommand", () => {
it("uses cmd.exe for win32", () => {
expect(buildNodeShellCommand("echo hi", "win32")).toEqual([
"cmd.exe",
"/d",
"/s",
"/c",
"echo hi",
]);
});
it("uses cmd.exe for windows labels", () => {
expect(buildNodeShellCommand("echo hi", "windows")).toEqual([
"cmd.exe",
"/d",
"/s",
"/c",
"echo hi",
]);
expect(buildNodeShellCommand("echo hi", "Windows 11")).toEqual([
"cmd.exe",
"/d",
"/s",
"/c",
"echo hi",
]);
});
it("uses /bin/sh for darwin", () => {
expect(buildNodeShellCommand("echo hi", "darwin")).toEqual(["/bin/sh", "-lc", "echo hi"]);
});
it("uses /bin/sh when platform missing", () => {
expect(buildNodeShellCommand("echo hi")).toEqual(["/bin/sh", "-lc", "echo hi"]);
});
});

View File

@@ -1,40 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
__testing,
consumeGatewaySigusr1RestartAuthorization,
isGatewaySigusr1RestartExternallyAllowed,
scheduleGatewaySigusr1Restart,
setGatewaySigusr1RestartPolicy,
} from "./restart.js";
describe("restart authorization", () => {
beforeEach(() => {
__testing.resetSigusr1State();
vi.useFakeTimers();
vi.spyOn(process, "kill").mockImplementation(() => true);
});
afterEach(async () => {
await vi.runOnlyPendingTimersAsync();
vi.useRealTimers();
vi.restoreAllMocks();
__testing.resetSigusr1State();
});
it("consumes a scheduled authorization once", async () => {
expect(consumeGatewaySigusr1RestartAuthorization()).toBe(false);
scheduleGatewaySigusr1Restart({ delayMs: 0 });
expect(consumeGatewaySigusr1RestartAuthorization()).toBe(true);
expect(consumeGatewaySigusr1RestartAuthorization()).toBe(false);
await vi.runAllTimersAsync();
});
it("tracks external restart policy", () => {
expect(isGatewaySigusr1RestartExternallyAllowed()).toBe(false);
setGatewaySigusr1RestartPolicy({ allowExternal: true });
expect(isGatewaySigusr1RestartExternallyAllowed()).toBe(true);
});
});

View File

@@ -1,26 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createTelegramRetryRunner } from "./retry-policy.js";
describe("createTelegramRetryRunner", () => {
afterEach(() => {
vi.useRealTimers();
});
it("retries when custom shouldRetry matches non-telegram error", async () => {
vi.useFakeTimers();
const runner = createTelegramRetryRunner({
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
shouldRetry: (err) => err instanceof Error && err.message === "boom",
});
const fn = vi
.fn<[], Promise<string>>()
.mockRejectedValueOnce(new Error("boom"))
.mockResolvedValue("ok");
const promise = runner(fn, "request");
await vi.runAllTimersAsync();
await expect(promise).resolves.toBe("ok");
expect(fn).toHaveBeenCalledTimes(2);
});
});

View File

@@ -1,39 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { getShellPathFromLoginShell, resetShellPathCacheForTests } from "./shell-env.js";
describe("getShellPathFromLoginShell", () => {
afterEach(() => resetShellPathCacheForTests());
it("returns PATH from login shell env", () => {
if (process.platform === "win32") {
return;
}
const exec = vi
.fn()
.mockReturnValue(Buffer.from("PATH=/custom/bin\0HOME=/home/user\0", "utf-8"));
const result = getShellPathFromLoginShell({ env: { SHELL: "/bin/sh" }, exec });
expect(result).toBe("/custom/bin");
});
it("caches the value", () => {
if (process.platform === "win32") {
return;
}
const exec = vi.fn().mockReturnValue(Buffer.from("PATH=/custom/bin\0", "utf-8"));
const env = { SHELL: "/bin/sh" } as NodeJS.ProcessEnv;
expect(getShellPathFromLoginShell({ env, exec })).toBe("/custom/bin");
expect(getShellPathFromLoginShell({ env, exec })).toBe("/custom/bin");
expect(exec).toHaveBeenCalledTimes(1);
});
it("returns null on exec failure", () => {
if (process.platform === "win32") {
return;
}
const exec = vi.fn(() => {
throw new Error("boom");
});
const result = getShellPathFromLoginShell({ env: { SHELL: "/bin/sh" }, exec });
expect(result).toBeNull();
});
});

View File

@@ -1,26 +0,0 @@
import { describe, expect, it } from "vitest";
import { parseSshTarget } from "./ssh-tunnel.js";
describe("parseSshTarget", () => {
it("parses user@host:port targets", () => {
expect(parseSshTarget("me@example.com:2222")).toEqual({
user: "me",
host: "example.com",
port: 2222,
});
});
it("parses host-only targets with default port", () => {
expect(parseSshTarget("example.com")).toEqual({
user: undefined,
host: "example.com",
port: 22,
});
});
it("rejects hostnames that start with '-'", () => {
expect(parseSshTarget("-V")).toBeNull();
expect(parseSshTarget("me@-badhost")).toBeNull();
expect(parseSshTarget("-oProxyCommand=echo")).toBeNull();
});
});

View File

@@ -1,17 +0,0 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { readSessionStoreJson5 } from "./state-migrations.fs.js";
describe("state migrations fs", () => {
it("treats array session stores as invalid", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-store-"));
const storePath = path.join(dir, "sessions.json");
await fs.writeFile(storePath, "[]", "utf-8");
const result = readSessionStoreJson5(storePath);
expect(result.ok).toBe(false);
expect(result.store).toEqual({});
});
});

View File

@@ -1,31 +0,0 @@
import os from "node:os";
import { describe, expect, it, vi } from "vitest";
import { listTailnetAddresses } from "./tailnet.js";
describe("tailnet address detection", () => {
it("detects tailscale IPv4 and IPv6 addresses", () => {
vi.spyOn(os, "networkInterfaces").mockReturnValue({
lo0: [
{ address: "127.0.0.1", family: "IPv4", internal: true, netmask: "" },
] as unknown as os.NetworkInterfaceInfo[],
utun9: [
{
address: "100.123.224.76",
family: "IPv4",
internal: false,
netmask: "",
},
{
address: "fd7a:115c:a1e0::8801:e04c",
family: "IPv6",
internal: false,
netmask: "",
},
] as unknown as os.NetworkInterfaceInfo[],
});
const out = listTailnetAddresses();
expect(out.ipv4).toEqual(["100.123.224.76"]);
expect(out.ipv6).toEqual(["fd7a:115c:a1e0::8801:e04c"]);
});
});

View File

@@ -1,35 +0,0 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
defaultVoiceWakeTriggers,
loadVoiceWakeConfig,
setVoiceWakeTriggers,
} from "./voicewake.js";
describe("voicewake store", () => {
it("returns defaults when missing", async () => {
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-voicewake-"));
const cfg = await loadVoiceWakeConfig(baseDir);
expect(cfg.triggers).toEqual(defaultVoiceWakeTriggers());
expect(cfg.updatedAtMs).toBe(0);
});
it("sanitizes and persists triggers", async () => {
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-voicewake-"));
const saved = await setVoiceWakeTriggers([" hi ", "", " there "], baseDir);
expect(saved.triggers).toEqual(["hi", "there"]);
expect(saved.updatedAtMs).toBeGreaterThan(0);
const loaded = await loadVoiceWakeConfig(baseDir);
expect(loaded.triggers).toEqual(["hi", "there"]);
expect(loaded.updatedAtMs).toBeGreaterThan(0);
});
it("falls back to defaults when triggers empty", async () => {
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-voicewake-"));
const saved = await setVoiceWakeTriggers(["", " "], baseDir);
expect(saved.triggers).toEqual(defaultVoiceWakeTriggers());
});
});