test: enable noUncheckedIndexedAccess for extension tests (#105343)

This commit is contained in:
Peter Steinberger
2026-07-12 12:48:22 +01:00
committed by GitHub
parent 5819f837a9
commit 81941f2d68
158 changed files with 2110 additions and 1074 deletions
+11 -4
View File
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import {
createPluginStateKeyedStoreForTests,
resetPluginStateStoreForTests,
@@ -89,7 +90,7 @@ describe("acpx doctor state migration", () => {
"utf8",
);
const migration = stateMigrations[0];
const migration = expectDefined(stateMigrations[0], "ACPX state migration");
await expect(migration.detectLegacyState(migrationParams())).resolves.toMatchObject({
preview: [
expect.stringContaining("ACPX gateway instance id"),
@@ -154,7 +155,7 @@ describe("acpx doctor state migration", () => {
"utf8",
);
const migration = stateMigrations[0];
const migration = expectDefined(stateMigrations[0], "ACPX state migration");
await expect(migration.detectLegacyState(migrationParams())).resolves.toBeNull();
await expect(migration.migrateLegacyState(migrationParams())).resolves.toEqual({
@@ -212,7 +213,10 @@ describe("acpx doctor state migration", () => {
state: "open",
});
const result = await stateMigrations[0].migrateLegacyState(migrationParams());
const result = await expectDefined(
stateMigrations[0],
"ACPX state migration",
).migrateLegacyState(migrationParams());
expect(result.changes).toEqual([]);
expect(result.warnings).toEqual([
@@ -254,7 +258,10 @@ describe("acpx doctor state migration", () => {
createdAt: 2,
});
const result = await stateMigrations[0].migrateLegacyState(migrationParams());
const result = await expectDefined(
stateMigrations[0],
"ACPX state migration",
).migrateLegacyState(migrationParams());
expect(result.warnings).toEqual([]);
expect(result.changes).toEqual([
@@ -4,6 +4,7 @@ import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { expectDefined } from "@openclaw/normalization-core";
import { bundledPluginFile } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it } from "vitest";
@@ -117,7 +118,11 @@ rl.on("line", (line) => process.stdout.write(line + "\n"));
.split(/\r?\n/)
.map((line) => JSON.parse(line) as { method: string; params: Record<string, unknown> });
expect(lines[0].params.mcpServers).toEqual([
const initialize = expectDefined(lines[0], "MCP initialize message");
const initialized = expectDefined(lines[1], "MCP initialized message");
const prompt = expectDefined(lines[2], "MCP prompt message");
expect(initialize.params.mcpServers).toEqual([
{
name: "canva",
command: "npx",
@@ -125,9 +130,9 @@ rl.on("line", (line) => process.stdout.write(line + "\n"));
env: [{ name: "CANVA_TOKEN", value: "secret" }],
},
]);
expect(lines[1].params.mcpServers).toEqual(lines[0].params.mcpServers);
expect(lines[2].method).toBe("session/prompt");
expect(lines[2].params.mcpServers).toBeUndefined();
expect(initialized.params.mcpServers).toEqual(initialize.params.mcpServers);
expect(prompt.method).toBe("session/prompt");
expect(prompt.params.mcpServers).toBeUndefined();
});
it("reports target stdin pipe failures without an unhandled stream error", async () => {
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import {
createPluginStateKeyedStoreForTests,
resetPluginStateStoreForTests,
@@ -51,7 +52,7 @@ describe("active-memory doctor state migration", () => {
}),
);
const migration = stateMigrations[0];
const migration = expectDefined(stateMigrations[0], "active-memory state migration");
await expect(
migration.detectLegacyState({
config: {},
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -1,6 +1,7 @@
// Amazon Bedrock tests cover index plugin behavior.
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
import {
@@ -1244,7 +1245,7 @@ describe("amazon-bedrock provider plugin", () => {
role: string;
content: Array<Record<string, unknown>>;
}>;
const lastUserContent = messages[0].content;
const lastUserContent = expectDefined(messages[0], "last user message").content;
expect(lastUserContent).toHaveLength(2);
expect(lastUserContent[1]).toEqual({ cachePoint: { type: "default" } });
});
@@ -1311,7 +1312,7 @@ describe("amazon-bedrock provider plugin", () => {
role: string;
content: Array<Record<string, unknown>>;
}>;
expect(messages[0].content).toHaveLength(2);
expect(expectDefined(messages[0], "cached user message").content).toHaveLength(2);
});
it("does not inject cache points for regular Anthropic model IDs handled by the shared runtime", async () => {
@@ -1415,12 +1416,13 @@ describe("amazon-bedrock provider plugin", () => {
content: Array<Record<string, unknown>>;
}>;
// First user message should NOT have a cache point
expect(messages[0].content).toHaveLength(1);
expect(expectDefined(messages[0], "first user message").content).toHaveLength(1);
// Assistant message untouched
expect(messages[1].content).toHaveLength(1);
expect(expectDefined(messages[1], "assistant message").content).toHaveLength(1);
// Last user message should have a cache point
expect(messages[2].content).toHaveLength(2);
expect(messages[2].content[1]).toEqual({ cachePoint: { type: "default" } });
const lastUserContent = expectDefined(messages[2], "last user message").content;
expect(lastUserContent).toHaveLength(2);
expect(lastUserContent[1]).toEqual({ cachePoint: { type: "default" } });
});
it("injects cache points for opaque application inference profile ARNs after profile lookup", async () => {
+3 -2
View File
@@ -1,4 +1,5 @@
// Anthropic tests cover stream wrappers plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
@@ -296,7 +297,7 @@ describe("Anthropic service_tier payload wrappers", () => {
);
it("fast mode injects service_tier=standard_only when disabled for API keys", () => {
const payload = serviceTierWrapperCases[0].run({
const payload = expectDefined(serviceTierWrapperCases[0], "disabled fast-mode case").run({
apiKey: "sk-ant-api03-test-key",
enabled: false,
});
@@ -317,7 +318,7 @@ describe("Anthropic service_tier payload wrappers", () => {
});
it("explicit service tier injects service_tier=standard_only for regular API keys", () => {
const payload = serviceTierWrapperCases[1].run({
const payload = expectDefined(serviceTierWrapperCases[1], "explicit service-tier case").run({
apiKey: "sk-ant-api03-test-key",
serviceTier: "standard_only",
});
@@ -1,4 +1,5 @@
// Browser tests cover browser tool.schema plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import { BrowserToolSchema } from "./browser-tool.schema.js";
import { ACT_MAX_VIEWPORT_DIMENSION } from "./browser/act-policy.js";
@@ -12,48 +13,80 @@ type SchemaProperty = {
};
type BrowserSchemaRecord = Record<string, SchemaProperty>;
function requireSchemaProperty<T>(properties: Record<string, T>, name: string, context: string): T {
return expectDefined(properties[name], context);
}
describe("browser tool schema", () => {
it("advertises the viewport resize maximum on nested and flattened act params", () => {
const properties = BrowserToolSchema.properties as SchemaRecord;
const requestProperties = properties.request.properties ?? {};
const requestProperties =
requireSchemaProperty(properties, "request", "browser request schema").properties ?? {};
expect(properties.width.maximum).toBe(ACT_MAX_VIEWPORT_DIMENSION);
expect(properties.height.maximum).toBe(ACT_MAX_VIEWPORT_DIMENSION);
expect(requestProperties.width.maximum).toBe(ACT_MAX_VIEWPORT_DIMENSION);
expect(requestProperties.height.maximum).toBe(ACT_MAX_VIEWPORT_DIMENSION);
expect(requireSchemaProperty(properties, "width", "browser width schema").maximum).toBe(
ACT_MAX_VIEWPORT_DIMENSION,
);
expect(requireSchemaProperty(properties, "height", "browser height schema").maximum).toBe(
ACT_MAX_VIEWPORT_DIMENSION,
);
expect(
requireSchemaProperty(requestProperties, "width", "browser request width schema").maximum,
).toBe(ACT_MAX_VIEWPORT_DIMENSION);
expect(
requireSchemaProperty(requestProperties, "height", "browser request height schema").maximum,
).toBe(ACT_MAX_VIEWPORT_DIMENSION);
});
it("describes targetId as a compatible tab reference", () => {
const properties = BrowserToolSchema.properties as BrowserSchemaRecord;
const requestProperties = properties.request.properties as BrowserSchemaRecord;
const targetId = requireSchemaProperty(properties, "targetId", "browser targetId schema");
const requestProperties = requireSchemaProperty(properties, "request", "browser request schema")
.properties as BrowserSchemaRecord;
const requestTargetId = requireSchemaProperty(
requestProperties,
"targetId",
"browser request targetId schema",
);
expect(properties.targetId.description).toContain("Prefer suggestedTargetId");
expect(properties.targetId.description).toContain("raw CDP targetId");
expect(requestProperties.targetId.description).toBe(properties.targetId.description);
expect(targetId.description).toContain("Prefer suggestedTargetId");
expect(targetId.description).toContain("raw CDP targetId");
expect(requestTargetId.description).toBe(targetId.description);
});
it("exposes explicit download actions and their output path", () => {
const properties = BrowserToolSchema.properties as BrowserSchemaRecord;
expect(properties.action.enum).toEqual(expect.arrayContaining(["download", "waitfordownload"]));
expect(requireSchemaProperty(properties, "action", "browser action schema").enum).toEqual(
expect.arrayContaining(["download", "waitfordownload"]),
);
expect(properties.path).toBeDefined();
});
it("exposes scrollIntoView on nested and flattened act params", () => {
const properties = BrowserToolSchema.properties as BrowserSchemaRecord;
const requestProperties = properties.request.properties as BrowserSchemaRecord;
const requestProperties = requireSchemaProperty(properties, "request", "browser request schema")
.properties as BrowserSchemaRecord;
expect(properties.kind.enum).toContain("scrollIntoView");
expect(requestProperties.kind.enum).toContain("scrollIntoView");
expect(requireSchemaProperty(properties, "kind", "browser action kind schema").enum).toContain(
"scrollIntoView",
);
expect(
requireSchemaProperty(requestProperties, "kind", "browser request kind schema").enum,
).toContain("scrollIntoView");
});
it("exposes batch actions on nested and flattened act params", () => {
const properties = BrowserToolSchema.properties as BrowserSchemaRecord;
const requestProperties = properties.request.properties as BrowserSchemaRecord;
const requestProperties = requireSchemaProperty(properties, "request", "browser request schema")
.properties as BrowserSchemaRecord;
expect(properties.kind.enum).toContain("batch");
expect(requireSchemaProperty(properties, "kind", "browser action kind schema").enum).toContain(
"batch",
);
expect(properties.actions).toBeDefined();
expect(requestProperties.kind.enum).toContain("batch");
expect(
requireSchemaProperty(requestProperties, "kind", "browser request kind schema").enum,
).toContain("batch");
expect(requestProperties.actions).toBeDefined();
});
});
@@ -1,4 +1,5 @@
// Browser tests cover cdp.helpers.fuzz plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import {
appendCdpPath,
@@ -44,7 +45,8 @@ function randInt(rng: () => number, loInclusive: number, hiInclusive: number): n
}
function pick<T>(rng: () => number, arr: readonly T[]): T {
return arr[randInt(rng, 0, arr.length - 1)];
const index = randInt(rng, 0, arr.length - 1);
return expectDefined(arr[index], `fuzz choice at index ${index}`);
}
function randHost(rng: () => number): string {
@@ -1,6 +1,7 @@
// Browser tests cover profiles service plugin behavior.
import fs from "node:fs";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../test-support.js";
import { getRuntimeConfig } from "../config/config.js";
@@ -198,10 +199,11 @@ describe("BrowserProfilesService", () => {
expect(Object.hasOwn(createdProfiles, profileName)).toBe(true);
writeConfigFile.mockClear();
const createdProfile = expectDefined(createdProfiles[profileName], "created browser profile");
vi.mocked(getRuntimeConfig).mockReturnValue({
browser: {
defaultProfile: "openclaw",
profiles: { [profileName]: createdProfiles[profileName] },
profiles: { [profileName]: createdProfile },
},
});
await service.deleteProfile(profileName);
@@ -1,4 +1,5 @@
// Browser tests cover profiles plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import { resolveBrowserConfig } from "./config.js";
import {
@@ -186,21 +187,21 @@ describe("port collision prevention", () => {
describe("color allocation", () => {
it("allocates next unused color from palette", () => {
const first = expectDefined(PROFILE_COLORS[0], "first browser profile color");
const second = expectDefined(PROFILE_COLORS[1], "second browser profile color");
const third = expectDefined(PROFILE_COLORS[2], "third browser profile color");
const fourth = expectDefined(PROFILE_COLORS[3], "fourth browser profile color");
const cases = [
{ name: "none used", used: new Set<string>(), expected: PROFILE_COLORS[0] },
{ name: "none used", used: new Set<string>(), expected: first },
{
name: "first color used",
used: new Set([PROFILE_COLORS[0].toUpperCase()]),
expected: PROFILE_COLORS[1],
used: new Set([first.toUpperCase()]),
expected: second,
},
{
name: "multiple used colors",
used: new Set([
PROFILE_COLORS[0].toUpperCase(),
PROFILE_COLORS[1].toUpperCase(),
PROFILE_COLORS[2].toUpperCase(),
]),
expected: PROFILE_COLORS[3],
used: new Set([first.toUpperCase(), second.toUpperCase(), third.toUpperCase()]),
expected: fourth,
},
] as const;
for (const testCase of cases) {
@@ -1,6 +1,7 @@
// Browser tests cover pw session plugin behavior.
import fs from "node:fs/promises";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import type { Frame, Page } from "playwright-core";
import { afterEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_DOWNLOAD_DIR } from "./paths.js";
@@ -939,7 +940,7 @@ describe("pw-session ensurePageState", () => {
mode: "aria",
});
expect(state.roleRefs).toBeDefined();
expect(state.roleRefs!.e1.role).toBe("heading");
expect(expectDefined(state.roleRefs?.e1, "stored Playwright role ref").role).toBe("heading");
expect(state.roleRefsMode).toBe("aria");
});
});
@@ -1,6 +1,11 @@
// Browser tests cover pw tools core ssrf guard plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
function requireInvocationOrder(mock: { invocationCallOrder: number[] }, context: string): number {
return expectDefined(mock.invocationCallOrder[0], context);
}
const pageState = vi.hoisted(() => ({
page: null as Record<string, unknown> | null,
locator: null as Record<string, unknown> | null,
@@ -1002,9 +1007,12 @@ describe("pw-tools-core browser SSRF guards", () => {
ssrfPolicy: { allowPrivateNetwork: false },
});
expect(sessionMocks.withPageNavigationRequestGuard.mock.invocationCallOrder[0]).toBeLessThan(
evaluate.mock.invocationCallOrder[0],
);
expect(
requireInvocationOrder(
sessionMocks.withPageNavigationRequestGuard.mock,
"request guard invocation",
),
).toBeLessThan(requireInvocationOrder(evaluate.mock, "page evaluation invocation"));
});
it("preserves helper compatibility when no ssrfPolicy is provided", async () => {
@@ -1069,8 +1077,11 @@ describe("pw-tools-core browser SSRF guards", () => {
targetId: "tab-1",
});
expect(
sessionMocks.assertPageNavigationCompletedSafely.mock.invocationCallOrder[0],
).toBeLessThan(ariaSnapshot.mock.invocationCallOrder[0]);
requireInvocationOrder(
sessionMocks.assertPageNavigationCompletedSafely.mock,
"safe-navigation assertion invocation",
),
).toBeLessThan(requireInvocationOrder(ariaSnapshot.mock, "ARIA snapshot invocation"));
});
it("re-checks current page URL before role snapshots", async () => {
@@ -1097,8 +1108,11 @@ describe("pw-tools-core browser SSRF guards", () => {
targetId: "tab-1",
});
expect(
sessionMocks.assertPageNavigationCompletedSafely.mock.invocationCallOrder[0],
).toBeLessThan(ariaSnapshot.mock.invocationCallOrder[0]);
requireInvocationOrder(
sessionMocks.assertPageNavigationCompletedSafely.mock,
"safe-navigation assertion invocation",
),
).toBeLessThan(requireInvocationOrder(ariaSnapshot.mock, "ARIA snapshot invocation"));
});
it("re-checks current page URL before aria snapshots", async () => {
@@ -1120,7 +1134,15 @@ describe("pw-tools-core browser SSRF guards", () => {
targetId: "tab-1",
});
expect(
sessionMocks.assertPageNavigationCompletedSafely.mock.invocationCallOrder[0],
).toBeLessThan(pageCdpMocks.withPageScopedCdpClient.mock.invocationCallOrder[0]);
requireInvocationOrder(
sessionMocks.assertPageNavigationCompletedSafely.mock,
"safe-navigation assertion invocation",
),
).toBeLessThan(
requireInvocationOrder(
pageCdpMocks.withPageScopedCdpClient.mock,
"page-scoped CDP invocation",
),
);
});
});
@@ -1,4 +1,5 @@
// Browser tests cover pw tools core.interactions.navigation guard plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it, vi } from "vitest";
import {
getPwToolsCoreNavigationGuardMocks,
@@ -11,6 +12,10 @@ import {
installPwToolsCoreTestHooks();
const mod = await import("./pw-tools-core.js");
function requireInvocationOrder(mock: { invocationCallOrder: number[] }, context: string): number {
return expectDefined(mock.invocationCallOrder[0], context);
}
function createMutableFrame(initialUrl: string) {
let currentUrl = initialUrl;
return {
@@ -575,8 +580,11 @@ describe("pw-tools-core interaction navigation guard", () => {
},
);
expect(
getPwToolsCoreSessionMocks().withPageNavigationRequestGuard.mock.invocationCallOrder[0],
).toBeLessThan(page.evaluate.mock.invocationCallOrder[0]);
requireInvocationOrder(
getPwToolsCoreSessionMocks().withPageNavigationRequestGuard.mock,
"navigation request guard invocation",
),
).toBeLessThan(requireInvocationOrder(page.evaluate.mock, "page evaluation invocation"));
} finally {
vi.useRealTimers();
}
@@ -1227,8 +1235,11 @@ describe("pw-tools-core interaction navigation guard", () => {
targetId: "T1",
});
expect(
getPwToolsCoreSessionMocks().withPageNavigationRequestGuard.mock.invocationCallOrder[0],
).toBeLessThan(page.evaluate.mock.invocationCallOrder[0]);
requireInvocationOrder(
getPwToolsCoreSessionMocks().withPageNavigationRequestGuard.mock,
"navigation request guard invocation",
),
).toBeLessThan(requireInvocationOrder(page.evaluate.mock, "page evaluation invocation"));
});
it("propagates the SSRF policy through batch interaction actions", async () => {
@@ -1,3 +1,4 @@
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import {
ANNOTATION_OVERLAY_ATTR,
@@ -131,8 +132,18 @@ describe("planAnnotations - viewport off-screen accounting", () => {
describe("planAnnotations - fullpage mode", () => {
it("returns box equal to doc (document coordinates)", () => {
const plan = planAnnotations({ inputs: sampleInputs, space: "fullpage" });
expect(plan.annotations[0].box).toEqual({ x: 100, y: 200, width: 50, height: 20 });
expect(plan.annotations[1].box).toEqual({ x: 300, y: 1500, width: 80, height: 18 });
expect(expectDefined(plan.annotations[0], "first full-page annotation").box).toEqual({
x: 100,
y: 200,
width: 50,
height: 20,
});
expect(expectDefined(plan.annotations[1], "second full-page annotation").box).toEqual({
x: 300,
y: 1500,
width: 80,
height: 18,
});
});
it("does not require scroll", () => {
@@ -149,7 +160,12 @@ describe("planAnnotations - element mode", () => {
space: "element",
elementRect,
});
expect(plan.annotations[0].box).toEqual({ x: 10, y: 10, width: 40, height: 20 });
expect(expectDefined(plan.annotations[0], "element annotation").box).toEqual({
x: 10,
y: 10,
width: 40,
height: 20,
});
});
it("filters out inputs that do not overlap element rect", () => {
@@ -162,7 +178,7 @@ describe("planAnnotations - element mode", () => {
elementRect,
});
expect(plan.annotations).toHaveLength(1);
expect(plan.annotations[0].ref).toBe("e1");
expect(expectDefined(plan.annotations[0], "overlapping element annotation").ref).toBe("e1");
expect(plan.overlayItems).toHaveLength(1);
});
@@ -1,5 +1,6 @@
// Browser tests cover server context.existing session plugin behavior.
import fs from "node:fs";
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import "../test-support/browser-security.mock.js";
import type { BrowserServerState } from "./server-context.js";
@@ -163,8 +164,12 @@ describe("browser server-context existing-session profile", () => {
it("reports endpoint cdpUrl for existing-session profiles", async () => {
fs.mkdirSync("/tmp/brave-profile", { recursive: true });
const state = makeState();
const chromeLiveProfile = expectDefined(
state.resolved.profiles["chrome-live"],
"chrome-live browser profile",
);
state.resolved.profiles["chrome-live"] = {
...state.resolved.profiles["chrome-live"],
...chromeLiveProfile,
cdpUrl: "http://openclaw:relay-token@127.0.0.1:9222",
};
const ctx = createBrowserRouteContext({ getState: () => state });
@@ -1,4 +1,5 @@
// Browser tests cover server context.remote profile tab ops.fallback plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it, vi } from "vitest";
import { withBrowserFetchPreconnect } from "../../test-fetch.js";
import {
@@ -412,9 +413,11 @@ describe("browser remote profile fallback and attachOnly behavior", () => {
expect(Date.now() - startedAt).toBeLessThan(700);
expect(fetchMock).toHaveBeenCalledTimes(1);
const [fetchUrl, fetchInit] =
(fetchMock.mock.calls as Array<[string | URL, RequestInit & { dispatcher?: unknown }]>)[0] ??
[];
const call = expectDefined(
(fetchMock.mock.calls as Array<[string | URL, RequestInit & { dispatcher?: unknown }]>)[0],
"remote profile fetch call",
);
const [fetchUrl, fetchInit] = call;
expect(String(fetchUrl)).toBe(
"https://1.1.1.1:9222/chrome/json/new?token=abc&url=https%3A%2F%2Fexample.com",
);
@@ -1,4 +1,5 @@
// Browser tests cover server context.remote profile tab ops.playwright plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it, vi } from "vitest";
import {
installRemoteProfileTestLifecycle,
@@ -371,8 +372,9 @@ describe("browser remote profile tab ops via Playwright", () => {
dangerouslyAllowPrivateNetwork: false,
hostnameAllowlist: ["browserless.example.com"],
};
const remoteProfile = expectDefined(state.resolved.profiles.remote, "remote browser profile");
state.resolved.profiles.remote = {
...state.resolved.profiles.remote,
...remoteProfile,
cdpUrl: "http://10.0.0.42:9222",
cdpPort: 9222,
};
@@ -1,5 +1,6 @@
// Browser tests cover server.agent contract core plugin behavior.
import fs from "node:fs";
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "./constants.js";
import { BROWSER_NAVIGATION_BLOCKED_MESSAGE } from "./errors.js";
@@ -84,6 +85,9 @@ async function postActAndReadError(base: string, body?: unknown): Promise<ActErr
const state = getBrowserControlServerTestState();
const cdpMocks = getCdpMocks();
const pwMocks = getPwMocks();
function requirePwMock<K extends keyof typeof pwMocks>(name: K): NonNullable<(typeof pwMocks)[K]> {
return expectDefined(pwMocks[name], `Playwright mock ${name}`);
}
describe("browser control server", () => {
installAgentContractHooks();
@@ -231,9 +235,9 @@ describe("browser control server", () => {
});
expect(response.ok).toBe(true);
const execArgs = mockFirstArg(pwMocks.executeActViaPlaywright, 0, "executeAct");
const execArgs = mockFirstArg(requirePwMock("executeActViaPlaywright"), 0, "executeAct");
expect(execArgs.browserProxyMode).toBe("explicit-browser-proxy");
const hoverArgs = mockFirstArg(pwMocks.hoverViaPlaywright, 0, "hover");
const hoverArgs = mockFirstArg(requirePwMock("hoverViaPlaywright"), 0, "hover");
expect(hoverArgs.browserProxyMode).toBe("explicit-browser-proxy");
},
slowTimeoutMs,
@@ -254,7 +258,7 @@ describe("browser control server", () => {
});
expect(response.ok).toBe(true);
const execArgs = mockFirstArg(pwMocks.executeActViaPlaywright, 0, "executeAct");
const execArgs = mockFirstArg(requirePwMock("executeActViaPlaywright"), 0, "executeAct");
const action = execArgs.action as { targetId?: string };
expect(action.targetId).toBe("abcd1234");
},
@@ -273,7 +277,7 @@ describe("browser control server", () => {
});
expect(response.ok).toBe(true);
const execArgs = mockFirstArg(pwMocks.executeActViaPlaywright, 0, "executeAct");
const execArgs = mockFirstArg(requirePwMock("executeActViaPlaywright"), 0, "executeAct");
const action = execArgs.action as { actions?: Array<{ targetId?: string }> };
expect(action.actions?.[0]?.targetId).toBe("abcd1234");
},
@@ -284,7 +288,7 @@ describe("browser control server", () => {
"returns the replacement targetId after an action-triggered target swap",
async () => {
const base = await startServerAndBase();
pwMocks.clickViaPlaywright.mockImplementationOnce(async () => {
requirePwMock("clickViaPlaywright").mockImplementationOnce(async () => {
vi.stubGlobal(
"fetch",
vi.fn(async (url: string) => {
@@ -320,7 +324,7 @@ describe("browser control server", () => {
"returns blocked dialog state for action-triggered modals",
async () => {
const base = await startServerAndBase();
pwMocks.executeActViaPlaywright.mockResolvedValueOnce({
requirePwMock("executeActViaPlaywright").mockResolvedValueOnce({
blockedByDialog: true,
browserState: {
dialogs: {
@@ -360,7 +364,7 @@ describe("browser control server", () => {
"returns action download metadata from /act responses",
async () => {
const base = await startServerAndBase();
pwMocks.executeActViaPlaywright.mockResolvedValueOnce({
requirePwMock("executeActViaPlaywright").mockResolvedValueOnce({
downloads: [
{
url: "https://example.com/report.pdf",
@@ -452,7 +456,7 @@ describe("browser control server", () => {
wsUrl: "ws://127.0.0.1/devtools/page/abcd1234",
limit: 1,
});
expect(pwMocks.storeAriaSnapshotRefsViaPlaywright).toHaveBeenCalledWith({
expect(requirePwMock("storeAriaSnapshotRefsViaPlaywright")).toHaveBeenCalledWith({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
nodes: [{ ref: "1", role: "link", name: "x", depth: 0 }],
@@ -464,7 +468,7 @@ describe("browser control server", () => {
};
expect(snapAi.ok).toBe(true);
expect(snapAi.format).toBe("ai");
expect(pwMocks.snapshotAiViaPlaywright).toHaveBeenCalledWith({
expect(requirePwMock("snapshotAiViaPlaywright")).toHaveBeenCalledWith({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
maxChars: DEFAULT_AI_SNAPSHOT_MAX_CHARS,
@@ -478,7 +482,7 @@ describe("browser control server", () => {
)) as { ok: boolean; format?: string };
expect(snapAiZero.ok).toBe(true);
expect(snapAiZero.format).toBe("ai");
const [lastCall] = pwMocks.snapshotAiViaPlaywright.mock.calls.at(-1) ?? [];
const [lastCall] = requirePwMock("snapshotAiViaPlaywright").mock.calls.at(-1) ?? [];
expect(lastCall).toEqual({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
@@ -487,7 +491,9 @@ describe("browser control server", () => {
},
});
pwMocks.snapshotRoleViaPlaywright.mockRejectedValueOnce(new Error("playwright stale page"));
requirePwMock("snapshotRoleViaPlaywright").mockRejectedValueOnce(
new Error("playwright stale page"),
);
const fallback = (await realFetch(`${base}/snapshot?format=ai&interactive=true`).then((r) =>
r.json(),
)) as { ok: boolean; format?: string; snapshot?: string };
@@ -510,7 +516,7 @@ describe("browser control server", () => {
it("agent contract: snapshot surfaces pending dialog state without reading the blocked page", async () => {
const base = await startServerAndBase();
const realFetch = getBrowserTestFetch();
pwMocks.getObservedBrowserStateViaPlaywright.mockResolvedValueOnce({
requirePwMock("getObservedBrowserStateViaPlaywright").mockResolvedValueOnce({
dialogs: {
pending: [
{
@@ -538,7 +544,7 @@ describe("browser control server", () => {
id: "d1",
message: "Continue?",
});
expect(pwMocks.snapshotAiViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("snapshotAiViaPlaywright")).not.toHaveBeenCalled();
});
it("agent contract: snapshot blocks pending dialog state on disallowed current tab URLs", async () => {
@@ -546,7 +552,7 @@ describe("browser control server", () => {
setBrowserControlServerTabUrl("http://127.0.0.1:8080/admin");
const base = await startServerAndBase();
const realFetch = getBrowserTestFetch();
pwMocks.getObservedBrowserStateViaPlaywright.mockResolvedValueOnce({
requirePwMock("getObservedBrowserStateViaPlaywright").mockResolvedValueOnce({
dialogs: {
pending: [
{
@@ -564,8 +570,8 @@ describe("browser control server", () => {
expect(res.status).toBe(400);
const body = (await res.json()) as { error?: unknown };
expect(body.error).toBe(BROWSER_NAVIGATION_BLOCKED_MESSAGE);
expect(pwMocks.getObservedBrowserStateViaPlaywright).not.toHaveBeenCalled();
expect(pwMocks.snapshotAiViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("getObservedBrowserStateViaPlaywright")).not.toHaveBeenCalled();
expect(requirePwMock("snapshotAiViaPlaywright")).not.toHaveBeenCalled();
});
it("agent contract: doctor deep runs a live snapshot probe", async () => {
@@ -595,7 +601,7 @@ describe("browser control server", () => {
});
expect(nav.ok).toBe(true);
expect(typeof nav.targetId).toBe("string");
const navigateArgs = mockFirstArg(pwMocks.navigateViaPlaywright, 0, "navigate");
const navigateArgs = mockFirstArg(requirePwMock("navigateViaPlaywright"), 0, "navigate");
expectRecordFields(navigateArgs, {
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
@@ -612,7 +618,7 @@ describe("browser control server", () => {
modifiers: ["Shift"],
});
expect(click.ok).toBe(true);
const clickArgs = mockFirstArg(pwMocks.clickViaPlaywright, 0, "click");
const clickArgs = mockFirstArg(requirePwMock("clickViaPlaywright"), 0, "click");
expectRecordFields(clickArgs, {
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
@@ -632,7 +638,7 @@ describe("browser control server", () => {
});
expect(clickSelector.status).toBe(200);
expect(((await clickSelector.json()) as { ok?: boolean }).ok).toBe(true);
const clickSelectorArgs = mockFirstArg(pwMocks.clickViaPlaywright, 1, "click");
const clickSelectorArgs = mockFirstArg(requirePwMock("clickViaPlaywright"), 1, "click");
expectRecordFields(clickSelectorArgs, {
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
@@ -653,7 +659,11 @@ describe("browser control server", () => {
});
expect(clickCoords.ok).toBe(true);
expect(clickCoords.url).toBe("https://example.com");
const clickCoordsArgs = mockFirstArg(pwMocks.clickCoordsViaPlaywright, 0, "click coords");
const clickCoordsArgs = mockFirstArg(
requirePwMock("clickCoordsViaPlaywright"),
0,
"click coords",
);
expectRecordFields(clickCoordsArgs, {
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
@@ -673,7 +683,7 @@ describe("browser control server", () => {
text: "",
});
expect(type.ok).toBe(true);
const typeArgs = mockFirstArg(pwMocks.typeViaPlaywright, 0, "type");
const typeArgs = mockFirstArg(requirePwMock("typeViaPlaywright"), 0, "type");
expectRecordFields(typeArgs, {
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
@@ -691,7 +701,7 @@ describe("browser control server", () => {
key: "Enter",
});
expect(press.ok).toBe(true);
const pressArgs = mockFirstArg(pwMocks.pressKeyViaPlaywright, 0, "press");
const pressArgs = mockFirstArg(requirePwMock("pressKeyViaPlaywright"), 0, "press");
expectRecordFields(pressArgs, {
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
@@ -707,7 +717,7 @@ describe("browser control server", () => {
ref: "2",
});
expect(hover.ok).toBe(true);
const hoverArgs = mockFirstArg(pwMocks.hoverViaPlaywright, 0, "hover");
const hoverArgs = mockFirstArg(requirePwMock("hoverViaPlaywright"), 0, "hover");
expectRecordFields(hoverArgs, {
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
@@ -723,7 +733,7 @@ describe("browser control server", () => {
ref: "2",
});
expect(scroll.ok).toBe(true);
const scrollArgs = mockFirstArg(pwMocks.scrollIntoViewViaPlaywright, 0, "scroll");
const scrollArgs = mockFirstArg(requirePwMock("scrollIntoViewViaPlaywright"), 0, "scroll");
expectRecordFields(scrollArgs, {
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
@@ -740,7 +750,7 @@ describe("browser control server", () => {
endRef: "4",
});
expect(drag.ok).toBe(true);
const dragArgs = mockFirstArg(pwMocks.dragViaPlaywright, 0, "drag");
const dragArgs = mockFirstArg(requirePwMock("dragViaPlaywright"), 0, "drag");
expectRecordFields(dragArgs, {
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { beforeAll, describe, expect, it } from "vitest";
import "../test-support/browser-security.mock.js";
import { BROWSER_NAVIGATION_BLOCKED_MESSAGE } from "./errors.js";
@@ -21,6 +22,9 @@ import { getBrowserTestFetch, type BrowserTestFetch } from "./test-support/fetch
const state = getBrowserControlServerTestState();
const pwMocks = getPwMocks();
function requirePwMock<K extends keyof typeof pwMocks>(name: K): NonNullable<(typeof pwMocks)[K]> {
return expectDefined(pwMocks[name], `Playwright mock ${name}`);
}
const realFetch: BrowserTestFetch = (input, init) => getBrowserTestFetch()(input, init);
beforeAll(async () => {
@@ -224,7 +228,7 @@ describe("browser control server", () => {
values: ["a", "b"],
});
expect(select.ok).toBe(true);
expectBrowserCallFields(pwMocks.selectOptionViaPlaywright, {
expectBrowserCallFields(requirePwMock("selectOptionViaPlaywright"), {
targetId: "abcd1234",
ref: "5",
values: ["a", "b"],
@@ -254,12 +258,12 @@ describe("browser control server", () => {
});
expect(fill.ok).toBe(true);
expectBrowserCallFields(
pwMocks.fillFormViaPlaywright,
requirePwMock("fillFormViaPlaywright"),
{
targetId: "abcd1234",
fields: [expected],
},
pwMocks.fillFormViaPlaywright.mock.calls.length - 1,
requirePwMock("fillFormViaPlaywright").mock.calls.length - 1,
);
}
@@ -269,7 +273,7 @@ describe("browser control server", () => {
height: 600,
});
expect(resize.ok).toBe(true);
expectBrowserCallFields(pwMocks.resizeViewportViaPlaywright, {
expectBrowserCallFields(requirePwMock("resizeViewportViaPlaywright"), {
targetId: "abcd1234",
width: 800,
height: 600,
@@ -282,7 +286,7 @@ describe("browser control server", () => {
});
expect(resizeZero.code).toBe("ACT_INVALID_REQUEST");
expect(resizeZero.error).toContain("resize requires positive width and height");
expect(pwMocks.resizeViewportViaPlaywright).toHaveBeenCalledTimes(1);
expect(requirePwMock("resizeViewportViaPlaywright")).toHaveBeenCalledTimes(1);
const resizeNegative = await postJson<{ error?: string; code?: string }>(`${base}/act`, {
kind: "resize",
@@ -291,7 +295,7 @@ describe("browser control server", () => {
});
expect(resizeNegative.code).toBe("ACT_INVALID_REQUEST");
expect(resizeNegative.error).toContain("resize requires positive width and height");
expect(pwMocks.resizeViewportViaPlaywright).toHaveBeenCalledTimes(1);
expect(requirePwMock("resizeViewportViaPlaywright")).toHaveBeenCalledTimes(1);
const resizeTooLarge = await postJson<{ error?: string; code?: string }>(`${base}/act`, {
kind: "resize",
@@ -300,14 +304,14 @@ describe("browser control server", () => {
});
expect(resizeTooLarge.code).toBe("ACT_INVALID_REQUEST");
expect(resizeTooLarge.error).toContain("resize width and height must not exceed 8192");
expect(pwMocks.resizeViewportViaPlaywright).toHaveBeenCalledTimes(1);
expect(requirePwMock("resizeViewportViaPlaywright")).toHaveBeenCalledTimes(1);
const wait = await postJson<{ ok: boolean }>(`${base}/act`, {
kind: "wait",
timeMs: 5,
});
expect(wait.ok).toBe(true);
expectBrowserCallFields(pwMocks.waitForViaPlaywright, {
expectBrowserCallFields(requirePwMock("waitForViaPlaywright"), {
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
timeMs: 5,
@@ -319,7 +323,7 @@ describe("browser control server", () => {
});
expect(evalRes.ok).toBe(true);
expect(evalRes.result).toBe("ok");
const evalCall = requireMockArg(pwMocks.evaluateViaPlaywright);
const evalCall = requireMockArg(requirePwMock("evaluateViaPlaywright"));
expectRecordFields(evalCall, "evaluate call", {
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
@@ -349,7 +353,7 @@ describe("browser control server", () => {
);
expect(batchRes.ok).toBe(true);
expectBrowserCallFields(pwMocks.batchViaPlaywright, {
expectBrowserCallFields(requirePwMock("batchViaPlaywright"), {
targetId: "abcd1234",
stopOnError: false,
evaluateEnabled: true,
@@ -384,7 +388,7 @@ describe("browser control server", () => {
});
expect(batchRes.ok).toBe(true);
expectRecordFields(requireMockArg(pwMocks.batchViaPlaywright), "batch call", {
expectRecordFields(requireMockArg(requirePwMock("batchViaPlaywright")), "batch call", {
actions: [
{
kind: "type",
@@ -414,7 +418,7 @@ describe("browser control server", () => {
expect(batchRes.error).toContain("click requires ref or selector");
expect(batchRes.code).toBe("ACT_INVALID_REQUEST");
expect(pwMocks.batchViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("batchViaPlaywright")).not.toHaveBeenCalled();
},
slowTimeoutMs,
);
@@ -431,7 +435,7 @@ describe("browser control server", () => {
expect(batchRes.error).toContain("batched action targetId must match request targetId");
expect(batchRes.code).toBe("ACT_TARGET_ID_MISMATCH");
expect(pwMocks.batchViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("batchViaPlaywright")).not.toHaveBeenCalled();
},
slowTimeoutMs,
);
@@ -447,7 +451,7 @@ describe("browser control server", () => {
});
expect(batchRes.error).toContain("click delayMs exceeds maximum of 5000ms");
expect(pwMocks.batchViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("batchViaPlaywright")).not.toHaveBeenCalled();
},
slowTimeoutMs,
);
@@ -463,14 +467,14 @@ describe("browser control server", () => {
});
expect(batchRes.error).toContain("batch exceeds maximum of 100 actions");
expect(pwMocks.batchViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("batchViaPlaywright")).not.toHaveBeenCalled();
},
slowTimeoutMs,
);
it("rejects loose response body numeric options before dispatch", async () => {
const base = await startServerAndBase();
const beforeCalls = pwMocks.responseBodyViaPlaywright.mock.calls.length;
const beforeCalls = requirePwMock("responseBodyViaPlaywright").mock.calls.length;
const timeoutRes = await postJson<{ error?: string }>(`${base}/response/body`, {
url: "**/api/data",
@@ -484,16 +488,16 @@ describe("browser control server", () => {
});
expect(maxCharsRes.error).toContain("maxChars must be a positive integer.");
expect(pwMocks.responseBodyViaPlaywright).toHaveBeenCalledTimes(beforeCalls);
expect(requirePwMock("responseBodyViaPlaywright")).toHaveBeenCalledTimes(beforeCalls);
});
it("rejects loose hook and download timeout options before dispatch", async () => {
const base = await startServerAndBase();
const uploadCalls = pwMocks.armFileUploadViaPlaywright.mock.calls.length;
const atomicUploadCalls = pwMocks.uploadViaPlaywright.mock.calls.length;
const dialogCalls = pwMocks.armDialogViaPlaywright.mock.calls.length;
const waitCalls = pwMocks.waitForDownloadViaPlaywright.mock.calls.length;
const downloadCalls = pwMocks.downloadViaPlaywright.mock.calls.length;
const uploadCalls = requirePwMock("armFileUploadViaPlaywright").mock.calls.length;
const atomicUploadCalls = requirePwMock("uploadViaPlaywright").mock.calls.length;
const dialogCalls = requirePwMock("armDialogViaPlaywright").mock.calls.length;
const waitCalls = requirePwMock("waitForDownloadViaPlaywright").mock.calls.length;
const downloadCalls = requirePwMock("downloadViaPlaywright").mock.calls.length;
const uploadRes = await postJson<{ error?: string }>(`${base}/hooks/file-chooser`, {
paths: ["a.txt"],
@@ -520,11 +524,11 @@ describe("browser control server", () => {
});
expect(downloadRes.error).toContain("timeoutMs must be a positive integer.");
expect(pwMocks.armFileUploadViaPlaywright).toHaveBeenCalledTimes(uploadCalls);
expect(pwMocks.uploadViaPlaywright).toHaveBeenCalledTimes(atomicUploadCalls);
expect(pwMocks.armDialogViaPlaywright).toHaveBeenCalledTimes(dialogCalls);
expect(pwMocks.waitForDownloadViaPlaywright).toHaveBeenCalledTimes(waitCalls);
expect(pwMocks.downloadViaPlaywright).toHaveBeenCalledTimes(downloadCalls);
expect(requirePwMock("armFileUploadViaPlaywright")).toHaveBeenCalledTimes(uploadCalls);
expect(requirePwMock("uploadViaPlaywright")).toHaveBeenCalledTimes(atomicUploadCalls);
expect(requirePwMock("armDialogViaPlaywright")).toHaveBeenCalledTimes(dialogCalls);
expect(requirePwMock("waitForDownloadViaPlaywright")).toHaveBeenCalledTimes(waitCalls);
expect(requirePwMock("downloadViaPlaywright")).toHaveBeenCalledTimes(downloadCalls);
});
it("agent contract: hooks + response + downloads + screenshot", async () => {
@@ -535,7 +539,7 @@ describe("browser control server", () => {
timeoutMs: 1234,
});
expectOkResult(upload);
expectBrowserCallFields(pwMocks.armFileUploadViaPlaywright, {
expectBrowserCallFields(requirePwMock("armFileUploadViaPlaywright"), {
targetId: "abcd1234",
// The server resolves paths (which adds a drive letter on Windows for `\\tmp\\...` style roots).
paths: [path.resolve(DEFAULT_UPLOAD_DIR, "a.txt")],
@@ -547,7 +551,7 @@ describe("browser control server", () => {
ref: "e12",
});
expectOkResult(uploadWithRef);
expectBrowserCallFields(pwMocks.uploadViaPlaywright, {
expectBrowserCallFields(requirePwMock("uploadViaPlaywright"), {
targetId: "abcd1234",
paths: [path.resolve(DEFAULT_UPLOAD_DIR, "b.txt")],
ref: "e12",
@@ -572,7 +576,7 @@ describe("browser control server", () => {
timeoutMs: 5678,
});
expectOkResult(dialog);
expectBrowserCallFields(pwMocks.armDialogViaPlaywright, {
expectBrowserCallFields(requirePwMock("armDialogViaPlaywright"), {
targetId: "abcd1234",
accept: true,
dialogId: "d1",
@@ -616,11 +620,15 @@ describe("browser control server", () => {
});
expect(shot.ok).toBe(true);
expect(typeof shot.path).toBe("string");
expectRecordFields(requireMockArg(pwMocks.takeScreenshotViaPlaywright), "screenshot call", {
element: "body",
type: "jpeg",
timeoutMs: 3333,
});
expectRecordFields(
requireMockArg(requirePwMock("takeScreenshotViaPlaywright")),
"screenshot call",
{
element: "body",
type: "jpeg",
timeoutMs: 3333,
},
);
});
it("blocks file chooser traversal / absolute paths outside uploads dir", async () => {
@@ -630,14 +638,14 @@ describe("browser control server", () => {
paths: ["../../../../etc/passwd"],
});
expect(traversal.error).toContain("Invalid path");
expect(pwMocks.armFileUploadViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("armFileUploadViaPlaywright")).not.toHaveBeenCalled();
const absOutside = path.join(path.parse(DEFAULT_UPLOAD_DIR).root, "etc", "passwd");
const abs = await postJson<{ error?: string }>(`${base}/hooks/file-chooser`, {
paths: [absOutside],
});
expect(abs.error).toContain("Invalid path");
expect(pwMocks.armFileUploadViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("armFileUploadViaPlaywright")).not.toHaveBeenCalled();
});
it("agent contract: stop endpoint", async () => {
@@ -656,7 +664,7 @@ describe("browser control server", () => {
path: "../../pwned.zip",
});
expect(res.error).toContain("Invalid path");
expect(pwMocks.traceStopViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("traceStopViaPlaywright")).not.toHaveBeenCalled();
});
it("trace stop accepts in-root relative output path", async () => {
@@ -666,7 +674,7 @@ describe("browser control server", () => {
});
expect(res.ok).toBe(true);
expect(res.path).toContain("safe-trace.zip");
const traceCall = requireMockArg(pwMocks.traceStopViaPlaywright);
const traceCall = requireMockArg(requirePwMock("traceStopViaPlaywright"));
expect(typeof traceCall.cdpUrl).toBe("string");
expectRecordFields(traceCall, "trace stop call", {
targetId: "abcd1234",
@@ -713,7 +721,7 @@ describe("browser control server", () => {
path: "../../pwned.pdf",
});
expect(waitRes.error).toContain("Invalid path");
expect(pwMocks.waitForDownloadViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("waitForDownloadViaPlaywright")).not.toHaveBeenCalled();
});
it("download rejects traversal path outside downloads dir", async () => {
@@ -723,7 +731,7 @@ describe("browser control server", () => {
path: "../../pwned.pdf",
});
expect(downloadRes.error).toContain("Invalid path");
expect(pwMocks.downloadViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("downloadViaPlaywright")).not.toHaveBeenCalled();
});
it.runIf(process.platform !== "win32")(
@@ -737,7 +745,7 @@ describe("browser control server", () => {
path: pathEscape,
});
expect(res.error).toContain("Invalid path");
expect(pwMocks.traceStopViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("traceStopViaPlaywright")).not.toHaveBeenCalled();
},
});
},
@@ -754,7 +762,7 @@ describe("browser control server", () => {
path: pathEscape,
});
expect(res.error).toContain("Invalid path");
expect(pwMocks.waitForDownloadViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("waitForDownloadViaPlaywright")).not.toHaveBeenCalled();
},
});
},
@@ -772,7 +780,7 @@ describe("browser control server", () => {
path: pathEscape,
});
expect(res.error).toContain("Invalid path");
expect(pwMocks.downloadViaPlaywright).not.toHaveBeenCalled();
expect(requirePwMock("downloadViaPlaywright")).not.toHaveBeenCalled();
},
});
},
@@ -787,7 +795,7 @@ describe("browser control server", () => {
},
);
expect(res.ok).toBe(true);
const waitCall = requireMockArg(pwMocks.waitForDownloadViaPlaywright);
const waitCall = requireMockArg(requirePwMock("waitForDownloadViaPlaywright"));
expect(typeof waitCall.cdpUrl).toBe("string");
expectRecordFields(waitCall, "wait download call", {
targetId: "abcd1234",
@@ -802,7 +810,7 @@ describe("browser control server", () => {
path: "safe-download.pdf",
});
expect(res.ok).toBe(true);
const downloadCall = requireMockArg(pwMocks.downloadViaPlaywright);
const downloadCall = requireMockArg(requirePwMock("downloadViaPlaywright"));
expect(typeof downloadCall.cdpUrl).toBe("string");
expectRecordFields(downloadCall, "download call", {
targetId: "abcd1234",
@@ -2,6 +2,7 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it, vi } from "vitest";
import {
DEFAULT_BROWSER_SCREENSHOT_DESCRIPTION_PROMPT,
@@ -115,7 +116,9 @@ describe("describeBrowserScreenshot", () => {
maxSide: 800,
});
expect(saveMediaBuffer).toHaveBeenCalledWith(Buffer.from("small"), "image/jpeg", "browser");
expect(describeResult.mock.calls[0][0].filePath).toBe("/tmp/resized.jpg");
expect(
expectDefined(describeResult.mock.calls[0]?.[0], "browser vision request").filePath,
).toBe("/tmp/resized.jpg");
});
it("returns null when image understanding is skipped or not configured", async () => {
@@ -148,7 +151,9 @@ describe("describeBrowserScreenshot", () => {
makeDeps(describeLocal),
);
expect(describeLocal.mock.calls[0][0].activeModel).toBeUndefined();
expect(
expectDefined(describeLocal.mock.calls[0]?.[0], "local browser vision request").activeModel,
).toBeUndefined();
});
});
@@ -1,4 +1,5 @@
// Browser tests cover browser request.profile from body plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
const {
@@ -82,7 +83,10 @@ async function runBrowserRequest(
) {
const respond = vi.fn();
const nodeRegistry = createContext(invokeResult, connectedNodes);
await browserHandlers["browser.request"]({
await expectDefined(
browserHandlers["browser.request"],
"browser request handler",
)({
params,
respond: respond as never,
context: { nodeRegistry } as never,
@@ -1,4 +1,5 @@
// Browser tests cover browser request.shared control state plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, describe, expect, it, vi } from "vitest";
import { getFreePort } from "../browser/test-port.js";
import type { OpenClawConfig } from "../config/config.js";
@@ -84,7 +85,10 @@ function browserConfig(params: {
async function browserRequestStatus(): Promise<unknown> {
const respond = vi.fn();
await browserHandlers["browser.request"]({
await expectDefined(
browserHandlers["browser.request"],
"browser request handler",
)({
params: {
method: "GET",
path: "/",
@@ -1,4 +1,5 @@
// Browser tests cover browser request.timeout plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -47,7 +48,10 @@ describe("browser.request local timeout", () => {
it("applies timeoutMs to local browser dispatches", async () => {
const respond = vi.fn();
await browserHandlers["browser.request"]({
await expectDefined(
browserHandlers["browser.request"],
"browser request handler",
)({
params: {
method: "POST",
path: "/tabs/open",
@@ -81,7 +85,10 @@ describe("browser.request local timeout", () => {
it("caps timeoutMs before local browser dispatches", async () => {
const respond = vi.fn();
await browserHandlers["browser.request"]({
await expectDefined(
browserHandlers["browser.request"],
"browser request handler",
)({
params: {
method: "POST",
path: "/tabs/open",
+13 -11
View File
@@ -1,6 +1,7 @@
// Byteplus tests cover index plugin behavior.
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
import { describe, expect, it } from "vitest";
import plugin from "./index.js";
@@ -9,27 +10,28 @@ import { BYTEPLUS_CODING_MODEL_CATALOG, BYTEPLUS_MODEL_CATALOG } from "./models.
describe("byteplus plugin", () => {
it("augments the catalog with bundled standard and plan models", async () => {
const provider = await registerSingleProviderPlugin(plugin);
const standardModel = expectDefined(BYTEPLUS_MODEL_CATALOG[0], "BytePlus standard model");
const codingModel = expectDefined(BYTEPLUS_CODING_MODEL_CATALOG[0], "BytePlus coding model");
const entries = await provider.augmentModelCatalog?.({
env: process.env,
entries: [],
} as never);
const standardEntry = entries?.find(
(entry) => entry.provider === "byteplus" && entry.id === BYTEPLUS_MODEL_CATALOG[0].id,
(entry) => entry.provider === "byteplus" && entry.id === standardModel.id,
);
expect(standardEntry?.name).toBe(BYTEPLUS_MODEL_CATALOG[0].name);
expect(standardEntry?.reasoning).toBe(BYTEPLUS_MODEL_CATALOG[0].reasoning);
expect(standardEntry?.input).toEqual([...BYTEPLUS_MODEL_CATALOG[0].input]);
expect(standardEntry?.contextWindow).toBe(BYTEPLUS_MODEL_CATALOG[0].contextWindow);
expect(standardEntry?.name).toBe(standardModel.name);
expect(standardEntry?.reasoning).toBe(standardModel.reasoning);
expect(standardEntry?.input).toEqual([...standardModel.input]);
expect(standardEntry?.contextWindow).toBe(standardModel.contextWindow);
const planEntry = entries?.find(
(entry) =>
entry.provider === "byteplus-plan" && entry.id === BYTEPLUS_CODING_MODEL_CATALOG[0].id,
(entry) => entry.provider === "byteplus-plan" && entry.id === codingModel.id,
);
expect(planEntry?.name).toBe(BYTEPLUS_CODING_MODEL_CATALOG[0].name);
expect(planEntry?.reasoning).toBe(BYTEPLUS_CODING_MODEL_CATALOG[0].reasoning);
expect(planEntry?.input).toEqual([...BYTEPLUS_CODING_MODEL_CATALOG[0].input]);
expect(planEntry?.contextWindow).toBe(BYTEPLUS_CODING_MODEL_CATALOG[0].contextWindow);
expect(planEntry?.name).toBe(codingModel.name);
expect(planEntry?.reasoning).toBe(codingModel.reasoning);
expect(planEntry?.input).toEqual([...codingModel.input]);
expect(planEntry?.contextWindow).toBe(codingModel.contextWindow);
});
it("declares its coding provider auth alias in the manifest", () => {
+2 -1
View File
@@ -1,4 +1,5 @@
// Chutes tests cover models plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
buildChutesModelDefinition,
@@ -89,7 +90,7 @@ describe("chutes-models", () => {
});
it("buildChutesModelDefinition returns config with required fields", () => {
const entry = CHUTES_MODEL_CATALOG[0];
const entry = expectDefined(CHUTES_MODEL_CATALOG[0], "first Chutes catalog model");
const def = buildChutesModelDefinition(entry);
expect(def.id).toBe(entry.id);
expect(def.name).toBe(entry.name);
@@ -1,3 +1,4 @@
import { expectDefined } from "@openclaw/normalization-core";
import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/plugin-entry";
import {
clearLiveCatalogCacheForTests,
@@ -235,10 +236,13 @@ describe("ClawRouter provider catalog", () => {
it("does not advertise Gemini without a streaming route", async () => {
const catalog = structuredClone(CATALOG);
catalog.providers[3].routes = catalog.providers[3].routes.filter(
const geminiProvider = expectDefined(catalog.providers[3], "Gemini ClawRouter provider");
geminiProvider.routes = geminiProvider.routes.filter(
(route) => !route.path.includes(":streamGenerateContent"),
);
catalog.providers[3].models[0].capabilities = ["llm.generate"];
expectDefined(geminiProvider.models[0], "Gemini ClawRouter model").capabilities = [
"llm.generate",
];
const provider = await buildClawRouterProviderConfig({
apiKey: "clawrouter-test-key",
fetchGuard: buildFetchGuard(catalog).fetchGuard,
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import {
embeddedAgentLog,
isToolWrappedWithBeforeToolCallHook,
@@ -1282,8 +1283,9 @@ describe("Codex app-server dynamic tool build", () => {
const tools = await buildDynamicToolsForTest(params, workspaceDir, { sandbox: null as never });
expect(tools).toHaveLength(1);
expect(tools[0]).not.toBe(wrappedTool);
expect(isToolWrappedWithBeforeToolCallHook(tools[0])).toBe(true);
const tool = expectDefined(tools[0], "Codex dynamic tool");
expect(tool).not.toBe(wrappedTool);
expect(isToolWrappedWithBeforeToolCallHook(tool)).toBe(true);
});
it("passes runtime config into Codex exec dynamic tool construction", async () => {
@@ -1,4 +1,5 @@
// Copilot tests cover doctor contract api plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import {
legacyConfigRules,
@@ -7,6 +8,10 @@ import {
} from "./doctor-contract-api.js";
describe("copilot doctor contract", () => {
function requireSessionRouteOwner() {
return expectDefined(sessionRouteStateOwners[0], "Copilot session route state owner");
}
it("has no legacy config rules at MVP (no retired fields exist yet)", () => {
expect(legacyConfigRules).toEqual([]);
});
@@ -24,18 +29,18 @@ describe("copilot doctor contract", () => {
it("declares exactly one session route state owner for copilot", () => {
expect(sessionRouteStateOwners).toHaveLength(1);
const owner = sessionRouteStateOwners[0];
const owner = requireSessionRouteOwner();
expect(owner.id).toBe("copilot");
expect(owner.label).toBe("GitHub Copilot agent runtime");
});
it("claims the subscription Copilot providers (matches attempt.ts SUPPORTED_PROVIDERS)", () => {
const owner = sessionRouteStateOwners[0];
const owner = requireSessionRouteOwner();
expect(owner.providerIds).toEqual(["github-copilot"]);
});
it("claims the copilot runtime, session key, and auth profile prefix", () => {
const owner = sessionRouteStateOwners[0];
const owner = requireSessionRouteOwner();
expect(owner.runtimeIds).toEqual(["copilot"]);
expect(owner.cliSessionKeys).toEqual(["copilot"]);
expect(owner.authProfilePrefixes).toEqual(["github-copilot:"]);
+35 -18
View File
@@ -3,6 +3,7 @@ import fsp from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import type { CopilotClient, Tool as SdkTool } from "@github/copilot-sdk";
import { expectDefined } from "@openclaw/normalization-core";
import {
abortAgentHarnessRun,
attachModelProviderRequestTransport,
@@ -84,6 +85,18 @@ type FakeSession = {
type FakeSdk = ReturnType<typeof makeFakeSdk>;
function requireSession(sdk: FakeSdk): FakeSession {
return expectDefined(sdk.sessions[0], "first Copilot SDK session");
}
function requireCreateSessionConfig(sdk: FakeSdk): Record<string, unknown> {
return expectDefined(sdk.createSession.mock.calls[0]?.[0], "Copilot createSession config");
}
function requireResumeSessionConfig(sdk: FakeSdk): Record<string, unknown> {
return expectDefined(sdk.resumeSession.mock.calls[0]?.[1], "Copilot resumeSession config");
}
function createDeferred<T>() {
let rejectPromise: ((reason?: unknown) => void) | undefined;
let resolvePromise: ((value: T | PromiseLike<T>) => void) | undefined;
@@ -920,10 +933,12 @@ describe("runCopilotAttempt", () => {
await runCopilotAttempt(makeParams(), { pool });
const session = sdk.sessions[0];
const session = requireSession(sdk);
expect(session.on.mock.calls[0]?.[0]).toBe("assistant.message_delta");
expect(session.on.mock.invocationCallOrder[0]).toBeLessThan(
session.sendAndWait.mock.invocationCallOrder[0],
expect(
expectDefined(session.on.mock.invocationCallOrder[0], "Copilot subscribe order"),
).toBeLessThan(
expectDefined(session.sendAndWait.mock.invocationCallOrder[0], "Copilot send order"),
);
});
@@ -954,7 +969,7 @@ describe("runCopilotAttempt", () => {
});
await flushAsync();
const session = sdk.sessions[0];
const session = requireSession(sdk);
session.emit("assistant.message_delta", { deltaContent: "a", messageId: "msg-1" });
session.emit("assistant.message_delta", { deltaContent: "b", messageId: "msg-1" });
session.emit("assistant.message_delta", { deltaContent: "c", messageId: "msg-1" });
@@ -988,7 +1003,7 @@ describe("runCopilotAttempt", () => {
const runPromise = runCopilotAttempt(makeParams(), { createToolBridge, pool });
await flushAsync();
const session = sdk.sessions[0];
const session = requireSession(sdk);
session.emit("assistant.message_delta", { deltaContent: "a", messageId: "msg-1" });
session.emit("assistant.message_delta", { deltaContent: "b", messageId: "msg-1" });
session.emit("assistant.message_delta", { deltaContent: "c", messageId: "msg-1" });
@@ -1014,7 +1029,7 @@ describe("runCopilotAttempt", () => {
expect(sdk.resumeSession).toHaveBeenCalledTimes(1);
expect(sdk.resumeSession.mock.calls[0]?.[0]).toBe("resume-1");
expect(
(sdk.resumeSession.mock.calls[0][1] as { continuePendingWork?: boolean }).continuePendingWork,
(requireResumeSessionConfig(sdk) as { continuePendingWork?: boolean }).continuePendingWork,
).toBe(false);
expect(sdk.createSession).toHaveBeenCalledTimes(0);
});
@@ -1528,7 +1543,7 @@ describe("runCopilotAttempt", () => {
expect(queueAgentHarnessMessage("session-1", "2")).toBe(true);
const result = await attempt;
const cfg = sdk.createSession.mock.calls[0]?.[0];
const cfg = requireCreateSessionConfig(sdk);
expect(typeof cfg.onUserInputRequest).toBe("function");
expect(onBlockReply.mock.calls[0]?.[0]).toEqual(
expect.objectContaining({ text: expect.stringContaining("Pick a mode") }),
@@ -1543,7 +1558,7 @@ describe("runCopilotAttempt", () => {
await runCopilotAttempt(makeParams(), { pool });
const cfg = sdk.createSession.mock.calls[0]?.[0];
const cfg = requireCreateSessionConfig(sdk);
expect("enableSessionTelemetry" in cfg).toBe(false);
});
@@ -1598,7 +1613,7 @@ describe("runCopilotAttempt", () => {
await runCopilotAttempt(makeParams(), { pool });
const cfg = sdk.createSession.mock.calls[0]?.[0];
const cfg = requireCreateSessionConfig(sdk);
expect("infiniteSessions" in cfg).toBe(false);
});
@@ -1651,7 +1666,7 @@ describe("runCopilotAttempt", () => {
await runCopilotAttempt(makeParams(), { pool });
const cfg = sdk.createSession.mock.calls[0]?.[0];
const cfg = requireCreateSessionConfig(sdk);
// No rendered instructions => skip the systemMessage field so
// the SDK default (foundation only) applies. Avoids polluting
// session logs with an empty `append` and removes a no-op SDK
@@ -1693,7 +1708,7 @@ describe("runCopilotAttempt", () => {
{ pool },
);
const cfg = sdk.createSession.mock.calls[0]?.[0];
const cfg = requireCreateSessionConfig(sdk);
expect("systemMessage" in cfg).toBe(false);
});
@@ -2140,7 +2155,7 @@ describe("runCopilotAttempt", () => {
pool,
});
await flushAsync();
const session = sdk.sessions[0];
const session = requireSession(sdk);
session.emit("assistant.message_delta", { deltaContent: "partial-", messageId: "msg-1" });
await flushAsync();
// SDK timer fires before the slow delta consumer resolves.
@@ -2248,7 +2263,7 @@ describe("runCopilotAttempt", () => {
await runCopilotAttempt(makeParams(), { pool });
const session = sdk.sessions[0];
const session = requireSession(sdk);
expect(session.off).toHaveBeenCalledTimes(session.on.mock.calls.length);
expect(session.disconnect).toHaveBeenCalledTimes(1);
expect(pool["release"]).toHaveBeenCalledTimes(1);
@@ -2264,7 +2279,7 @@ describe("runCopilotAttempt", () => {
const pool = makeFakePool(sdk);
const result = await runCopilotAttempt(makeParams(), { pool });
const session = sdk.sessions[0];
const session = requireSession(sdk);
expect(result.promptError).toBe(error);
expect(session.off).toHaveBeenCalledTimes(session.on.mock.calls.length);
@@ -2604,7 +2619,7 @@ describe("runCopilotAttempt", () => {
await runCopilotAttempt(makeParams({ auth: { useLoggedInUser: true } as never }), { pool });
const cfg = sdk.createSession.mock.calls[0]?.[0];
const cfg = requireCreateSessionConfig(sdk);
// Per the SDK contract, passing both useLoggedInUser and a
// session-level gitHubToken would be contradictory. The
// logged-in identity already determines content exclusion /
@@ -2625,7 +2640,7 @@ describe("runCopilotAttempt", () => {
delete process.env.GITHUB_TOKEN;
try {
await runCopilotAttempt(makeParams({ auth: {} as never }), { pool });
const cfg = sdk.createSession.mock.calls[0]?.[0];
const cfg = requireCreateSessionConfig(sdk);
expect("gitHubToken" in cfg).toBe(false);
} finally {
if (prevOpenclaw !== undefined) {
@@ -2925,13 +2940,15 @@ describe("runCopilotAttempt", () => {
);
const calls = dualWriteMock.dualWriteCopilotTranscriptBestEffort.mock.calls;
const firstCall = expectDefined(calls[0], "first Copilot transcript mirror call");
const secondCall = expectDefined(calls[1], "second Copilot transcript mirror call");
const id1 = (
calls[0][0] as {
firstCall[0] as {
messages: Array<{ role: string; __openclaw?: { mirrorIdentity?: string } }>;
}
).messages.find((m) => m.role === "user")?.["__openclaw"]?.mirrorIdentity;
const id2 = (
calls[1][0] as {
secondCall[0] as {
messages: Array<{ role: string; __openclaw?: { mirrorIdentity?: string } }>;
}
).messages.find((m) => m.role === "user")?.["__openclaw"]?.mirrorIdentity;
+6 -2
View File
@@ -1,5 +1,6 @@
// Copilot tests cover event bridge plugin behavior.
import type { SessionEvent } from "@github/copilot-sdk";
// Copilot tests cover event bridge plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, describe, expect, it, vi } from "vitest";
import { attachEventBridge, type SessionLike } from "./event-bridge.js";
@@ -1133,7 +1134,10 @@ describe("attachEventBridge", () => {
const first = bridge.snapshot();
(first.assistantTexts as string[]).push("mutated");
(first.toolMetas as Array<{ meta?: string; toolName: string }>)[0].toolName = "mutated";
expectDefined(
(first.toolMetas as Array<{ meta?: string; toolName: string }>)[0],
"Copilot tool metadata",
).toolName = "mutated";
(first.usage as { input?: number }).input = 999;
const second = bridge.snapshot();
+5 -2
View File
@@ -1,5 +1,6 @@
// Copilot tests cover tool bridge plugin behavior.
import type { Tool as SdkTool, ToolInvocation, ToolResultObject } from "@github/copilot-sdk";
import { expectDefined } from "@openclaw/normalization-core";
import type { AnyAgentTool, SandboxContext } from "openclaw/plugin-sdk/agent-harness-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
@@ -1411,8 +1412,10 @@ describe("convertOpenClawToolToSdkTool", () => {
toolCallId: "call-42",
toolName: "tool-a",
});
expect(beforeExecute.mock.invocationCallOrder[0]).toBeLessThan(
sourceTool.execute.mock.invocationCallOrder[0],
expect(
expectDefined(beforeExecute.mock.invocationCallOrder[0], "Copilot before-execute invocation"),
).toBeLessThan(
expectDefined(sourceTool.execute.mock.invocationCallOrder[0], "Copilot tool invocation"),
);
});
+20 -15
View File
@@ -1,8 +1,13 @@
// Deepseek tests cover provider policy api plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types";
import { describe, expect, it } from "vitest";
import { normalizeConfig, resolveThinkingProfile } from "./provider-policy-api.js";
function requireModel(config: ModelProviderConfig, index: number) {
return expectDefined(config.models[index], `DeepSeek provider model ${index}`);
}
describe("deepseek provider-policy-api", () => {
it("advertises max thinking levels for DeepSeek V4 models", () => {
const expectedV4Levels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
@@ -50,7 +55,7 @@ describe("deepseek provider-policy-api", () => {
const result = normalizeConfig({ provider: "deepseek", providerConfig });
expect(result).not.toBe(providerConfig);
const model = result.models[0];
const model = requireModel(result, 0);
expect(model.contextWindow).toBe(1_000_000);
expect(model.maxTokens).toBe(384_000);
expect(model.cost).toEqual({
@@ -76,7 +81,7 @@ describe("deepseek provider-policy-api", () => {
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
const model = result.models[0];
const model = requireModel(result, 0);
expect(model.contextWindow).toBe(1_000_000);
expect(model.maxTokens).toBe(384_000);
expect(model.cost).toEqual({
@@ -102,7 +107,7 @@ describe("deepseek provider-policy-api", () => {
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
const model = result.models[0];
const model = requireModel(result, 0);
expect(model.contextWindow).toBe(1_000_000);
expect(model.maxTokens).toBe(384_000);
expect(model.cost).toEqual({
@@ -254,9 +259,9 @@ describe("deepseek provider-policy-api", () => {
const result = normalizeConfig({ provider: "deepseek", providerConfig });
expect(result.models[0].contextWindow).toBe(500_000);
expect(result.models[0].maxTokens).toBe(8_192);
expect(result.models[0].cost).toBe(userCost);
expect(requireModel(result, 0).contextWindow).toBe(500_000);
expect(requireModel(result, 0).maxTokens).toBe(8_192);
expect(requireModel(result, 0).cost).toBe(userCost);
});
it("preserves an old maxTokens value when another field makes the row user-owned", () => {
@@ -280,8 +285,8 @@ describe("deepseek provider-policy-api", () => {
const result = normalizeConfig({ provider: "deepseek", providerConfig });
expect(result).toBe(providerConfig);
expect(result.models[0]).toMatchObject({ contextWindow: 500_000, maxTokens: 8_192 });
expect(result.models[0].cost).toBe(userCost);
expect(requireModel(result, 0)).toMatchObject({ contextWindow: 500_000, maxTokens: 8_192 });
expect(requireModel(result, 0).cost).toBe(userCost);
});
it("preserves explicit user contextWindow override", () => {
@@ -300,7 +305,7 @@ describe("deepseek provider-policy-api", () => {
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
const model = result.models[0];
const model = requireModel(result, 0);
expect(model.contextWindow).toBe(500_000);
// cost should still be hydrated since it was missing
expect(model.cost).toEqual({
@@ -328,7 +333,7 @@ describe("deepseek provider-policy-api", () => {
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
const model = result.models[0];
const model = requireModel(result, 0);
expect(model.cost).toEqual(userCost);
// contextWindow should still be hydrated since it was missing
expect(model.contextWindow).toBe(1_000_000);
@@ -366,7 +371,7 @@ describe("deepseek provider-policy-api", () => {
const result = normalizeConfig({ provider: "deepseek", providerConfig });
expect(result.models[0].cost).toBe(userCost);
expect(requireModel(result, 0).cost).toBe(userCost);
});
it("preserves explicit user maxTokens override", () => {
@@ -385,7 +390,7 @@ describe("deepseek provider-policy-api", () => {
};
const result = normalizeConfig({ provider: "deepseek", providerConfig });
const model = result.models[0];
const model = requireModel(result, 0);
expect(model.maxTokens).toBe(100_000);
});
@@ -465,10 +470,10 @@ describe("deepseek provider-policy-api", () => {
const result = normalizeConfig({ provider: "deepseek", providerConfig });
expect(result).not.toBe(providerConfig);
// First model should be unchanged (same reference)
expect(result.models[0]).toBe(providerConfig.models[0]);
expect(requireModel(result, 0)).toBe(requireModel(providerConfig, 0));
// Second model should be hydrated
expect(result.models[1].contextWindow).toBe(1_000_000);
expect(result.models[1].cost).toEqual({
expect(requireModel(result, 1).contextWindow).toBe(1_000_000);
expect(requireModel(result, 1).cost).toEqual({
input: 0.435,
output: 0.87,
cacheRead: 0.003625,
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import {
createPluginStateKeyedStoreForTests,
resetPluginStateStoreForTests,
@@ -73,7 +74,7 @@ describe("device-pair doctor notify migration", () => {
"utf8",
);
const migration = stateMigrations[0];
const migration = expectDefined(stateMigrations[0], "device-pair state migration");
await expect(migration.detectLegacyState(migrationParams())).resolves.toMatchObject({
preview: [expect.stringContaining("Device Pair notify subscribers")],
});
@@ -108,7 +109,7 @@ describe("device-pair doctor notify migration", () => {
"utf8",
);
const migration = stateMigrations[0];
const migration = expectDefined(stateMigrations[0], "device-pair state migration");
await expect(migration.detectLegacyState(migrationParams())).resolves.toBeNull();
await expect(migration.migrateLegacyState(migrationParams())).resolves.toEqual({
@@ -1,3 +1,4 @@
import { expectDefined } from "@openclaw/normalization-core";
// Diagnostics Prometheus tests cover service plugin behavior.
import type { DiagnosticEventPrivateData } from "openclaw/plugin-sdk/diagnostic-runtime";
// Diagnostics Prometheus tests cover service plugin behavior.
@@ -697,7 +698,7 @@ describe("diagnostics-prometheus service", () => {
});
expect(listeners).toHaveLength(1);
listeners[0](
expectDefined(listeners[0], "Prometheus diagnostics listener")(
{
...baseEvent(),
type: "model.usage",
@@ -729,7 +730,7 @@ describe("diagnostics-prometheus service", () => {
throw new Error(`${prefix}😀`);
},
});
listeners[0](
expectDefined(listeners[0], "Prometheus diagnostics listener")(
{
...baseEvent(),
type: "model.usage",
+5 -5
View File
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import type { IncomingMessage, ServerResponse } from "node:http";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
import { createMockServerResponse } from "openclaw/plugin-sdk/test-env";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@@ -139,15 +140,14 @@ describe("PlaywrightDiffScreenshotter", () => {
expect(launchMock).toHaveBeenCalledTimes(1);
expect(pages).toHaveLength(1);
expect(pages[0]?.pdf).toHaveBeenCalledTimes(1);
const pdfCall = firstMockCall(pages[0]?.pdf, "PDF render")[0] as
| Record<string, unknown>
| undefined;
const page = expectDefined(pages[0], "diffs browser page");
expect(page.pdf).toHaveBeenCalledTimes(1);
const pdfCall = firstMockCall(page.pdf, "PDF render")[0] as Record<string, unknown> | undefined;
if (!pdfCall) {
throw new Error("expected PDF render call");
}
expect(pdfCall).not.toHaveProperty("pageRanges");
expect(pages[0]?.screenshot).toHaveBeenCalledTimes(0);
expect(page.screenshot).toHaveBeenCalledTimes(0);
await expect(fs.readFile(pdfPath, "utf8")).resolves.toContain("%PDF-1.7");
});
+5 -4
View File
@@ -2,6 +2,7 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
const disableAutoStartKey = Symbol.for("openclaw.diffs.disableAutoStart");
@@ -318,7 +319,7 @@ describe("toolbar button toggles", () => {
const toolbar = renderHeaderMetadata();
const buttons = toolbar.querySelectorAll("button");
buttons[0].click();
expectDefined(buttons[0], "diff-style toggle").click();
expect(fileDiffRerenderMock).toHaveBeenCalled();
@@ -340,7 +341,7 @@ describe("toolbar button toggles", () => {
const toolbar = renderHeaderMetadata();
const buttons = toolbar.querySelectorAll("button");
buttons[3].click();
expectDefined(buttons[3], "theme toggle").click();
const lastOpts = fileDiffSetOptionsMock.mock.calls[
fileDiffSetOptionsMock.mock.calls.length - 1
@@ -361,7 +362,7 @@ describe("toolbar button toggles", () => {
const toolbar = renderHeaderMetadata();
const buttons = toolbar.querySelectorAll("button");
buttons[1].click();
expectDefined(buttons[1], "wrap toggle").click();
const lastOpts = fileDiffSetOptionsMock.mock.calls[
fileDiffSetOptionsMock.mock.calls.length - 1
@@ -381,7 +382,7 @@ describe("toolbar button toggles", () => {
const toolbar = renderHeaderMetadata();
const buttons = toolbar.querySelectorAll("button");
buttons[2].click();
expectDefined(buttons[2], "background toggle").click();
const lastOpts = fileDiffSetOptionsMock.mock.calls[
fileDiffSetOptionsMock.mock.calls.length - 1
@@ -1,4 +1,5 @@
// Discord tests cover handle action plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -788,7 +789,10 @@ describe("handleDiscordMessageAction", () => {
});
expect(handleDiscordActionMock).toHaveBeenCalledTimes(1);
const payload = handleDiscordActionMock.mock.calls[0]?.[0];
const payload = expectDefined(
handleDiscordActionMock.mock.calls[0]?.[0],
"Discord search action payload",
);
expect(payload).toMatchObject({
action: "searchMessages",
content: "test query",
@@ -815,7 +819,10 @@ describe("handleDiscordMessageAction", () => {
});
expect(handleDiscordActionMock).toHaveBeenCalledTimes(1);
const payload = handleDiscordActionMock.mock.calls[0]?.[0];
const payload = expectDefined(
handleDiscordActionMock.mock.calls[0]?.[0],
"Discord guild search action payload",
);
expect(payload).toMatchObject({
action: "searchMessages",
content: "guild-wide query",
@@ -1,3 +1,4 @@
import { expectDefined } from "@openclaw/normalization-core";
// Discord tests cover runtime plugin behavior.
import { ChannelType, PermissionFlagsBits } from "discord-api-types/v10";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
@@ -1255,8 +1256,9 @@ describe("handleDiscordMessagingAction", () => {
};
const expectedMs = Date.parse("2026-01-15T10:00:00.000Z");
expect(payload.messages[0].timestampMs).toBe(expectedMs);
expect(payload.messages[0].timestampUtc).toBe(new Date(expectedMs).toISOString());
const message = expectDefined(payload.messages[0], "Discord message result");
expect(message.timestampMs).toBe(expectedMs);
expect(message.timestampUtc).toBe(new Date(expectedMs).toISOString());
});
it("rejects unexpected readMessages payloads with a boundary error", async () => {
@@ -1808,8 +1810,9 @@ describe("handleDiscordMessagingAction", () => {
};
const expectedMs = Date.parse("2026-01-15T12:00:00.000Z");
expect(payload.pins[0].timestampMs).toBe(expectedMs);
expect(payload.pins[0].timestampUtc).toBe(new Date(expectedMs).toISOString());
const pin = expectDefined(payload.pins[0], "Discord pin result");
expect(pin.timestampMs).toBe(expectedMs);
expect(pin.timestampUtc).toBe(new Date(expectedMs).toISOString());
});
it("rejects Discord pin reads for non-allowlisted target channels", async () => {
+4 -3
View File
@@ -1,4 +1,5 @@
// Discord tests cover chunk plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { countLines, hasBalancedFences } from "openclaw/plugin-sdk/test-fixtures";
import { describe, expect, it } from "vitest";
import { chunkDiscordText, chunkDiscordTextWithMode } from "./chunk.js";
@@ -148,9 +149,9 @@ describe("chunkDiscordText", () => {
}
// Ensure italics reopen on subsequent chunks
expect(chunks[0]).toContain("_1. line");
expect(expectDefined(chunks[0], "first Discord chunk")).toContain("_1. line");
// Second chunk should reopen italics at the start
expect(chunks[1].trimStart().startsWith("_")).toBe(true);
expect(expectDefined(chunks[1], "second Discord chunk").trimStart().startsWith("_")).toBe(true);
});
it("keeps reasoning italics balanced when chunks split by char limit", () => {
@@ -200,7 +201,7 @@ describe("chunkDiscordText", () => {
const chunks = chunkDiscordText(text, { maxLines: 10, maxChars: 2000 });
expect(chunks.length).toBeGreaterThan(1);
const second = chunks[1];
const second = expectDefined(chunks[1], "second Discord chunk");
expect(second.startsWith("_")).toBe(true);
expect(second).toContain(" 11. indented line");
});
@@ -1,5 +1,6 @@
// Discord tests cover gateway plugin behavior.
import { EventEmitter } from "node:events";
import { expectDefined } from "@openclaw/normalization-core";
import {
GatewayCloseCodes,
GatewayDispatchEvents,
@@ -454,7 +455,7 @@ describe("GatewayPlugin", () => {
});
gateway.connect(false);
const oldSocket = gateway.sockets[0];
const oldSocket = expectDefined(gateway.sockets[0], "old Discord gateway socket");
oldSocket.emit("open");
gateway.connect(false);
const heartbeat = setInterval(() => {}, 1_000);
@@ -1,4 +1,5 @@
// Discord tests cover message handler.queue plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { DiscordRetryableInboundError } from "./inbound-dedupe.js";
@@ -217,8 +218,16 @@ describe("createDiscordMessageHandler queue behavior", () => {
}),
);
expect(replyTypingFeedback.onReplyStart).toHaveBeenCalledTimes(1);
expect(replyTypingFeedback.onReplyStart.mock.invocationCallOrder[0]).toBeLessThan(
processDiscordMessageMock.mock.invocationCallOrder[0],
expect(
expectDefined(
replyTypingFeedback.onReplyStart.mock.invocationCallOrder[0],
"Discord reply-start invocation",
),
).toBeLessThan(
expectDefined(
processDiscordMessageMock.mock.invocationCallOrder[0],
"Discord message processing invocation",
),
);
});
@@ -1,4 +1,5 @@
// Discord tests cover presence plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import { resolveDiscordPresenceUpdate } from "./presence.js";
@@ -14,6 +15,10 @@ function expectPresenceUpdate(
return result;
}
function expectActivity(result: DiscordPresenceUpdate) {
return expectDefined(result.activities[0], "Discord presence activity");
}
describe("resolveDiscordPresenceUpdate", () => {
it("returns online presence when no config is provided", () => {
const result = expectPresenceUpdate(resolveDiscordPresenceUpdate({}));
@@ -32,21 +37,21 @@ describe("resolveDiscordPresenceUpdate", () => {
);
expect(result.status).toBe("online");
expect(result.activities).toHaveLength(1);
expect(result.activities[0].state).toBe("Helping humans");
expect(expectActivity(result).state).toBe("Helping humans");
});
it("uses custom activity type by default", () => {
const result = expectPresenceUpdate(resolveDiscordPresenceUpdate({ activity: "test" }));
expect(result.activities[0].type).toBe(4);
expect(result.activities[0].name).toBe("Custom Status");
expect(expectActivity(result).type).toBe(4);
expect(expectActivity(result).name).toBe("Custom Status");
});
it("respects explicit activityType", () => {
const result = expectPresenceUpdate(
resolveDiscordPresenceUpdate({ activity: "test", activityType: 3 }),
);
expect(result.activities[0].type).toBe(3);
expect(result.activities[0].name).toBe("test");
expect(expectActivity(result).type).toBe(3);
expect(expectActivity(result).name).toBe("test");
});
it("sets streaming URL for type 1", () => {
@@ -57,6 +62,6 @@ describe("resolveDiscordPresenceUpdate", () => {
activityUrl: "https://twitch.tv/test",
}),
);
expect(result.activities[0].url).toBe("https://twitch.tv/test");
expect(expectActivity(result).url).toBe("https://twitch.tv/test");
});
});
@@ -1,4 +1,5 @@
// Discord tests cover reply delivery plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@@ -133,7 +134,9 @@ describe("deliverDiscordReply", () => {
expect(params.replyToMode).toBe("all");
const deps = params.deps!;
await deps.discord("channel:101", "probe", { verbose: false });
await expectDefined(deps.discord, "Discord reply sender")("channel:101", "probe", {
verbose: false,
});
expect(firstMockArg(sendMessageDiscordMock, "sendMessageDiscord", 0)).toBe("channel:101");
expect(firstMockArg(sendMessageDiscordMock, "sendMessageDiscord", 1)).toBe("probe");
const sendOptions = objectArgAt(sendMessageDiscordMock, 2);
@@ -515,10 +518,14 @@ describe("deliverDiscordReply", () => {
});
const deps = firstDeliverParams().deps!;
await deps.discordVoice("channel:123", "https://example.com/voice.ogg", {
cfg,
reply: { messageId: "reply-1", scope: "all" },
});
await expectDefined(deps.discordVoice, "Discord voice reply sender")(
"channel:123",
"https://example.com/voice.ogg",
{
cfg,
reply: { messageId: "reply-1", scope: "all" },
},
);
expect(firstMockArg(sendVoiceMessageDiscordMock, "sendVoiceMessageDiscord", 0)).toBe(
"channel:123",
@@ -2,6 +2,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { ChannelType } from "discord-api-types/v10";
import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runtime";
import type { OpenKeyedStoreOptions } from "openclaw/plugin-sdk/plugin-state-runtime";
@@ -473,11 +474,12 @@ describe("thread binding lifecycle", () => {
});
expect(updated).toHaveLength(1);
const updatedBinding = expectDefined(updated[0], "idle-timeout thread binding");
expect(updated[0]?.lastActivityAt).toBe(new Date("2026-02-20T23:15:00.000Z").getTime());
expect(updated[0]?.boundAt).toBe(boundAt);
expect(
resolveThreadBindingInactivityExpiresAt({
record: updated[0],
record: updatedBinding,
defaultIdleTimeoutMs: manager.getIdleTimeoutMs(),
}),
).toBe(new Date("2026-02-21T01:15:00.000Z").getTime());
@@ -514,11 +516,12 @@ describe("thread binding lifecycle", () => {
});
expect(updated).toHaveLength(1);
const updatedBinding = expectDefined(updated[0], "max-age thread binding");
expect(updated[0]?.boundAt).toBe(new Date("2026-02-20T10:30:00.000Z").getTime());
expect(updated[0]?.lastActivityAt).toBe(new Date("2026-02-20T10:30:00.000Z").getTime());
expect(
resolveThreadBindingMaxAgeExpiresAt({
record: updated[0],
record: updatedBinding,
defaultMaxAgeMs: manager.getMaxAgeMs(),
}),
).toBe(new Date("2026-02-20T13:30:00.000Z").getTime());
@@ -1,4 +1,5 @@
// Discord tests cover thread session close plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const hoisted = vi.hoisted(() => {
@@ -155,7 +156,9 @@ describe("closeDiscordThreadSessions", () => {
});
expect(count).toBe(1);
expect(store[uppercaseKey].updatedAt).toBe(0);
expect(expectDefined(store[uppercaseKey], "uppercase Discord thread session").updatedAt).toBe(
0,
);
});
it("returns 0 immediately when threadId is empty without touching the store", async () => {
@@ -1,5 +1,6 @@
// Discord tests cover manager plugin behavior.
import { PassThrough, type Readable } from "node:stream";
import { expectDefined } from "@openclaw/normalization-core";
import type {
RealtimeVoiceAgentControlResult,
RealtimeVoiceForcedConsultCoordinator,
@@ -1792,7 +1793,9 @@ describe("DiscordVoiceManager", () => {
expect(realtimeSessionMock.handleBargeIn).toHaveBeenCalled();
const lastTimestampCall = realtimeSessionMock.setMediaTimestamp.mock.invocationCallOrder.at(-1);
const firstBargeInCall = realtimeSessionMock.handleBargeIn.mock.invocationCallOrder[0];
expect(lastTimestampCall).toBeLessThan(firstBargeInCall);
expect(expectDefined(lastTimestampCall, "last media timestamp invocation")).toBeLessThan(
expectDefined(firstBargeInCall, "first barge-in invocation"),
);
expect(player.stop).not.toHaveBeenCalled();
expect(realtimeSessionMock.sendAudio).toHaveBeenCalled();
bridgeParams?.onEvent?.({ direction: "server", type: "response.done" });
@@ -1,6 +1,7 @@
// Feishu tests cover monitor.webhook e2e plugin behavior.
import crypto from "node:crypto";
import type { Server } from "node:http";
import { expectDefined } from "@openclaw/normalization-core";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { createFeishuRuntimeMockModule } from "./monitor.test-mocks.js";
import {
@@ -254,7 +255,10 @@ describe("Feishu webhook signed-request e2e", () => {
encryptKey: "encrypt_key",
rawBody: JSON.stringify(payload),
});
headers["x-lark-signature"] = headers["x-lark-signature"].slice(0, 12);
headers["x-lark-signature"] = expectDefined(
headers["x-lark-signature"],
"Feishu webhook signature",
).slice(0, 12);
const response = await fetch(url, {
method: "POST",
+83 -76
View File
@@ -299,11 +299,16 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
) as TypingDispatcherOptions;
}
function firstStreamingCloseText(instanceIndex = 0): string {
const close = streamingInstances[instanceIndex]?.close;
if (!close) {
function requireStreamingInstance(instanceIndex: number): StreamingSessionStub {
const instance = streamingInstances[instanceIndex];
if (!instance) {
throw new Error(`Expected streaming instance ${instanceIndex}`);
}
return instance;
}
function firstStreamingCloseText(instanceIndex = 0): string {
const close = requireStreamingInstance(instanceIndex).close;
return String(firstMockArg(close, "streaming close"));
}
@@ -321,10 +326,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
instanceIndex: number,
expected: Record<string, unknown>,
): Record<string, unknown> {
const start = streamingInstances[instanceIndex]?.start;
if (!start) {
throw new Error(`Expected streaming instance ${instanceIndex}`);
}
const start = requireStreamingInstance(instanceIndex).start;
expect(firstMockArg(start, "streaming start")).toBe("oc_chat");
expect(firstMockArg(start, "streaming start", 1)).toBe("chat_id");
return expectRecordFields(
@@ -335,7 +337,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
}
function streamingUpdateTexts(instanceIndex = 0): string[] {
return streamingInstances[instanceIndex].update.mock.calls.map((call: unknown[]) =>
return requireStreamingInstance(instanceIndex).update.mock.calls.map((call: unknown[]) =>
typeof call[0] === "string" ? call[0] : "",
);
}
@@ -466,8 +468,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].credentials).toMatchObject({ httpTimeoutMs: 45_000 });
expect(streamingInstances[0].close).toHaveBeenCalledWith("plain text", {
expect(requireStreamingInstance(0).credentials).toMatchObject({ httpTimeoutMs: 45_000 });
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("plain text", {
note: "Agent: agent",
});
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
@@ -537,8 +539,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].discard).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).not.toHaveBeenCalled();
expect(requireStreamingInstance(0).discard).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).not.toHaveBeenCalled();
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(2);
expectMockArgFields(sendMessageFeishuMock, "first message send params", {
text: "final text",
@@ -577,12 +579,12 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].start).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).start).toHaveBeenCalledTimes(1);
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(1);
expectMockArgFields(sendMessageFeishuMock, "message send params", {
text: "tool summary",
});
expect(streamingInstances[0].close).toHaveBeenCalledWith("plain final answer", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("plain final answer", {
note: "Agent: agent",
});
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
@@ -681,7 +683,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith("plain block", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("plain block", {
note: "Agent: agent",
});
});
@@ -748,7 +750,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith("```md\nanswer\n```", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("```md\nanswer\n```", {
note: "Agent: agent",
});
});
@@ -786,7 +788,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].start).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).start).toHaveBeenCalledTimes(1);
expectStreamingStartOptions(0, {
replyToMessageId: undefined,
replyInThread: undefined,
@@ -794,7 +796,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
header: { title: "agent", template: "blue" },
note: "Agent: agent",
});
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
});
@@ -844,9 +846,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].start).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith("```md\npartial answer\n```", {
expect(requireStreamingInstance(0).start).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("```md\npartial answer\n```", {
note: "Agent: agent",
});
});
@@ -860,8 +862,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith(
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith(
"```md\n完整回复第一段 + 第二段\n```",
{
note: "Agent: agent",
@@ -880,8 +882,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith(
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith(
"The file is ready.\n\n⚠️ Exec failed",
{ note: "Agent: agent" },
);
@@ -901,7 +903,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith(
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith(
"The file is ready.\n\n⚠️ Exec failed",
{ note: "Agent: agent" },
);
@@ -916,7 +918,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith("⚠️ Exec failed", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("⚠️ Exec failed", {
note: "Agent: agent",
});
});
@@ -933,8 +935,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].discard).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).not.toHaveBeenCalled();
expect(requireStreamingInstance(0).discard).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).not.toHaveBeenCalled();
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(1);
expectLastMockArgFields(sendMessageFeishuMock, "message send params", {
text: "123456789012345678\n\n⚠️ Exec failed",
@@ -950,8 +952,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.deliver({ text: "```md\n同一条回复\n```" }, { kind: "final" });
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith("```md\n同一条回复\n```", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("```md\n同一条回复\n```", {
note: "Agent: agent",
});
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
@@ -980,10 +982,13 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.deliver({ text: "```md\nidle streamed reply\n```" }, { kind: "final" });
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith("```md\nidle streamed reply\n```", {
note: "Agent: agent",
});
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith(
"```md\nidle streamed reply\n```",
{
note: "Agent: agent",
},
);
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
@@ -1000,8 +1005,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].start).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith("plain final answer", {
expect(requireStreamingInstance(0).start).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("plain final answer", {
note: "Agent: agent",
});
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
@@ -1032,7 +1037,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith("plain streamed answer", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("plain streamed answer", {
note: "Agent: agent",
});
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
@@ -1063,8 +1068,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith("First complete answer", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("First complete answer", {
note: "Agent: agent",
});
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
@@ -1094,7 +1099,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
@@ -1161,8 +1166,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith("hellolo world", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("hellolo world", {
note: "Agent: agent",
});
});
@@ -1188,8 +1193,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith("```md\npartial\n```", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("```md\npartial\n```", {
note: "Agent: agent",
});
});
@@ -1218,7 +1223,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith(
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith(
"Preparing the lookup plan with enough text to count as one block.Found the answer.",
{
note: "Agent: agent",
@@ -1247,7 +1252,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
await options.onIdle?.();
expect(streamingInstances[0].close).toHaveBeenCalledWith("visible answer", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("visible answer", {
note: "Agent: agent",
});
});
@@ -1315,8 +1320,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].discard).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).not.toHaveBeenCalled();
expect(requireStreamingInstance(0).discard).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).not.toHaveBeenCalled();
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
@@ -1341,8 +1346,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].discard).not.toHaveBeenCalled();
expect(streamingInstances[0].close).toHaveBeenCalledWith("caption from stream", {
expect(requireStreamingInstance(0).discard).not.toHaveBeenCalled();
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("caption from stream", {
note: "Agent: agent",
});
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
@@ -1463,8 +1468,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].start).toHaveBeenCalledTimes(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).start).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
expectMockArgFields(sendMediaFeishuMock, "media send params", {
mediaUrl: "https://example.com/a.png",
@@ -1560,7 +1565,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
const updateCalls = streamingInstances[0].update.mock.calls.map((c: unknown[]) =>
const updateCalls = requireStreamingInstance(0).update.mock.calls.map((c: unknown[]) =>
typeof c[0] === "string" ? c[0] : "",
);
const reasoningUpdate = updateCalls.find((c) => c.includes("Thinking"));
@@ -1575,7 +1580,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
throw new Error("expected combined reasoning and final-answer streaming update");
}
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
const closeArg = firstStreamingCloseText();
expect(closeArg).toContain("> 💭 **Thinking**");
expect(closeArg).toContain("---");
@@ -1633,7 +1638,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
const closeArg = firstStreamingCloseText();
expect(closeArg).toContain("> 💭 **Thinking**");
expect(closeArg).toContain("> deep thought");
@@ -1672,7 +1677,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
// Deliver the same raw answer text again — should be deduped
await options.deliver({ text: "```ts\nfinal answer\n```" }, { kind: "final" });
@@ -1784,7 +1789,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
const updateTexts = streamingUpdateTexts();
expect(updateTexts.join("\n")).toContain("🔎 Web Search");
expect(streamingInstances[0].close).toHaveBeenCalledWith("final answer", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("final answer", {
note: "Agent: agent",
});
});
@@ -1872,10 +1877,10 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(2);
expect(streamingInstances[0].close).toHaveBeenCalledWith("First answer", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("First answer", {
note: "Agent: agent",
});
expect(streamingInstances[1].close).toHaveBeenCalledWith("Second answer", {
expect(requireStreamingInstance(1).close).toHaveBeenCalledWith("Second answer", {
note: "Agent: agent",
});
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
@@ -1912,10 +1917,10 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(2);
expect(streamingInstances[0].close).toHaveBeenCalledWith("First answer", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("First answer", {
note: "Agent: agent",
});
expect(streamingInstances[1].close).toHaveBeenCalledWith("Recovered answer", {
expect(requireStreamingInstance(1).close).toHaveBeenCalledWith("Recovered answer", {
note: "Agent: agent",
});
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
@@ -1986,7 +1991,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await expect(result.ensureNoVisibleReplyFallback("zero-final-count")).resolves.toBe(false);
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
expect(requireStreamingInstance(0).close).toHaveBeenCalledTimes(1);
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
expect(result.getVisibleReplyState()).toEqual({
visibleReplySent: true,
@@ -1999,15 +2004,15 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
const { result, options } = createDispatcherHarness({ runtime });
await options.deliver({ text: "```md\nvisible answer\n```" }, { kind: "final" });
streamingInstances[0].close = vi.fn(async () => {
streamingInstances[0].active = false;
requireStreamingInstance(0).close = vi.fn(async () => {
requireStreamingInstance(0).active = false;
return false;
});
await options.onIdle?.();
await expect(result.ensureNoVisibleReplyFallback("zero-final-count")).resolves.toBe(true);
expect(streamingInstances[0].close).toHaveBeenCalledWith("```md\nvisible answer\n```", {
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("```md\nvisible answer\n```", {
note: "Agent: agent",
});
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(1);
@@ -2026,7 +2031,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.deliver({ text: "```md\nvisible answer\n```" }, { kind: "final" });
const streamingSession = streamingInstances[0];
const streamingSession = requireStreamingInstance(0);
let releaseClose: () => void = () => {};
const closeMock = vi.fn(async () => {
await new Promise<void>((resolve) => {
@@ -2096,7 +2101,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await expect(result.ensureNoVisibleReplyFallback("zero-final-count")).resolves.toBe(true);
expect(streamingInstances).toHaveLength(1);
expect(streamingInstances[0].close).toHaveBeenCalledWith("", { note: "Agent: agent" });
expect(requireStreamingInstance(0).close).toHaveBeenCalledWith("", { note: "Agent: agent" });
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(1);
expect(result.getVisibleReplyState()).toEqual({
visibleReplySent: true,
@@ -2141,9 +2146,10 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
it("cleans streaming state even when close throws", async () => {
const origPush = streamingInstances.push.bind(streamingInstances);
streamingInstances.push = (...args: StreamingSessionStub[]) => {
if (args.length > 0 && streamingInstances.length === 0) {
args[0].close = vi.fn(async () => {
args[0].active = false;
const firstInstance = args[0];
if (firstInstance && streamingInstances.length === 0) {
firstInstance.close = vi.fn(async () => {
firstInstance.active = false;
throw new Error("close failed");
});
}
@@ -2160,7 +2166,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(2);
expect(streamingInstances[1].close).toHaveBeenCalledWith("```md\nsecond\n```", {
expect(requireStreamingInstance(1).close).toHaveBeenCalledWith("```md\nsecond\n```", {
note: "Agent: agent",
});
} finally {
@@ -2189,8 +2195,9 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
// Intercept streaming instance creation to make first start() reject
const origPush = streamingInstances.push.bind(streamingInstances);
streamingInstances.push = (...args: StreamingSessionStub[]) => {
if (shouldFailStart) {
args[0].start = vi
const firstInstance = args[0];
if (shouldFailStart && firstInstance) {
firstInstance.start = vi
.fn()
.mockRejectedValue(new Error("Create card request failed with HTTP 400"));
shouldFailStart = false;
@@ -2235,8 +2242,8 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
await options.onIdle?.();
expect(streamingInstances).toHaveLength(2);
expect(streamingInstances[1].start).toHaveBeenCalled();
expect(streamingInstances[1].close).toHaveBeenCalled();
expect(requireStreamingInstance(1).start).toHaveBeenCalled();
expect(requireStreamingInstance(1).close).toHaveBeenCalled();
} finally {
streamingInstances.push = origPush;
nowSpy.mockRestore();
@@ -1,6 +1,7 @@
// File Transfer tests cover policy plugin behavior.
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Mock the plugin-sdk runtime-config surface so we can drive the policy
@@ -428,9 +429,11 @@ describe("persistAllowAlways", () => {
};
};
};
expect(root.plugins.entries["file-transfer"].config.nodes.n1.allowReadPaths).toContain(
"/srv/added.png",
const node = expectDefined(
root.plugins.entries["file-transfer"].config.nodes.n1,
"n1 file-transfer node",
);
expect(node.allowReadPaths).toContain("/srv/added.png");
});
it("creates a new node entry keyed by displayName when no entry exists", async () => {
@@ -459,9 +462,11 @@ describe("persistAllowAlways", () => {
};
};
};
expect(root.plugins.entries["file-transfer"].config.nodes["Lobster"].allowWritePaths).toContain(
"/srv/out.txt",
const node = expectDefined(
root.plugins.entries["file-transfer"].config.nodes.Lobster,
"Lobster file-transfer node",
);
expect(node.allowWritePaths).toContain("/srv/out.txt");
});
it("never persists under the '*' wildcard even when '*' is the matching key", async () => {
@@ -499,11 +504,12 @@ describe("persistAllowAlways", () => {
};
};
// The "*" entry must not have been mutated.
expect(root.plugins.entries["file-transfer"].config.nodes["*"].allowReadPaths).toEqual([
const nodes = root.plugins.entries["file-transfer"].config.nodes;
expect(expectDefined(nodes["*"], "wildcard file-transfer node").allowReadPaths).toEqual([
"/var/log/**",
]);
// A new entry keyed by displayName (not "*") must hold the new path.
expect(root.plugins.entries["file-transfer"].config.nodes["Lobster"].allowReadPaths).toEqual([
expect(expectDefined(nodes.Lobster, "Lobster file-transfer node").allowReadPaths).toEqual([
"/srv/added.png",
]);
});
@@ -562,7 +568,10 @@ describe("persistAllowAlways", () => {
};
};
};
const list = root.plugins.entries["file-transfer"].config.nodes.n1.allowReadPaths;
const list = expectDefined(
root.plugins.entries["file-transfer"].config.nodes.n1,
"n1 file-transfer node",
).allowReadPaths;
expect(list.reduce((count, p) => count + (p === "/tmp/x" ? 1 : 0), 0)).toBe(1);
});
});
@@ -1,9 +1,14 @@
// Firecrawl tests cover firecrawl client behavior — URL safety,
// scrape payload parsing, and search-item extraction.
import { expectDefined } from "@openclaw/normalization-core";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
let firecrawlClient: typeof import("./firecrawl-client.js").testing;
function requireSearchResult<T>(results: readonly T[], index: number): T {
return expectDefined(results[index], `firecrawl search result ${index}`);
}
beforeAll(async () => {
firecrawlClient = (
await vi.importActual<typeof import("./firecrawl-client.js")>("./firecrawl-client.js")
@@ -117,8 +122,14 @@ describe("resolveSearchItems", () => {
});
expect(result).toHaveLength(2);
expect(result[0]).toMatchObject({ url: "https://example.com", title: "Example" });
expect(result[1]).toMatchObject({ url: "https://openclaw.ai", title: "OpenClaw" });
expect(requireSearchResult(result, 0)).toMatchObject({
url: "https://example.com",
title: "Example",
});
expect(requireSearchResult(result, 1)).toMatchObject({
url: "https://openclaw.ai",
title: "OpenClaw",
});
});
it("extracts items from a results array", () => {
@@ -127,8 +138,8 @@ describe("resolveSearchItems", () => {
});
expect(result).toHaveLength(1);
expect(result[0].url).toBe("https://example.org");
expect(result[0].title).toBe("Org");
expect(requireSearchResult(result, 0).url).toBe("https://example.org");
expect(requireSearchResult(result, 0).title).toBe("Org");
});
it("extracts items from data.results (nested)", () => {
@@ -152,7 +163,7 @@ describe("resolveSearchItems", () => {
});
expect(result).toHaveLength(1);
expect(result[0].url).toBe("https://example.com/nested");
expect(requireSearchResult(result, 0).url).toBe("https://example.com/nested");
});
it("extracts items from data.web array (Firecrawl web search format)", () => {
@@ -163,8 +174,8 @@ describe("resolveSearchItems", () => {
});
expect(result).toHaveLength(1);
expect(result[0].url).toBe("https://example.com/web");
expect(result[0].title).toBe("Web Result");
expect(requireSearchResult(result, 0).url).toBe("https://example.com/web");
expect(requireSearchResult(result, 0).title).toBe("Web Result");
});
it("extracts items from web.results (top-level)", () => {
@@ -175,7 +186,7 @@ describe("resolveSearchItems", () => {
});
expect(result).toHaveLength(1);
expect(result[0].url).toBe("https://example.com/top-web");
expect(requireSearchResult(result, 0).url).toBe("https://example.com/top-web");
});
it("returns an empty array when no search items are present", () => {
@@ -198,7 +209,7 @@ describe("resolveSearchItems", () => {
});
expect(result).toHaveLength(1);
expect(result[0].title).toBe("OK");
expect(requireSearchResult(result, 0).title).toBe("OK");
});
it("resolves URL from alternate fields: sourceURL, sourceUrl, metadata.sourceURL", () => {
@@ -229,9 +240,9 @@ describe("resolveSearchItems", () => {
],
});
expect(result[0].description).toBe("explicit desc");
expect(result[1].description).toBe("snippet text");
expect(result[2].description).toBe("summary text");
expect(requireSearchResult(result, 0).description).toBe("explicit desc");
expect(requireSearchResult(result, 1).description).toBe("snippet text");
expect(requireSearchResult(result, 2).description).toBe("summary text");
});
it("reads content from multiple possible fields", () => {
@@ -243,9 +254,9 @@ describe("resolveSearchItems", () => {
],
});
expect(result[0].content).toBe("# md");
expect(result[1].content).toBe("plain content");
expect(result[2].content).toBe("raw text");
expect(requireSearchResult(result, 0).content).toBe("# md");
expect(requireSearchResult(result, 1).content).toBe("plain content");
expect(requireSearchResult(result, 2).content).toBe("raw text");
});
it("reads published date from multiple possible fields", () => {
@@ -258,10 +269,10 @@ describe("resolveSearchItems", () => {
],
});
expect(result[0].published).toBe("2025-01-01");
expect(result[1].published).toBe("2025-02-02");
expect(result[2].published).toBe("2025-03-03");
expect(result[3].published).toBe("2025-04-04");
expect(requireSearchResult(result, 0).published).toBe("2025-01-01");
expect(requireSearchResult(result, 1).published).toBe("2025-02-02");
expect(requireSearchResult(result, 2).published).toBe("2025-03-03");
expect(requireSearchResult(result, 3).published).toBe("2025-04-04");
});
it("resolves siteName by stripping www. prefix from URL hostname", () => {
@@ -272,8 +283,8 @@ describe("resolveSearchItems", () => {
],
});
expect(result[0].siteName).toBe("example.com");
expect(result[1].siteName).toBe("example.org");
expect(requireSearchResult(result, 0).siteName).toBe("example.com");
expect(requireSearchResult(result, 1).siteName).toBe("example.org");
});
it("sets description and content to undefined when absent", () => {
@@ -281,9 +292,9 @@ describe("resolveSearchItems", () => {
data: [{ url: "https://example.com", title: "Minimal" }],
});
expect(result[0].description).toBeUndefined();
expect(result[0].content).toBeUndefined();
expect(result[0].published).toBeUndefined();
expect(requireSearchResult(result, 0).description).toBeUndefined();
expect(requireSearchResult(result, 0).content).toBeUndefined();
expect(requireSearchResult(result, 0).published).toBeUndefined();
});
it("falls back from empty url to sourceURL within the same entry", () => {
@@ -295,8 +306,8 @@ describe("resolveSearchItems", () => {
});
expect(result).toHaveLength(2);
expect(result[0].url).toBe("https://fallback.com");
expect(result[1].url).toBe("https://only-source.com");
expect(requireSearchResult(result, 0).url).toBe("https://fallback.com");
expect(requireSearchResult(result, 1).url).toBe("https://only-source.com");
});
it("includes entries with empty title (title defaults to empty string)", () => {
@@ -308,8 +319,8 @@ describe("resolveSearchItems", () => {
});
expect(result).toHaveLength(2);
expect(result[0].title).toBe("");
expect(result[1].title).toBe("Has Title");
expect(requireSearchResult(result, 0).title).toBe("");
expect(requireSearchResult(result, 1).title).toBe("Has Title");
});
it("picks the first candidate array when multiple are present", () => {
@@ -321,7 +332,7 @@ describe("resolveSearchItems", () => {
});
expect(result).toHaveLength(1);
expect(result[0].url).toBe("https://from-data.com");
expect(requireSearchResult(result, 0).url).toBe("https://from-data.com");
});
it("treats non-object metadata as absent (number, string)", () => {
@@ -334,8 +345,8 @@ describe("resolveSearchItems", () => {
expect(result).toHaveLength(2);
// Both should still be resolved; metadata fallback should not crash.
expect(result[0].url).toBe("https://example.com/meta-num");
expect(result[1].url).toBe("https://example.com/meta-str");
expect(requireSearchResult(result, 0).url).toBe("https://example.com/meta-num");
expect(requireSearchResult(result, 1).url).toBe("https://example.com/meta-str");
});
it("sets siteName to undefined when url is not a valid URL", () => {
@@ -348,7 +359,7 @@ describe("resolveSearchItems", () => {
});
expect(result).toHaveLength(1);
expect(result[0].siteName).toBeUndefined();
expect(requireSearchResult(result, 0).siteName).toBeUndefined();
});
it("prefers record.title over metadata.title when both are present", () => {
@@ -363,7 +374,7 @@ describe("resolveSearchItems", () => {
});
expect(result).toHaveLength(1);
expect(result[0].title).toBe("record title");
expect(requireSearchResult(result, 0).title).toBe("record title");
});
it("falls back to metadata.title when record.title is absent", () => {
@@ -377,7 +388,7 @@ describe("resolveSearchItems", () => {
});
expect(result).toHaveLength(1);
expect(result[0].title).toBe("metadata title");
expect(requireSearchResult(result, 0).title).toBe("metadata title");
});
it("falls back to metadata.title when record.title is empty string", () => {
@@ -393,7 +404,7 @@ describe("resolveSearchItems", () => {
});
expect(result).toHaveLength(1);
expect(result[0].title).toBe("metadata title");
expect(requireSearchResult(result, 0).title).toBe("metadata title");
});
});
+20 -15
View File
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import {
clearRuntimeAuthProfileStoreSnapshots,
ensureAuthProfileStore,
@@ -31,6 +32,10 @@ const mocks = vi.hoisted(() => ({
resolveCopilotApiToken: vi.fn(),
}));
function requireAuthMethod<T>(methods: readonly T[], index: number): T {
return expectDefined(methods[index], `GitHub Copilot auth method ${index}`);
}
vi.mock("openclaw/plugin-sdk/ssrf-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/ssrf-runtime")>(
"openclaw/plugin-sdk/ssrf-runtime",
@@ -382,7 +387,7 @@ describe("github-copilot plugin", () => {
it("offers to reuse an existing token profile during interactive onboarding", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
const method = requireAuthMethod(provider.auth, 0);
const agentDir = await createAgentDir();
writeExistingCopilotTokenProfile(agentDir);
const prompter = {
@@ -427,7 +432,7 @@ describe("github-copilot plugin", () => {
it("can refresh an existing token profile during interactive onboarding", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
const method = requireAuthMethod(provider.auth, 0);
const agentDir = await createAgentDir();
writeExistingCopilotTokenProfile(agentDir);
const fetchMock = vi.fn(async (input: unknown, _init?: RequestInit) => {
@@ -563,7 +568,7 @@ describe("github-copilot plugin", () => {
it("forces re-login and clears the domain when switching from a tenant back to github.com", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
const method = requireAuthMethod(provider.auth, 0);
const agentDir = await createAgentDir();
writeExistingCopilotTokenProfile(agentDir);
const fetchMock = buildDeviceFlowFetchMock("github.com", "public-fresh-token");
@@ -623,7 +628,7 @@ describe("github-copilot plugin", () => {
it("forces re-login when switching from github.com to a tenant domain", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[1];
const method = requireAuthMethod(provider.auth, 1);
const agentDir = await createAgentDir();
writeExistingCopilotTokenProfile(agentDir);
const fetchMock = buildDeviceFlowFetchMock("acme.ghe.com", "tenant-fresh-token");
@@ -677,7 +682,7 @@ describe("github-copilot plugin", () => {
it("forces re-login when an existing public profile meets an env-only tenant domain", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[1];
const method = requireAuthMethod(provider.auth, 1);
const agentDir = await createAgentDir();
// Stored profile was minted for public github.com; config has no tenant.
writeExistingCopilotTokenProfile(agentDir);
@@ -737,7 +742,7 @@ describe("github-copilot plugin", () => {
it("still offers to reuse the token when re-running enterprise login for the same tenant", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[1];
const method = requireAuthMethod(provider.auth, 1);
const agentDir = await createAgentDir();
writeExistingCopilotTokenProfile(agentDir);
const prompter = {
@@ -786,7 +791,7 @@ describe("github-copilot plugin", () => {
it("honors COPILOT_GITHUB_DOMAIN over a divergent prompt value during enterprise login", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[1];
const method = requireAuthMethod(provider.auth, 1);
const agentDir = await createAgentDir();
// Device flow is mocked for the env tenant only; if login used the typed
// prompt value instead, the fetch mock would throw on an unexpected host.
@@ -934,7 +939,7 @@ describe("github-copilot plugin", () => {
it("stores GitHub Copilot token from non-interactive onboarding", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
const method = requireAuthMethod(provider.auth, 0);
const agentDir = await createAgentDir();
const runtime = { error: vi.fn(), exit: vi.fn() };
@@ -973,7 +978,7 @@ describe("github-copilot plugin", () => {
it("persists COPILOT_GITHUB_DOMAIN during non-interactive onboarding", async () => {
vi.stubEnv("COPILOT_GITHUB_DOMAIN", "acme.ghe.com");
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
const method = requireAuthMethod(provider.auth, 0);
const agentDir = await createAgentDir();
const runtime = { error: vi.fn(), exit: vi.fn() };
@@ -1011,7 +1016,7 @@ describe("github-copilot plugin", () => {
it("clears a persisted enterprise domain during public non-interactive onboarding", async () => {
vi.stubEnv("COPILOT_GITHUB_DOMAIN", "github.com");
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
const method = requireAuthMethod(provider.auth, 0);
const agentDir = await createAgentDir();
const runtime = { error: vi.fn(), exit: vi.fn() };
@@ -1037,7 +1042,7 @@ describe("github-copilot plugin", () => {
it("stores env-backed token refs for non-interactive onboarding ref mode", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
const method = requireAuthMethod(provider.auth, 0);
const agentDir = await createAgentDir();
const runtime = { error: vi.fn(), exit: vi.fn() };
@@ -1076,7 +1081,7 @@ describe("github-copilot plugin", () => {
it("falls back to GH_TOKEN during non-interactive onboarding", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
const method = requireAuthMethod(provider.auth, 0);
const agentDir = await createAgentDir();
const runtime = { error: vi.fn(), exit: vi.fn() };
const resolveApiKey = vi.fn(async ({ envVar }: { envVar?: string }) =>
@@ -1135,7 +1140,7 @@ describe("github-copilot plugin", () => {
it("preserves an existing primary model during non-interactive onboarding", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
const method = requireAuthMethod(provider.auth, 0);
const agentDir = await createAgentDir();
const runtime = { error: vi.fn(), exit: vi.fn() };
@@ -1177,7 +1182,7 @@ describe("github-copilot plugin", () => {
it("reuses an existing token profile during non-interactive onboarding", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
const method = requireAuthMethod(provider.auth, 0);
const agentDir = await createAgentDir();
const runtime = { error: vi.fn(), exit: vi.fn() };
writeExistingCopilotTokenProfile(agentDir);
@@ -1202,7 +1207,7 @@ describe("github-copilot plugin", () => {
it("does not emit a second missing-token error after ref-mode flag validation fails", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
const method = requireAuthMethod(provider.auth, 0);
const agentDir = await createAgentDir();
const runtime = { error: vi.fn(), exit: vi.fn() };
+2 -1
View File
@@ -1,4 +1,5 @@
// Github Copilot tests cover models plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { createProviderUsageFetch, makeResponse } from "openclaw/plugin-sdk/test-env";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { deriveCopilotApiBaseUrlFromToken, resolveCopilotApiToken } from "./token.js";
@@ -652,7 +653,7 @@ describe("fetchCopilotModelCatalog", () => {
});
expect(out).toHaveLength(1);
expect(out[0].name).toBe("GPT-5.5");
expect(expectDefined(out[0], "GitHub Copilot model").name).toBe("GPT-5.5");
});
it("falls back from malformed live token limits", async () => {
+14 -7
View File
@@ -3,6 +3,7 @@ import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { gzipSync } from "node:zlib";
import { expectDefined } from "@openclaw/normalization-core";
import type { Model } from "openclaw/plugin-sdk/llm";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@@ -1944,9 +1945,9 @@ describe("google transport stream", () => {
},
],
});
expect(firstModelTurn.parts[0].thoughtSignature).not.toBe(
"bXNnXzAxWEZEVURZSmdBQUNjblNNMlRUZ1FzQQ==",
);
expect(
expectDefined(firstModelTurn.parts[0], "first Gemini model part").thoughtSignature,
).not.toBe("bXNnXzAxWEZEVURZSmdBQUNjblNNMlRUZ1FzQQ==");
});
it("does not replay prior Gemini thought signatures onto a later foreign route", () => {
@@ -1978,7 +1979,10 @@ describe("google transport stream", () => {
},
],
});
expect(modelTurns[1]?.parts[0].thoughtSignature).not.toBe("Y2FsbF9zaWdfZ29vZ2xlXzE=");
const laterTurn = expectDefined(modelTurns[1], "later Gemini model turn");
expect(expectDefined(laterTurn.parts[0], "later Gemini model part").thoughtSignature).not.toBe(
"Y2FsbF9zaWdfZ29vZ2xlXzE=",
);
});
it("replaces invalid Gemini tool-call sentinel signatures with the skip fallback", () => {
@@ -2511,7 +2515,8 @@ describe("google transport stream", () => {
],
} as never);
const functionResponse = (params.contents[1] as GoogleTestContentTurn).parts[0]
const responseTurn = params.contents[1] as GoogleTestContentTurn;
const functionResponse = expectDefined(responseTurn.parts[0], "JSON tool response part")
.functionResponse as { response: { output: string } };
expect(functionResponse).toMatchObject({ name: "lookup" });
@@ -2567,7 +2572,8 @@ describe("google transport stream", () => {
],
} as never);
const functionResponse = (params.contents[1] as GoogleTestContentTurn).parts[0]
const responseTurn = params.contents[1] as GoogleTestContentTurn;
const functionResponse = expectDefined(responseTurn.parts[0], "resource tool response part")
.functionResponse as { response: { output: string } };
expect(functionResponse.response.output).toContain('"data":"[binary data omitted: 6 chars]"');
@@ -2612,7 +2618,8 @@ describe("google transport stream", () => {
],
} as never);
const functionResponse = (params.contents[1] as GoogleTestContentTurn).parts[0]
const responseTurn = params.contents[1] as GoogleTestContentTurn;
const functionResponse = expectDefined(responseTurn.parts[0], "redacted tool response part")
.functionResponse as { response: { output: string } };
expect(functionResponse.response.output).toContain('"visible":"safe-value"');
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
@@ -274,7 +275,9 @@ describe("googlechat google auth runtime", () => {
const body = new ReadableStream<Uint8Array>({
pull(controller) {
if (chunkIndex < chunks.length) {
controller.enqueue(chunks[chunkIndex++]);
controller.enqueue(
expectDefined(chunks[chunkIndex++], `Google auth chunk ${chunkIndex}`),
);
return;
}
controller.close();
@@ -1,6 +1,7 @@
// Googlechat tests cover monitor.webhook routing plugin behavior.
import { EventEmitter } from "node:events";
import type { IncomingMessage } from "node:http";
import { expectDefined } from "@openclaw/normalization-core";
import {
createEmptyPluginRegistry,
setActivePluginRegistry,
@@ -145,8 +146,8 @@ async function expectVerifiedRoute(params: {
expect(res.statusCode).toBe(params.expectedStatus);
const expectedCounts =
params.expectedSink === "A" ? [1, 0] : params.expectedSink === "B" ? [0, 1] : [0, 0];
expect(params.sinkA).toHaveBeenCalledTimes(expectedCounts[0]);
expect(params.sinkB).toHaveBeenCalledTimes(expectedCounts[1]);
expect(params.sinkA).toHaveBeenCalledTimes(expectDefined(expectedCounts[0], "sink A count"));
expect(params.sinkB).toHaveBeenCalledTimes(expectDefined(expectedCounts[1], "sink B count"));
}
function mockSecondVerifierSuccess() {
+3 -2
View File
@@ -1,4 +1,5 @@
// Huggingface tests cover models plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
@@ -43,7 +44,7 @@ afterEach(() => {
describe("huggingface models", () => {
it("buildHuggingfaceModelDefinition returns config with required fields", () => {
const entry = HUGGINGFACE_MODEL_CATALOG[0];
const entry = expectDefined(HUGGINGFACE_MODEL_CATALOG[0], "first Hugging Face catalog model");
const def = buildHuggingfaceModelDefinition(entry);
expect(def.id).toBe(entry.id);
expect(def.name).toBe(entry.name);
@@ -63,7 +64,7 @@ describe("huggingface models", () => {
it("discoverHuggingfaceModels returns static catalog in test env (VITEST)", async () => {
const models = await discoverHuggingfaceModels("hf_test_token");
expect(models).toHaveLength(HUGGINGFACE_MODEL_CATALOG.length);
expect(models[0].id).toBe("deepseek-ai/DeepSeek-R1");
expect(expectDefined(models[0], "first Hugging Face model").id).toBe("deepseek-ai/DeepSeek-R1");
});
it("uses the default discovery timeout for live Hugging Face fetches", async () => {
@@ -1,4 +1,5 @@
// Imessage tests cover doctor contract api plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { describe, expect, it } from "vitest";
import { legacyConfigRules, normalizeCompatibilityConfig } from "./doctor-contract-api.js";
@@ -49,7 +50,10 @@ describe("imessage normalizeCompatibilityConfig streaming aliases", () => {
});
expect(imessage.chunkMode).toBeUndefined();
expect(imessage.blockStreaming).toBeUndefined();
const personal = (imessage.accounts as Record<string, Record<string, unknown>>).personal;
const personal = expectDefined(
(imessage.accounts as Record<string, Record<string, unknown>>).personal,
"personal iMessage account",
);
expect(personal.streaming).toEqual({ block: { coalesce: { idleMs: 250 } } });
expect(personal.blockStreamingCoalesce).toBeUndefined();
for (const change of [
+2 -1
View File
@@ -1,4 +1,5 @@
// Inworld tests cover tts plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({
@@ -175,7 +176,7 @@ describe("listInworldVoices", () => {
const voices = await listInworldVoices({ apiKey: "test-key" });
expect(voices).toHaveLength(1);
expect(voices[0].id).toBe("Dennis");
expect(expectDefined(voices[0], "Inworld voice").id).toBe("Dennis");
});
it("returns empty array when no voices present", async () => {
@@ -1,4 +1,5 @@
// Line tests cover auto reply delivery plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it, vi } from "vitest";
import type { LineAutoReplyDeps } from "./auto-reply-delivery.js";
import { deliverLineAutoReply } from "./auto-reply-delivery.js";
@@ -245,7 +246,9 @@ describe("deliverLineAutoReply", () => {
);
const pushOrder = pushMessagesLine.mock.invocationCallOrder[0];
const replyOrder = replyMessageLine.mock.invocationCallOrder[0];
expect(pushOrder).toBeLessThan(replyOrder);
expect(expectDefined(pushOrder, "LINE push invocation")).toBeLessThan(
expectDefined(replyOrder, "LINE reply invocation"),
);
});
it("surfaces a visible partial delivery when a rich bubble fails alongside quick-reply text", async () => {
@@ -1,4 +1,5 @@
// Line tests cover channel.sendPayload plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import {
verifyChannelMessageAdapterCapabilityProofs,
verifyChannelMessageReceiveAckPolicyAdapterProofs,
@@ -368,7 +369,9 @@ describe("line outbound sendPayload", () => {
);
const mediaOrder = mocks.sendMessageLine.mock.invocationCallOrder[0];
const quickReplyOrder = mocks.pushTextMessageWithQuickReplies.mock.invocationCallOrder[0];
expect(mediaOrder).toBeLessThan(quickReplyOrder);
expect(expectDefined(mediaOrder, "LINE media invocation")).toBeLessThan(
expectDefined(quickReplyOrder, "LINE quick-reply invocation"),
);
});
it("keeps generic media payloads on the image-only send path", async () => {
+73 -29
View File
@@ -1,4 +1,5 @@
// Line tests cover markdown to line plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import {
extractMarkdownTables,
@@ -12,6 +13,10 @@ import {
hasMarkdownToConvert,
} from "./markdown-to-line.js";
function requireEntry<T>(entries: readonly T[], index: number, context: string): T {
return expectDefined(entries[index], context);
}
describe("extractMarkdownTables", () => {
it("extracts a simple 2-column table", () => {
const text = `Here is a table:
@@ -26,8 +31,8 @@ And some more text.`;
const { tables, textWithoutTables } = extractMarkdownTables(text);
expect(tables).toHaveLength(1);
expect(tables[0].headers).toEqual(["Name", "Value"]);
expect(tables[0].rows).toEqual([
expect(requireEntry(tables, 0, "first markdown table").headers).toEqual(["Name", "Value"]);
expect(requireEntry(tables, 0, "first markdown table").rows).toEqual([
["foo", "123"],
["bar", "456"],
]);
@@ -52,8 +57,8 @@ Table 2:
const { tables } = extractMarkdownTables(text);
expect(tables).toHaveLength(2);
expect(tables[0].headers).toEqual(["A", "B"]);
expect(tables[1].headers).toEqual(["X", "Y"]);
expect(requireEntry(tables, 0, "first markdown table").headers).toEqual(["A", "B"]);
expect(requireEntry(tables, 1, "second markdown table").headers).toEqual(["X", "Y"]);
});
it("handles tables with alignment markers", () => {
@@ -64,8 +69,12 @@ Table 2:
const { tables } = extractMarkdownTables(text);
expect(tables).toHaveLength(1);
expect(tables[0].headers).toEqual(["Left", "Center", "Right"]);
expect(tables[0].rows).toEqual([["a", "b", "c"]]);
expect(requireEntry(tables, 0, "first markdown table").headers).toEqual([
"Left",
"Center",
"Right",
]);
expect(requireEntry(tables, 0, "first markdown table").rows).toEqual([["a", "b", "c"]]);
});
it("returns empty when no tables present", () => {
@@ -90,8 +99,12 @@ console.log(x);
And more text.`;
const withLanguageResult = extractCodeBlocks(withLanguage);
expect(withLanguageResult.codeBlocks).toHaveLength(1);
expect(withLanguageResult.codeBlocks[0].language).toBe("javascript");
expect(withLanguageResult.codeBlocks[0].code).toBe("const x = 1;\nconsole.log(x);");
expect(requireEntry(withLanguageResult.codeBlocks, 0, "language code block").language).toBe(
"javascript",
);
expect(requireEntry(withLanguageResult.codeBlocks, 0, "language code block").code).toBe(
"const x = 1;\nconsole.log(x);",
);
expect(withLanguageResult.textWithoutCode).toContain("Here is some code:");
expect(withLanguageResult.textWithoutCode).toContain("And more text.");
expect(withLanguageResult.textWithoutCode).not.toContain("```");
@@ -101,8 +114,12 @@ plain code
\`\`\``;
const withoutLanguageResult = extractCodeBlocks(withoutLanguage);
expect(withoutLanguageResult.codeBlocks).toHaveLength(1);
expect(withoutLanguageResult.codeBlocks[0].language).toBeUndefined();
expect(withoutLanguageResult.codeBlocks[0].code).toBe("plain code");
expect(
requireEntry(withoutLanguageResult.codeBlocks, 0, "plain code block").language,
).toBeUndefined();
expect(requireEntry(withoutLanguageResult.codeBlocks, 0, "plain code block").code).toBe(
"plain code",
);
const multiple = `\`\`\`python
print("hello")
@@ -115,8 +132,12 @@ echo "world"
\`\`\``;
const multipleResult = extractCodeBlocks(multiple);
expect(multipleResult.codeBlocks).toHaveLength(2);
expect(multipleResult.codeBlocks[0].language).toBe("python");
expect(multipleResult.codeBlocks[1].language).toBe("bash");
expect(requireEntry(multipleResult.codeBlocks, 0, "first multiple code block").language).toBe(
"python",
);
expect(requireEntry(multipleResult.codeBlocks, 1, "second multiple code block").language).toBe(
"bash",
);
});
});
@@ -127,8 +148,14 @@ describe("extractLinks", () => {
const { links, textWithLinks } = extractLinks(text);
expect(links).toHaveLength(2);
expect(links[0]).toEqual({ text: "Google", url: "https://google.com" });
expect(links[1]).toEqual({ text: "GitHub", url: "https://github.com" });
expect(requireEntry(links, 0, "first markdown link")).toEqual({
text: "Google",
url: "https://google.com",
});
expect(requireEntry(links, 1, "second markdown link")).toEqual({
text: "GitHub",
url: "https://github.com",
});
expect(textWithLinks).toBe("Check out Google and GitHub.");
});
});
@@ -140,8 +167,14 @@ describe("convertLinksToFlexBubble", () => {
]);
const footer = bubble.footer as { contents: Array<{ action: { label: string } }> };
expect(footer.contents[0].action.label).toBe("1234567890123456789");
expect(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(footer.contents[0].action.label)).toBe(false);
expect(requireEntry(footer.contents, 0, "link footer content").action.label).toBe(
"1234567890123456789",
);
expect(
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(
requireEntry(footer.contents, 0, "link footer content").action.label,
),
).toBe(false);
});
});
@@ -221,10 +254,13 @@ describe("convertTableToFlexBubble", () => {
const body = bubble.body as {
contents: Array<{ contents?: Array<{ contents?: Array<{ text: string }> }> }>;
};
const rowsBox = body.contents[2] as { contents: Array<{ contents: Array<{ text: string }> }> };
const rowsBox = requireEntry(body.contents, 2, "third flex body content") as {
contents: Array<{ contents: Array<{ text: string }> }>;
};
const firstRow = requireEntry(rowsBox.contents, 0, "first table row");
expect(rowsBox.contents[0].contents[0].text).toBe("-");
expect(rowsBox.contents[0].contents[1].text).toBe("-");
expect(requireEntry(firstRow.contents, 0, "first empty table cell").text).toBe("-");
expect(requireEntry(firstRow.contents, 1, "second empty table cell").text).toBe("-");
});
it("strips bold markers and applies weight for fully bold cells", () => {
@@ -237,13 +273,17 @@ describe("convertTableToFlexBubble", () => {
const body = bubble.body as {
contents: Array<{ contents?: Array<{ text: string; weight?: string }> }>;
};
const headerRow = body.contents[0] as { contents: Array<{ text: string; weight?: string }> };
const dataRow = body.contents[2] as { contents: Array<{ text: string; weight?: string }> };
const headerRow = requireEntry(body.contents, 0, "first flex body content") as {
contents: Array<{ text: string; weight?: string }>;
};
const dataRow = requireEntry(body.contents, 2, "third flex body content") as {
contents: Array<{ text: string; weight?: string }>;
};
expect(headerRow.contents[0].text).toBe("Name");
expect(headerRow.contents[0].weight).toBe("bold");
expect(dataRow.contents[0].text).toBe("Alpha");
expect(dataRow.contents[0].weight).toBe("bold");
expect(requireEntry(headerRow.contents, 0, "first table header cell").text).toBe("Name");
expect(requireEntry(headerRow.contents, 0, "first table header cell").weight).toBe("bold");
expect(requireEntry(dataRow.contents, 0, "first table data cell").text).toBe("Alpha");
expect(requireEntry(dataRow.contents, 0, "first table data cell").weight).toBe("bold");
});
});
@@ -254,7 +294,9 @@ describe("convertCodeBlockToFlexBubble", () => {
const bubble = convertCodeBlockToFlexBubble(block);
const body = bubble.body as { contents: Array<{ text: string }> };
expect(body.contents[0].text).toBe("Code (typescript)");
expect(requireEntry(body.contents, 0, "first flex body content").text).toBe(
"Code (typescript)",
);
});
it("creates a code card without language", () => {
@@ -263,7 +305,7 @@ describe("convertCodeBlockToFlexBubble", () => {
const bubble = convertCodeBlockToFlexBubble(block);
const body = bubble.body as { contents: Array<{ text: string }> };
expect(body.contents[0].text).toBe("Code");
expect(requireEntry(body.contents, 0, "first flex body content").text).toBe("Code");
});
it("truncates very long code", () => {
@@ -273,7 +315,8 @@ describe("convertCodeBlockToFlexBubble", () => {
const bubble = convertCodeBlockToFlexBubble(block);
const body = bubble.body as { contents: Array<{ contents: Array<{ text: string }> }> };
const codeText = body.contents[1].contents[0].text;
const codeContent = requireEntry(body.contents, 1, "second flex body content");
const codeText = requireEntry(codeContent.contents, 0, "truncated code text").text;
expect(codeText.length).toBeLessThan(longCode.length);
expect(codeText).toContain("...");
});
@@ -286,7 +329,8 @@ describe("convertCodeBlockToFlexBubble", () => {
const bubble = convertCodeBlockToFlexBubble(block);
const body = bubble.body as { contents: Array<{ contents: Array<{ text: string }> }> };
const codeText = body.contents[1].contents[0].text;
const codeContent = requireEntry(body.contents, 1, "second flex body content");
const codeText = requireEntry(codeContent.contents, 0, "surrogate-safe code text").text;
expect(codeText.endsWith("\n...")).toBe(true);
expect(
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(codeText),
+12 -6
View File
@@ -1,4 +1,5 @@
// Line tests cover message cards plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import { datetimePickerAction, postbackAction, uriAction } from "./actions.js";
import { registerLineCardCommand } from "./card-command.js";
@@ -240,7 +241,8 @@ describe("createProductCarousel", () => {
const template = createProductCarousel([item]);
const columns = (template.template as { columns: Array<{ actions: Array<{ type: string }> }> })
.columns;
expect(columns[0].actions[0].type).toBe(expectedType);
const column = expectDefined(columns[0], "product carousel column");
expect(expectDefined(column.actions[0], "product carousel action").type).toBe(expectedType);
});
it("preserves the complete price when truncating a long description", () => {
@@ -253,8 +255,9 @@ describe("createProductCarousel", () => {
]);
const columns = (template.template as { columns: Array<{ text: string }> }).columns;
expect(columns[0].text).toBe(`${"x".repeat(53)}\n$12.99`);
expect(columns[0].text.length).toBe(60);
const column = expectDefined(columns[0], "priced product carousel column");
expect(column.text).toBe(`${"x".repeat(53)}\n$12.99`);
expect(column.text.length).toBe(60);
});
});
@@ -263,7 +266,7 @@ describe("flex cards", () => {
const card = createInfoCard("Title", "Body", "Footer text");
const footer = card.footer as { contents: Array<{ text: string }> };
expect(footer.contents[0].text).toBe("Footer text");
expect(expectDefined(footer.contents[0], "info-card footer content").text).toBe("Footer text");
});
it("limits list items to 8", () => {
@@ -280,7 +283,7 @@ describe("flex cards", () => {
const body = card.body as { contents: Array<{ text: string }> };
expect(body.contents.length).toBe(2);
expect(body.contents[1].text).toBe("Body text");
expect(expectDefined(body.contents[1], "image-card body content").text).toBe("Body text");
});
it("limits action-card actions to 4", () => {
@@ -412,7 +415,10 @@ describe("action label/data surrogate-safe truncation", () => {
};
};
};
const action = result.channelData.line.flexMessage.contents.footer.contents[0].action;
const action = expectDefined(
result.channelData.line.flexMessage.contents.footer.contents[0],
"LINE flex-message footer action",
).action;
expect(action.label).toBe("1234567890123456789");
expect(loneHighSurrogate.test(action.label)).toBe(false);
+8 -6
View File
@@ -200,12 +200,14 @@ describe("createGridLayout", () => {
const areas = createGridLayout(843, actions);
expect((areas[0].action as { text: string }).text).toBe("/help");
expect((areas[1].action as { text: string }).text).toBe("/status");
expect((areas[2].action as { text: string }).text).toBe("/settings");
expect((areas[3].action as { text: string }).text).toBe("/about");
expect((areas[4].action as { text: string }).text).toBe("/feedback");
expect((areas[5].action as { text: string }).text).toBe("/contact");
expect(areas.map((area) => (area.action as { text: string }).text)).toEqual([
"/help",
"/status",
"/settings",
"/about",
"/feedback",
"/contact",
]);
});
});
+5 -1
View File
@@ -1,4 +1,5 @@
// Line tests cover send plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const {
@@ -477,6 +478,9 @@ describe("LINE send helpers", () => {
const firstCall = pushMessageMock.mock.calls.at(0) as [
{ messages: Array<{ quickReply?: { items: unknown[] } }> },
];
expect(firstCall[0].messages[0].quickReply?.items).toHaveLength(13);
const payload = expectDefined(firstCall[0], "LINE push payload");
expect(expectDefined(payload.messages[0], "LINE push message").quickReply?.items).toHaveLength(
13,
);
});
});
+6 -2
View File
@@ -1,5 +1,6 @@
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import {
createPluginRegistryFixture,
registerVirtualTestPlugin,
@@ -129,8 +130,11 @@ describe("llama.cpp provider plugin", () => {
nodeLlamaCppImportUrl: expect.stringContaining("node-llama-cpp"),
},
);
const createdWorkerProvider =
await memoryHostEmbeddingMocks.createLocalEmbeddingProvider.mock.results[0].value;
const mockResult = expectDefined(
memoryHostEmbeddingMocks.createLocalEmbeddingProvider.mock.results[0],
"llama.cpp embedding provider result",
);
const createdWorkerProvider = await mockResult.value;
expect(createdWorkerProvider.embedBatchInputs).toHaveBeenCalledWith([{ text: "doc" }], {
signal: abortController.signal,
});
+12 -6
View File
@@ -1,4 +1,5 @@
import { execFileSync } from "node:child_process";
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import {
clockToMs,
@@ -71,8 +72,8 @@ describe("parseObservationSegments", () => {
});
const segments = parseObservationSegments({ raw, day: DAY, startMs, endMs });
expect(segments).toHaveLength(2);
expect(segments[0].startMs).toBe(startMs);
expect(segments[1].endMs).toBe(endMs);
expect(expectDefined(segments[0], "first observation segment").startMs).toBe(startMs);
expect(expectDefined(segments[1], "second observation segment").endMs).toBe(endMs);
});
it("returns empty on unparseable output", () => {
@@ -107,8 +108,9 @@ describe("parseCardsJson", () => {
});
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.drafts[0].category).toBe("coding");
expect(result.drafts[0].appPrimary).toBe("github.com");
const draft = expectDefined(result.drafts[0], "normalized logbook draft");
expect(draft.category).toBe("coding");
expect(draft.appPrimary).toBe("github.com");
}
});
@@ -119,7 +121,9 @@ describe("parseCardsJson", () => {
windowStartMs,
windowEndMs,
});
expect(result.ok && result.drafts[0].category).toBe("other");
expect(result.ok && expectDefined(result.drafts[0], "unknown-category draft").category).toBe(
"other",
);
});
it("trims sub-minute overlaps and rejects large ones", () => {
@@ -134,7 +138,9 @@ describe("parseCardsJson", () => {
});
expect(trimmed.ok).toBe(true);
if (trimmed.ok) {
expect(trimmed.drafts[1].startMs).toBe(trimmed.drafts[0].endMs);
const first = expectDefined(trimmed.drafts[0], "first overlap-trimmed draft");
const second = expectDefined(trimmed.drafts[1], "second overlap-trimmed draft");
expect(second.startMs).toBe(first.endMs);
}
const rejected = parseCardsJson({
+4 -3
View File
@@ -1,6 +1,7 @@
import { existsSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { LogbookStore, dayKeyFor } from "./store.js";
import type { LogbookCardDraft } from "./types.js";
@@ -107,14 +108,14 @@ describe("LogbookStore", () => {
}),
]);
const cards = store.cardsForDay(DAY);
expect(cards[0].distractions).toEqual([
expect(expectDefined(cards[0], "stored logbook card").distractions).toEqual([
{ startMs: base + 5 * 60_000, endMs: base + 10 * 60_000, title: "Twitter" },
]);
const stats = store.dayStats(DAY);
expect(stats.trackedMs).toBe(30 * 60_000);
expect(stats.distractionMs).toBe(5 * 60_000);
expect(stats.categories[0]).toEqual({ category: "coding", ms: 30 * 60_000 });
expect(stats.apps[0].domain).toBe("github.com");
expect(expectDefined(stats.apps[0], "logbook app statistic").domain).toBe("github.com");
});
it("prunes old frame rows and files but keeps recent ones", () => {
@@ -149,7 +150,7 @@ describe("LogbookStore", () => {
store.replaceObservations(batchId, DAY, [{ startMs: t0, endMs: t0 + 500, text: "retry run" }]);
const observations = store.observationsInRange(DAY, 0, Number.MAX_SAFE_INTEGER);
expect(observations).toHaveLength(1);
expect(observations[0].text).toBe("retry run");
expect(expectDefined(observations[0], "retried observation").text).toBe("retry run");
});
it("requeues errored batches for explicit retry", () => {
+11 -2
View File
@@ -1,4 +1,5 @@
// Matrix tests cover client plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { installMatrixTestRuntime } from "../test-runtime.js";
import type { CoreConfig } from "../types.js";
@@ -589,8 +590,16 @@ describe("resolveMatrixAuth", () => {
deviceId: "DEVICE123",
});
requireRecord(repairMeta.env, "repair env");
expect(repairCurrentTokenStorageMetaDeviceIdMock.mock.invocationCallOrder[0]).toBeLessThan(
saveBackfilledMatrixDeviceIdMock.mock.invocationCallOrder[0],
expect(
expectDefined(
repairCurrentTokenStorageMetaDeviceIdMock.mock.invocationCallOrder[0],
"Matrix token repair invocation",
),
).toBeLessThan(
expectDefined(
saveBackfilledMatrixDeviceIdMock.mock.invocationCallOrder[0],
"Matrix device save invocation",
),
);
expect(deviceId).toBe("DEVICE123");
});
@@ -1,4 +1,5 @@
// Matrix tests cover events plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it, vi } from "vitest";
import type { CoreConfig } from "../../types.js";
import type { MatrixAuth } from "../client.js";
@@ -981,8 +982,9 @@ describe("registerMatrixMonitorEvents verification routing", () => {
});
await vi.advanceTimersByTimeAsync(500);
const verification = expectDefined(verifications[0], "Matrix verification summary");
verifications[0] = {
...verifications[0],
...verification,
sas: {
decimal: [1234, 5678, 9012],
emoji: [
@@ -1,4 +1,5 @@
// Matrix tests cover crypto bootstrap plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { beforeEach, describe, expect, it, vi, type Mock } from "vitest";
import { MatrixCryptoBootstrapper, type MatrixCryptoBootstrapperDeps } from "./crypto-bootstrap.js";
import type { MatrixRecoveryKeyStore } from "./recovery-key-store.js";
@@ -308,8 +309,16 @@ describe("MatrixCryptoBootstrapper", () => {
});
expect(userHasCrossSigningKeys).toHaveBeenCalledWith("@bot:example.org", true);
expect(userHasCrossSigningKeys.mock.invocationCallOrder[0]).toBeLessThan(
bootstrapCrossSigning.mock.invocationCallOrder[0],
expect(
expectDefined(
userHasCrossSigningKeys.mock.invocationCallOrder[0],
"Matrix cross-signing lookup invocation",
),
).toBeLessThan(
expectDefined(
bootstrapCrossSigning.mock.invocationCallOrder[0],
"Matrix cross-signing bootstrap invocation",
),
);
});
@@ -1,4 +1,5 @@
// Matrix setup module handles plugin onboarding behavior.
import { expectDefined } from "@openclaw/normalization-core";
import type { OutputRuntimeEnv } from "openclaw/plugin-sdk/runtime";
import type { ChannelSetupWizardAdapter } from "openclaw/plugin-sdk/setup";
import { afterEach, vi } from "vitest";
@@ -66,7 +67,7 @@ export function createMatrixWizardPrompter(params: {
fallback: PromptHandler<T | Promise<T>> | undefined,
): Promise<T> => {
if (values && message in values) {
return values[message];
return expectDefined(values[message], `${kind} prompt value for ${message}`);
}
if (fallback) {
return await fallback(message);
@@ -1,4 +1,5 @@
// Mattermost tests cover client plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it, vi } from "vitest";
const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn());
@@ -62,6 +63,13 @@ function parseRequestJson(init: RequestInit | undefined): Record<string, unknown
return parsed as Record<string, unknown>;
}
function requireRequestCall(
calls: readonly { url: string; init?: RequestInit }[],
index = 0,
): { url: string; init?: RequestInit } {
return expectDefined(calls[index], `Mattermost request call ${index}`);
}
function streamingMattermostResponse(body: unknown): {
response: Response;
arrayBuffer: ReturnType<typeof vi.fn>;
@@ -129,7 +137,7 @@ async function updatePostAndCapture(
await updateMattermostPost(client, "post1", update);
return {
calls,
body: parseRequestJson(calls[0].init),
body: parseRequestJson(requireRequestCall(calls).init),
};
}
@@ -368,7 +376,7 @@ describe("createMattermostClient", () => {
fetchImpl: mockFetch,
});
await client.request("/users/me");
const headers = new Headers(calls[0].init?.headers);
const headers = new Headers(requireRequestCall(calls).init?.headers);
expect(headers.get("Authorization")).toBe("Bearer my-secret-token");
});
@@ -380,7 +388,7 @@ describe("createMattermostClient", () => {
fetchImpl: mockFetch,
});
await client.request("/posts", { method: "POST", body: JSON.stringify({ message: "hi" }) });
const headers = new Headers(calls[0].init?.headers);
const headers = new Headers(requireRequestCall(calls).init?.headers);
expect(headers.get("Content-Type")).toBe("application/json");
});
@@ -427,7 +435,7 @@ describe("createMattermostPost", () => {
message: "Hello world",
});
const body = parseRequestJson(calls[0].init);
const body = parseRequestJson(requireRequestCall(calls).init);
expect(body.channel_id).toBe("ch123");
expect(body.message).toBe("Hello world");
});
@@ -446,7 +454,7 @@ describe("createMattermostPost", () => {
rootId: "root456",
});
const body = parseRequestJson(calls[0].init);
const body = parseRequestJson(requireRequestCall(calls).init);
expect(body.root_id).toBe("root456");
});
@@ -464,7 +472,7 @@ describe("createMattermostPost", () => {
fileIds: ["file1", "file2"],
});
const body = parseRequestJson(calls[0].init);
const body = parseRequestJson(requireRequestCall(calls).init);
expect(body.file_ids).toEqual(["file1", "file2"]);
});
@@ -491,7 +499,7 @@ describe("createMattermostPost", () => {
props,
});
const body = parseRequestJson(calls[0].init);
const body = parseRequestJson(requireRequestCall(calls).init);
expect(body).toEqual({
channel_id: "ch123",
message: "Pick an option",
@@ -512,7 +520,7 @@ describe("createMattermostPost", () => {
message: "No props",
});
const body = parseRequestJson(calls[0].init);
const body = parseRequestJson(requireRequestCall(calls).init);
expect(body.props).toBeUndefined();
});
});
@@ -523,10 +531,7 @@ describe("updateMattermostPost", () => {
it("sends PUT to /posts/{id}", async () => {
const { calls } = await updatePostAndCapture({ message: "Updated" });
const firstCall = calls[0];
if (!firstCall) {
throw new Error("expected Mattermost update post request");
}
const firstCall = requireRequestCall(calls);
expect(firstCall.url).toContain("/posts/post1");
if (!firstCall.init) {
throw new Error("expected Mattermost update post request init");
@@ -1,5 +1,6 @@
// Mattermost tests cover monitor websocket plugin behavior.
import { once } from "node:events";
import { expectDefined } from "@openclaw/normalization-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { WebSocketServer } from "ws";
import type { RuntimeEnv } from "../../runtime-api.js";
@@ -186,9 +187,11 @@ describe("mattermost websocket monitor", () => {
await connectOnce();
expect(sockets).toHaveLength(2);
expect(sockets[0].closeCalls).toBe(1);
expect(sockets[1].sent).toHaveLength(1);
expect(JSON.parse(sockets[1].sent[0] ?? "")).toEqual({
const firstSocket = expectDefined(sockets[0], "first Mattermost socket");
const secondSocket = expectDefined(sockets[1], "second Mattermost socket");
expect(firstSocket.closeCalls).toBe(1);
expect(secondSocket.sent).toHaveLength(1);
expect(JSON.parse(expectDefined(secondSocket.sent[0], "Mattermost auth payload"))).toEqual({
action: "authentication_challenge",
data: { token: "token" },
seq: 1,
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import { expectDefined } from "@openclaw/normalization-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import {
ensureMemoryIndexSchema,
@@ -27,6 +28,10 @@ import { bm25RankToScore, buildFtsQuery } from "./src/memory/hybrid.js";
import { searchKeyword, searchVector } from "./src/memory/manager-search.js";
import { testing as shortTermTesting } from "./src/short-term-promotion.js";
function requireStateMigration(index: number) {
return expectDefined(stateMigrations[index], `Memory Core state migration ${index}`);
}
function createDoctorContext(env: NodeJS.ProcessEnv): PluginDoctorStateMigrationContext {
return {
openPluginStateKeyedStore<T>(options: OpenKeyedStoreOptions) {
@@ -506,7 +511,7 @@ describe("memory-core doctor dreaming migration", () => {
);
await fs.writeFile(lockPath, `${process.pid}:${Date.now()}\n`, "utf8");
const migration = stateMigrations[0];
const migration = requireStateMigration(0);
const preview = await migration.detectLegacyState(migrationParams());
expect(preview?.preview).toEqual([
expect.stringContaining("Memory Core daily ingestion"),
@@ -554,7 +559,7 @@ describe("memory-core doctor dreaming migration", () => {
const recallPath = path.join(workspaceDir, "memory", ".dreams", "short-term-recall.json");
await fs.writeFile(recallPath, "{", "utf8");
const result = await stateMigrations[0].migrateLegacyState(migrationParams());
const result = await requireStateMigration(0).migrateLegacyState(migrationParams());
expect(result.changes).toEqual([]);
expect(result.warnings).toEqual([
@@ -596,10 +601,10 @@ describe("memory-core doctor dreaming migration", () => {
);
const config = { agents: { list: [{ id: "main", default: true }] } };
const preview = await stateMigrations[0].detectLegacyState(migrationParams(config));
const preview = await requireStateMigration(0).detectLegacyState(migrationParams(config));
expect(preview?.preview).toEqual([expect.stringContaining("Memory Core short-term recall")]);
const result = await stateMigrations[0].migrateLegacyState(migrationParams(config));
const result = await requireStateMigration(0).migrateLegacyState(migrationParams(config));
expect(result.warnings).toEqual([]);
expect(result.changes).toEqual([
@@ -2,6 +2,7 @@
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { RequestScopedSubagentRuntimeError } from "openclaw/plugin-sdk/error-runtime";
import { resolveSessionTranscriptsDirForAgent } from "openclaw/plugin-sdk/memory-core-host-runtime-core";
@@ -2606,7 +2607,8 @@ describe("memory-core dreaming phases", () => {
nowMs,
});
expect(baseline).toHaveLength(1);
const baselineScore = baseline[0].score;
const baselineCandidate = expectDefined(baseline[0], "baseline promotion candidate");
const baselineScore = baselineCandidate.score;
const { beforeAgentReply } = createHarness(
{
@@ -2656,14 +2658,14 @@ describe("memory-core dreaming phases", () => {
minUniqueQueries: 0,
nowMs,
});
const reinforcedCandidate = requireCandidateByKey(reinforced, baseline[0].key);
const reinforcedCandidate = requireCandidateByKey(reinforced, baselineCandidate.key);
expect(reinforcedCandidate.score).toBeGreaterThan(baselineScore);
const phaseSignalStore = await shortTermTesting.readPhaseSignalStore(
workspaceDir,
new Date().toISOString(),
);
const baselineSignals = phaseSignalStore.entries[baseline[0].key];
const baselineSignals = phaseSignalStore.entries[baselineCandidate.key];
expect(baselineSignals?.lightHits).toBe(1);
expect(baselineSignals?.remHits).toBe(1);
});
+2 -1
View File
@@ -1,6 +1,7 @@
// Memory Core tests cover dreaming plugin behavior.
import fs from "node:fs/promises";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { readMemoryHostEvents } from "openclaw/plugin-sdk/memory-host-events";
import {
@@ -108,7 +109,7 @@ function createCronHarness(
if (index < 0) {
return {};
}
const current = jobs[index];
const current = expectDefined(jobs[index], `managed cron job ${id}`);
jobs[index] = {
...current,
...(patch.name ? { name: patch.name } : {}),
@@ -1,5 +1,6 @@
// Memory Core tests cover manager search plugin behavior.
import type { DatabaseSync } from "node:sqlite";
import { expectDefined } from "@openclaw/normalization-core";
import {
ensureMemoryIndexSchema,
loadSqliteVecExtension,
@@ -1543,8 +1544,10 @@ describe("searchVector sqlite-vec KNN", () => {
});
expect(results).toHaveLength(3);
// Strictly decreasing scores confirms top-K maintenance is intact.
for (let i = 1; i < results.length; i += 1) {
expect(results[i - 1].score).toBeGreaterThan(results[i].score);
let previous = expectDefined(results[0], "first vector-search result");
for (const current of results.slice(1)) {
expect(previous.score).toBeGreaterThan(current.score);
previous = current;
}
} finally {
db.close();
@@ -1581,9 +1584,11 @@ describe("searchVector sqlite-vec KNN", () => {
let normB = 0;
const len = Math.min(a.length, b.length);
for (let i = 0; i < len; i += 1) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
const aValue = expectDefined(a[i], `cosine vector a[${i}]`);
const bValue = expectDefined(b[i], `cosine vector b[${i}]`);
dot += aValue * bValue;
normA += aValue * aValue;
normB += bValue * bValue;
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
+16 -11
View File
@@ -1,4 +1,5 @@
// Memory Core tests cover mmr plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, it, expect } from "vitest";
import {
tokenize,
@@ -11,6 +12,10 @@ import {
type MMRItem,
} from "./mmr.js";
function requireEntry<T>(entries: readonly T[], index: number, context: string): T {
return expectDefined(entries[index], context);
}
describe("tokenize", () => {
it("normalizes, filters, and deduplicates token sets", () => {
const cases = [
@@ -253,9 +258,9 @@ describe("mmrRerank", () => {
it("lambda=0 maximizes diversity", () => {
const result = mmrRerank(diverseItems, { enabled: true, lambda: 0 });
// First item is still highest score (no penalty yet)
expect(result[0].id).toBe("1");
expect(requireEntry(result, 0, "first MMR result").id).toBe("1");
// Second should be most different from first
expect(result[1].id).toBe("3"); // elderberry... is most different
expect(requireEntry(result, 1, "second MMR result").id).toBe("3"); // elderberry... is most different
});
it("clamps lambda > 1 to 1", () => {
@@ -265,8 +270,8 @@ describe("mmrRerank", () => {
it("clamps lambda < 0 to 0", () => {
const result = mmrRerank(diverseItems, { enabled: true, lambda: -0.5 });
expect(result[0].id).toBe("1");
expect(result[1].id).toBe("3");
expect(requireEntry(result, 0, "first MMR result").id).toBe("1");
expect(requireEntry(result, 1, "second MMR result").id).toBe("3");
});
});
@@ -282,9 +287,9 @@ describe("mmrRerank", () => {
const result = mmrRerank(items, { enabled: true, lambda: 0.5 });
// First is always highest score
expect(result[0].id).toBe("1");
expect(requireEntry(result, 0, "first MMR result").id).toBe("1");
// Second should be the diverse database item, not another ML item
expect(result[1].id).toBe("3");
expect(requireEntry(result, 1, "second MMR result").id).toBe("3");
});
it("handles items with identical content", () => {
@@ -295,9 +300,9 @@ describe("mmrRerank", () => {
];
const result = mmrRerank(items, { enabled: true, lambda: 0.5 });
expect(result[0].id).toBe("1");
expect(requireEntry(result, 0, "first MMR result").id).toBe("1");
// Second should be different, not identical duplicate
expect(result[1].id).toBe("3");
expect(requireEntry(result, 1, "second MMR result").id).toBe("3");
});
it("handles all identical content gracefully", () => {
@@ -359,7 +364,7 @@ describe("mmrRerank", () => {
const result = mmrRerank(items, { lambda: 0.7 });
expect(result).toHaveLength(2);
// Higher score (less negative) should come first
expect(result[0].id).toBe("1");
expect(requireEntry(result, 0, "first MMR result").id).toBe("1");
});
});
});
@@ -446,9 +451,9 @@ describe("applyMMRToHybridResults", () => {
const reranked = applyMMRToHybridResults(results, { enabled: true, lambda: 0.5 });
// First stays the same (highest score)
expect(reranked[0].path).toBe("/a.ts");
expect(requireEntry(reranked, 0, "first reranked result").path).toBe("/a.ts");
// Second should be the diverse one
expect(reranked[1].path).toBe("/c.ts");
expect(requireEntry(reranked, 1, "second reranked result").path).toBe("/c.ts");
});
it("respects disabled config", () => {
@@ -3,6 +3,7 @@ import { EventEmitter } from "node:events";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import type { Mock } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -260,7 +261,8 @@ describe("QmdMemoryManager slugified path resolution", () => {
},
]);
await expect(manager.readFile({ relPath: results[0].path })).resolves.toEqual({
const result = expectDefined(results[0], "slugified QMD search result");
await expect(manager.readFile({ relPath: result.path })).resolves.toEqual({
path: actualRelative,
text: "line-1\nline-2\nline-3",
from: 1,
@@ -332,7 +334,8 @@ describe("QmdMemoryManager slugified path resolution", () => {
},
]);
await expect(manager.readFile({ relPath: results[0].path })).resolves.toEqual({
const result = expectDefined(results[0], "vault QMD search result");
await expect(manager.readFile({ relPath: result.path })).resolves.toEqual({
path: `qmd/${collectionName}/${actualRelative}`,
text: "vault memory",
from: 1,
@@ -391,7 +394,8 @@ describe("QmdMemoryManager slugified path resolution", () => {
},
]);
await expect(manager.readFile({ relPath: results[0].path })).resolves.toEqual({
const result = expectDefined(results[0], "exact QMD search result");
await expect(manager.readFile({ relPath: result.path })).resolves.toEqual({
path: exactRelative,
text: "exact slugified path",
from: 1,
@@ -5,6 +5,7 @@ import os from "node:os";
import path from "node:path";
import type { DatabaseSync } from "node:sqlite";
import { setTimeout as scheduleNativeTimeout } from "node:timers";
import { expectDefined } from "@openclaw/normalization-core";
import { withMockedWindowsPlatform } from "openclaw/plugin-sdk/test-node-mocks";
import type { Mock } from "vitest";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@@ -323,6 +324,14 @@ describe("QmdMemoryManager", () => {
return value;
}
function requireArgAfter(args: readonly string[], flag: string): string {
const index = args.indexOf(flag);
if (index < 0) {
throw new Error(`expected ${flag} argument`);
}
return expectDefined(args[index + 1], `${flag} argument value`);
}
function mockMessages(mock: Mock): string[] {
return mock.mock.calls.map((call: unknown[]) => String(call[0]));
}
@@ -4053,7 +4062,7 @@ describe("QmdMemoryManager", () => {
if (isMcporterCommand(cmd) && args[0] === "call") {
// Verify it calls qmd.query (v2) not qmd.deep_search (v1)
expect(args[1]).toBe("qmd.query");
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
const callArgs = JSON.parse(requireArgAfter(args, "--args"));
// Verify QMD 1.1+ searches array format
expect(callArgs).toHaveProperty("searches");
expect(Array.isArray(callArgs.searches)).toBe(true);
@@ -4099,7 +4108,7 @@ describe("QmdMemoryManager", () => {
const child = createMockChild({ autoClose: false });
if (isMcporterCommand(cmd) && args[0] === "call") {
expect(args[1]).toBe("qmd.query");
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
const callArgs = JSON.parse(requireArgAfter(args, "--args"));
expect(callArgs).toMatchObject({
searches: [
{ type: "lex", query: "hello" },
@@ -4141,7 +4150,7 @@ describe("QmdMemoryManager", () => {
spawnMock.mockImplementation((cmd: string, args: string[]) => {
const child = createMockChild({ autoClose: false });
if (isMcporterCommand(cmd) && args[0] === "call") {
captured = JSON.parse(args[args.indexOf("--args") + 1]);
captured = JSON.parse(requireArgAfter(args, "--args"));
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
return child;
}
@@ -4181,7 +4190,7 @@ describe("QmdMemoryManager", () => {
const child = createMockChild({ autoClose: false });
if (isMcporterCommand(cmd) && args[0] === "call") {
expect(args[1]).toBe("qmd.query");
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
const callArgs = JSON.parse(requireArgAfter(args, "--args"));
expect(callArgs.searches).toEqual([
{ type: "lex", query: "sqlite-vec-qmd backend health 2026-05-04 multi-agent" },
{ type: "vec", query: "sqlite vec qmd backend health 2026 05 04 multi agent" },
@@ -4220,7 +4229,7 @@ describe("QmdMemoryManager", () => {
const child = createMockChild({ autoClose: false });
if (isMcporterCommand(cmd) && args[0] === "call") {
expect(args[1]).toBe("qmd.query");
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
const callArgs = JSON.parse(requireArgAfter(args, "--args"));
expect(callArgs.searches).toEqual([{ type: "vec", query: "sqlite vec backend health" }]);
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
return child;
@@ -4305,7 +4314,7 @@ describe("QmdMemoryManager", () => {
}
if (toolSelector === "qmd.deep_search") {
// v1 tool exists — verify v1 args format
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
const callArgs = JSON.parse(requireArgAfter(args, "--args"));
expect(callArgs).toHaveProperty("query");
expect(callArgs).not.toHaveProperty("searches");
// Return empty results (avoids needing a SQLite fixture)
@@ -4353,7 +4362,7 @@ describe("QmdMemoryManager", () => {
const child = createMockChild({ autoClose: false });
if (isMcporterCommand(cmd) && args[0] === "call") {
expect(args[1]).toBe("qmd.hybrid_search");
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
const callArgs = JSON.parse(requireArgAfter(args, "--args"));
expect(callArgs.query).toBe("hello");
expect(callArgs.limit).toBe(expectedLimit);
expect(callArgs.minScore).toBe(0);
@@ -4609,7 +4618,7 @@ describe("QmdMemoryManager", () => {
const child = createMockChild({ autoClose: false });
if (isMcporterCommand(cmd) && args[0] === "call") {
expect(args[1]).toBe("qmd.query");
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
const callArgs = JSON.parse(requireArgAfter(args, "--args"));
expect(callArgs).toHaveProperty("searches", [{ type: "lex", query: "hello" }]);
expect(callArgs).toHaveProperty("collections", ["workspace-main"]);
expect(callArgs).not.toHaveProperty("query");
@@ -4648,7 +4657,7 @@ describe("QmdMemoryManager", () => {
const child = createMockChild({ autoClose: false });
if (isMcporterCommand(cmd) && args[0] === "call") {
expect(args[1]).toBe("qmd.query");
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
const callArgs = JSON.parse(requireArgAfter(args, "--args"));
expect(callArgs).toMatchObject({
searches: [
{ type: "lex", query: "hello" },
@@ -4706,7 +4715,7 @@ describe("QmdMemoryManager", () => {
});
return child;
}
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
const callArgs = JSON.parse(requireArgAfter(args, "--args"));
expect(selector).toBe("qmd.search");
expect(callArgs.query).toBe("hello");
expect(callArgs.limit).toBe(expectedLimit);
@@ -4753,7 +4762,7 @@ describe("QmdMemoryManager", () => {
const child = createMockChild({ autoClose: false });
if (isMcporterCommand(cmd) && args[0] === "call") {
selectors.push(args[1] ?? "");
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
const callArgs = JSON.parse(requireArgAfter(args, "--args"));
collections.push(String(callArgs.collection ?? ""));
expect(callArgs.query).toBe("hello");
expect(callArgs.limit).toBe(expectedLimit);
@@ -6515,7 +6524,8 @@ describe("QmdMemoryManager", () => {
},
]);
expect(inner.resolveReadPath(results[0].path)).toBe(exportedSessionPath);
const result = expectDefined(results[0], "QMD session search result");
expect(inner.resolveReadPath(result.path)).toBe(exportedSessionPath);
const realLstat = fs.lstat;
const lstatSpy = vi.spyOn(fs, "lstat").mockImplementation(async (target, options) => {
if (typeof target === "string" && path.resolve(target) === exportedSessionPath) {
@@ -6535,7 +6545,7 @@ describe("QmdMemoryManager", () => {
});
try {
const readResult = await manager.readFile({ relPath: results[0].path });
const readResult = await manager.readFile({ relPath: result.path });
expect(readResult).toEqual({
path: "qmd/sessions-main/session-1.md",
text: "# Session session-1\n\nsession canary\n",
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import type { OpenKeyedStoreOptions } from "openclaw/plugin-sdk/plugin-state-runtime";
import { createPluginStateKeyedStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
@@ -356,7 +357,7 @@ describe("short-term promotion", () => {
const entries = Object.entries(await readRecallStoreEntries(workspaceDir));
expect(entries).toHaveLength(1);
const [key, entry] = entries[0];
const [key, entry] = expectDefined(entries[0], "stable claim recall entry");
expect(key.endsWith(`:${claimHash}`)).toBe(true);
expect(entry.claimHash).toBe(claimHash);
expect(entry.recallCount).toBe(1);
@@ -1069,7 +1070,9 @@ describe("short-term promotion", () => {
expect(fasterDecay).toHaveLength(1);
expect(slowerDecay[0]?.components.recency).toBeCloseTo(0.5, 3);
expect(fasterDecay[0]?.components.recency).toBeCloseTo(0.25, 3);
expect(slowerDecay[0].score).toBeGreaterThan(fasterDecay[0].score);
const slowerResult = expectDefined(slowerDecay[0], "slower decay result");
const fasterResult = expectDefined(fasterDecay[0], "faster decay result");
expect(slowerResult.score).toBeGreaterThan(fasterResult.score);
});
});
@@ -1158,7 +1161,9 @@ describe("short-term promotion", () => {
nowMs,
});
expect(ranked[0]?.path).toBe("memory/2026-04-02.md");
expect(ranked[0].score).toBeGreaterThan(ranked[1].score);
const boostedResult = expectDefined(ranked[0], "boosted phase-signal result");
const baselineResult = expectDefined(ranked[1], "baseline phase-signal result");
expect(boostedResult.score).toBeGreaterThan(baselineResult.score);
const phaseStore = await testing.readPhaseSignalStore(
workspaceDir,
@@ -1240,7 +1245,9 @@ describe("short-term promotion", () => {
expect(staleSignalRank).toHaveLength(1);
expect(freshSignalRank).toHaveLength(1);
expect(freshSignalRank[0].score).toBeGreaterThan(staleSignalRank[0].score);
const freshResult = expectDefined(freshSignalRank[0], "fresh phase-signal result");
const staleResult = expectDefined(staleSignalRank[0], "stale phase-signal result");
expect(freshResult.score).toBeGreaterThan(staleResult.score);
});
});
@@ -3629,10 +3636,11 @@ describe("short-term promotion", () => {
});
const store = await testing.readRecallStore(workspaceDir, new Date(nowMs).toISOString());
const entryKey = Object.keys(store.entries)[0];
store.entries[entryKey].dailyCount = 6;
store.entries[entryKey].recallCount = 0;
store.entries[entryKey].groundedCount = 1;
const entryKey = expectDefined(Object.keys(store.entries)[0], "signal-count recall key");
const entry = expectDefined(store.entries[entryKey], "signal-count recall entry");
entry.dailyCount = 6;
entry.recallCount = 0;
entry.groundedCount = 1;
await testing.writeRawRecallStore(workspaceDir, store);
const ranked = await rankShortTermPromotionCandidates({
@@ -3644,10 +3652,11 @@ describe("short-term promotion", () => {
});
expect(ranked.length).toBe(1);
expect(ranked[0].recallCount).toBe(0);
expect(ranked[0].dailyCount).toBe(6);
expect(ranked[0].groundedCount).toBe(1);
expect(ranked[0].signalCount).toBe(7);
const rankedResult = expectDefined(ranked[0], "signal-count ranking result");
expect(rankedResult.recallCount).toBe(0);
expect(rankedResult.dailyCount).toBe(6);
expect(rankedResult.groundedCount).toBe(1);
expect(rankedResult.signalCount).toBe(7);
const applied = await applyShortTermPromotions({
workspaceDir,
@@ -3688,7 +3697,8 @@ describe("short-term promotion", () => {
const entries = Object.values(await readRecallStoreEntries(workspaceDir));
expect(entries).toHaveLength(1);
expect(readEntrySnippet(entries[0])).toBe(prefix);
const entry = expectDefined(entries[0], "UTF-16 recall entry");
expect(readEntrySnippet(entry)).toBe(prefix);
});
});
+8 -2
View File
@@ -11,6 +11,7 @@
import { Buffer } from "node:buffer";
import fs from "node:fs/promises";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { Command } from "commander";
import {
clearMemoryPluginState,
@@ -2518,8 +2519,13 @@ describe("memory plugin e2e", () => {
expect(loadLanceDbModule).toHaveBeenCalledTimes(1);
expect(ensureGlobalUndiciEnvProxyDispatcher).toHaveBeenCalledOnce();
expect(ensureGlobalUndiciEnvProxyDispatcher.mock.invocationCallOrder[0]).toBeLessThan(
embeddingsCreate.mock.invocationCallOrder[0],
expect(
expectDefined(
ensureGlobalUndiciEnvProxyDispatcher.mock.invocationCallOrder[0],
"LanceDB proxy dispatcher invocation",
),
).toBeLessThan(
expectDefined(embeddingsCreate.mock.invocationCallOrder[0], "LanceDB embedding invocation"),
);
expect(embeddingsCreate).toHaveBeenCalledWith({
model: "text-embedding-3-small",
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import type { OpenKeyedStoreOptions } from "openclaw/plugin-sdk/plugin-state-runtime";
import {
createPluginStateKeyedStoreForTests,
@@ -19,6 +20,10 @@ import {
resolveMemoryWikiSourceSyncStatePath,
} from "./src/source-sync-state.js";
function requireStateMigration(index: number) {
return expectDefined(stateMigrations[index], `Memory Wiki state migration ${index}`);
}
const tempDirs: string[] = [];
async function makeTempDir(): Promise<string> {
@@ -93,7 +98,7 @@ describe("memory-wiki doctor source sync migration", () => {
})}\n`,
);
const params = migrationParams({ stateDir, vaultRoot });
const migration = stateMigrations[0];
const migration = requireStateMigration(0);
await expect(migration.detectLegacyState(params)).resolves.toEqual({
preview: [expect.stringContaining("Memory Wiki source sync:")],
@@ -241,7 +246,7 @@ describe("memory-wiki doctor source sync migration", () => {
},
});
await expect(stateMigrations[0].migrateLegacyState(params)).resolves.toEqual({
await expect(requireStateMigration(0).migrateLegacyState(params)).resolves.toEqual({
changes: [
"Migrated Memory Wiki source sync -> plugin state (1 imported, 1 existing)",
expect.stringContaining("Archived Memory Wiki source-sync legacy source ->"),
@@ -298,13 +303,13 @@ describe("memory-wiki doctor source sync migration", () => {
}
const params = migrationParams({ stateDir, vaultRoot, agentIds });
await expect(stateMigrations[0].detectLegacyState(params)).resolves.toEqual({
await expect(requireStateMigration(0).detectLegacyState(params)).resolves.toEqual({
preview: [
expect.stringContaining(path.join(vaultRoot, "support")),
expect.stringContaining(path.join(vaultRoot, "marketing")),
],
});
await expect(stateMigrations[0].migrateLegacyState(params)).resolves.toMatchObject({
await expect(requireStateMigration(0).migrateLegacyState(params)).resolves.toMatchObject({
warnings: [],
});
+4 -2
View File
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { Command } from "commander";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
@@ -751,7 +752,8 @@ cli note
(entry) => entry !== "index.md",
);
expect(sourceFiles).toHaveLength(1);
const pageContent = await fs.readFile(path.join(rootDir, "sources", sourceFiles[0]), "utf8");
const sourceFile = expectDefined(sourceFiles[0], "imported ChatGPT source file");
const pageContent = await fs.readFile(path.join(rootDir, "sources", sourceFile), "utf8");
expect(pageContent).toContain("ChatGPT Export: Travel preference check");
expect(pageContent).toContain("I prefer aisle seats");
expect(pageContent).toContain("Preference signals:");
@@ -789,7 +791,7 @@ cli note
const conversations = JSON.parse(await fs.readFile(conversationsPath, "utf8")) as Array<
Record<string, unknown>
>;
conversations[0].update_time = 9_000_000_000_000;
expectDefined(conversations[0], "first ChatGPT conversation").update_time = 9_000_000_000_000;
await fs.writeFile(conversationsPath, `${JSON.stringify(conversations, null, 2)}\n`, "utf8");
const result = await runWikiChatGptImport({
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../api.js";
import {
@@ -170,14 +171,16 @@ describe("default wiki prompt section", () => {
},
],
};
const firstPage = expectDefined(firstDigest.pages[0], "first Memory Wiki digest page");
const secondPage = expectDefined(firstDigest.pages[1], "second Memory Wiki digest page");
const secondDigest = {
...firstDigest,
pages: [
{
...firstDigest.pages[1],
topClaims: firstDigest.pages[1].topClaims.toReversed(),
...secondPage,
topClaims: secondPage.topClaims.toReversed(),
},
firstDigest.pages[0],
firstPage,
],
};
@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { FsSafeError } from "openclaw/plugin-sdk/security-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import { applyMemoryWikiMutation } from "./apply.js";
@@ -121,7 +122,11 @@ async function createChatGptImportFixture(prefix: string) {
return {
config,
exportDir,
pagePath: path.join(rootDir, "sources", sourceFiles[0]),
pagePath: path.join(
rootDir,
"sources",
expectDefined(sourceFiles[0], "imported Memory Wiki source file"),
),
};
}
@@ -1,4 +1,5 @@
// Msteams tests cover bot framework plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { setMSTeamsRuntime } from "../runtime.js";
import {
@@ -183,7 +184,9 @@ describe("downloadMSTeamsBotFrameworkAttachment", () => {
expect(media?.path).toBe(runtime.savePath);
expect(media?.contentType).toBe(runtime.savedContentType);
expect(runtime.saveCalls).toHaveLength(1);
expect(runtime.saveCalls[0].buffer.toString("utf-8")).toBe("PDFBYTES");
expect(expectDefined(runtime.saveCalls[0], "MSTeams save call").buffer.toString("utf-8")).toBe(
"PDFBYTES",
);
});
it("skips malformed attachment view content-length before saving media", async () => {
@@ -415,8 +418,12 @@ describe("downloadMSTeamsBotFrameworkAttachment", () => {
// Both the attachment info call and the view call should be observed,
// confirming the guarded fetch path still preserves caller fetch hooks.
expect(fetchCalls).toHaveLength(2);
expect(fetchCalls[0].url.endsWith("/v3/attachments/att-1")).toBe(true);
expect(fetchCalls[1].url.endsWith("/v3/attachments/att-1/views/original")).toBe(true);
expect(expectDefined(fetchCalls[0], "attachment info fetch").url).toMatch(
/\/v3\/attachments\/att-1$/,
);
expect(expectDefined(fetchCalls[1], "attachment view fetch").url).toMatch(
/\/v3\/attachments\/att-1\/views\/original$/,
);
for (const call of fetchCalls) {
const init = call.init as RequestInit & { dispatcher?: unknown };
expect(init?.dispatcher).toBeDefined();
@@ -1,4 +1,5 @@
// Msteams tests cover file consent helpers plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { prepareFileConsentActivity, requiresFileConsent } from "./file-consent-helpers.js";
import {
@@ -194,10 +195,10 @@ describe("prepareFileConsentActivity", () => {
conversationId: "conv456",
});
const attachment = (result.activity.attachments as unknown[])[0] as Record<
string,
{ description: string }
>;
const attachment = expectDefined(
(result.activity.attachments as Array<{ content: { description: string } }>)[0],
"default file-consent attachment",
);
expect(attachment.content.description).toBe("File: document.docx");
});
@@ -212,10 +213,10 @@ describe("prepareFileConsentActivity", () => {
description: "Q4 Financial Report",
});
const attachment = (result.activity.attachments as unknown[])[0] as Record<
string,
{ description: string }
>;
const attachment = expectDefined(
(result.activity.attachments as Array<{ content: { description: string } }>)[0],
"described file-consent attachment",
);
expect(attachment.content.description).toBe("Q4 Financial Report");
});
@@ -229,10 +230,14 @@ describe("prepareFileConsentActivity", () => {
conversationId: "conv000",
});
const attachment = (result.activity.attachments as unknown[])[0] as Record<
string,
{ acceptContext: { uploadId: string } }
>;
const attachment = expectDefined(
(
result.activity.attachments as Array<{
content: { acceptContext: { uploadId: string } };
}>
)[0],
"file-consent upload attachment",
);
expect(attachment.content.acceptContext.uploadId).toBe(mockUploadId);
});
@@ -1,4 +1,5 @@
// Msteams tests cover inbound media plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it, vi } from "vitest";
vi.mock("../attachments.js", () => ({
@@ -405,7 +406,7 @@ describe("resolveMSTeamsInboundMedia bot framework DM routing", () => {
expect(call?.attachmentIds).toEqual(["att-0", "att-1"]);
expect(downloadMSTeamsGraphMedia).not.toHaveBeenCalled();
expect(mediaList).toHaveLength(1);
expect(mediaList[0].path).toBe("/tmp/report.pdf");
expect(expectDefined(mediaList[0], "MSTeams inbound media").path).toBe("/tmp/report.pdf");
});
it("skips Graph fallback for an 'a:' conversation without an exact Graph chat ID", async () => {
+5 -1
View File
@@ -2,6 +2,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
import { describe, expect, it } from "vitest";
import { resolveNextcloudTalkAccount } from "./accounts.js";
@@ -328,7 +329,10 @@ describe("nextcloud talk setup", () => {
});
it("clears stored bot secret fields when the wizard switches to env", async () => {
const credential = nextcloudTalkSetupWizard.credentials[0];
const credential = expectDefined(
nextcloudTalkSetupWizard.credentials[0],
"Nextcloud Talk credential",
);
const next = await credential.applyUseEnv?.({
cfg: {
channels: {
+8 -3
View File
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import {
createPluginStateKeyedStoreForTests,
resetPluginStateStoreForTests,
@@ -13,6 +14,10 @@ import type {
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { stateMigrations } from "./doctor-contract-api.js";
function requireStateMigration(index: number) {
return expectDefined(stateMigrations[index], `Nostr state migration ${index}`);
}
function createDoctorContext(env: NodeJS.ProcessEnv): PluginDoctorStateMigrationContext {
return {
openPluginStateKeyedStore<T>(options: OpenKeyedStoreOptions) {
@@ -62,14 +67,14 @@ describe("nostr doctor state migration", () => {
);
const context = createDoctorContext(env);
const busResult = await stateMigrations[0].migrateLegacyState({
const busResult = await requireStateMigration(0).migrateLegacyState({
config: {},
env,
stateDir,
oauthDir: path.join(stateDir, "oauth"),
context,
});
const profileResult = await stateMigrations[1].migrateLegacyState({
const profileResult = await requireStateMigration(1).migrateLegacyState({
config: {},
env,
stateDir,
@@ -117,7 +122,7 @@ describe("nostr doctor state migration", () => {
);
const context = createDoctorContext(env);
await stateMigrations[0].migrateLegacyState({
await requireStateMigration(0).migrateLegacyState({
config: {},
env,
stateDir,
@@ -1,4 +1,5 @@
// Nostr tests cover nostr bus.integration plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createMetrics, createNoopMetrics, type MetricEvent } from "./metrics.js";
import { createSeenTracker } from "./seen-tracker.js";
@@ -34,6 +35,10 @@ function createPlainMetrics() {
return createMetrics();
}
function requireRecordEntry<T>(entries: Record<string, T>, key: string, context: string): T {
return expectDefined(entries[key], context);
}
// ============================================================================
// Seen Tracker Integration Tests
// ============================================================================
@@ -298,9 +303,9 @@ describe("Metrics", () => {
metrics.emit("event.duplicate");
expect(events).toHaveLength(3);
expect(events[0].name).toBe("event.received");
expect(events[1].name).toBe("event.processed");
expect(events[2].name).toBe("event.duplicate");
expect(expectDefined(events[0], "first Nostr metric event").name).toBe("event.received");
expect(expectDefined(events[1], "second Nostr metric event").name).toBe("event.processed");
expect(expectDefined(events[2], "third Nostr metric event").name).toBe("event.duplicate");
});
it("includes labels in metric events", () => {
@@ -308,7 +313,9 @@ describe("Metrics", () => {
metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL });
expect(events[0].labels).toEqual({ relay: TEST_RELAY_URL });
expect(expectDefined(events[0], "first Nostr metric event").labels).toEqual({
relay: TEST_RELAY_URL,
});
});
it("accumulates counters in snapshot", () => {
@@ -336,14 +343,18 @@ describe("Metrics", () => {
metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_1 });
const snapshot = metrics.getSnapshot();
const relayOne = snapshot.relays[TEST_RELAY_URL_1];
const relayOne = requireRecordEntry(snapshot.relays, TEST_RELAY_URL_1, "Nostr relay metrics");
if (!relayOne) {
throw new Error("expected first relay metrics");
}
expect(relayOne.connects).toBe(1);
expect(relayOne.errors).toBe(2);
expect(snapshot.relays[TEST_RELAY_URL_2].connects).toBe(1);
expect(snapshot.relays[TEST_RELAY_URL_2].errors).toBe(0);
expect(
requireRecordEntry(snapshot.relays, TEST_RELAY_URL_2, "Nostr relay metrics").connects,
).toBe(1);
expect(
requireRecordEntry(snapshot.relays, TEST_RELAY_URL_2, "Nostr relay metrics").errors,
).toBe(0);
});
it("tracks circuit breaker state changes", () => {
@@ -352,14 +363,26 @@ describe("Metrics", () => {
metrics.emit("relay.circuit_breaker.open", 1, { relay: TEST_RELAY_URL_PRIMARY });
let snapshot = metrics.getSnapshot();
expect(snapshot.relays[TEST_RELAY_URL_PRIMARY].circuitBreakerState).toBe("open");
expect(snapshot.relays[TEST_RELAY_URL_PRIMARY].circuitBreakerOpens).toBe(1);
expect(
requireRecordEntry(snapshot.relays, TEST_RELAY_URL_PRIMARY, "Nostr relay metrics")
.circuitBreakerState,
).toBe("open");
expect(
requireRecordEntry(snapshot.relays, TEST_RELAY_URL_PRIMARY, "Nostr relay metrics")
.circuitBreakerOpens,
).toBe(1);
metrics.emit("relay.circuit_breaker.close", 1, { relay: TEST_RELAY_URL_PRIMARY });
snapshot = metrics.getSnapshot();
expect(snapshot.relays[TEST_RELAY_URL_PRIMARY].circuitBreakerState).toBe("closed");
expect(snapshot.relays[TEST_RELAY_URL_PRIMARY].circuitBreakerCloses).toBe(1);
expect(
requireRecordEntry(snapshot.relays, TEST_RELAY_URL_PRIMARY, "Nostr relay metrics")
.circuitBreakerState,
).toBe("closed");
expect(
requireRecordEntry(snapshot.relays, TEST_RELAY_URL_PRIMARY, "Nostr relay metrics")
.circuitBreakerCloses,
).toBe(1);
});
it("tracks all rejection reasons", () => {
@@ -400,7 +423,11 @@ describe("Metrics", () => {
metrics.emit("relay.message.auth", 1, { relay: TEST_RELAY_URL_PRIMARY });
const snapshot = metrics.getSnapshot();
const relay = snapshot.relays[TEST_RELAY_URL_PRIMARY];
const relay = requireRecordEntry(
snapshot.relays,
TEST_RELAY_URL_PRIMARY,
"Nostr relay metrics",
);
expect(relay.messagesReceived.event).toBe(1);
expect(relay.messagesReceived.eose).toBe(1);
expect(relay.messagesReceived.closed).toBe(1);
@@ -487,9 +514,15 @@ describe("Circuit Breaker Behavior", () => {
const cbEvents = events.filter((e) => e.name.startsWith("relay.circuit_breaker"));
expect(cbEvents).toHaveLength(3);
expect(cbEvents[0].name).toBe("relay.circuit_breaker.open");
expect(cbEvents[1].name).toBe("relay.circuit_breaker.half_open");
expect(cbEvents[2].name).toBe("relay.circuit_breaker.close");
expect(expectDefined(cbEvents[0], "circuit breaker open event").name).toBe(
"relay.circuit_breaker.open",
);
expect(expectDefined(cbEvents[1], "circuit breaker half-open event").name).toBe(
"relay.circuit_breaker.half_open",
);
expect(expectDefined(cbEvents[2], "circuit breaker close event").name).toBe(
"relay.circuit_breaker.close",
);
});
});
@@ -510,8 +543,12 @@ describe("Health Scoring", () => {
metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_BAD });
const snapshot = metrics.getSnapshot();
expect(snapshot.relays[TEST_RELAY_URL_GOOD].errors).toBe(0);
expect(snapshot.relays[TEST_RELAY_URL_BAD].errors).toBe(3);
expect(
requireRecordEntry(snapshot.relays, TEST_RELAY_URL_GOOD, "Nostr relay metrics").errors,
).toBe(0);
expect(
requireRecordEntry(snapshot.relays, TEST_RELAY_URL_BAD, "Nostr relay metrics").errors,
).toBe(3);
});
});
@@ -4,6 +4,7 @@
import { IncomingMessage, ServerResponse } from "node:http";
import { Socket } from "node:net";
import { expectDefined } from "@openclaw/normalization-core";
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
clearNostrProfileRateLimitStateForTest,
@@ -46,7 +47,7 @@ import { TEST_HEX_PUBLIC_KEY, TEST_SETUP_RELAY_URLS } from "./test-fixtures.js";
// Test Helpers
// ============================================================================
const TEST_PROFILE_RELAY_URL = TEST_SETUP_RELAY_URLS[0];
const TEST_PROFILE_RELAY_URL = expectDefined(TEST_SETUP_RELAY_URLS[0], "Nostr profile relay URL");
afterAll(() => {
runtimeScopeMock.mockReset();
@@ -1,4 +1,5 @@
// OC Path tests cover find plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import { findOcPaths } from "../find.js";
import { parseJsonc } from "../jsonc/parse.js";
@@ -7,6 +8,10 @@ import { formatOcPath, hasWildcard, OcPathError, parseOcPath } from "../oc-path.
import { parseMd } from "../parse.js";
import { resolveOcPath, setOcPath } from "../universal.js";
function requireFirstResult<T>(results: readonly T[]): T {
return expectDefined(results[0], "first OC path match");
}
describe("hasWildcard", () => {
it("detects single-segment * in any slot", () => {
expect(hasWildcard(parseOcPath("oc://X/*/y"))).toBe(true);
@@ -71,8 +76,8 @@ describe("findOcPaths — non-wildcard fast-path", () => {
const ast = parseJsonc('{"name":"x"}').ast;
const out = findOcPaths(ast, parseOcPath("oc://wf/name"));
expect(out).toHaveLength(1);
expect(out[0].match.kind).toBe("leaf");
expect(formatOcPath(out[0].path)).toBe("oc://wf/name");
expect(requireFirstResult(out).match.kind).toBe("leaf");
expect(formatOcPath(requireFirstResult(out).path)).toBe("oc://wf/name");
});
it("returns empty for unresolved plain path", () => {
@@ -153,7 +158,8 @@ describe("findOcPaths — slash-deep JSONC paths", () => {
parseOcPath("oc://openclaw.json/agents/[id=reviewer]/tools/exec/security"),
);
expect(out).toHaveLength(1);
expect(out[0]?.match.kind === "leaf" && out[0].match.valueText).toBe("allowlist");
const result = requireFirstResult(out);
expect(result.match.kind === "leaf" && result.match.valueText).toBe("allowlist");
});
it("expands ** in slash-deep JSON paths", () => {
@@ -210,8 +216,9 @@ describe("findOcPaths — JSONL kind", () => {
it("predicate [event=action] at line slot filters by top-level field", () => {
const out = findOcPaths(jsonl, parseOcPath("oc://session/[event=action]/userId"));
expect(out).toHaveLength(1);
if (out[0]?.match.kind === "leaf") {
expect(out[0].match.valueText).toBe("u1");
const result = requireFirstResult(out);
if (result.match.kind === "leaf") {
expect(result.match.valueText).toBe("u1");
}
});
@@ -338,8 +345,9 @@ describe("value predicates — numeric operators (v1.1)", () => {
it("> finds models exceeding the per-request output cap", () => {
const out = findOcPaths(jsonc, parseOcPath(`${PREFIX}/[maxTokens>128000]/id`));
expect(out).toHaveLength(1);
if (out[0].match.kind === "leaf") {
expect(out[0].match.valueText).toBe("claude-opus-4-7");
const result = requireFirstResult(out);
if (result.match.kind === "leaf") {
expect(result.match.valueText).toBe("claude-opus-4-7");
}
});
@@ -352,8 +360,9 @@ describe("value predicates — numeric operators (v1.1)", () => {
it("< filters small context windows", () => {
const out = findOcPaths(jsonc, parseOcPath(`${PREFIX}/[contextWindow<500000]/id`));
expect(out).toHaveLength(1);
if (out[0].match.kind === "leaf") {
expect(out[0].match.valueText).toBe("claude-sonnet-4-7");
const result = requireFirstResult(out);
if (result.match.kind === "leaf") {
expect(result.match.valueText).toBe("claude-sonnet-4-7");
}
});
@@ -447,16 +456,17 @@ describe("findOcPaths — Markdown kind", () => {
it("* in field slot enumerates each item kv key", () => {
const out = findOcPaths(md, parseOcPath("oc://SKILL.md/Tools/send-email/*"));
expect(out).toHaveLength(1);
expect(out[0].match.kind).toBe("leaf");
if (out[0].match.kind === "leaf") {
expect(out[0].match.valueText).toBe("enabled");
const result = requireFirstResult(out);
expect(result.match.kind).toBe("leaf");
if (result.match.kind === "leaf") {
expect(result.match.valueText).toBe("enabled");
}
});
it("* in item slot + matching field returns each item whose kv key matches", () => {
const out = findOcPaths(md, parseOcPath("oc://SKILL.md/Tools/*/send_email"));
expect(out).toHaveLength(1);
expect(out[0].path.item).toBe("send-email");
expect(requireFirstResult(out).path.item).toBe("send-email");
});
it("** at section slot matches items at every depth (cross-kind symmetry)", () => {
@@ -541,7 +551,7 @@ describe("union segments — md", () => {
const ast = parseMd(RAW).ast;
const out = findOcPaths(ast, parseOcPath("oc://X.md/limits/alias/{alias,nope}"));
expect(out.length).toBe(1);
expect(out[0]?.path.field).toBe("alias");
expect(requireFirstResult(out)?.path.field).toBe("alias");
});
});
@@ -570,14 +580,14 @@ describe("predicate segments — md", () => {
const ast = parseMd(RAW).ast;
const out = findOcPaths(ast, parseOcPath("oc://X.md/limits/[enabled=false]/*"));
expect(out.length).toBe(1);
expect(out[0]?.path.item).toBe("enabled");
expect(requireFirstResult(out)?.path.item).toBe("enabled");
});
it("matches the kv pair at the field slot", () => {
const ast = parseMd(RAW).ast;
const out = findOcPaths(ast, parseOcPath("oc://X.md/limits/max-tokens/[max-tokens=4096]"));
expect(out.length).toBe(1);
expect(out[0]?.path.field).toBe("max-tokens");
expect(requireFirstResult(out)?.path.field).toBe("max-tokens");
});
it("returns empty when no section's item matches", () => {
@@ -1,4 +1,5 @@
// OC Path tests cover roundtrip property plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import { emitMd } from "../../emit.js";
import { parseMd } from "../../parse.js";
@@ -91,7 +92,10 @@ function generateCorpus(count: number): string[] {
seed = (seed * 1664525 + 1013904223) % 2 ** 32;
return seed / 2 ** 32;
};
const choose = <T>(arr: readonly T[]): T => arr[Math.floor(rand() * arr.length)];
const choose = <T>(arr: readonly T[]): T => {
const index = Math.floor(rand() * arr.length);
return expectDefined(arr[index], `round-trip corpus choice ${index}`);
};
const headings = ["Boundaries", "Tools", "Memory", "Identity", "User", "Heartbeat", "Skills"];
const fmKeys = ["name", "description", "tier", "enabled", "timeout", "url"];
@@ -1,4 +1,5 @@
// OC Path tests cover universal plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import { emitMd } from "../emit.js";
import { emitJsonc } from "../jsonc/emit.js";
@@ -342,7 +343,10 @@ describe("setOcPath — jsonl leaf", () => {
expect(r.ok).toBe(true);
if (r.ok) {
const out = emitJsonl(r.ast as Parameters<typeof emitJsonl>[0]);
expect(JSON.parse(out.split("\n")[0])).toEqual({ event: "start", n: 42 });
expect(JSON.parse(expectDefined(out.split("\n")[0], "first emitted JSONL line"))).toEqual({
event: "start",
n: 42,
});
}
});
@@ -352,7 +356,9 @@ describe("setOcPath — jsonl leaf", () => {
expect(r.ok).toBe(true);
if (r.ok) {
const out = emitJsonl(r.ast as Parameters<typeof emitJsonl>[0]);
expect(JSON.parse(out.split("\n")[0])).toEqual({ event: "replaced" });
expect(JSON.parse(expectDefined(out.split("\n")[0], "replaced JSONL line"))).toEqual({
event: "replaced",
});
}
});
@@ -490,7 +496,10 @@ describe("setOcPath — jsonl insertion (session append)", () => {
const out = emitJsonl(r.ast as Parameters<typeof emitJsonl>[0]);
const lines = out.split("\n").filter((l) => l.length > 0);
expect(lines).toHaveLength(2);
expect(JSON.parse(lines[1])).toEqual({ event: "step", n: 1 });
expect(JSON.parse(expectDefined(lines[1], "appended JSONL line"))).toEqual({
event: "step",
n: 1,
});
}
});
+2 -1
View File
@@ -1,4 +1,5 @@
// Ollama tests cover index plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import {
describeImageWithModel,
describeImagesWithModel,
@@ -1860,7 +1861,7 @@ describe("ollama plugin", () => {
);
expect(mediaProviders).toHaveLength(1);
const [ollamaMedia] = mediaProviders;
const ollamaMedia = expectDefined(mediaProviders[0], "Ollama media provider");
expect(ollamaMedia.id).toBe("ollama");
expect(ollamaMedia.capabilities).toEqual(["image"]);
expect(ollamaMedia.describeImage).toBe(describeImageWithModel);
+4 -2
View File
@@ -4,6 +4,7 @@ import * as fsSync from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import { isLocalOllamaBaseUrl } from "./src/discovery-shared.js";
import { createOllamaEmbeddingProvider } from "./src/embedding-provider.js";
@@ -292,8 +293,9 @@ describe.skipIf(!LIVE)("ollama live", () => {
expect(embeddings).toHaveLength(2);
expect(embeddings[0]?.length ?? 0).toBeGreaterThan(0);
expect(embeddings[1]?.length).toBe(embeddings[0]?.length);
expect(Math.hypot(...embeddings[0])).toBeGreaterThan(0.99);
expect(Math.hypot(...embeddings[0])).toBeLessThan(1.01);
const firstEmbedding = expectDefined(embeddings[0], "first Ollama embedding");
expect(Math.hypot(...firstEmbedding)).toBeGreaterThan(0.99);
expect(Math.hypot(...firstEmbedding)).toBeLessThan(1.01);
},
45_000,
);
@@ -1,4 +1,5 @@
// Ollama tests cover discovery shared plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { describe, expect, it } from "vitest";
import {
@@ -150,8 +151,11 @@ describe("resolveOllamaDiscoveryResult — hosted Ollama Cloud guard", () => {
buildProvider: buildMockProvider,
});
expect(result).not.toBeNull();
expect(result!.provider.models).toHaveLength(1);
expect(result!.provider.models[0].id).toBe("minimax-m3:cloud");
const discoveryResult = expectDefined(result, "Ollama Cloud discovery result");
expect(discoveryResult.provider.models).toHaveLength(1);
expect(expectDefined(discoveryResult.provider.models[0], "Ollama Cloud model").id).toBe(
"minimax-m3:cloud",
);
});
it("does not call buildProvider for remote base URL without explicit models", async () => {
@@ -1,4 +1,5 @@
// Ollama tests cover provider models plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { jsonResponse, requestBodyText, requestUrl } from "openclaw/plugin-sdk/test-env";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
@@ -45,11 +46,12 @@ describe("ollama provider models", () => {
{ name: "llama3:8b", contextWindow: 65536, capabilities: undefined },
{ name: "deepseek-r1:14b", contextWindow: undefined, capabilities: undefined },
]);
const fallbackModel = expectDefined(enriched[1], "fallback Ollama model");
expect(
buildOllamaModelDefinition(
enriched[1].name,
enriched[1].contextWindow,
enriched[1].capabilities,
fallbackModel.name,
fallbackModel.contextWindow,
fallbackModel.capabilities,
).compat?.supportsTools,
).toBe(true);
});

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