test: tighten assertions and harness coverage

This commit is contained in:
Peter Steinberger
2026-05-08 05:27:57 +01:00
parent f62618f805
commit 9ef37d1907
822 changed files with 8918 additions and 6533 deletions

View File

@@ -98,7 +98,10 @@ describe("embedded acpx plugin config", () => {
});
const server = resolved.mcpServers["openclaw-plugin-tools"];
expect(server).toBeDefined();
expect(server).toMatchObject({
command: process.execPath,
args: expect.any(Array),
});
expect(server.command).toBe(process.execPath);
expect(Array.isArray(server.args)).toBe(true);
expect(server.args?.length).toBeGreaterThan(0);
@@ -113,7 +116,10 @@ describe("embedded acpx plugin config", () => {
});
const server = resolved.mcpServers["openclaw-tools"];
expect(server).toBeDefined();
expect(server).toMatchObject({
command: process.execPath,
args: expect.any(Array),
});
expect(server.command).toBe(process.execPath);
expect(Array.isArray(server.args)).toBe(true);
expect(server.args?.length).toBeGreaterThan(0);

View File

@@ -12,7 +12,7 @@ describe("acpx package manifest", () => {
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
) as AcpxPackageManifest;
expect(packageJson.dependencies?.acpx).toBeDefined();
expect(packageJson.dependencies?.acpx).toEqual(expect.any(String));
expect(packageJson.dependencies?.["@zed-industries/codex-acp"]).toBe("0.13.0");
expect(packageJson.dependencies?.["@agentclientprotocol/claude-agent-acp"]).toBe("0.32.0");
expect(packageJson.devDependencies?.["@agentclientprotocol/claude-agent-acp"]).toBeUndefined();

View File

@@ -325,10 +325,15 @@ describe("createAcpxRuntimeService", () => {
await service.start(ctx);
const backend = getAcpRuntimeBackend("acpx");
expect(backend?.runtime).toBeDefined();
if (!backend) {
throw new Error("expected ACPX runtime backend");
}
expect(backend.runtime).toMatchObject({
ensureSession: expect.any(Function),
});
expect(acpxRuntimeConstructorMock).not.toHaveBeenCalled();
await backend?.runtime.ensureSession({
await backend.runtime.ensureSession({
agent: "codex",
mode: "oneshot",
sessionKey: "agent:codex:acp:test",
@@ -509,7 +514,9 @@ describe("createAcpxRuntimeService", () => {
await service.start(ctx);
expect(probeAvailability).not.toHaveBeenCalled();
expect(getAcpRuntimeBackend("acpx")).toBeTruthy();
expect(getAcpRuntimeBackend("acpx")).toMatchObject({
runtime: expect.any(Object),
});
await service.stop?.(ctx);
});

View File

@@ -156,6 +156,17 @@ describe("active-memory plugin", () => {
vi
.mocked(api.logger.warn)
.mock.calls.some((call: unknown[]) => String(call[0]).includes(needle));
const expectPrependContextResult = (result: unknown) => {
expect(result).toMatchObject({
prependContext: expect.any(String),
});
};
const requireNonEmptyString = (value: unknown, message: string): string => {
if (typeof value !== "string" || value.length === 0) {
throw new Error(message);
}
return value;
};
beforeEach(async () => {
vi.clearAllMocks();
@@ -931,7 +942,7 @@ describe("active-memory plugin", () => {
);
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
expect(result).toBeDefined();
expectPrependContextResult(result);
});
it("skips sessions whose conversation id is in deniedChatIds even when chat type is allowed", async () => {
@@ -1033,7 +1044,7 @@ describe("active-memory plugin", () => {
);
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
expect(result).toBeDefined();
expectPrependContextResult(result);
});
it("matches per-peer direct session keys (agent:<id>:direct:<peer>)", async () => {
@@ -1057,7 +1068,7 @@ describe("active-memory plugin", () => {
);
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
expect(result).toBeDefined();
expectPrependContextResult(result);
});
it("matches per-account-channel-peer direct session keys (agent:<id>:<channel>:<account>:direct:<peer>)", async () => {
@@ -1082,7 +1093,7 @@ describe("active-memory plugin", () => {
);
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
expect(result).toBeDefined();
expectPrependContextResult(result);
});
it("strips :thread:<id> suffix before matching allowedChatIds (group)", async () => {
@@ -1109,7 +1120,7 @@ describe("active-memory plugin", () => {
);
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
expect(result).toBeDefined();
expectPrependContextResult(result);
});
it("strips :thread:<id> suffix before matching deniedChatIds (direct)", async () => {
@@ -1630,13 +1641,13 @@ describe("active-memory plugin", () => {
const deprecationMessage = warnCalls
.map(([first]) => (typeof first === "string" ? first : ""))
.find((message) => message.includes("config.modelFallbackPolicy is deprecated"));
expect(deprecationMessage).toBeDefined();
const message = requireNonEmptyString(deprecationMessage, "deprecation warning missing");
// Positive: the warning describes chain-resolution last-resort behavior.
expect(deprecationMessage).toContain("chain-resolution");
expect(deprecationMessage).toContain("last-resort");
expect(message).toContain("chain-resolution");
expect(message).toContain("last-resort");
// Negative: the warning explicitly disclaims runtime failover, since
// that's the wrong mental model the previous wording invited.
expect(deprecationMessage).toMatch(/NOT a runtime failover/i);
expect(message).toMatch(/NOT a runtime failover/i);
});
it("does not use a built-in fallback model even when default-remote is configured", async () => {
@@ -1760,9 +1771,9 @@ describe("active-memory plugin", () => {
const debugLine = entries?.[0]?.lines.find((line) =>
line.startsWith("🔎 Active Memory Debug:"),
);
expect(debugLine).toBeDefined();
expect(debugLine).toContain("backend=qmd");
expect(debugLine).toContain("hits=3");
const line = requireNonEmptyString(debugLine, "active memory debug line missing");
expect(line).toContain("backend=qmd");
expect(line).toContain("hits=3");
});
it("replaces stale structured active-memory lines on a later empty run", async () => {
@@ -2033,6 +2044,7 @@ describe("active-memory plugin", () => {
it("returns partial transcript text on timeout when transcripts are temporary by default", async () => {
__testing.setMinimumTimeoutMsForTests(1);
__testing.setSetupGraceTimeoutMsForTests(0);
__testing.setTimeoutPartialDataGraceMsForTests(100);
api.pluginConfig = {
agents: ["main"],
timeoutMs: 250,
@@ -2299,9 +2311,9 @@ describe("active-memory plugin", () => {
maxBytes: 10 * 1024 * 1024,
});
expect(result).toBeTruthy();
expect(result?.length).toBeLessThanOrEqual(128);
expect(result).toContain("alpha beta gamma");
const partialText = requireNonEmptyString(result, "partial assistant text missing");
expect(partialText.length).toBeLessThanOrEqual(128);
expect(partialText).toContain("alpha beta gamma");
expect(readFileSpy).not.toHaveBeenCalled();
});
@@ -2953,9 +2965,9 @@ describe("active-memory plugin", () => {
.mocked(api.logger.info)
.mock.calls.map((call: unknown[]) => String(call[0]));
const startLine = infoLines.find((line: string) => line.includes(" start timeoutMs="));
expect(startLine).toBeTruthy();
expect(startLine && startLine.length < 500).toBe(true);
expect(startLine).toContain("...");
const line = requireNonEmptyString(startLine, "active memory start log line missing");
expect(line.length).toBeLessThan(500);
expect(line).toContain("...");
});
it("uses a canonical agent session key when only sessionId is available", async () => {

View File

@@ -539,9 +539,10 @@ describe("amazon-bedrock provider plugin", () => {
const discovery = pluginJson.configSchema?.properties?.discovery;
const guardrail = pluginJson.configSchema?.properties?.guardrail;
expect(discovery).toBeDefined();
expect(discovery.type).toBe("object");
expect(discovery.additionalProperties).toBe(false);
expect(discovery).toMatchObject({
type: "object",
additionalProperties: false,
});
expect(discovery.properties.enabled).toEqual({ type: "boolean" });
expect(discovery.properties.region).toEqual({ type: "string" });
expect(discovery.properties.providerFilter).toEqual({
@@ -561,9 +562,10 @@ describe("amazon-bedrock provider plugin", () => {
minimum: 1,
});
expect(guardrail).toBeDefined();
expect(guardrail.type).toBe("object");
expect(guardrail.additionalProperties).toBe(false);
expect(guardrail).toMatchObject({
type: "object",
additionalProperties: false,
});
// Required fields
expect(guardrail.required).toEqual(["guardrailIdentifier", "guardrailVersion"]);

View File

@@ -48,7 +48,7 @@ function createModelRegistry(models: ProviderRuntimeModel[]) {
}
describe("anthropic provider replay hooks", () => {
it("registers the claude-cli backend", async () => {
it("registers the claude-cli backend", () => {
const captured = capturePluginRegistration({ register: anthropicPlugin.register });
expect(captured.cliBackends).toContainEqual(
@@ -383,9 +383,11 @@ describe("anthropic provider replay hooks", () => {
const provider = await registerSingleProviderPlugin(anthropicPlugin);
const cliAuth = provider.auth.find((entry) => entry.id === "cli");
expect(cliAuth).toBeDefined();
if (!cliAuth) {
throw new Error("expected Anthropic CLI auth method");
}
const result = await cliAuth?.run({
const result = await cliAuth.run({
config: {},
} as never);

View File

@@ -88,8 +88,7 @@ describe("anthropic stream wrappers", () => {
it("strips context-1m for Claude CLI or legacy token auth and warns", () => {
const warn = vi.spyOn(__testing.log, "warn").mockImplementation(() => undefined);
const headers = runWrapper("sk-ant-oat01-123");
expect(headers?.["anthropic-beta"]).toBeDefined();
expect(headers?.["anthropic-beta"]).toContain(OAUTH_BETA);
expect(headers?.["anthropic-beta"]).toEqual(expect.stringContaining(OAUTH_BETA));
expect(headers?.["anthropic-beta"]).not.toContain(CONTEXT_1M_BETA);
expect(warn).toHaveBeenCalledOnce();
});
@@ -97,8 +96,7 @@ describe("anthropic stream wrappers", () => {
it("keeps context-1m for API key auth", () => {
const warn = vi.spyOn(__testing.log, "warn").mockImplementation(() => undefined);
const headers = runWrapper("sk-ant-api-123");
expect(headers?.["anthropic-beta"]).toBeDefined();
expect(headers?.["anthropic-beta"]).toContain(CONTEXT_1M_BETA);
expect(headers?.["anthropic-beta"]).toEqual(expect.stringContaining(CONTEXT_1M_BETA));
expect(warn).not.toHaveBeenCalled();
});
@@ -165,80 +163,70 @@ describe("createAnthropicThinkingPrefillWrapper", () => {
});
});
describe("createAnthropicFastModeWrapper", () => {
function runFastModeWrapper(params: {
apiKey?: string;
provider?: string;
api?: string;
baseUrl?: string;
enabled?: boolean;
}): Record<string, unknown> | undefined {
return runPayloadWrapper(params, (base) =>
createAnthropicFastModeWrapper(base, params.enabled ?? true),
);
}
type ServiceTierWrapperParams = {
apiKey?: string;
provider?: string;
api?: string;
enabled?: boolean;
serviceTier?: "auto" | "standard_only";
};
it("does not inject service_tier for OAuth token", () => {
const payload = runFastModeWrapper({ apiKey: "sk-ant-oat01-test-token" });
const serviceTierWrapperCases: Array<{
name: string;
run: (params: ServiceTierWrapperParams) => Record<string, unknown> | undefined;
}> = [
{
name: "fast mode",
run: (params) =>
runPayloadWrapper(params, (base) =>
createAnthropicFastModeWrapper(base, params.enabled ?? true),
),
},
{
name: "explicit service tier",
run: (params) =>
runPayloadWrapper(params, (base) =>
createAnthropicServiceTierWrapper(base, params.serviceTier ?? "auto"),
),
},
];
describe("Anthropic service_tier payload wrappers", () => {
it.each(serviceTierWrapperCases)("$name skips service_tier for OAuth token", ({ run }) => {
const payload = run({ apiKey: "sk-ant-oat01-test-token" });
expect(payload?.service_tier).toBeUndefined();
});
it("injects service_tier for regular API keys", () => {
const payload = runFastModeWrapper({ apiKey: "sk-ant-api03-test-key" });
it.each(serviceTierWrapperCases)("$name injects service_tier for regular API keys", ({ run }) => {
const payload = run({ apiKey: "sk-ant-api03-test-key" });
expect(payload?.service_tier).toBe("auto");
});
it("injects service_tier=standard_only when disabled for API keys", () => {
const payload = runFastModeWrapper({ apiKey: "sk-ant-api03-test-key", enabled: false });
it.each(serviceTierWrapperCases)(
"$name does not inject service_tier for non-anthropic provider",
({ run }) => {
const payload = run({
apiKey: "sk-ant-api03-test-key",
provider: "openai",
api: "openai-completions",
});
expect(payload?.service_tier).toBeUndefined();
},
);
it("fast mode injects service_tier=standard_only when disabled for API keys", () => {
const payload = serviceTierWrapperCases[0].run({
apiKey: "sk-ant-api03-test-key",
enabled: false,
});
expect(payload?.service_tier).toBe("standard_only");
});
it("does not inject service_tier for non-anthropic provider", () => {
const payload = runFastModeWrapper({
apiKey: "sk-ant-api03-test-key",
provider: "openai",
api: "openai-completions",
});
expect(payload?.service_tier).toBeUndefined();
});
});
describe("createAnthropicServiceTierWrapper", () => {
function runServiceTierWrapper(params: {
apiKey?: string;
provider?: string;
api?: string;
serviceTier?: "auto" | "standard_only";
}): Record<string, unknown> | undefined {
return runPayloadWrapper(params, (base) =>
createAnthropicServiceTierWrapper(base, params.serviceTier ?? "auto"),
);
}
it("does not inject service_tier for OAuth token", () => {
const payload = runServiceTierWrapper({ apiKey: "sk-ant-oat01-test-token" });
expect(payload?.service_tier).toBeUndefined();
});
it("injects service_tier for regular API keys", () => {
const payload = runServiceTierWrapper({ apiKey: "sk-ant-api03-test-key" });
expect(payload?.service_tier).toBe("auto");
});
it("injects service_tier=standard_only for regular API keys", () => {
const payload = runServiceTierWrapper({
it("explicit service tier injects service_tier=standard_only for regular API keys", () => {
const payload = serviceTierWrapperCases[1].run({
apiKey: "sk-ant-api03-test-key",
serviceTier: "standard_only",
});
expect(payload?.service_tier).toBe("standard_only");
});
it("does not inject service_tier for non-anthropic provider", () => {
const payload = runServiceTierWrapper({
apiKey: "sk-ant-api03-test-key",
provider: "openai",
api: "openai-completions",
});
expect(payload?.service_tier).toBeUndefined();
});
});

View File

@@ -775,8 +775,10 @@ describe("gateway bonjour advertiser", () => {
const disableLog = logger.warn.mock.calls.find(
(call) => typeof call[0] === "string" && call[0].includes("disabling advertiser after"),
);
expect(disableLog).toBeDefined();
expect(String(disableLog?.[0])).toMatch(/restarts within \d+ minutes/);
if (!disableLog) {
throw new Error("expected advertiser disable warning after repeated restarts");
}
expect(String(disableLog[0])).toMatch(/restarts within \d+ minutes/);
const advertiseCallsAtDisable = advertise.mock.calls.length;
const createServiceCallsAtDisable = createService.mock.calls.length;

View File

@@ -211,7 +211,7 @@ describe("cdp.helpers", () => {
});
describe("fetchBrowserJson loopback auth (bridge auth registry)", () => {
it("falls back to per-port bridge auth when config auth is not available", async () => {
it("falls back to per-port bridge auth when config auth is not available", () => {
const port = 18765;
const getBridgeAuthForPort = vi.fn((candidate: number) =>
candidate === port ? { token: "registry-token" } : undefined,

View File

@@ -455,9 +455,11 @@ describe("openCdpWebSocket option handling", () => {
it("clamps a non-finite handshakeTimeoutMs to the default", () => {
// Exercises the Number.isFinite false side of the handshake-timeout
// ternary in openCdpWebSocket.
const ws = openCdpWebSocket("ws://127.0.0.1:1/devtools/browser/X", {
const url = "ws://127.0.0.1:1/devtools/browser/X";
const ws = openCdpWebSocket(url, {
handshakeTimeoutMs: Number.NaN,
});
expect(ws.url).toBe(url);
// Ensure we don't leak the socket even though we never await it.
ws.once("error", () => {});
ws.close();
@@ -466,9 +468,11 @@ describe("openCdpWebSocket option handling", () => {
it("honours an explicit, finite handshakeTimeoutMs", () => {
// Exercises the truthy side of the handshake-timeout ternary: both
// typeof === "number" AND Number.isFinite must be true.
const ws = openCdpWebSocket("ws://127.0.0.1:1/devtools/browser/X", {
const url = "ws://127.0.0.1:1/devtools/browser/X";
const ws = openCdpWebSocket(url, {
handshakeTimeoutMs: 500,
});
expect(ws.url).toBe(url);
ws.once("error", () => {});
ws.close();
});
@@ -476,16 +480,20 @@ describe("openCdpWebSocket option handling", () => {
it("omits the direct-loopback agent for non-loopback targets", () => {
// Exercises the falsy side of `agent ? { agent } : {}` — the loopback
// agent helper returns undefined for non-loopback hosts.
const ws = openCdpWebSocket("ws://93.184.216.34:9222/devtools/browser/X");
const url = "ws://93.184.216.34:9222/devtools/browser/X";
const ws = openCdpWebSocket(url);
expect(ws.url).toBe(url);
ws.once("error", () => {});
ws.close();
});
it("injects custom headers when opts.headers is a non-empty object", () => {
// Exercises the truthy side of `Object.keys(headers).length ? ... : {}`.
const ws = openCdpWebSocket("ws://127.0.0.1:1/devtools/browser/X", {
const url = "ws://127.0.0.1:1/devtools/browser/X";
const ws = openCdpWebSocket(url, {
headers: { "X-Custom": "abc" },
});
expect(ws.url).toBe(url);
ws.once("error", () => {});
ws.close();
});

View File

@@ -94,18 +94,25 @@ beforeEach(() => {
mockState.naturalViewport = { w: 1920, h: 1080, dpr: 1 };
});
function requireSentMessage(method: string) {
const message = sentMessages.find((m) => m.method === method);
if (!message) {
throw new Error(`expected ${method} CDP message`);
}
return message;
}
describe("CDP screenshot params", () => {
it("viewport screenshot omits fromSurface and captureBeyondViewport", async () => {
await captureScreenshot({ wsUrl: "ws://localhost:9222/devtools/page/X", format: "png" });
const call = sentMessages.find((m) => m.method === "Page.captureScreenshot");
expect(call).toBeDefined();
expect(call!.params).toMatchObject({
const call = requireSentMessage("Page.captureScreenshot");
expect(call.params).toMatchObject({
format: "png",
});
expect(call!.params).not.toHaveProperty("fromSurface");
expect(call!.params).not.toHaveProperty("captureBeyondViewport");
expect(call!.params).not.toHaveProperty("clip");
expect(call.params).not.toHaveProperty("fromSurface");
expect(call.params).not.toHaveProperty("captureBeyondViewport");
expect(call.params).not.toHaveProperty("clip");
const emulationCalls = sentMessages.filter(
(m) => m.method === "Emulation.setDeviceMetricsOverride",
@@ -152,10 +159,9 @@ describe("CDP screenshot params", () => {
});
// Clear is called first in the finally block
const clearCall = sentMessages.find((m) => m.method === "Emulation.clearDeviceMetricsOverride");
expect(clearCall).toBeDefined();
const captureCall = sentMessages.find((m) => m.method === "Page.captureScreenshot");
expect(captureCall?.params).toMatchObject({ captureBeyondViewport: true });
requireSentMessage("Emulation.clearDeviceMetricsOverride");
const captureCall = requireSentMessage("Page.captureScreenshot");
expect(captureCall.params).toMatchObject({ captureBeyondViewport: true });
// Viewport drifted after clear → re-apply saved dimensions
expect(secondSetCall.params).toMatchObject({
@@ -183,17 +189,15 @@ describe("CDP screenshot params", () => {
// Only the expand call — no re-apply after clear
expect(setCalls).toHaveLength(1);
const clearCall = sentMessages.find((m) => m.method === "Emulation.clearDeviceMetricsOverride");
expect(clearCall).toBeDefined();
requireSentMessage("Emulation.clearDeviceMetricsOverride");
});
it("fullPage viewport dimensions never shrink below current innerWidth/Height", async () => {
await captureScreenshot({ wsUrl: "ws://localhost:9222/devtools/page/X", fullPage: true });
const expandCall = sentMessages.find((m) => m.method === "Emulation.setDeviceMetricsOverride");
expect(expandCall).toBeDefined();
expect(Number(expandCall!.params!.width)).toBeGreaterThanOrEqual(800);
expect(Number(expandCall!.params!.height)).toBeGreaterThanOrEqual(600);
const expandCall = requireSentMessage("Emulation.setDeviceMetricsOverride");
expect(Number(expandCall.params?.width)).toBeGreaterThanOrEqual(800);
expect(Number(expandCall.params?.height)).toBeGreaterThanOrEqual(600);
});
});

View File

@@ -68,7 +68,7 @@ describe("browser default executable detection", () => {
vi.mocked(os.homedir).mockReturnValue("/Users/test");
});
it("prefers default Chromium browser on macOS", async () => {
it("prefers default Chromium browser on macOS", () => {
mockMacDefaultBrowser("com.google.Chrome", "/Applications/Google Chrome.app");
mockChromeExecutableExists();
@@ -81,7 +81,7 @@ describe("browser default executable detection", () => {
expect(exe?.kind).toBe("chrome");
});
it("detects Edge via LaunchServices bundle ID (com.microsoft.edgemac)", async () => {
it("detects Edge via LaunchServices bundle ID (com.microsoft.edgemac)", () => {
const edgeExecutablePath = "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge";
// macOS LaunchServices registers Edge as "com.microsoft.edgemac", which
// differs from the CFBundleIdentifier "com.microsoft.Edge" in the app's
@@ -127,7 +127,7 @@ describe("browser default executable detection", () => {
expect(exe?.kind).toBe("edge");
});
it("falls back to Chrome when Edge LaunchServices lookup has no app path", async () => {
it("falls back to Chrome when Edge LaunchServices lookup has no app path", () => {
vi.mocked(execFileSync).mockImplementation((cmd, args) => {
const argsStr = Array.isArray(args) ? args.join(" ") : "";
if (cmd === "/usr/bin/plutil" && argsStr.includes("LSHandlers")) {
@@ -150,7 +150,7 @@ describe("browser default executable detection", () => {
expect(exe?.kind).toBe("chrome");
});
it("falls back when default browser is non-Chromium on macOS", async () => {
it("falls back when default browser is non-Chromium on macOS", () => {
mockMacDefaultBrowser("com.apple.Safari");
mockChromeExecutableExists();

View File

@@ -340,6 +340,7 @@ describe("chrome.ts internal", () => {
extraArgs: [],
} as unknown as ResolvedBrowserConfig;
const running = await launchOpenClawChrome(resolved, profile);
expect(running.pid).toBe(4242);
running.proc.kill?.("SIGTERM");
},
});
@@ -925,6 +926,7 @@ describe("chrome.ts internal", () => {
extraArgs: [],
} as unknown as ResolvedBrowserConfig;
const running = await launchOpenClawChrome(resolved, profile);
expect(running.pid).toBe(4242);
running.proc.kill?.("SIGTERM");
},
});
@@ -969,6 +971,7 @@ describe("chrome.ts internal", () => {
extraArgs: [],
} as unknown as ResolvedBrowserConfig;
const running = await launchOpenClawChrome(resolved, profile);
expect(running.pid).toBe(4242);
running.proc.kill?.("SIGTERM");
},
});
@@ -1106,6 +1109,8 @@ describe("chrome.ts internal", () => {
extraArgs: [],
} as unknown as ResolvedBrowserConfig;
const running = await launchOpenClawChrome(resolved, profile);
expect(spawnCount).toBe(2);
expect(running.proc).toBe(runtimeProc);
running.proc.kill?.("SIGTERM");
},
});
@@ -1160,6 +1165,8 @@ describe("chrome.ts internal", () => {
extraArgs: [],
} as unknown as ResolvedBrowserConfig;
const running = await launchOpenClawChrome(resolved, profile);
expect(callCount).toBe(2);
expect(running.proc).toBe(runtimeProc);
running.proc.kill?.("SIGTERM");
},
});
@@ -1213,6 +1220,7 @@ describe("chrome.ts internal", () => {
extraArgs: [],
} as unknown as ResolvedBrowserConfig;
const running = await launchOpenClawChrome(resolved, profile);
expect(running.pid).toBe(4242);
running.proc.kill?.("SIGTERM");
},
});

View File

@@ -17,6 +17,14 @@ import {
} from "./client.js";
describe("browser client", () => {
function requireSnapshotCall(calls: string[]): string {
const call = calls.find((url) => url.includes("/snapshot?"));
if (!call) {
throw new Error("expected browser snapshot request");
}
return call;
}
function stubSnapshotFetch(calls: string[]) {
vi.stubGlobal(
"fetch",
@@ -85,9 +93,7 @@ describe("browser client", () => {
}),
).resolves.toMatchObject({ ok: true, format: "ai" });
const snapshotCall = calls.find((url) => url.includes("/snapshot?"));
expect(snapshotCall).toBeTruthy();
const parsed = new URL(snapshotCall as string);
const parsed = new URL(requireSnapshotCall(calls));
expect(parsed.searchParams.get("labels")).toBe("1");
expect(parsed.searchParams.get("mode")).toBe("efficient");
});
@@ -101,9 +107,7 @@ describe("browser client", () => {
refs: "aria",
});
const snapshotCall = calls.find((url) => url.includes("/snapshot?"));
expect(snapshotCall).toBeTruthy();
const parsed = new URL(snapshotCall as string);
const parsed = new URL(requireSnapshotCall(calls));
expect(parsed.searchParams.get("refs")).toBe("aria");
});
@@ -115,9 +119,7 @@ describe("browser client", () => {
profile: "chrome",
});
const snapshotCall = calls.find((url) => url.includes("/snapshot?"));
expect(snapshotCall).toBeTruthy();
const parsed = new URL(snapshotCall as string);
const parsed = new URL(requireSnapshotCall(calls));
expect(parsed.searchParams.get("format")).toBeNull();
expect(parsed.searchParams.get("profile")).toBe("chrome");
});

View File

@@ -13,9 +13,10 @@ describe("ensureBrowserControlAuth", () => {
expect(result.auth.password).toBeUndefined();
}
describe("trusted-proxy mode", () => {
it("should skip auto-generation in test mode", async () => {
const cfg: OpenClawConfig = {
it.each([
{
name: "trusted-proxy",
cfg: {
gateway: {
auth: {
mode: "trusted-proxy",
@@ -25,35 +26,40 @@ describe("ensureBrowserControlAuth", () => {
},
trustedProxies: ["192.168.1.1"],
},
};
await expectNoAutoGeneratedAuth(cfg);
});
});
describe("password mode", () => {
it("should skip auto-generation in test mode", async () => {
const cfg: OpenClawConfig = {
} satisfies OpenClawConfig,
},
{
name: "password",
cfg: {
gateway: {
auth: {
mode: "password",
},
},
};
await expectNoAutoGeneratedAuth(cfg);
});
});
describe("none mode", () => {
it("should skip auto-generation in test mode", async () => {
const cfg: OpenClawConfig = {
} satisfies OpenClawConfig,
},
{
name: "none",
cfg: {
gateway: {
auth: {
mode: "none",
},
},
};
await expectNoAutoGeneratedAuth(cfg);
});
} satisfies OpenClawConfig,
},
{
name: "token",
cfg: {
gateway: {
auth: {
mode: "token",
},
},
} satisfies OpenClawConfig,
},
])("skips auto-generation in test mode for $name mode", async ({ cfg }) => {
await expectNoAutoGeneratedAuth(cfg);
});
describe("token mode", () => {
@@ -75,23 +81,5 @@ describe("ensureBrowserControlAuth", () => {
expect(result.generatedToken).toBeUndefined();
expect(result.auth.token).toBe("existing-token-123");
});
it("should skip auto-generation in test environment", async () => {
const cfg: OpenClawConfig = {
gateway: {
auth: {
mode: "token",
},
},
};
const result = await ensureBrowserControlAuth({
cfg,
env: { NODE_ENV: "test" },
});
expect(result.generatedToken).toBeUndefined();
expect(result.auth.token).toBeUndefined();
});
});
});

View File

@@ -18,6 +18,7 @@ describeLive("browser (live): remote CDP tab persistence", () => {
await pw.closePlaywrightBrowserConnection().catch(() => {});
const created = await pw.createPageViaPlaywright({ cdpUrl: CDP_URL, url: "about:blank" });
expect(created.targetId).toEqual(expect.any(String));
try {
await waitFor(
async () => {

View File

@@ -331,8 +331,10 @@ describe("pw-tools-core", () => {
});
await Promise.resolve();
expect(responseHandler).toBeDefined();
responseHandler?.(resp);
if (!responseHandler) {
throw new Error("expected Playwright response handler");
}
responseHandler(resp);
const res = await p;
expect(res.url).toBe("https://example.com/api/data");

View File

@@ -67,6 +67,13 @@ const { resolveBrowserConfig, resolveProfile } = await import("./config.js");
const { refreshResolvedBrowserConfigFromDisk, resolveBrowserProfileWithHotReload } =
await import("./resolved-config-refresh.js");
function requireValue<T>(value: T | null | undefined, message: string): T {
if (value == null) {
throw new Error(message);
}
return value;
}
describe("server-context hot-reload profiles", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -76,7 +83,7 @@ describe("server-context hot-reload profiles", () => {
mockState.cachedConfig = null; // Clear simulated cache
});
it("forProfile hot-reloads newly added profiles from config", async () => {
it("forProfile hot-reloads newly added profiles from config", () => {
// Start with only openclaw profile
// 1. Prime the cache by calling getRuntimeConfig() first
const cfg = getRuntimeConfig();
@@ -117,7 +124,7 @@ describe("server-context hot-reload profiles", () => {
expect(profile?.cdpUrl).toBe("http://127.0.0.1:9222");
// 5. Verify the new profile was merged into the cached state
expect(state.resolved.profiles.desktop).toBeDefined();
expect(state.resolved.profiles).toHaveProperty("desktop");
// 6. Verify GLOBAL cache was NOT cleared - subsequent simple getRuntimeConfig() still sees STALE value
// This confirms the fix: we read fresh config for the specific profile lookup without flushing the global cache
@@ -125,7 +132,7 @@ describe("server-context hot-reload profiles", () => {
expect(stillStaleCfg.browser?.profiles?.desktop).toBeUndefined();
});
it("forProfile still throws for profiles that don't exist in fresh config", async () => {
it("forProfile still throws for profiles that don't exist in fresh config", () => {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const state = {
@@ -145,7 +152,7 @@ describe("server-context hot-reload profiles", () => {
).toBeNull();
});
it("forProfile refreshes existing profile config after getRuntimeConfig cache updates", async () => {
it("forProfile refreshes existing profile config after getRuntimeConfig cache updates", () => {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const state = {
@@ -167,7 +174,7 @@ describe("server-context hot-reload profiles", () => {
expect(state.resolved.profiles.openclaw?.cdpPort).toBe(19999);
});
it("listProfiles refreshes config before enumerating profiles", async () => {
it("listProfiles refreshes config before enumerating profiles", () => {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const state = {
@@ -188,11 +195,13 @@ describe("server-context hot-reload profiles", () => {
expect(Object.keys(state.resolved.profiles)).toContain("desktop");
});
it("marks existing runtime state for reconcile when profile invariants change", async () => {
it("marks existing runtime state for reconcile when profile invariants change", () => {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const openclawProfile = resolveProfile(resolved, "openclaw");
expect(openclawProfile).toBeTruthy();
const openclawProfile = requireValue(
resolveProfile(resolved, "openclaw"),
"openclaw profile missing",
);
const state: BrowserServerState = {
server: null,
port: 18791,
@@ -201,7 +210,7 @@ describe("server-context hot-reload profiles", () => {
[
"openclaw",
{
profile: openclawProfile!,
profile: openclawProfile,
running: { pid: 123 } as never,
lastTargetId: "tab-1",
reconcile: null,
@@ -219,19 +228,20 @@ describe("server-context hot-reload profiles", () => {
mode: "cached",
});
const runtime = state.profiles.get("openclaw");
expect(runtime).toBeTruthy();
expect(runtime?.profile.cdpPort).toBe(19999);
expect(runtime?.lastTargetId).toBeNull();
expect(runtime?.reconcile?.reason).toContain("cdpPort");
const runtime = requireValue(state.profiles.get("openclaw"), "openclaw runtime missing");
expect(runtime.profile.cdpPort).toBe(19999);
expect(runtime.lastTargetId).toBeNull();
expect(runtime.reconcile?.reason).toContain("cdpPort");
});
it("marks local managed runtime state for reconcile when profile headless changes", async () => {
it("marks local managed runtime state for reconcile when profile headless changes", () => {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const openclawProfile = resolveProfile(resolved, "openclaw");
expect(openclawProfile).toBeTruthy();
expect(openclawProfile?.headless).toBe(true);
const openclawProfile = requireValue(
resolveProfile(resolved, "openclaw"),
"openclaw profile missing",
);
expect(openclawProfile.headless).toBe(true);
const state: BrowserServerState = {
server: null,
port: 18791,
@@ -240,7 +250,7 @@ describe("server-context hot-reload profiles", () => {
[
"openclaw",
{
profile: openclawProfile!,
profile: openclawProfile,
running: { pid: 123 } as never,
lastTargetId: "tab-1",
reconcile: null,
@@ -262,14 +272,13 @@ describe("server-context hot-reload profiles", () => {
mode: "cached",
});
const runtime = state.profiles.get("openclaw");
expect(runtime).toBeTruthy();
expect(runtime?.profile.headless).toBe(false);
expect(runtime?.lastTargetId).toBeNull();
expect(runtime?.reconcile?.reason).toContain("headless");
const runtime = requireValue(state.profiles.get("openclaw"), "openclaw runtime missing");
expect(runtime.profile.headless).toBe(false);
expect(runtime.lastTargetId).toBeNull();
expect(runtime.reconcile?.reason).toContain("headless");
});
it("marks local managed runtime state for reconcile when profile executablePath changes", async () => {
it("marks local managed runtime state for reconcile when profile executablePath changes", () => {
mockState.cfgProfiles.openclaw = {
cdpPort: 18800,
color: "#FF4500",
@@ -278,9 +287,11 @@ describe("server-context hot-reload profiles", () => {
mockState.cachedConfig = null;
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const openclawProfile = resolveProfile(resolved, "openclaw");
expect(openclawProfile).toBeTruthy();
expect(openclawProfile?.executablePath).toBe("/usr/bin/chrome-old");
const openclawProfile = requireValue(
resolveProfile(resolved, "openclaw"),
"openclaw profile missing",
);
expect(openclawProfile.executablePath).toBe("/usr/bin/chrome-old");
const state: BrowserServerState = {
server: null,
port: 18791,
@@ -289,7 +300,7 @@ describe("server-context hot-reload profiles", () => {
[
"openclaw",
{
profile: openclawProfile!,
profile: openclawProfile,
running: { pid: 123 } as never,
lastTargetId: "tab-1",
reconcile: null,
@@ -311,14 +322,13 @@ describe("server-context hot-reload profiles", () => {
mode: "cached",
});
const runtime = state.profiles.get("openclaw");
expect(runtime).toBeTruthy();
expect(runtime?.profile.executablePath).toBe("/usr/bin/chrome-new");
expect(runtime?.lastTargetId).toBeNull();
expect(runtime?.reconcile?.reason).toContain("executablePath");
const runtime = requireValue(state.profiles.get("openclaw"), "openclaw runtime missing");
expect(runtime.profile.executablePath).toBe("/usr/bin/chrome-new");
expect(runtime.lastTargetId).toBeNull();
expect(runtime.reconcile?.reason).toContain("executablePath");
});
it("does not reconcile existing-session runtime when only headless changes", async () => {
it("does not reconcile existing-session runtime when only headless changes", () => {
mockState.cfgProfiles.remote = {
cdpUrl: "http://127.0.0.1:9222",
color: "#0066CC",
@@ -328,11 +338,13 @@ describe("server-context hot-reload profiles", () => {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const remoteProfile = resolveProfile(resolved, "remote");
expect(remoteProfile).toBeTruthy();
expect(remoteProfile?.driver).toBe("existing-session");
expect(remoteProfile?.attachOnly).toBe(true);
expect(remoteProfile?.headless).toBe(true);
const remoteProfile = requireValue(
resolveProfile(resolved, "remote"),
"remote profile missing",
);
expect(remoteProfile.driver).toBe("existing-session");
expect(remoteProfile.attachOnly).toBe(true);
expect(remoteProfile.headless).toBe(true);
const state: BrowserServerState = {
server: null,
@@ -342,7 +354,7 @@ describe("server-context hot-reload profiles", () => {
[
"remote",
{
profile: remoteProfile!,
profile: remoteProfile,
running: { pid: 456 } as never,
lastTargetId: "tab-remote",
reconcile: null,
@@ -365,15 +377,14 @@ describe("server-context hot-reload profiles", () => {
mode: "cached",
});
const runtime = state.profiles.get("remote");
expect(runtime).toBeTruthy();
expect(runtime?.profile.driver).toBe("existing-session");
expect(runtime?.profile.headless).toBe(false);
expect(runtime?.lastTargetId).toBe("tab-remote");
expect(runtime?.reconcile).toBeNull();
const runtime = requireValue(state.profiles.get("remote"), "remote runtime missing");
expect(runtime.profile.driver).toBe("existing-session");
expect(runtime.profile.headless).toBe(false);
expect(runtime.lastTargetId).toBe("tab-remote");
expect(runtime.reconcile).toBeNull();
});
it("does not reconcile remote cdp runtime when only headless changes", async () => {
it("does not reconcile remote cdp runtime when only headless changes", () => {
mockState.cfgProfiles.remote = {
cdpUrl: "http://10.0.0.42:9222",
color: "#0066CC",
@@ -382,12 +393,14 @@ describe("server-context hot-reload profiles", () => {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const remoteProfile = resolveProfile(resolved, "remote");
expect(remoteProfile).toBeTruthy();
expect(remoteProfile?.driver).toBe("openclaw");
expect(remoteProfile?.attachOnly).toBe(false);
expect(remoteProfile?.cdpIsLoopback).toBe(false);
expect(remoteProfile?.headless).toBe(true);
const remoteProfile = requireValue(
resolveProfile(resolved, "remote"),
"remote profile missing",
);
expect(remoteProfile.driver).toBe("openclaw");
expect(remoteProfile.attachOnly).toBe(false);
expect(remoteProfile.cdpIsLoopback).toBe(false);
expect(remoteProfile.headless).toBe(true);
const state: BrowserServerState = {
server: null,
@@ -397,7 +410,7 @@ describe("server-context hot-reload profiles", () => {
[
"remote",
{
profile: remoteProfile!,
profile: remoteProfile,
running: { pid: 789 } as never,
lastTargetId: "tab-remote-cdp",
reconcile: null,
@@ -419,12 +432,11 @@ describe("server-context hot-reload profiles", () => {
mode: "cached",
});
const runtime = state.profiles.get("remote");
expect(runtime).toBeTruthy();
expect(runtime?.profile.driver).toBe("openclaw");
expect(runtime?.profile.cdpIsLoopback).toBe(false);
expect(runtime?.profile.headless).toBe(false);
expect(runtime?.lastTargetId).toBe("tab-remote-cdp");
expect(runtime?.reconcile).toBeNull();
const runtime = requireValue(state.profiles.get("remote"), "remote runtime missing");
expect(runtime.profile.driver).toBe("openclaw");
expect(runtime.profile.cdpIsLoopback).toBe(false);
expect(runtime.profile.headless).toBe(false);
expect(runtime.lastTargetId).toBe("tab-remote-cdp");
expect(runtime.reconcile).toBeNull();
});
});

View File

@@ -17,9 +17,11 @@ describe("browser manage start timeout option", () => {
await program.parseAsync(["browser", "--timeout", "60000", "start"], { from: "user" });
const startCall = findBrowserManageCall("/start");
expect(startCall).toBeDefined();
expect(startCall?.[0]).toMatchObject({ timeout: "60000" });
expect(startCall?.[2]).toBeUndefined();
if (!startCall) {
throw new Error("expected browser /start call");
}
expect(startCall[0]).toMatchObject({ timeout: "60000" });
expect(startCall[2]).toBeUndefined();
});
it("passes headless=true for browser start --headless", async () => {

View File

@@ -46,7 +46,6 @@ describe("browser state option collisions", () => {
const getLastRequest = () => {
const call = mocks.callBrowserRequest.mock.calls.at(-1);
expect(call).toBeDefined();
if (!call) {
throw new Error("expected browser request call");
}
@@ -101,9 +100,7 @@ describe("browser state option collisions", () => {
],
{ from: "user" },
);
const call = mocks.callBrowserRequest.mock.calls.at(-1);
expect(call).toBeDefined();
const request = call![1] as { body?: { cookie?: { url?: string } } };
const request = getLastRequest() as { body?: { cookie?: { url?: string } } };
expect(request.body?.cookie?.url).toBe("https://example.com");
});
@@ -113,9 +110,7 @@ describe("browser state option collisions", () => {
["browser", "--url", "https://inherited.example.com", "cookies", "set", "session", "abc"],
{ from: "user" },
);
const call = mocks.callBrowserRequest.mock.calls.at(-1);
expect(call).toBeDefined();
const request = call![1] as { body?: { cookie?: { url?: string } } };
const request = getLastRequest() as { body?: { cookie?: { url?: string } } };
expect(request.body?.cookie?.url).toBe("https://inherited.example.com");
});

View File

@@ -72,8 +72,10 @@ describe("registerBrowserCli lazy browser subcommands", () => {
expect(browser?.commands.map((command) => command.name())).toContain("status");
expect(browser?.commands.map((command) => command.name())).toContain("snapshot");
const doctor = browser?.commands.find((command) => command.name() === "doctor");
expect(doctor).toBeDefined();
expect(doctor?.options.map((option) => option.long)).toContain("--deep");
if (!doctor) {
throw new Error("expected browser doctor command placeholder");
}
expect(doctor.options.map((option) => option.long)).toContain("--deep");
expect(manageMocks.registerBrowserManageCommands).not.toHaveBeenCalled();
expect(inspectMocks.registerBrowserInspectCommands).not.toHaveBeenCalled();
expect(actionInputMocks.registerBrowserActionInputCommands).not.toHaveBeenCalled();

View File

@@ -70,8 +70,12 @@ describe("canvas a2ui copy", () => {
await copyA2uiAssets({ srcDir, outDir });
await expect(fs.stat(path.join(outDir, "index.html"))).resolves.toBeTruthy();
await expect(fs.stat(path.join(outDir, "a2ui.bundle.js"))).resolves.toBeTruthy();
await expect(fs.readFile(path.join(outDir, "index.html"), "utf8")).resolves.toBe(
"<html></html>",
);
await expect(fs.readFile(path.join(outDir, "a2ui.bundle.js"), "utf8")).resolves.toBe(
"console.log(1);",
);
});
});
});

View File

@@ -333,7 +333,9 @@ describe("canvas host", () => {
try {
const watcher = watcherState.watchers[watcherStart];
expect(watcher).toBeTruthy();
if (!watcher) {
throw new Error("expected Canvas host watcher");
}
const upgraded = handler.handleUpgrade(
{ url: CANVAS_WS_PATH } as IncomingMessage,
{} as Duplex,
@@ -342,12 +344,14 @@ describe("canvas host", () => {
expect(upgraded).toBe(true);
expect(TrackingWebSocketServerClass.latestInstance?.connectionCount).toBe(1);
const ws = TrackingWebSocketServerClass.latestSocket;
expect(ws).toBeTruthy();
if (!ws) {
throw new Error("expected Canvas host websocket");
}
await fs.writeFile(index, "<html><body>v2</body></html>", "utf8");
watcher.__emit("all", "change", index);
await reloadSent;
expect(ws?.sent[0]).toBe("reload");
expect(ws.sent[0]).toBe("reload");
} finally {
await handler.close();
}

View File

@@ -362,7 +362,7 @@ describe("CodexAppServerClient", () => {
);
});
it("does not write to stdin after the child process exits", async () => {
it("does not write to stdin after the child process exits", () => {
const harness = createClientHarness();
clients.push(harness.client);

View File

@@ -784,14 +784,18 @@ allowed_sandbox_modes = ["read-only", "workspace-write"]
expect(manifestKeys).toEqual([...CODEX_APP_SERVER_CONFIG_KEYS].toSorted());
for (const key of CODEX_APP_SERVER_CONFIG_KEYS) {
expect(manifest.uiHints[`appServer.${key}`]).toBeTruthy();
expect(manifest.uiHints[`appServer.${key}`]).toMatchObject({
label: expect.any(String),
});
}
const computerUseManifestKeys = Object.keys(
manifest.configSchema.properties.computerUse.properties,
).toSorted();
expect(computerUseManifestKeys).toEqual([...CODEX_COMPUTER_USE_CONFIG_KEYS].toSorted());
for (const key of CODEX_COMPUTER_USE_CONFIG_KEYS) {
expect(manifest.uiHints[`computerUse.${key}`]).toBeTruthy();
expect(manifest.uiHints[`computerUse.${key}`]).toMatchObject({
label: expect.any(String),
});
}
const codexPluginsProperties = manifest.configSchema.properties.codexPlugins;
const codexPluginsManifestKeys = Object.keys(codexPluginsProperties.properties).toSorted();

View File

@@ -911,10 +911,17 @@ describe("createCodexDynamicToolBridge", () => {
},
{ signal: callController.signal },
);
await vi.waitFor(() => expect(capturedSignal).toBeDefined());
await vi.waitFor(() => {
if (!capturedSignal) {
throw new Error("expected dynamic tool call signal");
}
});
if (!capturedSignal) {
throw new Error("expected dynamic tool call signal");
}
callController.abort(new Error("deadline"));
expect(capturedSignal?.aborted).toBe(true);
expect(capturedSignal.aborted).toBe(true);
resolveTool?.(textToolResult("done"));
await expect(result).resolves.toEqual(expectInputText("done"));

View File

@@ -57,7 +57,8 @@ describe("codex app-server session binding", () => {
modelProvider: "openai",
dynamicToolsFingerprint: "tools-v1",
});
await expect(fs.stat(resolveCodexAppServerBindingPath(sessionFile))).resolves.toBeTruthy();
const bindingStat = await fs.stat(resolveCodexAppServerBindingPath(sessionFile));
expect(bindingStat.isFile()).toBe(true);
});
it("round-trips plugin app policy context with app ids as record keys", async () => {

View File

@@ -73,8 +73,10 @@ function readDiagnosticsConfirmationToken(
): string {
const text = result.text ?? "";
const token = new RegExp(`${escapeRegExp(commandPrefix)} confirm ([a-f0-9]{12})`).exec(text)?.[1];
expect(token).toBeTruthy();
return token as string;
if (!token) {
throw new Error(`expected ${commandPrefix} confirmation token in command output`);
}
return token;
}
function escapeRegExp(value: string): string {

View File

@@ -12,7 +12,7 @@ describe("codex package manifest", () => {
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
) as CodexPackageManifest;
expect(packageJson.dependencies?.["@mariozechner/pi-coding-agent"]).toBeDefined();
expect(packageJson.dependencies).toHaveProperty("@mariozechner/pi-coding-agent");
expect(packageJson.dependencies?.["@openai/codex"]).toBe(
MANAGED_CODEX_APP_SERVER_PACKAGE_VERSION,
);

View File

@@ -31,6 +31,14 @@ function withPluginsEnabled<T>(cfg: T): T {
} as T;
}
function requireProvider<T extends { id: string }>(providers: T[], id: string): T {
const provider = providers.find((entry) => entry.id === id);
if (!provider) {
throw new Error(`expected ${id} provider to be registered`);
}
return provider;
}
describeLive("comfy live", () => {
let cfg = {} as OpenClawConfig;
let agentDir = "";
@@ -62,9 +70,8 @@ describeLive("comfy live", () => {
it.skipIf(!isComfyCapabilityConfigured({ cfg: cfg as never, agentDir, capability: "image" }))(
"runs an image workflow",
async () => {
const provider = imageProviders.find((entry) => entry.id === "comfy");
expect(provider).toBeDefined();
const result = await provider!.generateImage({
const provider = requireProvider(imageProviders, "comfy");
const result = await provider.generateImage({
provider: "comfy",
model: "workflow",
prompt: "A tiny orange lobster icon on a clean background.",
@@ -81,9 +88,8 @@ describeLive("comfy live", () => {
it.skipIf(!isComfyCapabilityConfigured({ cfg: cfg as never, agentDir, capability: "video" }))(
"runs a video workflow",
async () => {
const provider = videoProviders.find((entry) => entry.id === "comfy");
expect(provider).toBeDefined();
const result = await provider!.generateVideo({
const provider = requireProvider(videoProviders, "comfy");
const result = await provider.generateVideo({
provider: "comfy",
model: "workflow",
prompt: "A tiny paper lobster gently waving, cinematic motion.",
@@ -100,9 +106,8 @@ describeLive("comfy live", () => {
it.skipIf(!isComfyCapabilityConfigured({ cfg: cfg as never, agentDir, capability: "music" }))(
"runs a music workflow",
async () => {
const provider = musicProviders.find((entry) => entry.id === "comfy");
expect(provider).toBeDefined();
const result = await provider!.generateMusic({
const provider = requireProvider(musicProviders, "comfy");
const result = await provider.generateMusic({
provider: "comfy",
model: "workflow",
prompt: "A gentle ambient synth loop with warm analog pads.",

View File

@@ -60,6 +60,7 @@ describeLive("deepgram live", () => {
outputFormat: "ulaw_8000",
timeoutMs: 30_000,
});
expect(speech.byteLength).toBeGreaterThan(0);
await runRealtimeSttLiveTest({
provider,

View File

@@ -44,8 +44,7 @@ describe("DeepInfra provider config", () => {
it("sets DeepInfra alias on the provided model ref", () => {
const result = applyDeepInfraProviderConfig(emptyCfg, DEEPINFRA_DEFAULT_MODEL_REF);
const agentModel = result.agents?.defaults?.models?.[DEEPINFRA_DEFAULT_MODEL_REF];
expect(agentModel).toBeDefined();
expect(agentModel?.alias).toBe("DeepInfra");
expect(agentModel).toMatchObject({ alias: "DeepInfra" });
});
it("attaches the alias to a non-default model ref when provided", () => {

View File

@@ -142,18 +142,16 @@ describeLive("deepseek plugin live", () => {
};
let capturedPayload: Record<string, unknown> | undefined;
const streamFn = createDeepSeekV4ThinkingWrapper(streamSimple, "high");
expect(streamFn).toBeDefined();
const stream = streamFn?.(resolveDeepSeekV4LiveModel(), context, {
const stream = streamFn(resolveDeepSeekV4LiveModel(), context, {
apiKey: DEEPSEEK_KEY,
maxTokens: 64,
onPayload: (payload) => {
capturedPayload = payload as Record<string, unknown>;
},
});
expect(stream).toBeDefined();
const result = await (await stream!).result();
const result = await (await stream).result();
if (result.stopReason === "error") {
throw new Error(result.errorMessage || "DeepSeek V4 replay returned error with no message");
}
@@ -204,18 +202,16 @@ describeLive("deepseek plugin live", () => {
};
let capturedPayload: Record<string, unknown> | undefined;
const streamFn = createDeepSeekV4ThinkingWrapper(streamSimple, "high");
expect(streamFn).toBeDefined();
const stream = streamFn?.(resolveDeepSeekV4LiveModel(), context, {
const stream = streamFn(resolveDeepSeekV4LiveModel(), context, {
apiKey: DEEPSEEK_KEY,
maxTokens: 64,
onPayload: (payload) => {
capturedPayload = payload as Record<string, unknown>;
},
});
expect(stream).toBeDefined();
const result = await (await stream!).result();
const result = await (await stream).result();
if (result.stopReason === "error") {
throw new Error(
result.errorMessage || "DeepSeek V4 plain replay returned error with no message",

View File

@@ -119,6 +119,16 @@ function createPayloadCapturingStream(capture: PayloadCapture) {
};
}
function requireThinkingWrapper(
wrapper: ReturnType<typeof createDeepSeekV4ThinkingWrapper>,
label: string,
): NonNullable<ReturnType<typeof createDeepSeekV4ThinkingWrapper>> {
if (!wrapper) {
throw new Error(`expected DeepSeek thinking wrapper for ${label}`);
}
return wrapper;
}
describe("deepseek provider plugin", () => {
it("registers DeepSeek with api-key auth wizard metadata", async () => {
const provider = await registerSingleProviderPlugin(deepseekPlugin);
@@ -225,9 +235,11 @@ describe("deepseek provider plugin", () => {
return stream;
};
const wrapThinkingOff = createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "off");
expect(wrapThinkingOff).toBeDefined();
await wrapThinkingOff?.(
const wrapThinkingOff = requireThinkingWrapper(
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "off"),
"off",
);
await wrapThinkingOff(
{
provider: "deepseek",
id: "deepseek-v4-pro",
@@ -240,9 +252,11 @@ describe("deepseek provider plugin", () => {
expect(capturedPayload).toMatchObject({ thinking: { type: "disabled" } });
expect(capturedPayload).not.toHaveProperty("reasoning_effort");
const wrapThinkingXhigh = createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "xhigh");
expect(wrapThinkingXhigh).toBeDefined();
await wrapThinkingXhigh?.(
const wrapThinkingXhigh = requireThinkingWrapper(
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "xhigh"),
"xhigh",
);
await wrapThinkingXhigh(
{
provider: "deepseek",
id: "deepseek-v4-pro",
@@ -264,9 +278,11 @@ describe("deepseek provider plugin", () => {
const context = deepSeekReasoningToolReplayContext();
const baseStreamFn = createPayloadCapturingStream(capture);
const wrapThinkingHigh = createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high");
expect(wrapThinkingHigh).toBeDefined();
await wrapThinkingHigh?.(model, context, {});
const wrapThinkingHigh = requireThinkingWrapper(
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high"),
"high",
);
await wrapThinkingHigh(model, context, {});
expect(capture.payload).toMatchObject({
thinking: { type: "enabled" },
@@ -301,9 +317,11 @@ describe("deepseek provider plugin", () => {
);
const baseStreamFn = createPayloadCapturingStream(capture);
const wrapThinkingHigh = createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high");
expect(wrapThinkingHigh).toBeDefined();
await wrapThinkingHigh?.(model, context, {});
const wrapThinkingHigh = requireThinkingWrapper(
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high"),
"high",
);
await wrapThinkingHigh(model, context, {});
expect((capture.payload?.messages as Array<Record<string, unknown>>)[1]).toMatchObject({
role: "assistant",
@@ -338,9 +356,11 @@ describe("deepseek provider plugin", () => {
} as Context;
const baseStreamFn = createPayloadCapturingStream(capture);
const wrapThinkingHigh = createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high");
expect(wrapThinkingHigh).toBeDefined();
await wrapThinkingHigh?.(model, context, {});
const wrapThinkingHigh = requireThinkingWrapper(
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "high"),
"high",
);
await wrapThinkingHigh(model, context, {});
expect((capture.payload?.messages as Array<Record<string, unknown>>)[1]).toMatchObject({
role: "assistant",
@@ -355,12 +375,11 @@ describe("deepseek provider plugin", () => {
const context = deepSeekReasoningToolReplayContext();
const baseStreamFn = createPayloadCapturingStream(capture);
const wrapThinkingNone = createDeepSeekV4ThinkingWrapper(
baseStreamFn as never,
"none" as never,
const wrapThinkingNone = requireThinkingWrapper(
createDeepSeekV4ThinkingWrapper(baseStreamFn as never, "none" as never),
"none",
);
expect(wrapThinkingNone).toBeDefined();
await wrapThinkingNone?.(model, context, {});
await wrapThinkingNone(model, context, {});
expect(capture.payload).toMatchObject({ thinking: { type: "disabled" } });
expect(capture.payload).not.toHaveProperty("reasoning_effort");

View File

@@ -131,7 +131,9 @@ describe("PlaywrightDiffScreenshotter", () => {
expect(pages).toHaveLength(1);
expect(pages[0]?.pdf).toHaveBeenCalledTimes(1);
const pdfCall = pages[0]?.pdf.mock.calls[0]?.[0] as Record<string, unknown> | undefined;
expect(pdfCall).toBeDefined();
if (!pdfCall) {
throw new Error("expected PDF render call");
}
expect(pdfCall).not.toHaveProperty("pageRanges");
expect(pages[0]?.screenshot).toHaveBeenCalledTimes(0);
await expect(fs.readFile(pdfPath, "utf8")).resolves.toContain("%PDF-1.7");

View File

@@ -11,6 +11,6 @@ describe("diffs package manifest", () => {
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
) as DiffsPackageManifest;
expect(packageJson.dependencies?.["@pierre/diffs"]).toBeDefined();
expect(packageJson.dependencies).toHaveProperty("@pierre/diffs");
});
});

View File

@@ -38,7 +38,9 @@ describe("diffs tool", () => {
const text = readTextContent(result, 0);
expect(text).toContain("http://127.0.0.1:18789/plugins/diffs/view/");
expect((result?.details as Record<string, unknown>).viewerUrl).toBeDefined();
expect(readDetails(result).viewerUrl).toEqual(
expect.stringContaining("http://127.0.0.1:18789/plugins/diffs/view/"),
);
});
it("uses configured viewerBaseUrl when tool input omits baseUrl", async () => {
@@ -92,16 +94,15 @@ describe("diffs tool", () => {
);
});
it("does not expose reserved format in the tool schema", async () => {
it("does not expose reserved format in the tool schema", () => {
const tool = createDiffsTool({
api: createApi(),
store,
defaults: DEFAULT_DIFFS_TOOL_DEFAULTS,
});
const parameters = tool.parameters as { properties?: Record<string, unknown> };
expect(parameters.properties).toBeDefined();
expect(parameters.properties).not.toHaveProperty("format");
const properties = readParametersProperties(tool.parameters);
expect(properties).not.toHaveProperty("format");
});
it("returns an image artifact in image mode", async () => {
@@ -132,16 +133,17 @@ describe("diffs tool", () => {
expect(readTextContent(result, 0)).toContain("Diff PNG generated at:");
expect(readTextContent(result, 0)).toContain("Use the `message` tool");
expect(result?.content).toHaveLength(1);
expect((result?.details as Record<string, unknown>).filePath).toBeDefined();
expect((result?.details as Record<string, unknown>).imagePath).toBeDefined();
expect((result?.details as Record<string, unknown>).format).toBe("png");
expect((result?.details as Record<string, unknown>).fileQuality).toBe("standard");
expect((result?.details as Record<string, unknown>).imageQuality).toBe("standard");
expect((result?.details as Record<string, unknown>).fileScale).toBe(2);
expect((result?.details as Record<string, unknown>).imageScale).toBe(2);
expect((result?.details as Record<string, unknown>).fileMaxWidth).toBe(960);
expect((result?.details as Record<string, unknown>).imageMaxWidth).toBe(960);
expect((result?.details as Record<string, unknown>).viewerUrl).toBeUndefined();
const details = readDetails(result);
expect(requireString(details.filePath, "filePath")).toMatch(/preview\.png$/);
expect(requireString(details.imagePath, "imagePath")).toMatch(/preview\.png$/);
expect(details.format).toBe("png");
expect(details.fileQuality).toBe("standard");
expect(details.imageQuality).toBe("standard");
expect(details.fileScale).toBe(2);
expect(details.imageScale).toBe(2);
expect(details.fileMaxWidth).toBe(960);
expect(details.imageMaxWidth).toBe(960);
expect(details.viewerUrl).toBeUndefined();
expect(cleanupSpy).toHaveBeenCalledTimes(1);
});
@@ -206,8 +208,8 @@ describe("diffs tool", () => {
mode: "file",
ttlSeconds: 1,
});
const filePath = (result?.details as Record<string, unknown>).filePath as string;
await expect(fs.stat(filePath)).resolves.toBeDefined();
const filePath = requireString(readDetails(result).filePath, "filePath");
await fs.access(filePath);
vi.setSystemTime(new Date(now.getTime() + 2_000));
await store.cleanupExpired();
@@ -564,6 +566,32 @@ function createPdfScreenshotter(
return { screenshotHtml };
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function readDetails(result: unknown): Record<string, unknown> {
const details = (result as { details?: unknown } | null | undefined)?.details;
if (!isRecord(details)) {
throw new Error("expected diffs tool result details");
}
return details;
}
function readParametersProperties(parameters: unknown): Record<string, unknown> {
if (isRecord(parameters) && isRecord(parameters.properties)) {
return parameters.properties;
}
throw new Error("expected diffs tool parameter properties");
}
function requireString(value: unknown, label: string): string {
if (typeof value !== "string" || value.length === 0) {
throw new Error(`expected ${label}`);
}
return value;
}
function readTextContent(result: unknown, index: number): string {
const content = (result as { content?: Array<{ type?: string; text?: string }> } | undefined)
?.content;

View File

@@ -18,26 +18,63 @@ afterEach(() => {
vi.unstubAllEnvs();
});
describe("resolveDiscordAccount allowFrom precedence", () => {
it("uses configured defaultAccount when accountId is omitted", () => {
const resolved = resolveDiscordAccount({
cfg: {
channels: {
discord: {
defaultAccount: "work",
accounts: {
work: { token: "token-work", name: "Work" },
const defaultAccountOmissionCases = [
{
name: "resolveDiscordAccount",
assert: () => {
const resolved = resolveDiscordAccount({
cfg: {
channels: {
discord: {
defaultAccount: "work",
accounts: {
work: { token: "token-work", name: "Work" },
},
},
},
},
},
});
});
expect(resolved.accountId).toBe("work");
expect(resolved.name).toBe("Work");
expect(resolved.token).toBe("token-work");
});
expect(resolved.accountId).toBe("work");
expect(resolved.name).toBe("Work");
expect(resolved.token).toBe("token-work");
},
},
{
name: "createDiscordActionGate",
assert: () => {
const gate = createDiscordActionGate({
cfg: {
channels: {
discord: {
actions: { reactions: false },
defaultAccount: "work",
accounts: {
work: {
token: "token-work",
actions: { reactions: true },
},
},
},
},
},
});
expect(gate("reactions")).toBe(true);
},
},
];
describe("Discord defaultAccount omission contract", () => {
it.each(defaultAccountOmissionCases)(
"$name uses configured defaultAccount when accountId is omitted",
({ assert }) => {
assert();
},
);
});
describe("resolveDiscordAccount allowFrom precedence", () => {
it("prefers accounts.default.allowFrom over top-level for default account", () => {
const resolved = resolveDiscordAccount({
cfg: {
@@ -93,29 +130,6 @@ describe("resolveDiscordAccount allowFrom precedence", () => {
});
});
describe("createDiscordActionGate", () => {
it("uses configured defaultAccount when accountId is omitted", () => {
const gate = createDiscordActionGate({
cfg: {
channels: {
discord: {
actions: { reactions: false },
defaultAccount: "work",
accounts: {
work: {
token: "token-work",
actions: { reactions: true },
},
},
},
},
},
});
expect(gate("reactions")).toBe(true);
});
});
describe("resolveDiscordMaxLinesPerMessage", () => {
it("falls back to merged root discord maxLinesPerMessage when runtime config omits it", () => {
const resolved = resolveDiscordMaxLinesPerMessage({

View File

@@ -11,8 +11,10 @@ const fetchChannelPermissionsDiscordMock = vi.fn();
function readDiscordGuilds(cfg: OpenClawConfig) {
const guilds = cfg.channels?.discord?.guilds;
expect(guilds).toBeDefined();
return guilds ?? {};
if (!guilds) {
throw new Error("expected discord guilds config");
}
return guilds;
}
describe("discord audit", () => {

View File

@@ -26,7 +26,9 @@ describe("discord channel message adapter", () => {
it("backs declared durable-final capabilities with outbound send proofs", async () => {
const adapter = discordPlugin.message;
expect(adapter).toBeDefined();
if (!adapter) {
throw new Error("Expected discord plugin to expose a channel message adapter");
}
const proveText = async () => {
resetDiscordOutboundMocks(hoisted);

View File

@@ -135,10 +135,15 @@ describe("discord component registry", () => {
});
const confirm = result.entries.find((entry) => entry.label === "Confirm");
const cancel = result.entries.find((entry) => entry.label === "Cancel");
expect(confirm?.consumptionGroupId).toBeTruthy();
expect(cancel?.consumptionGroupId).toBe(confirm?.consumptionGroupId);
if (!confirm?.consumptionGroupId) {
throw new Error("expected confirm entry to carry a consumption group id");
}
if (!cancel) {
throw new Error("expected cancel entry");
}
expect(cancel.consumptionGroupId).toBe(confirm.consumptionGroupId);
expect(confirm?.consumptionGroupEntryIds).toEqual(
expect.arrayContaining([confirm?.id, cancel?.id]),
expect.arrayContaining([confirm.id, cancel.id]),
);
registerDiscordComponentEntries({

View File

@@ -8,13 +8,19 @@ import {
scanDiscordNumericIdEntries,
} from "./doctor.js";
function getDiscordCompatibilityNormalizer(): NonNullable<
typeof discordDoctor.normalizeCompatibilityConfig
> {
const normalize = discordDoctor.normalizeCompatibilityConfig;
if (!normalize) {
throw new Error("Expected discord doctor to expose normalizeCompatibilityConfig");
}
return normalize;
}
describe("discord doctor", () => {
it("normalizes legacy discord streaming aliases for runtime config", () => {
const normalize = discordDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
return;
}
const normalize = getDiscordCompatibilityNormalizer();
const result = normalize({
cfg: {
@@ -76,11 +82,7 @@ describe("discord doctor", () => {
});
it("moves account voice.tts.edge into providers.microsoft", () => {
const normalize = discordDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
return;
}
const normalize = getDiscordCompatibilityNormalizer();
const result = normalize({
cfg: {
@@ -117,11 +119,7 @@ describe("discord doctor", () => {
});
it("moves legacy guild channel allow toggles into enabled", () => {
const normalize = discordDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
return;
}
const normalize = getDiscordCompatibilityNormalizer();
const result = normalize({
cfg: {
@@ -169,11 +167,7 @@ describe("discord doctor", () => {
});
it("moves legacy guild channel agentId into a top-level route binding", () => {
const normalize = discordDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
return;
}
const normalize = getDiscordCompatibilityNormalizer();
const result = normalize({
cfg: {
@@ -213,11 +207,7 @@ describe("discord doctor", () => {
});
it("moves account-scoped guild channel agentId into an account-scoped route binding", () => {
const normalize = discordDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
return;
}
const normalize = getDiscordCompatibilityNormalizer();
const result = normalize({
cfg: {
@@ -263,11 +253,7 @@ describe("discord doctor", () => {
});
it("removes legacy guild channel agentId when a matching route binding already exists", () => {
const normalize = discordDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
return;
}
const normalize = getDiscordCompatibilityNormalizer();
const existingBinding = {
agentId: "video",

View File

@@ -544,7 +544,7 @@ describe("RequestClient", () => {
);
});
it("serializes message multipart uploads with payload_json", async () => {
it("serializes message multipart uploads with payload_json", () => {
const headers = new Headers();
const body = serializeRequestBody(
{

View File

@@ -253,6 +253,20 @@ describe("discord native /think autocomplete", () => {
} as OpenClawConfig;
}
function requireThinkLevelCommand() {
const command = findCommandByNativeName("think", "discord", {
includeBundledChannelFallback: false,
});
if (!command) {
throw new Error("expected Discord /think command");
}
const levelArg = command.args?.find((entry) => entry.name === "level");
if (!levelArg) {
throw new Error("expected Discord /think level arg");
}
return { command, levelArg };
}
it("uses the session override context for /think choices", async () => {
const cfg = createConfig();
const interaction = {
@@ -269,15 +283,7 @@ describe("discord native /think autocomplete", () => {
respond: (choices: Array<{ name: string; value: string }>) => Promise<void>;
};
const command = findCommandByNativeName("think", "discord", {
includeBundledChannelFallback: false,
});
expect(command).toBeTruthy();
const levelArg = command?.args?.find((entry) => entry.name === "level");
expect(levelArg).toBeTruthy();
if (!command || !levelArg) {
return;
}
const { command, levelArg } = requireThinkLevelCommand();
const context = await resolveDiscordNativeChoiceContext({
interaction,
@@ -346,15 +352,7 @@ describe("discord native /think autocomplete", () => {
accountId: "default",
threadBindings: createNoopThreadBindingManager("default"),
});
const command = findCommandByNativeName("think", "discord", {
includeBundledChannelFallback: false,
});
const levelArg = command?.args?.find((entry) => entry.name === "level");
expect(command).toBeTruthy();
expect(levelArg).toBeTruthy();
if (!command || !levelArg) {
return;
}
const { command, levelArg } = requireThinkLevelCommand();
const choices = resolveCommandArgChoices({
command,
@@ -401,15 +399,7 @@ describe("discord native /think autocomplete", () => {
expect(context).toBeNull();
expect(ensureConfiguredBindingRouteReadyMock).toHaveBeenCalledTimes(1);
const command = findCommandByNativeName("think", "discord", {
includeBundledChannelFallback: false,
});
const levelArg = command?.args?.find((entry) => entry.name === "level");
expect(command).toBeTruthy();
expect(levelArg).toBeTruthy();
if (!command || !levelArg) {
return;
}
const { command, levelArg } = requireThinkLevelCommand();
const choices = resolveCommandArgChoices({
command,
arg: levelArg,

View File

@@ -315,8 +315,10 @@ describe("runDiscordGatewayLifecycle", () => {
),
).toBe(true);
expect(resolveWait).toBeDefined();
resolveWait?.();
if (!resolveWait) {
throw new Error("expected lifecycle wait resolver");
}
resolveWait();
await expect(lifecyclePromise).resolves.toBeUndefined();
});

View File

@@ -18,8 +18,10 @@ function createGatewayInfoBody(overrides?: {
}
function resolveGatewayInfoFetch(resolve: ((value: Response) => void) | undefined): void {
expect(resolve).toBeDefined();
resolve!({
if (!resolve) {
throw new Error("expected pending gateway info fetch resolver");
}
resolve({
ok: true,
status: 200,
text: async () => createGatewayInfoBody(),
@@ -449,7 +451,7 @@ describe("createDiscordGatewayPlugin", () => {
);
});
it("uses proxy agent for gateway WebSocket when configured", async () => {
it("uses proxy agent for gateway WebSocket when configured", () => {
const runtime = createRuntime();
const plugin = createDiscordGatewayPlugin({
@@ -473,7 +475,7 @@ describe("createDiscordGatewayPlugin", () => {
expect(runtime.error).not.toHaveBeenCalled();
});
it("falls back to the default gateway plugin when proxy is invalid", async () => {
it("falls back to the default gateway plugin when proxy is invalid", () => {
const runtime = createRuntime();
const plugin = createDiscordGatewayPlugin({
@@ -535,7 +537,7 @@ describe("createDiscordGatewayPlugin", () => {
expect(runtime.error).not.toHaveBeenCalled();
});
it("falls back to the default gateway plugin when proxy is remote", async () => {
it("falls back to the default gateway plugin when proxy is remote", () => {
const runtime = createRuntime();
const plugin = createDiscordGatewayPlugin({

View File

@@ -79,7 +79,7 @@ describe("resolveDiscordRestFetch", () => {
expect(runtime.error).not.toHaveBeenCalled();
});
it("falls back to global fetch when proxy URL is invalid", async () => {
it("falls back to global fetch when proxy URL is invalid", () => {
const runtime = {
log: vi.fn(),
error: vi.fn(),

View File

@@ -6,6 +6,8 @@ import {
resolveDiscordThreadStarter,
} from "./threading.js";
type ResolvedThreadStarter = NonNullable<Awaited<ReturnType<typeof resolveDiscordThreadStarter>>>;
type ThreadStarterRestMessage = {
content?: string | null;
embeds?: Array<{ title?: string | null; description?: string | null }>;
@@ -65,6 +67,15 @@ function createStarterMessage(overrides: ThreadStarterRestMessage = {}): ThreadS
};
}
function requireThreadStarter(
result: Awaited<ReturnType<typeof resolveDiscordThreadStarter>>,
): ResolvedThreadStarter {
if (!result) {
throw new Error("expected resolved Discord thread starter");
}
return result;
}
async function resolveStarter(params: {
message: ThreadStarterRestMessage;
parentId?: string;
@@ -152,10 +163,10 @@ describe("resolveDiscordThreadStarter", () => {
resolveTimestampMs: () => 456,
});
expect(result).toBeTruthy();
expect(result!.text).toContain("forwarded task content");
expect(result!.author).toBe("Bob");
expect(result!.timestamp).toBe(456);
const starter = requireThreadStarter(result);
expect(starter.text).toContain("forwarded task content");
expect(starter.author).toBe("Bob");
expect(starter.timestamp).toBe(456);
});
it("prefers content over forwarded message snapshots", async () => {
@@ -167,8 +178,7 @@ describe("resolveDiscordThreadStarter", () => {
}),
});
expect(result).toBeTruthy();
expect(result!.text).toBe("direct content");
expect(requireThreadStarter(result).text).toBe("direct content");
});
it("joins multiple forwarded message snapshots", async () => {
@@ -182,9 +192,9 @@ describe("resolveDiscordThreadStarter", () => {
}),
});
expect(result).toBeTruthy();
expect(result!.text).toContain("first forwarded message");
expect(result!.text).toContain("second forwarded message");
const starter = requireThreadStarter(result);
expect(starter.text).toContain("first forwarded message");
expect(starter.text).toContain("second forwarded message");
});
it("preserves forwarded attachment placeholders in thread starter context", async () => {
@@ -206,9 +216,9 @@ describe("resolveDiscordThreadStarter", () => {
}),
});
expect(result).toBeTruthy();
expect(result!.text).toContain("[Forwarded message]");
expect(result!.text).toContain("<media:image> (1 image)");
const starter = requireThreadStarter(result);
expect(starter.text).toContain("[Forwarded message]");
expect(starter.text).toContain("<media:image> (1 image)");
});
it("preserves forwarded sticker placeholders in thread starter context", async () => {
@@ -229,9 +239,9 @@ describe("resolveDiscordThreadStarter", () => {
}),
});
expect(result).toBeTruthy();
expect(result!.text).toContain("[Forwarded message]");
expect(result!.text).toContain("<media:sticker> (1 sticker)");
const starter = requireThreadStarter(result);
expect(starter.text).toContain("[Forwarded message]");
expect(starter.text).toContain("<media:sticker> (1 sticker)");
});
it("uses the thread id as the message channel id for forum parents", async () => {
@@ -241,7 +251,7 @@ describe("resolveDiscordThreadStarter", () => {
parentType: ChannelType.GuildForum,
});
expect(result?.text).toBe("starter content");
expect(requireThreadStarter(result).text).toBe("starter content");
expect(get).toHaveBeenCalledWith(
expect.stringContaining("/channels/thread-1/messages/thread-1"),
);

View File

@@ -8,8 +8,10 @@ import { createDiscordRequestClient, DISCORD_REST_TIMEOUT_MS } from "./proxy-req
describe("createDiscordRequestClient", () => {
it("preserves the REST client's abort signal for proxied fetch calls", async () => {
const fetchSpy = vi.fn(async (_input: string | URL | Request, init?: RequestInit) => {
expect(init?.signal).toBeDefined();
expect(init!.signal!.aborted).toBe(false);
if (!(init?.signal instanceof AbortSignal)) {
throw new Error("Expected proxied fetch init to include an AbortSignal");
}
expect(init.signal.aborted).toBe(false);
return createJsonResponse([]);
});
@@ -67,8 +69,10 @@ describe("createDiscordRequestClient", () => {
await client.get("/channels/123/messages");
expect(receivedSignal).toBeDefined();
expect(receivedSignal!.aborted).toBe(false);
if (!receivedSignal) {
throw new Error("Expected proxied fetch to receive the REST timeout signal");
}
expect(receivedSignal.aborted).toBe(false);
});
it("exports a reasonable timeout constant", () => {

View File

@@ -234,13 +234,15 @@ describe("Discord security audit findings", () => {
if (testCase.expectNoNameBasedFinding) {
expect(nameBasedFinding).toBeUndefined();
} else {
expect(nameBasedFinding).toBeDefined();
expect(nameBasedFinding?.severity).toBe(testCase.expectNameBasedSeverity);
if (!nameBasedFinding) {
throw new Error(`expected name-based finding for ${testCase.name}`);
}
expect(nameBasedFinding.severity).toBe(testCase.expectNameBasedSeverity);
for (const snippet of testCase.detailIncludes ?? []) {
expect(nameBasedFinding?.detail).toContain(snippet);
expect(nameBasedFinding.detail).toContain(snippet);
}
for (const snippet of testCase.detailExcludes ?? []) {
expect(nameBasedFinding?.detail).not.toContain(snippet);
expect(nameBasedFinding.detail).not.toContain(snippet);
}
}
});

View File

@@ -10,6 +10,28 @@ vi.mock("./send.shared.js", () => ({
const { readMessagesDiscord, searchMessagesDiscord } = await import("./send.messages.js");
const restErrorCases: Array<{
name: string;
invoke: () => Promise<unknown>;
}> = [
{
name: "readMessagesDiscord",
invoke: () => readMessagesDiscord("C1", {}, { cfg: {} as never }),
},
{
name: "searchMessagesDiscord",
invoke: () => searchMessagesDiscord({ guildId: "G1", content: "test" }, { cfg: {} as never }),
},
];
describe("Discord message REST error handling", () => {
it.each(restErrorCases)("$name propagates REST errors", async ({ invoke }) => {
restMock.get.mockRejectedValueOnce(new Error("Discord API error"));
await expect(invoke()).rejects.toThrow("Discord API error");
});
});
describe("readMessagesDiscord", () => {
it("returns messages from the REST client", async () => {
const messages = [{ id: "1", content: "hello" }];
@@ -20,14 +42,6 @@ describe("readMessagesDiscord", () => {
expect(result).toEqual(messages);
expect(restMock.get).toHaveBeenCalledWith(expect.stringContaining("C1"), { limit: 5 });
});
it("propagates REST errors", async () => {
restMock.get.mockRejectedValueOnce(new Error("Discord API error"));
await expect(readMessagesDiscord("C1", {}, { cfg: {} as never })).rejects.toThrow(
"Discord API error",
);
});
});
describe("searchMessagesDiscord", () => {
@@ -42,12 +56,4 @@ describe("searchMessagesDiscord", () => {
expect(result).toEqual(results);
});
it("propagates REST errors", async () => {
restMock.get.mockRejectedValueOnce(new Error("Discord API error"));
await expect(
searchMessagesDiscord({ guildId: "G1", content: "test" }, { cfg: {} as never }),
).rejects.toThrow("Discord API error");
});
});

View File

@@ -263,9 +263,29 @@ describe("DiscordVoiceManager", () => {
]);
};
const getSessionEntry = (
manager: InstanceType<typeof managerModule.DiscordVoiceManager>,
guildId = "g1",
) => {
const entry = (manager as unknown as { sessions: Map<string, unknown> }).sessions.get(guildId);
if (!entry) {
throw new Error(`expected Discord voice session for guild ${guildId}`);
}
return entry;
};
const getLastAudioPlayer = () => {
const player = createAudioPlayerMock.mock.results.at(-1)?.value as
| { state: { status: string } }
| undefined;
if (!player) {
throw new Error("expected Discord voice audio player to be created");
}
return player;
};
const emitDecryptFailure = (manager: InstanceType<typeof managerModule.DiscordVoiceManager>) => {
const entry = (manager as unknown as { sessions: Map<string, unknown> }).sessions.get("g1");
expect(entry).toBeDefined();
const entry = getSessionEntry(manager);
(
manager as unknown as { handleReceiveError: (e: unknown, err: unknown) => void }
).handleReceiveError(
@@ -424,10 +444,8 @@ describe("DiscordVoiceManager", () => {
await manager.join({ guildId: "g1", channelId: "1001" });
const player = createAudioPlayerMock.mock.results.at(-1)?.value;
const entry = (manager as unknown as { sessions: Map<string, unknown> }).sessions.get("g1");
expect(entry).toBeDefined();
expect(player).toBeDefined();
const player = getLastAudioPlayer();
const entry = getSessionEntry(manager);
player.state.status = "playing";
await (
@@ -567,29 +585,24 @@ describe("DiscordVoiceManager", () => {
await manager.join({ guildId: "g1", channelId: "1001" });
const entry = (manager as unknown as { sessions: Map<string, unknown> }).sessions.get(
"g1",
) as
| {
guildId: string;
channelId: string;
capture: {
activeSpeakers: Set<string>;
activeCaptureStreams: Map<
string,
{ generation: number; stream: { destroy: () => void } }
>;
captureFinalizeTimers: Map<string, unknown>;
captureGenerations: Map<string, number>;
};
}
| undefined;
expect(entry).toBeDefined();
const entry = getSessionEntry(manager) as {
guildId: string;
channelId: string;
capture: {
activeSpeakers: Set<string>;
activeCaptureStreams: Map<
string,
{ generation: number; stream: { destroy: () => void } }
>;
captureFinalizeTimers: Map<string, unknown>;
captureGenerations: Map<string, number>;
};
};
const firstStream = { destroy: vi.fn() };
entry?.capture.activeSpeakers.add("u1");
entry?.capture.captureGenerations.set("u1", 1);
entry?.capture.activeCaptureStreams.set("u1", { generation: 1, stream: firstStream });
entry.capture.activeSpeakers.add("u1");
entry.capture.captureGenerations.set("u1", 1);
entry.capture.activeCaptureStreams.set("u1", { generation: 1, stream: firstStream });
(
manager as unknown as {

View File

@@ -73,6 +73,7 @@ describeLive("elevenlabs plugin live", () => {
outputFormat: "ulaw_8000",
timeoutMs: 30_000,
});
expect(speech.byteLength).toBeGreaterThan(0);
await runRealtimeSttLiveTest({
provider,

View File

@@ -9,7 +9,7 @@ describe("elevenLabsMediaUnderstandingProvider", () => {
expect(elevenLabsMediaUnderstandingProvider.id).toBe("elevenlabs");
expect(elevenLabsMediaUnderstandingProvider.capabilities).toEqual(["audio"]);
expect(elevenLabsMediaUnderstandingProvider.defaultModels?.audio).toBe("scribe_v2");
expect(elevenLabsMediaUnderstandingProvider.transcribeAudio).toBeDefined();
expect(elevenLabsMediaUnderstandingProvider.transcribeAudio).toBeTypeOf("function");
});
it("posts multipart audio to ElevenLabs speech-to-text", async () => {

View File

@@ -12,14 +12,16 @@ import {
function expectFalJsonPost(params: { call: number; url: string; body: Record<string, unknown> }) {
const request = fetchWithSsrFGuardMock.mock.calls[params.call - 1]?.[0];
expect(request).toBeTruthy();
expect(request?.url).toBe(params.url);
expect(request?.auditContext).toBe("fal-image-generate");
expect(request?.init?.method).toBe("POST");
const headers = new Headers(request?.init?.headers);
if (!request) {
throw new Error(`expected fal fetch request #${params.call}`);
}
expect(request.url).toBe(params.url);
expect(request.auditContext).toBe("fal-image-generate");
expect(request.init?.method).toBe("POST");
const headers = new Headers(request.init?.headers);
expect(headers.get("authorization")).toBe("Key fal-test-key");
expect(headers.get("content-type")).toBe("application/json");
expect(JSON.parse(String(request?.init?.body))).toEqual(params.body);
expect(JSON.parse(String(request.init?.body))).toEqual(params.body);
}
describe("fal image-generation provider", () => {

View File

@@ -13,7 +13,11 @@ describe("feishu setup entry", () => {
it("declares the setup entry without importing Feishu runtime dependencies", async () => {
const { default: setupEntry } = await import("./setup-entry.js");
expect(setupEntry.kind).toBe("bundled-channel-setup-entry");
expect(typeof setupEntry.loadSetupPlugin).toBe("function");
expect(setupEntry).toEqual(
expect.objectContaining({
kind: "bundled-channel-setup-entry",
loadSetupPlugin: expect.any(Function),
}),
);
});
});

View File

@@ -111,6 +111,16 @@ type HttpInstanceLike = {
post: (url: string, body?: unknown, options?: Record<string, unknown>) => Promise<unknown>;
};
function requireHttpInstance(value: unknown): HttpInstanceLike {
if (isRecord(value) && typeof value.get === "function" && typeof value.post === "function") {
return {
get: value.get as HttpInstanceLike["get"],
post: value.post as HttpInstanceLike["post"],
};
}
throw new Error("expected Feishu HTTP instance");
}
function readCallOptions(
mock: { mock: { calls: unknown[][] } },
index = -1,
@@ -222,25 +232,12 @@ afterAll(() => {
});
describe("createFeishuClient HTTP timeout", () => {
const getLastClientHttpInstance = (): HttpInstanceLike | undefined => {
const httpInstance = readCallOptions(clientCtorMock).httpInstance;
if (
isRecord(httpInstance) &&
typeof httpInstance.get === "function" &&
typeof httpInstance.post === "function"
) {
return {
get: httpInstance.get as HttpInstanceLike["get"],
post: httpInstance.post as HttpInstanceLike["post"],
};
}
return undefined;
};
const readLastClientHttpInstance = (): HttpInstanceLike =>
requireHttpInstance(readCallOptions(clientCtorMock).httpInstance);
const expectGetCallTimeout = async (timeout: number) => {
const httpInstance = getLastClientHttpInstance();
expect(httpInstance).toBeDefined();
await httpInstance?.get("https://example.com/api");
const httpInstance = readLastClientHttpInstance();
await httpInstance.get("https://example.com/api");
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith(
"https://example.com/api",
expect.objectContaining({ timeout }),
@@ -250,16 +247,18 @@ describe("createFeishuClient HTTP timeout", () => {
it("passes a custom httpInstance with default timeout to Lark.Client", () => {
createFeishuClient({ appId: "app_1", appSecret: "secret_1", accountId: "timeout-test" }); // pragma: allowlist secret
expect(readCallOptions(clientCtorMock).httpInstance).toBeDefined();
expect(readLastClientHttpInstance()).toMatchObject({
get: expect.any(Function),
post: expect.any(Function),
});
});
it("injects default timeout into HTTP request options", async () => {
createFeishuClient({ appId: "app_2", appSecret: "secret_2", accountId: "timeout-inject" }); // pragma: allowlist secret
const httpInstance = getLastClientHttpInstance();
const httpInstance = readLastClientHttpInstance();
expect(httpInstance).toBeDefined();
await httpInstance?.post(
await httpInstance.post(
"https://example.com/api",
{ data: 1 },
{ headers: { "X-Custom": "yes" } },
@@ -275,10 +274,9 @@ describe("createFeishuClient HTTP timeout", () => {
it("allows explicit timeout override per-request", async () => {
createFeishuClient({ appId: "app_3", appSecret: "secret_3", accountId: "timeout-override" }); // pragma: allowlist secret
const httpInstance = getLastClientHttpInstance();
const httpInstance = readLastClientHttpInstance();
expect(httpInstance).toBeDefined();
await httpInstance?.get("https://example.com/api", { timeout: 5_000 });
await httpInstance.get("https://example.com/api", { timeout: 5_000 });
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith(
"https://example.com/api",
@@ -362,9 +360,8 @@ describe("createFeishuClient HTTP timeout", () => {
});
expect(clientCtorMock.mock.calls.length).toBe(2);
const httpInstance = getLastClientHttpInstance();
expect(httpInstance).toBeDefined();
await httpInstance?.get("https://example.com/api");
const httpInstance = readLastClientHttpInstance();
await httpInstance.get("https://example.com/api");
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith(
"https://example.com/api",

View File

@@ -61,7 +61,6 @@ describe("createFeishuCommentReplyDispatcher", () => {
function latestReplyDispatcherOptions() {
const options = createReplyDispatcherWithTypingMock.mock.calls.at(-1)?.[0];
expect(options).toBeDefined();
if (!options) {
throw new Error("expected reply dispatcher options");
}

View File

@@ -163,7 +163,10 @@ describe("feishu_doc image fetch hardening", () => {
});
registerFeishuDocTools(harness.api);
const tool = harness.resolveTool("feishu_doc", context);
expect(tool).toBeDefined();
if (!tool) {
throw new Error("expected Feishu doc tool");
}
expect(tool.execute).toEqual(expect.any(Function));
return tool;
}
@@ -206,8 +209,8 @@ describe("feishu_doc image fetch hardening", () => {
expect(blockDescendantCreateMock).toHaveBeenCalledTimes(1);
const call = blockDescendantCreateMock.mock.calls[0]?.[0];
expect(call?.data.children_id).toEqual(["h1", "t1", "h2"]);
expect(call?.data.descendants).toBeDefined();
expect(call?.data.descendants.length).toBeGreaterThanOrEqual(3);
expect(call?.data.descendants).toEqual(expect.arrayContaining(blocks));
expect(call?.data.descendants).toHaveLength(3);
expect(result.details.blocks_added).toBe(3);
});

View File

@@ -566,8 +566,10 @@ describe("sendMediaFeishu msg_type routing", () => {
);
expectMediaTimeoutClientConfigured();
expect(result.buffer).toEqual(Buffer.from("image-data"));
expect(capturedPath).toBeDefined();
expectPathIsolatedToTmpRoot(capturedPath as string, imageKey);
if (!capturedPath) {
throw new Error("expected Feishu image temp path");
}
expectPathIsolatedToTmpRoot(capturedPath, imageKey);
});
it("uses isolated temp paths for message resource downloads", async () => {
@@ -589,8 +591,10 @@ describe("sendMediaFeishu msg_type routing", () => {
});
expect(result.buffer).toEqual(Buffer.from("resource-data"));
expect(capturedPath).toBeDefined();
expectPathIsolatedToTmpRoot(capturedPath as string, fileKey);
if (!capturedPath) {
throw new Error("expected Feishu resource temp path");
}
expectPathIsolatedToTmpRoot(capturedPath, fileKey);
});
it("rejects invalid image keys before calling feishu api", async () => {

View File

@@ -188,7 +188,6 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
throw new Error("feishuOutbound.chunker missing");
}
expect(() => chunker("hello world", 5)).not.toThrow();
expect(chunker("hello world", 5)).toEqual(["hello", "world"]);
});

View File

@@ -298,7 +298,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
expect(sendMediaFeishuMock).not.toHaveBeenCalled();
});
it("disables block streaming by default to prevent silent reply drops", async () => {
it("disables block streaming by default to prevent silent reply drops", () => {
const result = createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
@@ -334,7 +334,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
});
it("keeps core block streaming disabled when Feishu blockStreaming is explicitly false", async () => {
it("keeps core block streaming disabled when Feishu blockStreaming is explicitly false", () => {
resolveFeishuAccountMock.mockReturnValue({
accountId: "main",
appId: "app_id",
@@ -910,7 +910,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
expect(reasoningUpdate).not.toMatch(/> _.*_/);
const combinedUpdate = updateCalls.find((c) => c.includes("Thinking") && c.includes("---"));
expect(combinedUpdate).toBeDefined();
if (!combinedUpdate) {
throw new Error("expected combined reasoning and final-answer streaming update");
}
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
const closeArg = streamingInstances[0].close.mock.calls[0][0] as string;

View File

@@ -509,21 +509,36 @@ describe("resolveFeishuCardTemplate", () => {
});
});
describe("buildStructuredCard", () => {
it("uses schema-2.0 width config instead of legacy wide screen mode", () => {
const card = buildStructuredCard("hello") as {
config: {
width_mode?: string;
enable_forward?: boolean;
wide_screen_mode?: boolean;
};
function expectSchema2WidthConfig(card: unknown) {
const typedCard = card as {
config: {
width_mode?: string;
enable_forward?: boolean;
wide_screen_mode?: boolean;
};
};
expect(card.config.width_mode).toBe("fill");
expect(card.config.enable_forward).toBeUndefined();
expect(card.config.wide_screen_mode).toBeUndefined();
expect(typedCard.config.width_mode).toBe("fill");
expect(typedCard.config.enable_forward).toBeUndefined();
expect(typedCard.config.wide_screen_mode).toBeUndefined();
}
describe("Feishu card schema config", () => {
it.each([
{
name: "structured card",
build: () => buildStructuredCard("hello"),
},
{
name: "markdown card",
build: () => buildMarkdownCard("hello"),
},
])("$name uses schema-2.0 width config instead of legacy wide screen mode", ({ build }) => {
expectSchema2WidthConfig(build());
});
});
describe("buildStructuredCard", () => {
it("falls back to blue when the header template is unsupported", () => {
const card = buildStructuredCard("hello", {
header: {
@@ -542,19 +557,3 @@ describe("buildStructuredCard", () => {
);
});
});
describe("buildMarkdownCard", () => {
it("uses schema-2.0 width config instead of legacy wide screen mode", () => {
const card = buildMarkdownCard("hello") as {
config: {
width_mode?: string;
enable_forward?: boolean;
wide_screen_mode?: boolean;
};
};
expect(card.config.width_mode).toBe("fill");
expect(card.config.enable_forward).toBeUndefined();
expect(card.config.wide_screen_mode).toBeUndefined();
});
});

View File

@@ -101,21 +101,24 @@ describe("feishu setup wizard", () => {
) as never,
});
await expect(
runSetupWizardConfigure({
configure: feishuConfigure,
cfg: {
channels: {
feishu: {
appId: { source: "env", id: "FEISHU_APP_ID", provider: "default" },
appSecret: { source: "env", id: "FEISHU_APP_SECRET", provider: "default" },
},
const result = await runSetupWizardConfigure({
configure: feishuConfigure,
cfg: {
channels: {
feishu: {
appId: { source: "env", id: "FEISHU_APP_ID", provider: "default" },
appSecret: { source: "env", id: "FEISHU_APP_SECRET", provider: "default" },
},
} as never,
prompter,
runtime: createNonExitingRuntimeEnv(),
}),
).resolves.toBeTruthy();
},
} as never,
prompter,
runtime: createNonExitingRuntimeEnv(),
});
expect(result.cfg.channels?.feishu).toMatchObject({
appId: "cli_from_prompt",
appSecret: "secret_from_prompt",
});
});
});

View File

@@ -67,7 +67,10 @@ describe("handleFileFetch — fs errors", () => {
const r = await handleFileFetch({ path: tmpRoot });
expect(r).toMatchObject({ ok: false, code: "IS_DIRECTORY" });
// canonical path is reported back so the caller can re-check policy
expect(r.ok ? null : r.canonicalPath).toBeTruthy();
if (r.ok) {
throw new Error("expected directory fetch to fail");
}
expect(r.canonicalPath).toBe(tmpRoot);
});
});

View File

@@ -214,7 +214,7 @@ describe("firecrawl tools", () => {
expect(authHeader).toBe("Bearer firecrawl-test-key");
});
it("blocks private and non-http scrape targets before Firecrawl requests", async () => {
it("blocks private and non-http scrape targets before Firecrawl requests", () => {
expect(() =>
firecrawlClientTesting.assertFirecrawlScrapeTargetAllowed("https://example.com/page"),
).not.toThrow();

View File

@@ -318,7 +318,7 @@ describe("github-copilot token", () => {
({ deriveCopilotApiBaseUrlFromToken, resolveCopilotApiToken } = await import("./token.js"));
});
it("derives baseUrl from token", async () => {
it("derives baseUrl from token", () => {
expect(deriveCopilotApiBaseUrlFromToken("token;proxy-ep=proxy.example.com;")).toBe(
"https://api.example.com",
);

View File

@@ -15,7 +15,7 @@ function requireStreamFn(streamFn: ReturnType<typeof wrapCopilotProviderStream>)
}
describe("wrapCopilotAnthropicStream", () => {
it("adds Copilot headers and Anthropic cache markers for Claude payloads", async () => {
it("adds Copilot headers and Anthropic cache markers for Claude payloads", () => {
const payloads: Array<{
messages: Array<Record<string, unknown>>;
}> = [];

View File

@@ -20,6 +20,17 @@ const convertMessagesForTest = convertMessages as unknown as (
context: Context,
) => ReturnType<typeof convertMessages>;
function requireRecordProperty(
record: Record<string, unknown>,
key: string,
): Record<string, unknown> {
const value = record[key];
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`expected object property ${key}`);
}
return value as Record<string, unknown>;
}
describe("google-shared convertTools", () => {
it("preserves parameters when type is missing", () => {
const tools = [
@@ -41,7 +52,9 @@ describe("google-shared convertTools", () => {
);
expect(params.type).toBeUndefined();
expect(params.properties).toBeDefined();
expect(params.properties).toEqual({
action: { type: "string" },
});
expect(params.required).toEqual(["action"]);
});
@@ -290,7 +303,9 @@ describe("google-shared convertMessages", () => {
(part) => typeof part === "object" && part !== null && "functionResponse" in part,
);
const toolResponse = asRecord(toolResponsePart);
expect(toolResponse.functionResponse).toBeTruthy();
expect(requireRecordProperty(toolResponse, "functionResponse")).toMatchObject({
name: "myTool",
});
expect(contents[3].role).toBe("user");
});
@@ -320,7 +335,9 @@ describe("google-shared convertMessages", () => {
(part) => typeof part === "object" && part !== null && "functionCall" in part,
);
const toolCall = asRecord(toolCallPart);
expect(toolCall.functionCall).toBeTruthy();
expect(requireRecordProperty(toolCall, "functionCall")).toMatchObject({
name: "myTool",
});
});
it("strips tool call and response ids for google-gemini-cli", () => {

View File

@@ -190,8 +190,10 @@ describe("google provider plugin hooks", () => {
name: "Google Provider",
});
const provider = requireRegisteredProvider(providers, "google");
expect(provider.resolveThinkingProfile).toBeDefined();
const resolveThinkingProfile = provider.resolveThinkingProfile!;
if (!provider.resolveThinkingProfile) {
throw new Error("expected Google provider thinking profile resolver");
}
const resolveThinkingProfile = provider.resolveThinkingProfile;
const gemini3Profile = resolveThinkingProfile({
provider: "google",
modelId: "gemini-3.1-pro-preview",
@@ -246,9 +248,11 @@ describe("google provider plugin hooks", () => {
onClearAudio() {},
});
expect(bridge).toBeDefined();
expect(() => bridge?.sendAudio(Buffer.alloc(160))).not.toThrow();
expect(() => bridge?.setMediaTimestamp(20)).not.toThrow();
expect(() => bridge?.sendUserMessage?.("hello")).not.toThrow();
if (!bridge) {
throw new Error("expected Google realtime bridge");
}
expect(() => bridge.sendAudio(Buffer.alloc(160))).not.toThrow();
expect(() => bridge.setMediaTimestamp(20)).not.toThrow();
expect(() => bridge.sendUserMessage?.("hello")).not.toThrow();
});
});

View File

@@ -25,6 +25,7 @@ describe("google model id helpers", () => {
it("keeps bare Gemini 3.1 Pro as an alias for Google's preview-suffixed API id", () => {
expect(normalizeGoogleModelId("gemini-3-pro")).toBe("gemini-3.1-pro-preview");
expect(normalizeGoogleModelId("gemini-3-pro-preview")).toBe("gemini-3.1-pro-preview");
expect(normalizeGoogleModelId("gemini-3.1-pro")).toBe("gemini-3.1-pro-preview");
expect(normalizeGoogleModelId("gemini-3.1-pro-preview")).toBe("gemini-3.1-pro-preview");
});

View File

@@ -1,7 +1,7 @@
const ANTIGRAVITY_BARE_PRO_IDS = new Set(["gemini-3-pro", "gemini-3.1-pro", "gemini-3-1-pro"]);
export function normalizeGoogleModelId(id: string): string {
if (id === "gemini-3-pro") {
if (id === "gemini-3-pro" || id === "gemini-3-pro-preview") {
return "gemini-3.1-pro-preview";
}
if (id === "gemini-3-flash") {

View File

@@ -450,7 +450,7 @@ describe("extractGeminiCliCredentials", () => {
setOAuthCredentialsFsForTest();
});
it("returns null when gemini binary is not in PATH", async () => {
it("returns null when gemini binary is not in PATH", () => {
process.env.PATH = "/nonexistent";
mockExistsSync.mockReturnValue(false);
@@ -458,7 +458,7 @@ describe("extractGeminiCliCredentials", () => {
expect(extractGeminiCliCredentials()).toBeNull();
});
it("extracts credentials from oauth2.js in known path", async () => {
it("extracts credentials from oauth2.js in known path", () => {
installGeminiLayout({ oauth2Exists: true, oauth2Content: FAKE_OAUTH2_CONTENT });
clearCredentialsCache();
@@ -467,7 +467,7 @@ describe("extractGeminiCliCredentials", () => {
expectFakeCliCredentials(result);
});
it("extracts credentials when PATH entry is an npm global shim", async () => {
it("extracts credentials when PATH entry is an npm global shim", () => {
installNpmShimLayout({ oauth2Exists: true, oauth2Content: FAKE_OAUTH2_CONTENT });
clearCredentialsCache();
@@ -476,7 +476,7 @@ describe("extractGeminiCliCredentials", () => {
expectFakeCliCredentials(result);
});
it("extracts credentials from bundled npm installs", async () => {
it("extracts credentials from bundled npm installs", () => {
installBundledNpmLayout({
bundleContent: `
const OAUTH_CLIENT_ID = "${FAKE_CLIENT_ID}";
@@ -490,7 +490,7 @@ describe("extractGeminiCliCredentials", () => {
expectFakeCliCredentials(result);
});
it("extracts credentials from Homebrew libexec installs", async () => {
it("extracts credentials from Homebrew libexec installs", () => {
installHomebrewLibexecLayout({ oauth2Content: FAKE_OAUTH2_CONTENT });
clearCredentialsCache();
@@ -499,21 +499,21 @@ describe("extractGeminiCliCredentials", () => {
expectFakeCliCredentials(result);
});
it("returns null when oauth2.js cannot be found", async () => {
it("returns null when oauth2.js cannot be found", () => {
installGeminiLayout({ oauth2Exists: false, readdir: [] });
clearCredentialsCache();
expect(extractGeminiCliCredentials()).toBeNull();
});
it("returns null when oauth2.js lacks credentials", async () => {
it("returns null when oauth2.js lacks credentials", () => {
installGeminiLayout({ oauth2Exists: true, oauth2Content: "// no credentials here" });
clearCredentialsCache();
expect(extractGeminiCliCredentials()).toBeNull();
});
it("caches credentials after first extraction", async () => {
it("caches credentials after first extraction", () => {
installGeminiLayout({ oauth2Exists: true, oauth2Content: FAKE_OAUTH2_CONTENT });
clearCredentialsCache();
@@ -529,7 +529,7 @@ describe("extractGeminiCliCredentials", () => {
expect(mockReadFileSync.mock.calls.length).toBe(readCount);
});
it("skips unrelated oauth2.js files when gemini resolves inside a Windows nvm root", async () => {
it("skips unrelated oauth2.js files when gemini resolves inside a Windows nvm root", () => {
const { unrelatedOauth2Path } = installWindowsNvmLayoutWithUnrelatedOauth({
oauth2Content: FAKE_OAUTH2_CONTENT,
unrelatedOauth2Content: "// unrelated oauth file",
@@ -657,6 +657,23 @@ describe("loginGeminiCliOAuth", () => {
return JSON.parse(value);
}
function requireString(value: string | null | undefined, label: string): string {
if (!value) {
throw new Error(`Expected ${label}`);
}
return value;
}
function requireRecordedRequest(
request: RecordedFetchRequest | undefined,
label: string,
): RecordedFetchRequest {
if (!request) {
throw new Error(`Expected ${label} request`);
}
return request;
}
type LoginGeminiCliOAuthFn = (options: {
isRemote: boolean;
openUrl: () => Promise<void>;
@@ -757,8 +774,10 @@ describe("loginGeminiCliOAuth", () => {
`gl-node/${process.versions.node}`,
);
const clientMetadata = getHeaderValue(firstHeaders, "Client-Metadata");
expect(clientMetadata).toBeDefined();
const clientMetadata = requireString(
getHeaderValue(firstHeaders, "Client-Metadata"),
"Client-Metadata",
);
expect(parseJsonString(clientMetadata, "Client-Metadata")).toEqual(
EXPECTED_LOAD_CODE_ASSIST_METADATA,
);
@@ -784,13 +803,16 @@ describe("loginGeminiCliOAuth", () => {
const { loginGeminiCliOAuth } = await import("./oauth.js");
const { authUrl } = await runRemoteLoginWithCapturedAuthUrl(loginGeminiCliOAuth);
const authState = new URL(authUrl).searchParams.get("state");
expect(authState).toBeTruthy();
const authState = requireString(new URL(authUrl).searchParams.get("state"), "OAuth state");
const tokenRequest = requests.find((request) => request.url === TOKEN_URL);
expect(tokenRequest).toBeDefined();
const codeVerifier = getFormField(tokenRequest?.init?.body, "code_verifier");
expect(codeVerifier).toBeTruthy();
const tokenRequest = requireRecordedRequest(
requests.find((request) => request.url === TOKEN_URL),
"token",
);
const codeVerifier = requireString(
getFormField(tokenRequest.init?.body, "code_verifier"),
"PKCE code verifier",
);
expect(codeVerifier).not.toBe(authState);
});

View File

@@ -51,7 +51,7 @@ describe("googlechat message actions", () => {
vi.resetModules();
});
it("describes send and reaction actions only when enabled accounts exist", async () => {
it("describes send and reaction actions only when enabled accounts exist", () => {
listEnabledGoogleChatAccounts.mockReturnValueOnce([]);
expect(googlechatMessageActions.describeMessageTool?.({ cfg: {} as never })).toBeNull();

View File

@@ -455,7 +455,9 @@ describe("googlechatPlugin outbound resolveTarget", () => {
if (result.ok) {
throw new Error("Expected invalid target to fail");
}
expect(result.error).toBeDefined();
expect(result.error.message).toBe(
"Google Chat target is required (<spaces/{space}|users/{user}>)",
);
});
it("errors when no target is provided", () => {
@@ -467,7 +469,9 @@ describe("googlechatPlugin outbound resolveTarget", () => {
if (result.ok) {
throw new Error("Expected missing target to fail");
}
expect(result.error).toBeDefined();
expect(result.error.message).toBe(
"Google Chat target is required (<spaces/{space}|users/{user}>)",
);
});
});

View File

@@ -378,7 +378,7 @@ describe("googlechat google auth runtime", () => {
expect(second.interceptors.response.add).toHaveBeenCalledOnce();
});
it("normalizes Google auth request headers before upstream interceptors run", async () => {
it("normalizes Google auth request headers before upstream interceptors run", () => {
const config = {
headers: { "x-test": "1" },
url: new URL("https://www.googleapis.com/oauth2/v1/certs"),

View File

@@ -44,11 +44,53 @@ function readRequestBody(request: GuardRequest): string {
return body;
}
const guardedSuccessReleaseCases = [
{
name: "listInworldVoices",
run: async () => {
const { release } = queueGuardedResponse(
new Response(JSON.stringify({ voices: [] }), { status: 200 }),
);
await listInworldVoices({ apiKey: "test-key" });
return release;
},
},
{
name: "inworldTTS",
run: async () => {
const chunk = Buffer.from("audio").toString("base64");
const { release } = queueGuardedResponse(
new Response(JSON.stringify({ result: { audioContent: chunk } }), { status: 200 }),
);
await inworldTTS({ text: "test", apiKey: "test-key" });
return release;
},
},
];
afterAll(() => {
vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime");
vi.resetModules();
});
describe("Inworld guarded dispatcher lifecycle", () => {
afterEach(() => {
fetchWithSsrFGuardMock.mockReset();
vi.restoreAllMocks();
});
it.each(guardedSuccessReleaseCases)(
"$name releases the guarded dispatcher after success",
async ({ run }) => {
const release = await run();
expect(release).toHaveBeenCalledTimes(1);
},
);
});
describe("listInworldVoices", () => {
afterEach(() => {
fetchWithSsrFGuardMock.mockReset();
@@ -148,16 +190,6 @@ describe("listInworldVoices", () => {
expect(lastGuardRequest().url).toBe("https://api.inworld.ai/voices/v1/voices?languages=EN_US");
});
it("releases the guarded dispatcher after success", async () => {
const { release } = queueGuardedResponse(
new Response(JSON.stringify({ voices: [] }), { status: 200 }),
);
await listInworldVoices({ apiKey: "test-key" });
expect(release).toHaveBeenCalledTimes(1);
});
});
describe("inworldTTS", () => {
@@ -297,17 +329,6 @@ describe("inworldTTS", () => {
expect(buffer).toEqual(Buffer.from("audio"));
});
it("releases the guarded dispatcher after success", async () => {
const chunk = Buffer.from("audio").toString("base64");
const { release } = queueGuardedResponse(
new Response(JSON.stringify({ result: { audioContent: chunk } }), { status: 200 }),
);
await inworldTTS({ text: "test", apiKey: "test-key" });
expect(release).toHaveBeenCalledTimes(1);
});
it("releases the guarded dispatcher after failure", async () => {
const { release } = queueGuardedResponse(new Response("fail", { status: 500 }));

View File

@@ -420,10 +420,12 @@ describe("irc setup", () => {
prompter,
accountId: "work",
});
expect(updated).toBeDefined();
if (!updated) {
throw new Error("expected IRC allowFrom setup to return updated config");
}
expect(updated?.channels?.irc?.allowFrom).toEqual(["alice", "bob!ident@example.org"]);
expect(updated?.channels?.irc?.accounts?.work?.allowFrom).toBeUndefined();
expect(updated.channels?.irc?.allowFrom).toEqual(["alice", "bob!ident@example.org"]);
expect(updated.channels?.irc?.accounts?.work?.allowFrom).toBeUndefined();
});
it("keeps startAccount pending until abort, then stops the monitor", async () => {

View File

@@ -19,6 +19,14 @@ import {
const emptyCfg: OpenClawConfig = {};
const KILOCODE_MODEL_IDS = ["kilo/auto"];
function requireKilocodeProvider(cfg: OpenClawConfig) {
const provider = cfg.models?.providers?.kilocode;
if (!provider) {
throw new Error("expected Kilocode provider config");
}
return provider;
}
describe("Kilo Gateway provider config", () => {
describe("constants", () => {
it("KILOCODE_BASE_URL points to kilo openrouter endpoint", () => {
@@ -50,10 +58,9 @@ describe("Kilo Gateway provider config", () => {
describe("applyKilocodeProviderConfig", () => {
it("registers kilocode provider with correct baseUrl and api", () => {
const result = applyKilocodeProviderConfig(emptyCfg);
const provider = result.models?.providers?.kilocode;
expect(provider).toBeDefined();
expect(provider?.baseUrl).toBe(KILOCODE_BASE_URL);
expect(provider?.api).toBe("openai-completions");
const provider = requireKilocodeProvider(result);
expect(provider.baseUrl).toBe(KILOCODE_BASE_URL);
expect(provider.api).toBe("openai-completions");
});
it("includes the default model in the provider model list", () => {
@@ -95,8 +102,7 @@ describe("Kilo Gateway provider config", () => {
it("sets Kilo Gateway alias in agent default models", () => {
const result = applyKilocodeProviderConfig(emptyCfg);
const agentModel = result.agents?.defaults?.models?.[KILOCODE_DEFAULT_MODEL_REF];
expect(agentModel).toBeDefined();
expect(agentModel?.alias).toBe("Kilo Gateway");
expect(agentModel).toMatchObject({ alias: "Kilo Gateway" });
});
it("preserves existing alias if already set", () => {
@@ -133,9 +139,8 @@ describe("Kilo Gateway provider config", () => {
expect(resolveAgentModelPrimaryValue(result.agents?.defaults?.model)).toBe(
KILOCODE_DEFAULT_MODEL_REF,
);
const provider = result.models?.providers?.kilocode;
expect(provider).toBeDefined();
expect(provider?.baseUrl).toBe(KILOCODE_BASE_URL);
const provider = requireKilocodeProvider(result);
expect(provider.baseUrl).toBe(KILOCODE_BASE_URL);
});
});

View File

@@ -26,6 +26,17 @@ type MockKilocodeFetch = ((
mock: { calls: unknown[][] };
};
function requireModelById(
models: Awaited<ReturnType<typeof discoverKilocodeModels>>,
id: string,
): Awaited<ReturnType<typeof discoverKilocodeModels>>[number] {
const model = models.find((candidate) => candidate.id === id);
if (!model) {
throw new Error(`expected Kilocode model ${id}`);
}
return model;
}
function makeGatewayModel(overrides: Record<string, unknown> = {}) {
return {
id: "anthropic/claude-sonnet-4",
@@ -115,14 +126,13 @@ describe("discoverKilocodeModels", () => {
it("static catalog has correct defaults for kilo/auto", async () => {
const models = await discoverKilocodeModels();
const auto = models.find((m) => m.id === "kilo/auto");
expect(auto).toBeDefined();
expect(auto?.name).toBe("Kilo Auto");
expect(auto?.reasoning).toBe(true);
expect(auto?.input).toEqual(["text", "image"]);
expect(auto?.contextWindow).toBe(1000000);
expect(auto?.maxTokens).toBe(128000);
expect(auto?.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
const auto = requireModelById(models, "kilo/auto");
expect(auto.name).toBe("Kilo Auto");
expect(auto.reasoning).toBe(true);
expect(auto.input).toEqual(["text", "image"]);
expect(auto.contextWindow).toBe(1000000);
expect(auto.maxTokens).toBe(128000);
expect(auto.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
});
});
@@ -158,14 +168,13 @@ describe("discoverKilocodeModels (fetch path)", () => {
expect(models.length).toBe(2);
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
expect(sonnet).toBeDefined();
expect(sonnet?.cost.input).toBeCloseTo(3.0);
expect(sonnet?.cost.output).toBeCloseTo(15.0);
expect(sonnet?.cost.cacheRead).toBeCloseTo(0.3);
expect(sonnet?.cost.cacheWrite).toBeCloseTo(3.75);
expect(sonnet?.input).toEqual(["text", "image"]);
expect(sonnet?.reasoning).toBe(true);
const sonnet = requireModelById(models, "anthropic/claude-sonnet-4");
expect(sonnet.cost.input).toBeCloseTo(3.0);
expect(sonnet.cost.output).toBeCloseTo(15.0);
expect(sonnet.cost.cacheRead).toBeCloseTo(0.3);
expect(sonnet.cost.cacheWrite).toBeCloseTo(3.75);
expect(sonnet.input).toEqual(["text", "image"]);
expect(sonnet.reasoning).toBe(true);
expect(sonnet?.contextWindow).toBe(200000);
expect(sonnet?.maxTokens).toBe(8192);
});
@@ -244,10 +253,9 @@ describe("discoverKilocodeModels (fetch path)", () => {
});
await withFetchPathTest(mockFetch, async () => {
const models = await discoverKilocodeModels();
const auto = models.find((m) => m.id === "kilo/auto");
expect(auto).toBeDefined();
expect(auto?.name).toBe("Kilo: Auto");
expect(auto?.cost.input).toBeCloseTo(5.0);
const auto = requireModelById(models, "kilo/auto");
expect(auto.name).toBe("Kilo: Auto");
expect(auto.cost.input).toBeCloseTo(5.0);
expect(models.some((m) => m.id === "anthropic/claude-sonnet-4")).toBe(true);
});
});

View File

@@ -327,7 +327,7 @@ describe("buildLineMessageContext", () => {
expect(context!.route.matchedBy).toBe("binding.peer");
});
it("normalizes LINE ACP binding conversation ids through the plugin bindings surface", async () => {
it("normalizes LINE ACP binding conversation ids through the plugin bindings surface", () => {
const compiled = lineBindingsAdapter.compileConfiguredBinding({
conversationId: "line:user:U1234567890abcdef1234567890abcdef",
});
@@ -346,7 +346,7 @@ describe("buildLineMessageContext", () => {
});
});
it("normalizes canonical LINE targets through the plugin bindings surface", async () => {
it("normalizes canonical LINE targets through the plugin bindings surface", () => {
const compiled = lineBindingsAdapter.compileConfiguredBinding({
conversationId: "line:U1234567890abcdef1234567890abcdef",
});

View File

@@ -31,6 +31,16 @@ let getLineRuntimeState: typeof import("./monitor.js").getLineRuntimeState;
let clearLineRuntimeStateForTests: typeof import("./monitor.js").clearLineRuntimeStateForTests;
let innerLineWebhookHandlerMock: ReturnType<typeof vi.fn<LineNodeWebhookHandler>>;
function requireRegisteredRoute(): { handler: LineNodeWebhookHandler } {
const route = registerWebhookTargetWithPluginRouteMock.mock.calls[0]?.[0]?.route as
| { handler: LineNodeWebhookHandler }
| undefined;
if (!route) {
throw new Error("expected registered LINE webhook route");
}
return route;
}
vi.mock("./bot.js", () => ({
createLineBot: createLineBotMock,
}));
@@ -305,10 +315,7 @@ describe("monitorLineProvider lifecycle", () => {
runtime: {} as RuntimeEnv,
});
const route = registerWebhookTargetWithPluginRouteMock.mock.calls[0]?.[0]?.route as
| { handler: (req: IncomingMessage, res: ServerResponse) => Promise<void> }
| undefined;
expect(route).toBeDefined();
const route = requireRegisteredRoute();
const payload = JSON.stringify({ events: [{ type: "message" }] });
const signature = crypto.createHmac("SHA256", "second-secret").update(payload).digest("base64");
@@ -318,7 +325,7 @@ describe("monitorLineProvider lifecycle", () => {
}) as unknown as IncomingMessage;
const res = createRouteResponse();
await route!.handler(req, res);
await route.handler(req, res);
const firstBot = createLineBotMock.mock.results[0]?.value as {
handleWebhook: ReturnType<typeof vi.fn>;
@@ -350,10 +357,7 @@ describe("monitorLineProvider lifecycle", () => {
runtime: {} as RuntimeEnv,
});
const route = registerWebhookTargetWithPluginRouteMock.mock.calls[0]?.[0]?.route as
| { handler: (req: IncomingMessage, res: ServerResponse) => Promise<void> }
| undefined;
expect(route).toBeDefined();
const route = requireRegisteredRoute();
const payload = JSON.stringify({ events: [{ type: "message" }] });
const signature = crypto.createHmac("SHA256", "shared-secret").update(payload).digest("base64");
@@ -363,7 +367,7 @@ describe("monitorLineProvider lifecycle", () => {
}) as unknown as IncomingMessage;
const res = createRouteResponse();
await route!.handler(req, res);
await route.handler(req, res);
const firstBot = createLineBotMock.mock.results[0]?.value as {
handleWebhook: ReturnType<typeof vi.fn>;
@@ -391,10 +395,7 @@ describe("monitorLineProvider lifecycle", () => {
runtime: {} as RuntimeEnv,
});
const route = registerWebhookTargetWithPluginRouteMock.mock.calls[0]?.[0]?.route as
| { handler: (req: IncomingMessage, res: ServerResponse) => Promise<void> }
| undefined;
expect(route).toBeDefined();
const route = requireRegisteredRoute();
const createHeldPostRequest = () => {
const req = Object.assign(new EventEmitter(), {
destroyed: false,
@@ -420,12 +421,12 @@ describe("monitorLineProvider lifecycle", () => {
};
const firstRequests = Array.from({ length: limit }, () =>
route!.handler(createHeldPostRequest(), createRouteResponse()),
route.handler(createHeldPostRequest(), createRouteResponse()),
);
await new Promise((resolve) => setImmediate(resolve));
const overflowResponse = createRouteResponse();
await route!.handler(createSignedPostRequest(), overflowResponse);
await route.handler(createSignedPostRequest(), overflowResponse);
const bot = createLineBotMock.mock.results[0]?.value as {
handleWebhook: ReturnType<typeof vi.fn>;

View File

@@ -4,6 +4,18 @@ import { hasLineDirectives, parseLineDirectives } from "./reply-payload-transfor
const getLineData = (result: ReturnType<typeof parseLineDirectives>) =>
(result.channelData?.line as Record<string, unknown> | undefined) ?? {};
type TestFlexMessage = {
altText?: string;
contents?: { footer?: { contents?: unknown[] }; body?: { contents?: unknown[] } };
};
function requireFlexMessage(value: unknown, label: string): TestFlexMessage {
if (!value || typeof value !== "object") {
throw new Error(`expected flex message for ${label}`);
}
return value as TestFlexMessage;
}
describe("hasLineDirectives", () => {
it("matches expected detection across directive patterns", () => {
const cases: Array<{ text: string; expected: boolean }> = [
@@ -249,22 +261,18 @@ describe("parseLineDirectives", () => {
for (const testCase of cases) {
const result = parseLineDirectives({ text: testCase.text });
const flexMessage = getLineData(result).flexMessage as {
altText?: string;
contents?: { footer?: { contents?: unknown[] }; body?: { contents?: unknown[] } };
};
expect(flexMessage, testCase.name).toBeDefined();
const flexMessage = requireFlexMessage(getLineData(result).flexMessage, testCase.name);
if (testCase.expectedAltText !== undefined) {
expect(flexMessage?.altText, testCase.name).toBe(testCase.expectedAltText);
expect(flexMessage.altText, testCase.name).toBe(testCase.expectedAltText);
}
if (testCase.expectedText !== undefined) {
expect(result.text, testCase.name).toBe(testCase.expectedText);
}
if (testCase.expectFooter) {
expect(flexMessage?.contents?.footer?.contents?.length, testCase.name).toBeGreaterThan(0);
expect(flexMessage.contents?.footer?.contents?.length, testCase.name).toBeGreaterThan(0);
}
if ("expectBodyContents" in testCase && testCase.expectBodyContents) {
expect(flexMessage?.contents?.body?.contents, testCase.name).toBeDefined();
expect(flexMessage.contents?.body?.contents, testCase.name).toEqual(expect.any(Array));
}
}
});
@@ -285,9 +293,8 @@ describe("parseLineDirectives", () => {
for (const testCase of cases) {
const result = parseLineDirectives({ text: testCase.text });
const flexMessage = getLineData(result).flexMessage as { altText?: string };
expect(flexMessage).toBeDefined();
expect(flexMessage?.altText).toBe(testCase.altText);
const flexMessage = requireFlexMessage(getLineData(result).flexMessage, testCase.text);
expect(flexMessage.altText).toBe(testCase.altText);
}
});
});
@@ -307,9 +314,8 @@ describe("parseLineDirectives", () => {
for (const testCase of cases) {
const result = parseLineDirectives({ text: testCase.text });
const flexMessage = getLineData(result).flexMessage as { altText?: string };
expect(flexMessage).toBeDefined();
expect(flexMessage?.altText).toBe(testCase.altText);
const flexMessage = requireFlexMessage(getLineData(result).flexMessage, testCase.text);
expect(flexMessage.altText).toBe(testCase.altText);
}
});
});
@@ -329,9 +335,8 @@ describe("parseLineDirectives", () => {
for (const testCase of cases) {
const result = parseLineDirectives({ text: testCase.text });
const flexMessage = getLineData(result).flexMessage as { altText?: string };
expect(flexMessage).toBeDefined();
expect(flexMessage?.altText).toBe(testCase.altText);
const flexMessage = requireFlexMessage(getLineData(result).flexMessage, testCase.text);
expect(flexMessage.altText).toBe(testCase.altText);
}
});
});
@@ -351,10 +356,9 @@ describe("parseLineDirectives", () => {
for (const testCase of cases) {
const result = parseLineDirectives({ text: testCase.text });
const flexMessage = getLineData(result).flexMessage as { altText?: string };
expect(flexMessage).toBeDefined();
const flexMessage = requireFlexMessage(getLineData(result).flexMessage, testCase.text);
if (testCase.contains) {
expect(flexMessage?.altText).toContain(testCase.contains);
expect(flexMessage.altText).toContain(testCase.contains);
}
}
});

View File

@@ -183,7 +183,7 @@ describe("line setup wizard", () => {
expect(result.cfg.channels?.line?.channelSecret).toBe("line-secret");
});
it("reads the named-account DM policy instead of the channel root", async () => {
it("reads the named-account DM policy instead of the channel root", () => {
expect(
lineSetupWizard.dmPolicy?.getCurrent(
{
@@ -205,14 +205,14 @@ describe("line setup wizard", () => {
).toBe("allowlist");
});
it("reports account-scoped config keys for named accounts", async () => {
it("reports account-scoped config keys for named accounts", () => {
expect(lineSetupWizard.dmPolicy?.resolveConfigKeys?.({} as OpenClawConfig, "work")).toEqual({
policyKey: "channels.line.accounts.work.dmPolicy",
allowFromKey: "channels.line.accounts.work.allowFrom",
});
});
it("uses configured defaultAccount for omitted DM policy account context", async () => {
it("uses configured defaultAccount for omitted DM policy account context", () => {
const cfg = {
channels: {
line: {
@@ -246,7 +246,7 @@ describe("line setup wizard", () => {
expect(workAccount?.dmPolicy).toBe("open");
});
it('writes open policy state to the named account and preserves inherited allowFrom with "*"', async () => {
it('writes open policy state to the named account and preserves inherited allowFrom with "*"', () => {
const next = lineSetupWizard.dmPolicy?.setPolicy(
{
channels: {

View File

@@ -71,11 +71,17 @@ async function invokeWebhook(params: {
headers?: Record<string, string>;
onEvents?: ReturnType<typeof vi.fn>;
autoSign?: boolean;
runtime?: {
log: ReturnType<typeof vi.fn>;
error: ReturnType<typeof vi.fn>;
exit: ReturnType<typeof vi.fn>;
};
}) {
const onEventsMock = params.onEvents ?? vi.fn(async () => {});
const middleware = createLineWebhookMiddleware({
channelSecret: SECRET,
onEvents: onEventsMock as never,
runtime: params.runtime,
});
const headers = { ...params.headers };
@@ -97,6 +103,99 @@ async function invokeWebhook(params: {
return { res, onEvents: onEventsMock };
}
const parseResponseBody = (body: unknown) => {
if (typeof body !== "string") {
return body;
}
try {
return JSON.parse(body) as unknown;
} catch {
return body;
}
};
type WebhookPostResult = {
body: unknown;
contentType?: string;
dispatched: ReturnType<typeof vi.fn>;
runtimeError: ReturnType<typeof vi.fn>;
status: number | undefined;
};
type WebhookPostInvoker = (params: {
failWith?: Error;
rawBody: string;
signed: boolean;
}) => Promise<WebhookPostResult>;
async function invokeNodePostContract(params: {
failWith?: Error;
rawBody: string;
signed: boolean;
}) {
const dispatched = vi.fn(async () => {
if (params.failWith) {
throw params.failWith;
}
});
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
const handler = createLineNodeWebhookHandler({
channelSecret: SECRET,
bot: { handleWebhook: dispatched },
runtime,
readBody: async () => params.rawBody,
});
const { res, headers } = createRes();
await handler(
{
method: "POST",
headers: params.signed ? { "x-line-signature": sign(params.rawBody, SECRET) } : {},
} as unknown as IncomingMessage,
res,
);
return {
body: parseResponseBody(res.body),
contentType: headers["content-type"],
dispatched,
runtimeError: runtime.error,
status: res.statusCode,
};
}
async function invokeMiddlewarePostContract(params: {
failWith?: Error;
rawBody: string;
signed: boolean;
}) {
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
const onEvents = vi.fn(async () => {
if (params.failWith) {
throw params.failWith;
}
});
const { res, onEvents: dispatched } = await invokeWebhook({
body: params.rawBody,
headers: params.signed ? undefined : {},
autoSign: params.signed,
onEvents,
runtime,
});
return {
body: res.json.mock.calls.at(-1)?.[0],
dispatched,
runtimeError: runtime.error,
status: res.status.mock.calls.at(-1)?.[0],
};
}
const sharedWebhookPostContractCases = [
{ name: "node handler", invoke: invokeNodePostContract },
{ name: "middleware", invoke: invokeMiddlewarePostContract },
] satisfies Array<{
name: string;
invoke: WebhookPostInvoker;
}>;
async function expectSignedRawBodyWins(params: { rawBody: string | Buffer; signedUserId: string }) {
const onEvents = vi.fn(async () => {});
const reqBody = {
@@ -126,6 +225,66 @@ async function expectSignedRawBodyWins(params: { rawBody: string | Buffer; signe
expect(processedBody?.events?.[0]?.source?.userId).not.toBe("tampered-user");
}
describe("LINE webhook shared POST contract", () => {
it.each(sharedWebhookPostContractCases)(
"$name rejects verification-shaped requests without a signature",
async ({ invoke }) => {
const result = await invoke({ rawBody: JSON.stringify({ events: [] }), signed: false });
expect(result.status).toBe(400);
expect(result.body).toEqual({ error: "Missing X-Line-Signature header" });
if (result.contentType) {
expect(result.contentType).toBe("application/json");
}
expect(result.dispatched).not.toHaveBeenCalled();
},
);
it.each(sharedWebhookPostContractCases)(
"$name accepts signed verification-shaped requests without dispatching events",
async ({ invoke }) => {
const result = await invoke({ rawBody: JSON.stringify({ events: [] }), signed: true });
expect(result.status).toBe(200);
expect(result.body).toEqual({ status: "ok" });
if (result.contentType) {
expect(result.contentType).toBe("application/json");
}
expect(result.dispatched).not.toHaveBeenCalled();
},
);
it.each(sharedWebhookPostContractCases)(
"$name rejects missing signature when events are non-empty",
async ({ invoke }) => {
const result = await invoke({
rawBody: JSON.stringify({ events: [{ type: "message" }] }),
signed: false,
});
expect(result.status).toBe(400);
expect(result.body).toEqual({ error: "Missing X-Line-Signature header" });
expect(result.dispatched).not.toHaveBeenCalled();
},
);
it.each(sharedWebhookPostContractCases)(
"$name returns 500 when event processing fails and does not acknowledge with 200",
async ({ invoke }) => {
const result = await invoke({
failWith: new Error("transient failure"),
rawBody: JSON.stringify({ events: [{ type: "message" }] }),
signed: true,
});
expect(result.status).toBe(500);
expect(result.body).toEqual({ error: "Internal server error" });
expect(result.dispatched).toHaveBeenCalledTimes(1);
expect(result.runtimeError).toHaveBeenCalledTimes(1);
},
);
});
describe("createLineNodeWebhookHandler", () => {
it("returns 200 for GET", async () => {
const bot = { handleWebhook: vi.fn(async () => {}) };
@@ -161,32 +320,6 @@ describe("createLineNodeWebhookHandler", () => {
expect(res.body).toBeUndefined();
});
it("rejects verification-shaped requests without a signature", async () => {
const rawBody = JSON.stringify({ events: [] });
const { bot, handler } = createPostWebhookTestHarness(rawBody);
const { res, headers } = createRes();
await handler({ method: "POST", headers: {} } as unknown as IncomingMessage, res);
expect(res.statusCode).toBe(400);
expect(headers["content-type"]).toBe("application/json");
expect(res.body).toBe(JSON.stringify({ error: "Missing X-Line-Signature header" }));
expect(bot.handleWebhook).not.toHaveBeenCalled();
});
it("accepts signed verification-shaped requests without dispatching events", async () => {
const rawBody = JSON.stringify({ events: [] });
const { bot, handler, secret } = createPostWebhookTestHarness(rawBody);
const { res, headers } = createRes();
await runSignedPost({ handler, rawBody, secret, res });
expect(res.statusCode).toBe(200);
expect(headers["content-type"]).toBe("application/json");
expect(res.body).toBe(JSON.stringify({ status: "ok" }));
expect(bot.handleWebhook).not.toHaveBeenCalled();
});
it("returns 405 for non-GET/HEAD/POST methods", async () => {
const { bot, handler } = createPostWebhookTestHarness(JSON.stringify({ events: [] }));
@@ -198,17 +331,6 @@ describe("createLineNodeWebhookHandler", () => {
expect(bot.handleWebhook).not.toHaveBeenCalled();
});
it("rejects missing signature when events are non-empty", async () => {
const rawBody = JSON.stringify({ events: [{ type: "message" }] });
const { bot, handler } = createPostWebhookTestHarness(rawBody);
const { res } = createRes();
await handler({ method: "POST", headers: {} } as unknown as IncomingMessage, res);
expect(res.statusCode).toBe(400);
expect(bot.handleWebhook).not.toHaveBeenCalled();
});
it("rejects unsigned POST requests before reading the body", async () => {
const bot = { handleWebhook: vi.fn(async () => {}) };
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
@@ -316,31 +438,6 @@ describe("createLineNodeWebhookHandler", () => {
expect(res.statusCode).toBe(200);
});
it("returns 500 when event processing fails and does not acknowledge with 200", async () => {
const rawBody = JSON.stringify({ events: [{ type: "message" }] });
const { secret } = createPostWebhookTestHarness(rawBody);
const failingBot = {
handleWebhook: vi.fn(async () => {
throw new Error("transient failure");
}),
};
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
const failingHandler = createLineNodeWebhookHandler({
channelSecret: secret,
bot: failingBot,
runtime,
readBody: async () => rawBody,
});
const { res } = createRes();
await runSignedPost({ handler: failingHandler, rawBody, secret, res });
expect(res.statusCode).toBe(500);
expect(res.body).toBe(JSON.stringify({ error: "Internal server error" }));
expect(failingBot.handleWebhook).toHaveBeenCalledTimes(1);
expect(runtime.error).toHaveBeenCalledTimes(1);
});
it("returns 400 for invalid JSON payload even when signature is valid", async () => {
const rawBody = "not json";
const { bot, handler, secret } = createPostWebhookTestHarness(rawBody);
@@ -391,26 +488,6 @@ describe("createLineWebhookMiddleware", () => {
expect(onEvents).not.toHaveBeenCalled();
});
it("rejects verification-shaped requests without a signature", async () => {
const { res, onEvents } = await invokeWebhook({
body: JSON.stringify({ events: [] }),
headers: {},
autoSign: false,
});
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: "Missing X-Line-Signature header" });
expect(onEvents).not.toHaveBeenCalled();
});
it("accepts signed verification-shaped requests without dispatching events", async () => {
const { res, onEvents } = await invokeWebhook({
body: JSON.stringify({ events: [] }),
});
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ status: "ok" });
expect(onEvents).not.toHaveBeenCalled();
});
it("rejects oversized signed payloads before JSON parsing", async () => {
const largeBody = JSON.stringify({ events: [], payload: "x".repeat(70 * 1024) });
const { res, onEvents } = await invokeWebhook({ body: largeBody });
@@ -419,17 +496,6 @@ describe("createLineWebhookMiddleware", () => {
expect(onEvents).not.toHaveBeenCalled();
});
it("rejects missing signature when events are non-empty", async () => {
const { res, onEvents } = await invokeWebhook({
body: JSON.stringify({ events: [{ type: "message" }] }),
headers: {},
autoSign: false,
});
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: "Missing X-Line-Signature header" });
expect(onEvents).not.toHaveBeenCalled();
});
it("rejects signed requests when raw body is missing", async () => {
const { res, onEvents } = await invokeWebhook({
body: { events: [{ type: "message" }] },
@@ -484,30 +550,4 @@ describe("createLineWebhookMiddleware", () => {
expect(res.json).toHaveBeenCalledWith({ error: "Invalid webhook payload" });
expect(onEvents).not.toHaveBeenCalled();
});
it("returns 500 when event processing fails and does not acknowledge with 200", async () => {
const onEvents = vi.fn(async () => {
throw new Error("boom");
});
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
const rawBody = JSON.stringify({ events: [{ type: "message" }] });
const middleware = createLineWebhookMiddleware({
channelSecret: SECRET,
onEvents,
runtime,
});
const req = {
headers: { "x-line-signature": sign(rawBody, SECRET) },
body: rawBody,
} as any;
const res = createMiddlewareRes();
await middleware(req, res, {} as any);
expect(res.status).toHaveBeenCalledWith(500);
expect(res.status).not.toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ error: "Internal server error" });
expect(runtime.error).toHaveBeenCalled();
});
});

View File

@@ -74,8 +74,10 @@ describe("lmstudio-models", () => {
contextLength: number,
) => {
const loadCall = findModelLoadCall(fetchMock);
expect(loadCall).toBeDefined();
const loadInit = loadCall?.[1] as RequestInit;
if (!loadCall) {
throw new Error("expected LM Studio model load request");
}
const loadInit = loadCall[1] as RequestInit;
const loadBody = parseJsonRequestBody(loadInit) as { context_length: number };
expect(loadBody.context_length).toBe(contextLength);
};
@@ -361,8 +363,10 @@ describe("lmstudio-models", () => {
expect(fetchMock).toHaveBeenCalledTimes(2);
const loadCall = findModelLoadCall(fetchMock);
expect(loadCall).toBeDefined();
expect(loadCall?.[1]).toMatchObject({
if (!loadCall) {
throw new Error("expected LM Studio model load request");
}
expect(loadCall[1]).toMatchObject({
method: "POST",
headers: {
"X-Proxy-Auth": "required",

View File

@@ -336,7 +336,7 @@ describe("lobster plugin tool", () => {
).rejects.toThrow(/must stay within/);
});
it("can be gated off in sandboxed contexts", async () => {
it("can be gated off in sandboxed contexts", () => {
const api = fakeApi();
const factoryTool = (ctx: OpenClawPluginToolContext) => {
if (ctx.sandboxed) {

View File

@@ -51,11 +51,13 @@ describe("matrix plugin", () => {
},
],
});
expect(typeof registrar).toBe("function");
if (!registrar) {
throw new Error("expected Matrix CLI registrar to be registered");
}
expect(cliMocks.registerMatrixCli).not.toHaveBeenCalled();
const program = { command: vi.fn() };
const result = registrar?.({ program } as never);
const result = registrar({ program } as never);
await result;
expect(cliMocks.registerMatrixCli).toHaveBeenCalledWith({ program });

View File

@@ -95,9 +95,11 @@ describe("matrixMessageActions", () => {
expect(discovery.mediaSourceParams).toEqual({
"set-profile": ["avatarUrl", "avatarPath"],
});
expect(properties.displayName).toBeDefined();
expect(properties.avatarUrl).toBeDefined();
expect(properties.avatarPath).toBeDefined();
expect(properties).toMatchObject({
displayName: expect.any(Object),
avatarUrl: expect.any(Object),
avatarPath: expect.any(Object),
});
});
it("hides self-profile updates for non-owner discovery", () => {

View File

@@ -7,6 +7,25 @@ import { resolveMatrixConfigForAccount } from "./matrix/client/config.js";
import { installMatrixTestRuntime } from "./test-runtime.js";
import type { CoreConfig } from "./types.js";
function requireMatrixDirectory() {
const directory = matrixPlugin.directory;
if (!directory?.listPeers || !directory.listGroups) {
throw new Error("expected Matrix directory listPeers/listGroups");
}
return {
listPeers: directory.listPeers,
listGroups: directory.listGroups,
};
}
function requireMatrixReplyToModeResolver() {
const resolveReplyToMode = matrixPlugin.threading?.resolveReplyToMode;
if (!resolveReplyToMode) {
throw new Error("expected Matrix replyToMode resolver");
}
return resolveReplyToMode;
}
describe("matrix directory", () => {
const runtimeEnv: RuntimeEnv = createRuntimeEnv();
@@ -28,12 +47,10 @@ describe("matrix directory", () => {
},
} as unknown as CoreConfig;
expect(matrixPlugin.directory).toBeTruthy();
expect(matrixPlugin.directory?.listPeers).toBeTruthy();
expect(matrixPlugin.directory?.listGroups).toBeTruthy();
const directory = requireMatrixDirectory();
await expect(
matrixPlugin.directory!.listPeers!({
directory.listPeers({
cfg,
accountId: undefined,
query: undefined,
@@ -50,7 +67,7 @@ describe("matrix directory", () => {
);
await expect(
matrixPlugin.directory!.listGroups!({
directory.listGroups({
cfg,
accountId: undefined,
query: undefined,
@@ -79,16 +96,16 @@ describe("matrix directory", () => {
},
} as unknown as CoreConfig;
expect(matrixPlugin.threading?.resolveReplyToMode).toBeTruthy();
const resolveReplyToMode = requireMatrixReplyToModeResolver();
expect(
matrixPlugin.threading?.resolveReplyToMode?.({
resolveReplyToMode({
cfg,
accountId: "assistant",
chatType: "direct",
}),
).toBe("all");
expect(
matrixPlugin.threading?.resolveReplyToMode?.({
resolveReplyToMode({
cfg,
accountId: "default",
chatType: "direct",

View File

@@ -44,11 +44,13 @@ describe("matrix channel message adapter", () => {
it("backs declared durable-final capabilities with runtime outbound proofs", async () => {
const adapter = matrixPlugin.message;
expect(adapter).toBeDefined();
if (adapter?.send?.text === undefined || adapter.send.media === undefined) {
throw new Error("expected matrix text and media message adapter");
}
const proveText = async () => {
mocks.sendMessageMatrix.mockClear();
const result = await adapter!.send!.text!({
const result = await adapter.send.text({
cfg,
to: "room:!room:example",
text: "hello",
@@ -65,7 +67,7 @@ describe("matrix channel message adapter", () => {
const proveMedia = async () => {
mocks.sendMessageMatrix.mockClear();
const result = await adapter!.send!.media!({
const result = await adapter.send.media({
cfg,
to: "room:!room:example",
text: "caption",

View File

@@ -35,13 +35,18 @@ describe("matrix doctor", () => {
vi.clearAllMocks();
});
function normalizeMatrixDmConfig(dm: Record<string, unknown>) {
function runMatrixCompatibilityNormalize(
params: Parameters<NonNullable<typeof matrixDoctor.normalizeCompatibilityConfig>>[0],
) {
const normalize = matrixDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
throw new Error("expected Matrix doctor compatibility normalizer");
}
return normalize({
return normalize(params);
}
function normalizeMatrixDmConfig(dm: Record<string, unknown>) {
return runMatrixCompatibilityNormalize({
cfg: {
channels: {
matrix: {
@@ -155,13 +160,7 @@ describe("matrix doctor", () => {
});
it("normalizes legacy Matrix room allow aliases to enabled", () => {
const normalize = matrixDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
return;
}
const result = normalize({
const result = runMatrixCompatibilityNormalize({
cfg: {
channels: {
matrix: {
@@ -213,13 +212,7 @@ describe("matrix doctor", () => {
});
it("normalizes legacy Matrix private-network aliases", () => {
const normalize = matrixDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
return;
}
const result = normalize({
const result = runMatrixCompatibilityNormalize({
cfg: {
channels: {
matrix: {
@@ -261,13 +254,7 @@ describe("matrix doctor", () => {
});
it("migrates legacy channels.matrix.dm.policy 'trusted' with allowFrom to 'allowlist'", () => {
const normalize = matrixDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
return;
}
const result = normalize({
const result = runMatrixCompatibilityNormalize({
cfg: {
channels: {
matrix: {
@@ -331,13 +318,7 @@ describe("matrix doctor", () => {
});
it("migrates legacy per-account channels.matrix.accounts.<id>.dm.policy 'trusted'", () => {
const normalize = matrixDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
return;
}
const result = normalize({
const result = runMatrixCompatibilityNormalize({
cfg: {
channels: {
matrix: {
@@ -383,13 +364,7 @@ describe("matrix doctor", () => {
});
it("leaves modern dm.policy values untouched", () => {
const normalize = matrixDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
return;
}
const result = normalize({
const result = runMatrixCompatibilityNormalize({
cfg: {
channels: {
matrix: {

View File

@@ -96,7 +96,7 @@ describe("FileBackedMatrixSyncStore", () => {
type: "com.openclaw.test",
},
]);
expect(savedSync?.roomsData.join?.["!room:example.org"]).toBeTruthy();
expect(savedSync?.roomsData.join?.["!room:example.org"]).toEqual(expect.any(Object));
expect(secondStore.hasSavedSyncFromCleanShutdown()).toBe(false);
});

View File

@@ -241,7 +241,7 @@ describe("matrix credentials storage", () => {
}
});
it("migrates legacy matrix credential files on read", async () => {
it("migrates legacy matrix credential files on read", () => {
const { legacyPath, currentPath } = setupLegacyCredentialsFile({
cfg: {
channels: {

View File

@@ -59,8 +59,10 @@ async function triggerInvite(
inviteEvent: unknown = {},
) {
const inviteHandler = getInviteHandler();
expect(inviteHandler).toBeTruthy();
await inviteHandler!("!room:example.org", inviteEvent);
if (!inviteHandler) {
throw new Error("expected Matrix invite handler");
}
await inviteHandler("!room:example.org", inviteEvent);
}
describe("registerMatrixAutoJoin", () => {
@@ -83,7 +85,7 @@ describe("registerMatrixAutoJoin", () => {
expect(joinRoom).toHaveBeenCalledWith("!room:example.org");
});
it("does not auto-join invites by default", async () => {
it("does not auto-join invites by default", () => {
const { getInviteHandler, joinRoom } = registerAutoJoinHarness({});
expect(getInviteHandler()).toBeNull();
@@ -144,8 +146,10 @@ describe("registerMatrixAutoJoin", () => {
resolveRoom.mockRejectedValue(new Error("temporary homeserver failure"));
const inviteHandler = getInviteHandler();
expect(inviteHandler).toBeTruthy();
await expect(inviteHandler!("!room:example.org", {})).resolves.toBeUndefined();
if (!inviteHandler) {
throw new Error("expected Matrix invite handler");
}
await expect(inviteHandler("!room:example.org", {})).resolves.toBeUndefined();
expect(joinRoom).not.toHaveBeenCalled();
expect(error).toHaveBeenCalledWith(

View File

@@ -282,7 +282,7 @@ describe("registerMatrixMonitorEvents verification routing", () => {
expect(sendMessage).not.toHaveBeenCalled();
});
it("invalidates direct-room membership cache on room member events", async () => {
it("invalidates direct-room membership cache on room member events", () => {
const { invalidateRoom, roomEventListener } = createHarness();
roomEventListener("!room:example.org", {
@@ -299,7 +299,7 @@ describe("registerMatrixMonitorEvents verification routing", () => {
expect(invalidateRoom).toHaveBeenCalledWith("!room:example.org");
});
it("remembers invite provenance on room invites", async () => {
it("remembers invite provenance on room invites", () => {
const { invalidateRoom, rememberInvite, roomInviteListener } = createHarness();
if (!roomInviteListener) {
throw new Error("room.invite listener was not registered");
@@ -321,7 +321,7 @@ describe("registerMatrixMonitorEvents verification routing", () => {
expect(rememberInvite).toHaveBeenCalledWith("!room:example.org", "@alice:example.org");
});
it("ignores lifecycle-only invite events emitted with self sender ids", async () => {
it("ignores lifecycle-only invite events emitted with self sender ids", () => {
const { invalidateRoom, rememberInvite, roomInviteListener } = createHarness();
if (!roomInviteListener) {
throw new Error("room.invite listener was not registered");
@@ -342,7 +342,7 @@ describe("registerMatrixMonitorEvents verification routing", () => {
expect(rememberInvite).not.toHaveBeenCalled();
});
it("remembers invite provenance even when Matrix omits the direct invite hint", async () => {
it("remembers invite provenance even when Matrix omits the direct invite hint", () => {
const { invalidateRoom, rememberInvite, roomInviteListener } = createHarness();
if (!roomInviteListener) {
throw new Error("room.invite listener was not registered");
@@ -363,7 +363,7 @@ describe("registerMatrixMonitorEvents verification routing", () => {
expect(rememberInvite).toHaveBeenCalledWith("!room:example.org", "@alice:example.org");
});
it("does not synthesize invite provenance from room joins", async () => {
it("does not synthesize invite provenance from room joins", () => {
const { invalidateRoom, rememberInvite, roomJoinListener } = createHarness();
if (!roomJoinListener) {
throw new Error("room.join listener was not registered");

View File

@@ -3124,7 +3124,7 @@ describe("matrix monitor handler draft streaming", () => {
// The draft stream should have received "Block two", not empty string.
const sentBody = sendSingleTextMessageMatrixMock.mock.calls[0]?.[1];
expect(sentBody).toBeTruthy();
expect(sentBody).toBe("Block two");
await finish();
});

View File

@@ -31,9 +31,11 @@ describe("matrix reply context", () => {
body: longBody,
},
} as MatrixRawEvent);
expect(result).toBeDefined();
expect(result!.length).toBeLessThanOrEqual(500);
expect(result!.endsWith("...")).toBe(true);
if (result === undefined) {
throw new Error("expected truncated reply context");
}
expect(result.length).toBeLessThanOrEqual(500);
expect(result.endsWith("...")).toBe(true);
});
it("handles media-only reply events", () => {

View File

@@ -42,7 +42,7 @@ describe("resolveMatrixRoomConfig", () => {
aliases: [],
});
expect(result.matchSource).toBe("direct");
expect(result.config).toBeDefined();
expect(result.config).toMatchObject({ enabled: true });
});
it('returns matchSource="direct" for alias match', () => {
@@ -52,7 +52,7 @@ describe("resolveMatrixRoomConfig", () => {
aliases: ["#alias:example.org"],
});
expect(result.matchSource).toBe("direct");
expect(result.config).toBeDefined();
expect(result.config).toMatchObject({ enabled: true });
});
it('returns matchSource="wildcard" for wildcard match', () => {
@@ -62,7 +62,7 @@ describe("resolveMatrixRoomConfig", () => {
aliases: [],
});
expect(result.matchSource).toBe("wildcard");
expect(result.config).toBeDefined();
expect(result.config).toMatchObject({ enabled: true });
});
it("returns undefined matchSource when no match", () => {

View File

@@ -600,8 +600,11 @@ describe("MatrixClient request hardening", () => {
});
const store = lastCreateClientOpts?.store as { flush: () => Promise<void> } | undefined;
expect(store).toBeTruthy();
const flushSpy = vi.spyOn(store!, "flush").mockResolvedValue();
if (!store) {
throw new Error("expected Matrix sync store");
}
expect(store.flush).toEqual(expect.any(Function));
const flushSpy = vi.spyOn(store, "flush").mockResolvedValue();
await client.stopAndPersist();
@@ -1733,7 +1736,13 @@ describe("MatrixClient crypto bootstrapping", () => {
const status = await client.getOwnDeviceVerificationStatus();
expect(status.verified).toBe(true);
expect(status.backup).toBeDefined();
expect(status.backup).toMatchObject({
serverVersion: null,
activeVersion: null,
trusted: null,
keyLoadAttempted: false,
keyLoadError: null,
});
expect(status.serverDeviceKnown).toBeNull();
});

Some files were not shown because too many files have changed in this diff Show More