From 81941f2d688b40741a87d9a58ebfb00f08052dd9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 12 Jul 2026 12:48:22 +0100 Subject: [PATCH] test: enable noUncheckedIndexedAccess for extension tests (#105343) --- extensions/acpx/doctor-contract-api.test.ts | 15 +- .../src/runtime-internals/mcp-proxy.test.ts | 13 +- .../active-memory/doctor-contract-api.test.ts | 3 +- extensions/active-memory/index.test.ts | 311 ++++++++-------- extensions/amazon-bedrock/index.test.ts | 14 +- extensions/anthropic/stream-wrappers.test.ts | 5 +- .../browser/src/browser-tool.schema.test.ts | 65 +++- .../src/browser/cdp.helpers.fuzz.test.ts | 4 +- .../src/browser/profiles-service.test.ts | 4 +- .../browser/src/browser/profiles.test.ts | 19 +- .../browser/src/browser/pw-session.test.ts | 3 +- .../pw-tools-core.browser-ssrf-guard.test.ts | 40 +- ...core.interactions.navigation-guard.test.ts | 19 +- .../src/browser/screenshot-annotate.test.ts | 24 +- .../server-context.existing-session.test.ts | 7 +- ...xt.remote-profile-tab-ops.fallback.test.ts | 9 +- ....remote-profile-tab-ops.playwright.test.ts | 4 +- .../server.agent-contract-core.test.ts | 60 +-- ...-contract-form-layout-act-commands.test.ts | 100 ++--- extensions/browser/src/browser/vision.test.ts | 9 +- .../browser-request.profile-from-body.test.ts | 6 +- ...owser-request.shared-control-state.test.ts | 6 +- .../gateway/browser-request.timeout.test.ts | 11 +- extensions/byteplus/index.test.ts | 24 +- extensions/chutes/models.test.ts | 3 +- .../clawrouter/provider-catalog.test.ts | 8 +- .../src/app-server/dynamic-tool-build.test.ts | 6 +- .../copilot/doctor-contract-api.test.ts | 11 +- extensions/copilot/src/attempt.test.ts | 53 ++- extensions/copilot/src/event-bridge.test.ts | 8 +- extensions/copilot/src/tool-bridge.test.ts | 7 +- .../deepseek/provider-policy-api.test.ts | 35 +- .../device-pair/doctor-contract-api.test.ts | 5 +- .../src/service.test.ts | 5 +- extensions/diffs/src/browser.test.ts | 10 +- extensions/diffs/src/viewer-client.test.ts | 9 +- .../discord/src/actions/handle-action.test.ts | 11 +- .../discord/src/actions/runtime.test.ts | 11 +- extensions/discord/src/chunk.test.ts | 7 +- .../discord/src/internal/gateway.test.ts | 3 +- .../src/monitor/message-handler.queue.test.ts | 13 +- .../discord/src/monitor/presence.test.ts | 17 +- .../src/monitor/reply-delivery.test.ts | 17 +- .../monitor/thread-bindings.lifecycle.test.ts | 7 +- .../src/monitor/thread-session-close.test.ts | 5 +- .../discord/src/voice/manager.e2e.test.ts | 5 +- .../feishu/src/monitor.webhook-e2e.test.ts | 6 +- .../feishu/src/reply-dispatcher.test.ts | 159 ++++---- .../file-transfer/src/shared/policy.test.ts | 23 +- .../firecrawl/src/firecrawl-client.test.ts | 81 ++-- extensions/github-copilot/index.test.ts | 35 +- extensions/github-copilot/models.test.ts | 3 +- extensions/google/transport-stream.test.ts | 21 +- .../src/google-auth.runtime.test.ts | 5 +- .../src/monitor.webhook-routing.test.ts | 5 +- extensions/huggingface/models.test.ts | 5 +- .../imessage/doctor-contract-api.test.ts | 6 +- extensions/inworld/tts.test.ts | 3 +- .../line/src/auto-reply-delivery.test.ts | 5 +- .../line/src/channel.sendPayload.test.ts | 5 +- extensions/line/src/markdown-to-line.test.ts | 102 ++++-- extensions/line/src/message-cards.test.ts | 18 +- extensions/line/src/rich-menu.test.ts | 14 +- extensions/line/src/send.test.ts | 6 +- extensions/llama-cpp/index.test.ts | 8 +- extensions/logbook/src/analyze.test.ts | 18 +- extensions/logbook/src/store.test.ts | 7 +- extensions/matrix/src/matrix/client.test.ts | 13 +- .../matrix/src/matrix/monitor/events.test.ts | 4 +- .../src/matrix/sdk/crypto-bootstrap.test.ts | 13 +- .../matrix/src/onboarding.test-harness.ts | 3 +- .../mattermost/src/mattermost/client.test.ts | 29 +- .../src/mattermost/monitor-websocket.test.ts | 9 +- .../memory-core/doctor-contract-api.test.ts | 13 +- .../memory-core/src/dreaming-phases.test.ts | 8 +- extensions/memory-core/src/dreaming.test.ts | 3 +- .../src/memory/manager-search.test.ts | 15 +- extensions/memory-core/src/memory/mmr.test.ts | 27 +- .../qmd-manager.slugified-paths.test.ts | 10 +- .../src/memory/qmd-manager.test.ts | 36 +- .../src/short-term-promotion.test.ts | 36 +- extensions/memory-lancedb/index.test.ts | 10 +- .../memory-wiki/doctor-contract-api.test.ts | 13 +- extensions/memory-wiki/src/cli.test.ts | 6 +- .../memory-wiki/src/prompt-section.test.ts | 9 +- .../src/wiki-notes-read-retry.test.ts | 7 +- .../src/attachments/bot-framework.test.ts | 13 +- .../msteams/src/file-consent-helpers.test.ts | 29 +- .../src/monitor-handler/inbound-media.test.ts | 3 +- extensions/nextcloud-talk/src/setup.test.ts | 6 +- extensions/nostr/doctor-contract-api.test.ts | 11 +- .../nostr/src/nostr-bus.integration.test.ts | 71 +++- .../nostr/src/nostr-profile-http.test.ts | 3 +- .../oc-path/src/oc-path/tests/find.test.ts | 42 ++- .../scenarios/roundtrip-property.test.ts | 6 +- .../src/oc-path/tests/universal.test.ts | 15 +- extensions/ollama/index.test.ts | 3 +- extensions/ollama/ollama.live.test.ts | 6 +- .../ollama/src/discovery-shared.test.ts | 8 +- extensions/ollama/src/provider-models.test.ts | 8 +- extensions/ollama/src/stream-runtime.test.ts | 119 ++++-- extensions/ollama/src/stream.test.ts | 7 +- .../ollama/src/wsl2-crash-loop-check.test.ts | 5 +- extensions/openai/index.test.ts | 13 +- extensions/openshell/src/backend.e2e.test.ts | 8 +- .../openshell/src/openshell-core.test.ts | 7 +- .../parallel-free-web-search-provider.test.ts | 16 +- .../src/parallel-mcp-search.runtime.test.ts | 23 +- .../src/parallel-web-search-provider.test.ts | 3 +- .../phone-control/doctor-contract-api.test.ts | 3 +- extensions/qa-lab/src/ci-smoke-plan.test.ts | 7 +- extensions/qa-lab/src/cli.test.ts | 8 +- extensions/qa-lab/src/coverage-report.test.ts | 9 +- .../qa-lab/src/evidence-gallery.test.ts | 45 ++- .../qa-lab/src/evidence-summary.test.ts | 5 +- .../telegram/telegram-live.runtime.test.ts | 10 +- .../whatsapp/whatsapp-live.runtime.test.ts | 69 ++-- .../qa-lab/src/mantis/run.runtime.test.ts | 17 +- .../src/mantis/visual-task.runtime.test.ts | 29 +- .../src/runners/contract/scenarios.test.ts | 5 +- .../src/engine/engine-import-boundary.test.ts | 3 +- .../src/engine/gateway/active-cfg.test.ts | 3 +- .../engine/gateway/gateway-connection.test.ts | 13 +- extensions/qqbot/src/engine/utils/stt.test.ts | 5 +- extensions/qqbot/src/state-migrations.test.ts | 17 +- extensions/searxng/src/searxng-client.test.ts | 7 +- extensions/signal/src/format.chunking.test.ts | 19 +- .../event-handler.mention-gating.test.ts | 14 +- .../slack/src/channel-migration.test.ts | 9 +- extensions/slack/src/monitor/media.test.ts | 11 +- .../monitor/message-handler/prepare.test.ts | 3 +- extensions/slack/src/send.reconcile.test.ts | 6 +- extensions/sms/src/inbound.test.ts | 3 +- .../synology-chat/src/webhook-handler.test.ts | 5 +- .../src/bot-message-context.typing.test.ts | 17 +- .../telegram/src/bot-message-dispatch.test.ts | 345 ++++++++++++++---- extensions/telegram/src/bot-message.test.ts | 17 +- .../src/bot-native-command-menu.test.ts | 3 +- .../telegram/src/bot-native-commands.test.ts | 4 +- .../src/bot.create-telegram-bot.test.ts | 3 +- extensions/telegram/src/fetch.test.ts | 13 +- .../telegram/src/group-migration.test.ts | 9 +- .../telegram/src/outbound-adapter.test.ts | 13 +- .../telegram/src/polling-session.test.ts | 7 +- .../telegram-ingress-worker.runtime.test.ts | 12 +- extensions/thread-ownership/index.test.ts | 59 +-- .../src/outbound-tool-trace-sanitize.test.ts | 9 +- extensions/twitch/src/twitch-client.test.ts | 9 +- extensions/venice/models.test.ts | 3 +- .../voice-call/doctor-contract-api.test.ts | 15 +- .../src/webhook/realtime-handler.test.ts | 5 +- .../monitor/process-message.test.ts | 12 +- .../workboard/doctor-contract-api.test.ts | 23 +- extensions/workboard/src/command.test.ts | 3 +- extensions/workboard/src/tools.test.ts | 6 +- .../workspaces/src/default-workspace.test.ts | 13 +- .../src/monitor.polling.media-reply.test.ts | 3 +- test/tsconfig/tsconfig.extensions.test.json | 1 + 158 files changed, 2110 insertions(+), 1074 deletions(-) diff --git a/extensions/acpx/doctor-contract-api.test.ts b/extensions/acpx/doctor-contract-api.test.ts index 0c4a92d8f0e4..74d170bef6c6 100644 --- a/extensions/acpx/doctor-contract-api.test.ts +++ b/extensions/acpx/doctor-contract-api.test.ts @@ -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([ diff --git a/extensions/acpx/src/runtime-internals/mcp-proxy.test.ts b/extensions/acpx/src/runtime-internals/mcp-proxy.test.ts index 2c10ee281e2f..353ccdc747b1 100644 --- a/extensions/acpx/src/runtime-internals/mcp-proxy.test.ts +++ b/extensions/acpx/src/runtime-internals/mcp-proxy.test.ts @@ -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 }); - 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 () => { diff --git a/extensions/active-memory/doctor-contract-api.test.ts b/extensions/active-memory/doctor-contract-api.test.ts index 4965d5041e2e..50509040bd54 100644 --- a/extensions/active-memory/doctor-contract-api.test.ts +++ b/extensions/active-memory/doctor-contract-api.test.ts @@ -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: {}, diff --git a/extensions/active-memory/index.test.ts b/extensions/active-memory/index.test.ts index d33e0eda0ae3..efaa13ca0b1d 100644 --- a/extensions/active-memory/index.test.ts +++ b/extensions/active-memory/index.test.ts @@ -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 { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; import type { OpenKeyedStoreOptions } from "openclaw/plugin-sdk/plugin-state-runtime"; import { @@ -125,6 +126,8 @@ describe("active-memory plugin", () => { }); const hooks: Record = {}; + const requireHook = (name: string): Function => + expectDefined(hooks[name], `active-memory ${name} hook registration`); const hookOptions: Record | undefined> = {}; const registeredCommands: Record = {}; const runEmbeddedAgent = vi.fn(); @@ -403,7 +406,7 @@ describe("active-memory plugin", () => { return { payloads: [{ text: params.summary }] }; }); return requirePrependContext( - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: params.prompt, messages: [] }, { agentId: "main", @@ -611,7 +614,7 @@ describe("active-memory plugin", () => { }); it("runs recall without recording shared auth-profile failures", async () => { - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "main", @@ -625,7 +628,7 @@ describe("active-memory plugin", () => { }); it("runs recall on a dedicated active-memory lane", async () => { - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "main", @@ -651,7 +654,7 @@ describe("active-memory plugin", () => { return { payloads: [{ text: "- lemon pepper wings" }] }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "main", @@ -715,13 +718,13 @@ describe("active-memory plugin", () => { }; const event = { prompt: "what wings should i order?", messages: [] }; - await hooks.before_prompt_build(event, hookParams); + await requireHook("before_prompt_build")(event, hookParams); const firstSessionKey = requireNonEmptyString( lastRuntimeEmbeddedRunParams().sessionKey, "expected first runtime session key", ); testing.resetActiveRecallCacheForTests(); - await hooks.before_prompt_build(event, hookParams); + await requireHook("before_prompt_build")(event, hookParams); const secondSessionKey = requireNonEmptyString( lastRuntimeEmbeddedRunParams().sessionKey, "expected second runtime session key", @@ -735,7 +738,7 @@ describe("active-memory plugin", () => { new Error("session store is busy"), ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? retry cleanup", messages: [] }, { agentId: "main", @@ -758,7 +761,7 @@ describe("active-memory plugin", () => { new Error("session store remains busy"), ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? cleanup failure", messages: [] }, { agentId: "main", @@ -818,7 +821,7 @@ describe("active-memory plugin", () => { expect(statusResult.text).toBe("Active Memory: off for this session."); - const disabledResult = await hooks.before_prompt_build( + const disabledResult = await requireHook("before_prompt_build")( { prompt: "what wings should i order? active memory toggle", messages: [] }, { agentId: "main", @@ -845,7 +848,7 @@ describe("active-memory plugin", () => { expect(onResult.text).toContain("on for this session"); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? active memory toggle", messages: [] }, { agentId: "main", @@ -921,7 +924,7 @@ describe("active-memory plugin", () => { expect(statusOffResult.text).toBe("Active Memory: off globally."); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order while global active memory is off?", messages: [] }, { agentId: "main", @@ -957,7 +960,7 @@ describe("active-memory plugin", () => { expect(currentActiveMemoryConfig().enabled).toBe(true); expect(currentActiveMemoryConfig().agents).toEqual(["main"]); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order after global active memory is back on?", messages: [] }, { agentId: "main", @@ -1110,7 +1113,7 @@ describe("active-memory plugin", () => { }, }; - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order after a live config disable?", messages: [] }, { agentId: "main", @@ -1131,7 +1134,7 @@ describe("active-memory plugin", () => { }, }; - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order after active memory is removed?", messages: [] }, { agentId: "main", @@ -1146,7 +1149,7 @@ describe("active-memory plugin", () => { }); it("does not run for agents that are not explicitly targeted", async () => { - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "support", @@ -1184,7 +1187,7 @@ describe("active-memory plugin", () => { } const openKeyedStore = vi.spyOn(api.runtime.state, "openKeyedStore"); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "continue this native Codex task", messages: [] }, { agentId: "main", @@ -1202,7 +1205,7 @@ describe("active-memory plugin", () => { ); it("does not rewrite session state for skipped turns with no active-memory entry to clear", async () => { - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "support", @@ -1217,7 +1220,7 @@ describe("active-memory plugin", () => { }); it("does not run for non-interactive contexts", async () => { - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "main", @@ -1232,7 +1235,7 @@ describe("active-memory plugin", () => { }); it("does not run for dreaming-narrative cron session keys", async () => { - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "main", @@ -1252,7 +1255,7 @@ describe("active-memory plugin", () => { updatedAt: 1, }; - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "main", @@ -1267,7 +1270,7 @@ describe("active-memory plugin", () => { }); it("allows non-canonical session keys that merely contain the dreaming-narrative substring", async () => { - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "main", @@ -1286,7 +1289,7 @@ describe("active-memory plugin", () => { }); it("allows real webchat session keys whose peer id starts with a phased dreaming-narrative prefix", async () => { - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "main", @@ -1305,7 +1308,7 @@ describe("active-memory plugin", () => { }); it("defaults to direct-style sessions only", async () => { - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should we order?", messages: [] }, { agentId: "main", @@ -1321,7 +1324,7 @@ describe("active-memory plugin", () => { }); it("treats non-webchat main sessions as direct chats under the default dmScope", async () => { - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "main", @@ -1351,7 +1354,7 @@ describe("active-memory plugin", () => { session: { mainKey: "home" }, }; - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "main", @@ -1370,7 +1373,7 @@ describe("active-memory plugin", () => { }); it("treats topic-threaded Telegram main session keys as direct chats", async () => { - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "main", @@ -1389,7 +1392,7 @@ describe("active-memory plugin", () => { }); it("does not treat unknown topic-threaded session keys as direct chats", async () => { - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "main", @@ -1411,7 +1414,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should we order?", messages: [] }, { agentId: "main", @@ -1436,7 +1439,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should we order?", messages: [] }, { agentId: "main", @@ -1459,7 +1462,7 @@ describe("active-memory plugin", () => { }); it("uses messageProvider not raw Telegram direct channelId for embedded recall (#82177)", async () => { - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { agentId: "main", @@ -1486,7 +1489,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what did we decide?", messages: [] }, { agentId: "main", @@ -1512,7 +1515,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what should i work on next?", messages: [] }, { agentId: "main", @@ -1534,7 +1537,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what should i work on next?", messages: [] }, { agentId: "main", @@ -1557,7 +1560,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "hi", messages: [] }, { agentId: "main", @@ -1580,7 +1583,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "hi", messages: [] }, { agentId: "main", @@ -1606,7 +1609,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "hi", messages: [] }, { agentId: "main", @@ -1629,7 +1632,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "hi", messages: [] }, { agentId: "main", @@ -1654,7 +1657,7 @@ describe("active-memory plugin", () => { // The default main session key (agent:main:main) exposes no chat id; the // allowlist must not accidentally match it. - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "hi", messages: [] }, { agentId: "main", @@ -1681,7 +1684,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "hi", messages: [] }, { agentId: "main", @@ -1708,7 +1711,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "hi", messages: [] }, { agentId: "main", @@ -1732,7 +1735,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "hi", messages: [] }, { agentId: "main", @@ -1757,7 +1760,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "hi", messages: [] }, { agentId: "main", @@ -1784,7 +1787,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "hi", messages: [] }, { agentId: "main", @@ -1809,7 +1812,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "hi", messages: [] }, { agentId: "main", @@ -1825,7 +1828,7 @@ describe("active-memory plugin", () => { }); it("injects system context on a successful recall hit", async () => { - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [ @@ -1880,7 +1883,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? inherit-qmd-mode-check", messages: [], @@ -1904,7 +1907,7 @@ describe("active-memory plugin", () => { }); it("frames the blocking memory subagent as a memory search agent for another model", async () => { - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "What is my favorite food? strict-style-check", messages: [], @@ -1969,7 +1972,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "What did we decide about active memory?", messages: [], @@ -1994,7 +1997,7 @@ describe("active-memory plugin", () => { it("uses memory_recall by default when the memory slot selects LanceDB", async () => { setMemorySlot("memory-lancedb"); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "What did we decide about active memory?", messages: [], @@ -2019,7 +2022,7 @@ describe("active-memory plugin", () => { toolsAllow: ["lcm_grep"], }; - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "What did we decide about active memory?", messages: [], @@ -2075,7 +2078,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "What did we decide about active memory?", messages: [], @@ -2100,7 +2103,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "What did we decide about active memory?", messages: [], @@ -2125,7 +2128,7 @@ describe("active-memory plugin", () => { toolsAllow: ["*", "group:plugins", "read", "exec", "message", "web_search"], }; - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "What did we decide about active memory?", messages: [], @@ -2150,7 +2153,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "What is my favorite food? preference-style-check", messages: [], @@ -2178,7 +2181,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "What is my favorite food?", messages: [], @@ -2199,7 +2202,7 @@ describe("active-memory plugin", () => { }); it("keeps thinking off by default but allows an explicit thinking override", async () => { - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "What is my favorite food? default-thinking-check", messages: [], @@ -2221,7 +2224,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "What is my favorite food? thinking-override-check", messages: [], @@ -2245,7 +2248,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "What is my favorite food? prompt-append-check", messages: [], @@ -2274,7 +2277,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "What is my favorite food? prompt-override-check", messages: [], @@ -2304,7 +2307,7 @@ describe("active-memory plugin", () => { }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what should i remember from my 2024 trip and should i buy 2% milk?", messages: [], @@ -2326,7 +2329,7 @@ describe("active-memory plugin", () => { }); it("preserves canonical parent session scope in the blocking memory subagent session key", async () => { - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what should i grab on the way?", messages: [] }, { agentId: "main", @@ -2348,7 +2351,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? temp transcript", messages: [] }, { agentId: "main", @@ -2395,7 +2398,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? bare model default", messages: [] }, { agentId: "main", @@ -2417,7 +2420,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? no fallback", messages: [] }, { agentId: "main", @@ -2440,7 +2443,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? custom fallback", messages: [] }, { agentId: "main", @@ -2479,7 +2482,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? built-in fallback", messages: [] }, { agentId: "main", @@ -2516,7 +2519,7 @@ describe("active-memory plugin", () => { }; }); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? debug telemetry", messages: [], @@ -2572,7 +2575,7 @@ describe("active-memory plugin", () => { }, ); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "debug transcript bug", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -2977,7 +2980,7 @@ describe("active-memory plugin", () => { payloads: [{ text: "NONE" }], }); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what's up with you?", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3019,7 +3022,7 @@ describe("active-memory plugin", () => { payloads: [{ text: "NONE" }], }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "fair, okay gonna do them by throwing them in the garbage", messages: [] }, { agentId: "main", @@ -3042,7 +3045,7 @@ describe("active-memory plugin", () => { expect(testing.isMissingRegisteredMemoryToolsError(error)).toBe(true); runEmbeddedAgent.mockRejectedValueOnce(error); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? missing memory tools", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3068,7 +3071,7 @@ describe("active-memory plugin", () => { expect(testing.isMissingRegisteredMemoryToolsError(error)).toBe(true); runEmbeddedAgent.mockRejectedValueOnce(error); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? missing memory tools with policy", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3101,7 +3104,7 @@ describe("active-memory plugin", () => { expect(testing.isMissingRegisteredMemoryToolsError(error, toolsAllow)).toBe(true); runEmbeddedAgent.mockRejectedValueOnce(error); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what did we decide? missing custom memory tools", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3126,7 +3129,7 @@ describe("active-memory plugin", () => { expect(testing.isMissingRegisteredMemoryToolsError(error)).toBe(true); runEmbeddedAgent.mockRejectedValueOnce(error); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? memory tools filtered by policy", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3154,7 +3157,7 @@ describe("active-memory plugin", () => { expect(testing.isMissingRegisteredMemoryToolsError(error)).toBe(false); runEmbeddedAgent.mockRejectedValueOnce(error); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: `what wings should i order? ${reason}`, messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3182,7 +3185,7 @@ describe("active-memory plugin", () => { throw makeMemoryToolAllowlistError("no registered tools matched"); }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? missing memory tools after abort", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3235,7 +3238,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? timeout partial", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3285,7 +3288,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? timeout partial temp", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3346,7 +3349,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? timeout partial sqlite", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3390,7 +3393,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? empty timeout transcript", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3421,7 +3424,7 @@ describe("active-memory plugin", () => { async (params: { abortSignal?: AbortSignal }) => await waitForAbort(params.abortSignal), ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? missing timeout transcript", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3462,7 +3465,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? timeout boilerplate", messages: [] }, { agentId: "main", @@ -3512,7 +3515,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? abort partial", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3549,7 +3552,7 @@ describe("active-memory plugin", () => { throw new Error("synthetic failure"); }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? generic error", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -3754,7 +3757,7 @@ describe("active-memory plugin", () => { payloads: [{ text: "NONE" }], }); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? empty cache", messages: [] }, { agentId: "main", @@ -3763,7 +3766,7 @@ describe("active-memory plugin", () => { messageProvider: "webchat", }, ); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? empty cache", messages: [] }, { agentId: "main", @@ -3826,7 +3829,7 @@ describe("active-memory plugin", () => { }); }); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? timeout test", messages: [] }, { agentId: "main", @@ -3835,7 +3838,7 @@ describe("active-memory plugin", () => { messageProvider: "webchat", }, ); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? timeout test", messages: [] }, { agentId: "main", @@ -3864,7 +3867,7 @@ describe("active-memory plugin", () => { plugin.register(api as unknown as OpenClawPluginApi); runEmbeddedAgent.mockImplementationOnce(() => new Promise(() => {})); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? cleanup timeout", messages: [] }, { agentId: "main", @@ -3902,7 +3905,7 @@ describe("active-memory plugin", () => { }), ); - const resultPromise = hooks.before_prompt_build( + const resultPromise = requireHook("before_prompt_build")( { prompt: "what wings should i order? slow timeout persistence", messages: [] }, { agentId: "main", @@ -3938,7 +3941,7 @@ describe("active-memory plugin", () => { }); }); - const resultPromise = hooks.before_prompt_build( + const resultPromise = requireHook("before_prompt_build")( { prompt: "what wings should i order? slow successful persistence", messages: [] }, { agentId: "main", @@ -3961,7 +3964,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? session id cache", messages: [] }, { agentId: "main", @@ -3970,7 +3973,7 @@ describe("active-memory plugin", () => { messageProvider: "webchat", }, ); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? session id cache", messages: [] }, { agentId: "main", @@ -4010,7 +4013,7 @@ describe("active-memory plugin", () => { }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? late payload timeout", messages: [] }, { agentId: "main", @@ -4053,7 +4056,7 @@ describe("active-memory plugin", () => { return { payloads: [{ text: "remember the ramen place" }] }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? setup grace", messages: [] }, { agentId: "main", @@ -4086,7 +4089,7 @@ describe("active-memory plugin", () => { runEmbeddedAgent.mockImplementationOnce(() => new Promise(() => {})); const startedAt = Date.now(); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? hard deadline test", messages: [] }, { agentId: "main", @@ -4135,7 +4138,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? zero hit", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -4185,7 +4188,7 @@ describe("active-memory plugin", () => { return { payloads: [{ text: "User usually orders ramen." }] }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? zero hit with results", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -4278,7 +4281,7 @@ describe("active-memory plugin", () => { }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? unavailable then summary", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -4344,7 +4347,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? grounded timeout", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -4401,7 +4404,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? late unavailable", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -4471,7 +4474,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? unsettled timeout", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -4536,7 +4539,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? custom timeout failure", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -4604,7 +4607,7 @@ describe("active-memory plugin", () => { return { payloads: [{ text: "User usually orders ramen." }] }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? custom evidence", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -4633,7 +4636,7 @@ describe("active-memory plugin", () => { return { payloads: [{ text: "User usually orders ramen." }] }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? case insensitive", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -4683,7 +4686,7 @@ describe("active-memory plugin", () => { return { payloads: [{ text: "User usually orders ramen." }] }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? custom retry", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -4732,7 +4735,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? harness evidence", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -4779,7 +4782,7 @@ describe("active-memory plugin", () => { return { payloads: [{ text: "This ungrounded summary must not become recalled context." }] }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? custom content failure", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -4806,7 +4809,7 @@ describe("active-memory plugin", () => { }); plugin.register(api as unknown as OpenClawPluginApi); - const resultPromise = hooks.before_prompt_build( + const resultPromise = requireHook("before_prompt_build")( { prompt: "what food do i usually order? stalled toggle lookup", messages: [] }, { agentId: "main", @@ -4847,7 +4850,7 @@ describe("active-memory plugin", () => { plugin.register(api as unknown as OpenClawPluginApi); runEmbeddedAgent.mockImplementationOnce(() => new Promise(() => {})); - const resultPromise = hooks.before_prompt_build( + const resultPromise = requireHook("before_prompt_build")( { prompt: "what food do i usually order? delayed recall start", messages: [] }, { agentId: "main", @@ -4910,7 +4913,7 @@ describe("active-memory plugin", () => { return { payloads: [{ text: "This ungrounded summary must not become recalled context." }] }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? empty search", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -4955,7 +4958,7 @@ describe("active-memory plugin", () => { return { payloads: [{ text: "User usually orders tonkotsu ramen." }] }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? unavailable diagnostic", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -5016,7 +5019,7 @@ describe("active-memory plugin", () => { }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? rotated transcript", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -5059,7 +5062,7 @@ describe("active-memory plugin", () => { }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? rotated unavailable", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -5124,7 +5127,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? rotated sqlite unavailable", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -5166,7 +5169,7 @@ describe("active-memory plugin", () => { }, ); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? unavailable", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -5214,7 +5217,7 @@ describe("active-memory plugin", () => { return { payloads: [{ text: "User usually orders ramen after late flights." }] }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what food do i usually order? memory get miss", messages: [] }, { agentId: "main", @@ -5229,7 +5232,7 @@ describe("active-memory plugin", () => { }); it("returns undefined instead of throwing when an unexpected error escapes prompt building", async () => { - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what should i eat? escape test", messages: undefined as never }, { agentId: "main", @@ -5254,7 +5257,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? high timeout", messages: [] }, { agentId: "main", @@ -5276,7 +5279,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? capped timeout", messages: [] }, { agentId: "main", @@ -5297,7 +5300,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? log sanitization", messages: [] }, { agentId: "main", @@ -5333,7 +5336,7 @@ describe("active-memory plugin", () => { const sessionPrefix = `agent:main:${"x".repeat(288)}`; const hugeSession = `${sessionPrefix}😀tail`; - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? long log value", messages: [] }, { agentId: "main", @@ -5360,7 +5363,7 @@ describe("active-memory plugin", () => { channel: "telegram", }; - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? session id only", messages: [] }, { agentId: "main", @@ -5388,7 +5391,7 @@ describe("active-memory plugin", () => { updatedAt: 25, }; - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order? session id only telegram", messages: [] }, { agentId: "main", @@ -5429,7 +5432,7 @@ describe("active-memory plugin", () => { }; }); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what should i eat tonight?", messages: [] }, { agentId: "main", @@ -5460,7 +5463,7 @@ describe("active-memory plugin", () => { channel: "telegram", }; - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? wrapper channel hint", messages: [] }, { agentId: "main", @@ -5484,7 +5487,7 @@ describe("active-memory plugin", () => { }, }; - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? scoped stored channel", messages: [] }, { agentId: "main", @@ -5507,7 +5510,7 @@ describe("active-memory plugin", () => { }, }; - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? explicit channel hint", messages: [] }, { agentId: "main", @@ -5530,7 +5533,7 @@ describe("active-memory plugin", () => { }, }; - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? direct explicit channel", messages: [] }, { agentId: "main", @@ -5557,7 +5560,7 @@ describe("active-memory plugin", () => { ], }; - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "what wings should i order?", messages: [] }, { trigger: "heartbeat", sessionKey, messageProvider: "webchat" }, ); @@ -5587,7 +5590,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what should i grab on the way?", messages: [ @@ -5620,7 +5623,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what now?", messages: [ @@ -5659,7 +5662,7 @@ describe("active-memory plugin", () => { plugin.register(api as unknown as OpenClawPluginApi); const prefix = "a".repeat(479); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: `${prefix}😀tail`, messages: [], @@ -5683,7 +5686,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: [ "Conversation info:", @@ -5726,7 +5729,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what should i grab on the way?", messages: [ @@ -5757,7 +5760,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what should i grab on the way?", messages: [ @@ -5811,7 +5814,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what should i grab on the way?", messages: [ @@ -5853,7 +5856,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what should i grab on the way?", messages: [ @@ -5886,7 +5889,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what should i remember?", messages: [ @@ -5926,7 +5929,7 @@ describe("active-memory plugin", () => { }; }); - const result = await hooks.before_prompt_build( + const result = await requireHook("before_prompt_build")( { prompt: "u remember my flight preferences", messages: [] }, { agentId: "main", @@ -5987,7 +5990,7 @@ describe("active-memory plugin", () => { }); it("asks recall subagents to mark mutable operational facts stale unless source status is current", async () => { - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "is autonomous pickup running?", messages: [] }, { agentId: "main", @@ -6010,7 +6013,7 @@ describe("active-memory plugin", () => { }; plugin.register(api as unknown as OpenClawPluginApi); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? prompt-count-check", messages: [] }, { agentId: "main", @@ -6029,7 +6032,7 @@ describe("active-memory plugin", () => { const mkdtempSpy = vi.spyOn(fs, "mkdtemp"); const rmSpy = vi.spyOn(fs, "rm"); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? temp transcript path", messages: [] }, { agentId: "main", @@ -6059,7 +6062,7 @@ describe("active-memory plugin", () => { const rmSpy = vi.spyOn(fs, "rm").mockResolvedValue(undefined); const sessionKey = "agent:main:persist-transcript"; - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? persist transcript", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -6096,7 +6099,7 @@ describe("active-memory plugin", () => { plugin.register(api as unknown as OpenClawPluginApi); const mkdirSpy = vi.spyOn(fs, "mkdir").mockResolvedValue(undefined); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? unsafe transcript dir", messages: [] }, { agentId: "main", @@ -6135,7 +6138,7 @@ describe("active-memory plugin", () => { plugin.register(api as unknown as OpenClawPluginApi); const mkdirSpy = vi.spyOn(fs, "mkdir").mockResolvedValue(undefined); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what wings should i order? support agent transcript", messages: [] }, { agentId: "support/agent", @@ -6174,7 +6177,7 @@ describe("active-memory plugin", () => { payloads: [{ text: "- spicy ramen\u001b[31m\n- fries\r\n- blue cheese\t" }], }); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "what should i order?", messages: [] }, { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, ); @@ -6294,7 +6297,7 @@ describe("active-memory plugin", () => { ); // First two calls should actually attempt the subagent (and timeout). - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "circuit breaker test 1", messages: [] }, { agentId: "main", @@ -6303,7 +6306,7 @@ describe("active-memory plugin", () => { messageProvider: "webchat", }, ); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "circuit breaker test 2", messages: [] }, { agentId: "main", @@ -6315,7 +6318,7 @@ describe("active-memory plugin", () => { expect(runEmbeddedAgent).toHaveBeenCalledTimes(2); // Third call should be skipped by the circuit breaker. - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "circuit breaker test 3", messages: [] }, { agentId: "main", @@ -6350,7 +6353,7 @@ describe("active-memory plugin", () => { runEmbeddedAgent.mockImplementationOnce( async (params: { abortSignal?: AbortSignal }) => await waitForAbort(params.abortSignal), ); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "cb reset test timeout", messages: [] }, { agentId: "main", @@ -6362,7 +6365,7 @@ describe("active-memory plugin", () => { expect(runEmbeddedAgent).toHaveBeenCalledTimes(1); // Second call should be skipped by circuit breaker. - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "cb reset test skipped", messages: [] }, { agentId: "main", @@ -6384,7 +6387,7 @@ describe("active-memory plugin", () => { runEmbeddedAgent.mockImplementationOnce(async () => ({ payloads: [{ text: "- lemon pepper wings" }], })); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "cb reset test success", messages: [] }, { agentId: "main", @@ -6399,7 +6402,7 @@ describe("active-memory plugin", () => { runEmbeddedAgent.mockImplementationOnce(async () => ({ payloads: [{ text: "- buffalo wings" }], })); - await hooks.before_prompt_build( + await requireHook("before_prompt_build")( { prompt: "cb reset test still ok", messages: [] }, { agentId: "main", diff --git a/extensions/amazon-bedrock/index.test.ts b/extensions/amazon-bedrock/index.test.ts index f6e5bca57ff8..6c8932db0f81 100644 --- a/extensions/amazon-bedrock/index.test.ts +++ b/extensions/amazon-bedrock/index.test.ts @@ -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>; }>; - 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>; }>; - 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>; }>; // 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 () => { diff --git a/extensions/anthropic/stream-wrappers.test.ts b/extensions/anthropic/stream-wrappers.test.ts index 4141735aec3a..199415804186 100644 --- a/extensions/anthropic/stream-wrappers.test.ts +++ b/extensions/anthropic/stream-wrappers.test.ts @@ -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", }); diff --git a/extensions/browser/src/browser-tool.schema.test.ts b/extensions/browser/src/browser-tool.schema.test.ts index 5950dc24856d..3f82240aec21 100644 --- a/extensions/browser/src/browser-tool.schema.test.ts +++ b/extensions/browser/src/browser-tool.schema.test.ts @@ -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; +function requireSchemaProperty(properties: Record, 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(); }); }); diff --git a/extensions/browser/src/browser/cdp.helpers.fuzz.test.ts b/extensions/browser/src/browser/cdp.helpers.fuzz.test.ts index 8fd0da53c5aa..fee662408c6b 100644 --- a/extensions/browser/src/browser/cdp.helpers.fuzz.test.ts +++ b/extensions/browser/src/browser/cdp.helpers.fuzz.test.ts @@ -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(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 { diff --git a/extensions/browser/src/browser/profiles-service.test.ts b/extensions/browser/src/browser/profiles-service.test.ts index 2cc0a30d39c2..32a44ae11d44 100644 --- a/extensions/browser/src/browser/profiles-service.test.ts +++ b/extensions/browser/src/browser/profiles-service.test.ts @@ -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); diff --git a/extensions/browser/src/browser/profiles.test.ts b/extensions/browser/src/browser/profiles.test.ts index 412a6cec2478..5be11192d584 100644 --- a/extensions/browser/src/browser/profiles.test.ts +++ b/extensions/browser/src/browser/profiles.test.ts @@ -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(), expected: PROFILE_COLORS[0] }, + { name: "none used", used: new Set(), 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) { diff --git a/extensions/browser/src/browser/pw-session.test.ts b/extensions/browser/src/browser/pw-session.test.ts index e24c1e6436ef..a4454dc1590c 100644 --- a/extensions/browser/src/browser/pw-session.test.ts +++ b/extensions/browser/src/browser/pw-session.test.ts @@ -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"); }); }); diff --git a/extensions/browser/src/browser/pw-tools-core.browser-ssrf-guard.test.ts b/extensions/browser/src/browser/pw-tools-core.browser-ssrf-guard.test.ts index 65fcfc32ca38..77de5c95368a 100644 --- a/extensions/browser/src/browser/pw-tools-core.browser-ssrf-guard.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.browser-ssrf-guard.test.ts @@ -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 | null, locator: null as Record | 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", + ), + ); }); }); diff --git a/extensions/browser/src/browser/pw-tools-core.interactions.navigation-guard.test.ts b/extensions/browser/src/browser/pw-tools-core.interactions.navigation-guard.test.ts index ae55efca484c..73be07673082 100644 --- a/extensions/browser/src/browser/pw-tools-core.interactions.navigation-guard.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.interactions.navigation-guard.test.ts @@ -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 () => { diff --git a/extensions/browser/src/browser/screenshot-annotate.test.ts b/extensions/browser/src/browser/screenshot-annotate.test.ts index bce33211cde3..8bf340894ab6 100644 --- a/extensions/browser/src/browser/screenshot-annotate.test.ts +++ b/extensions/browser/src/browser/screenshot-annotate.test.ts @@ -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); }); diff --git a/extensions/browser/src/browser/server-context.existing-session.test.ts b/extensions/browser/src/browser/server-context.existing-session.test.ts index 1b3dd809a979..38cb41998e56 100644 --- a/extensions/browser/src/browser/server-context.existing-session.test.ts +++ b/extensions/browser/src/browser/server-context.existing-session.test.ts @@ -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 }); diff --git a/extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts index fbaccafd194f..cb7826862e97 100644 --- a/extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts +++ b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts @@ -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", ); diff --git a/extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts index 97c9c3069476..cf7cdb49c39d 100644 --- a/extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts +++ b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts @@ -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, }; diff --git a/extensions/browser/src/browser/server.agent-contract-core.test.ts b/extensions/browser/src/browser/server.agent-contract-core.test.ts index 4782b4720b16..d444191c7042 100644 --- a/extensions/browser/src/browser/server.agent-contract-core.test.ts +++ b/extensions/browser/src/browser/server.agent-contract-core.test.ts @@ -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(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", diff --git a/extensions/browser/src/browser/server.agent-contract-form-layout-act-commands.test.ts b/extensions/browser/src/browser/server.agent-contract-form-layout-act-commands.test.ts index 3f1327a56b65..a30337882ecd 100644 --- a/extensions/browser/src/browser/server.agent-contract-form-layout-act-commands.test.ts +++ b/extensions/browser/src/browser/server.agent-contract-form-layout-act-commands.test.ts @@ -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(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", diff --git a/extensions/browser/src/browser/vision.test.ts b/extensions/browser/src/browser/vision.test.ts index cfb24039ec37..cd2f7bd9f666 100644 --- a/extensions/browser/src/browser/vision.test.ts +++ b/extensions/browser/src/browser/vision.test.ts @@ -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(); }); }); diff --git a/extensions/browser/src/gateway/browser-request.profile-from-body.test.ts b/extensions/browser/src/gateway/browser-request.profile-from-body.test.ts index 4d740d659507..cf57fdde21c5 100644 --- a/extensions/browser/src/gateway/browser-request.profile-from-body.test.ts +++ b/extensions/browser/src/gateway/browser-request.profile-from-body.test.ts @@ -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, diff --git a/extensions/browser/src/gateway/browser-request.shared-control-state.test.ts b/extensions/browser/src/gateway/browser-request.shared-control-state.test.ts index 66ebb5fa0ae6..a7b4975b79cb 100644 --- a/extensions/browser/src/gateway/browser-request.shared-control-state.test.ts +++ b/extensions/browser/src/gateway/browser-request.shared-control-state.test.ts @@ -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 { const respond = vi.fn(); - await browserHandlers["browser.request"]({ + await expectDefined( + browserHandlers["browser.request"], + "browser request handler", + )({ params: { method: "GET", path: "/", diff --git a/extensions/browser/src/gateway/browser-request.timeout.test.ts b/extensions/browser/src/gateway/browser-request.timeout.test.ts index 7cab3ad2a993..0860206f7f81 100644 --- a/extensions/browser/src/gateway/browser-request.timeout.test.ts +++ b/extensions/browser/src/gateway/browser-request.timeout.test.ts @@ -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", diff --git a/extensions/byteplus/index.test.ts b/extensions/byteplus/index.test.ts index e9544f235f10..22e6c1b733b1 100644 --- a/extensions/byteplus/index.test.ts +++ b/extensions/byteplus/index.test.ts @@ -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", () => { diff --git a/extensions/chutes/models.test.ts b/extensions/chutes/models.test.ts index 0266a2c9fbe9..272e2b4d17a1 100644 --- a/extensions/chutes/models.test.ts +++ b/extensions/chutes/models.test.ts @@ -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); diff --git a/extensions/clawrouter/provider-catalog.test.ts b/extensions/clawrouter/provider-catalog.test.ts index 044884fdcbe0..3e1d7b3df5f8 100644 --- a/extensions/clawrouter/provider-catalog.test.ts +++ b/extensions/clawrouter/provider-catalog.test.ts @@ -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, diff --git a/extensions/codex/src/app-server/dynamic-tool-build.test.ts b/extensions/codex/src/app-server/dynamic-tool-build.test.ts index 58c98908e470..d6dc292cf8cb 100644 --- a/extensions/codex/src/app-server/dynamic-tool-build.test.ts +++ b/extensions/codex/src/app-server/dynamic-tool-build.test.ts @@ -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 () => { diff --git a/extensions/copilot/doctor-contract-api.test.ts b/extensions/copilot/doctor-contract-api.test.ts index a82cfe875f9b..f08aefa0e065 100755 --- a/extensions/copilot/doctor-contract-api.test.ts +++ b/extensions/copilot/doctor-contract-api.test.ts @@ -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:"]); diff --git a/extensions/copilot/src/attempt.test.ts b/extensions/copilot/src/attempt.test.ts index bfe3dbd7cb0b..bab800282bc3 100644 --- a/extensions/copilot/src/attempt.test.ts +++ b/extensions/copilot/src/attempt.test.ts @@ -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; +function requireSession(sdk: FakeSdk): FakeSession { + return expectDefined(sdk.sessions[0], "first Copilot SDK session"); +} + +function requireCreateSessionConfig(sdk: FakeSdk): Record { + return expectDefined(sdk.createSession.mock.calls[0]?.[0], "Copilot createSession config"); +} + +function requireResumeSessionConfig(sdk: FakeSdk): Record { + return expectDefined(sdk.resumeSession.mock.calls[0]?.[1], "Copilot resumeSession config"); +} + function createDeferred() { let rejectPromise: ((reason?: unknown) => void) | undefined; let resolvePromise: ((value: T | PromiseLike) => 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; diff --git a/extensions/copilot/src/event-bridge.test.ts b/extensions/copilot/src/event-bridge.test.ts index 41ae15d72e5a..f7faa8fddde9 100644 --- a/extensions/copilot/src/event-bridge.test.ts +++ b/extensions/copilot/src/event-bridge.test.ts @@ -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(); diff --git a/extensions/copilot/src/tool-bridge.test.ts b/extensions/copilot/src/tool-bridge.test.ts index b23ca80cd0f4..be7e123dec68 100644 --- a/extensions/copilot/src/tool-bridge.test.ts +++ b/extensions/copilot/src/tool-bridge.test.ts @@ -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"), ); }); diff --git a/extensions/deepseek/provider-policy-api.test.ts b/extensions/deepseek/provider-policy-api.test.ts index e02d8adb37e8..bafcea05d7c5 100644 --- a/extensions/deepseek/provider-policy-api.test.ts +++ b/extensions/deepseek/provider-policy-api.test.ts @@ -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, diff --git a/extensions/device-pair/doctor-contract-api.test.ts b/extensions/device-pair/doctor-contract-api.test.ts index 651abf3d2fe3..33828c0f19a9 100644 --- a/extensions/device-pair/doctor-contract-api.test.ts +++ b/extensions/device-pair/doctor-contract-api.test.ts @@ -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({ diff --git a/extensions/diagnostics-prometheus/src/service.test.ts b/extensions/diagnostics-prometheus/src/service.test.ts index a453ac0ee089..4608f714919f 100644 --- a/extensions/diagnostics-prometheus/src/service.test.ts +++ b/extensions/diagnostics-prometheus/src/service.test.ts @@ -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", diff --git a/extensions/diffs/src/browser.test.ts b/extensions/diffs/src/browser.test.ts index 2b33dc54e9da..52b58a341fe9 100644 --- a/extensions/diffs/src/browser.test.ts +++ b/extensions/diffs/src/browser.test.ts @@ -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 - | undefined; + const page = expectDefined(pages[0], "diffs browser page"); + expect(page.pdf).toHaveBeenCalledTimes(1); + const pdfCall = firstMockCall(page.pdf, "PDF render")[0] as Record | 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"); }); diff --git a/extensions/diffs/src/viewer-client.test.ts b/extensions/diffs/src/viewer-client.test.ts index b57fe3977806..e520efd2591a 100644 --- a/extensions/diffs/src/viewer-client.test.ts +++ b/extensions/diffs/src/viewer-client.test.ts @@ -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 diff --git a/extensions/discord/src/actions/handle-action.test.ts b/extensions/discord/src/actions/handle-action.test.ts index b15f445da4ea..ec3e3a456c64 100644 --- a/extensions/discord/src/actions/handle-action.test.ts +++ b/extensions/discord/src/actions/handle-action.test.ts @@ -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", diff --git a/extensions/discord/src/actions/runtime.test.ts b/extensions/discord/src/actions/runtime.test.ts index 2ccaa31faa2b..36a037074bdb 100644 --- a/extensions/discord/src/actions/runtime.test.ts +++ b/extensions/discord/src/actions/runtime.test.ts @@ -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 () => { diff --git a/extensions/discord/src/chunk.test.ts b/extensions/discord/src/chunk.test.ts index 0e9c81d15434..111905369ff6 100644 --- a/extensions/discord/src/chunk.test.ts +++ b/extensions/discord/src/chunk.test.ts @@ -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"); }); diff --git a/extensions/discord/src/internal/gateway.test.ts b/extensions/discord/src/internal/gateway.test.ts index 11eb5b847778..85e253b247c8 100644 --- a/extensions/discord/src/internal/gateway.test.ts +++ b/extensions/discord/src/internal/gateway.test.ts @@ -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); diff --git a/extensions/discord/src/monitor/message-handler.queue.test.ts b/extensions/discord/src/monitor/message-handler.queue.test.ts index 991472b788df..ec057d9bbd11 100644 --- a/extensions/discord/src/monitor/message-handler.queue.test.ts +++ b/extensions/discord/src/monitor/message-handler.queue.test.ts @@ -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", + ), ); }); diff --git a/extensions/discord/src/monitor/presence.test.ts b/extensions/discord/src/monitor/presence.test.ts index 6c2598e8c290..4adbeb023ea2 100644 --- a/extensions/discord/src/monitor/presence.test.ts +++ b/extensions/discord/src/monitor/presence.test.ts @@ -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"); }); }); diff --git a/extensions/discord/src/monitor/reply-delivery.test.ts b/extensions/discord/src/monitor/reply-delivery.test.ts index 811c3d8ef5c7..bf57a88449fb 100644 --- a/extensions/discord/src/monitor/reply-delivery.test.ts +++ b/extensions/discord/src/monitor/reply-delivery.test.ts @@ -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", diff --git a/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts b/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts index 531257e15ec2..54da04ece927 100644 --- a/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts +++ b/extensions/discord/src/monitor/thread-bindings.lifecycle.test.ts @@ -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()); diff --git a/extensions/discord/src/monitor/thread-session-close.test.ts b/extensions/discord/src/monitor/thread-session-close.test.ts index c96784f7c5b9..e75d7ff6cb71 100644 --- a/extensions/discord/src/monitor/thread-session-close.test.ts +++ b/extensions/discord/src/monitor/thread-session-close.test.ts @@ -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 () => { diff --git a/extensions/discord/src/voice/manager.e2e.test.ts b/extensions/discord/src/voice/manager.e2e.test.ts index 97e879645aa7..666ebc1c3ee8 100644 --- a/extensions/discord/src/voice/manager.e2e.test.ts +++ b/extensions/discord/src/voice/manager.e2e.test.ts @@ -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" }); diff --git a/extensions/feishu/src/monitor.webhook-e2e.test.ts b/extensions/feishu/src/monitor.webhook-e2e.test.ts index ee71bce82841..6a298da6d8d8 100644 --- a/extensions/feishu/src/monitor.webhook-e2e.test.ts +++ b/extensions/feishu/src/monitor.webhook-e2e.test.ts @@ -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", diff --git a/extensions/feishu/src/reply-dispatcher.test.ts b/extensions/feishu/src/reply-dispatcher.test.ts index fb53b96c17ba..02da81f4a949 100644 --- a/extensions/feishu/src/reply-dispatcher.test.ts +++ b/extensions/feishu/src/reply-dispatcher.test.ts @@ -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, ): Record { - 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((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(); diff --git a/extensions/file-transfer/src/shared/policy.test.ts b/extensions/file-transfer/src/shared/policy.test.ts index 5a4e1df1148f..0b15561db394 100644 --- a/extensions/file-transfer/src/shared/policy.test.ts +++ b/extensions/file-transfer/src/shared/policy.test.ts @@ -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); }); }); diff --git a/extensions/firecrawl/src/firecrawl-client.test.ts b/extensions/firecrawl/src/firecrawl-client.test.ts index 5b8e9d4b5a16..19fd899d09d0 100644 --- a/extensions/firecrawl/src/firecrawl-client.test.ts +++ b/extensions/firecrawl/src/firecrawl-client.test.ts @@ -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(results: readonly T[], index: number): T { + return expectDefined(results[index], `firecrawl search result ${index}`); +} + beforeAll(async () => { firecrawlClient = ( await vi.importActual("./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"); }); }); diff --git a/extensions/github-copilot/index.test.ts b/extensions/github-copilot/index.test.ts index bd707940dcc3..829c90e42672 100644 --- a/extensions/github-copilot/index.test.ts +++ b/extensions/github-copilot/index.test.ts @@ -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(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( "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() }; diff --git a/extensions/github-copilot/models.test.ts b/extensions/github-copilot/models.test.ts index e26062ad07a8..427f68807179 100644 --- a/extensions/github-copilot/models.test.ts +++ b/extensions/github-copilot/models.test.ts @@ -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 () => { diff --git a/extensions/google/transport-stream.test.ts b/extensions/google/transport-stream.test.ts index c4d740a79495..816f7574f56a 100644 --- a/extensions/google/transport-stream.test.ts +++ b/extensions/google/transport-stream.test.ts @@ -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"'); diff --git a/extensions/googlechat/src/google-auth.runtime.test.ts b/extensions/googlechat/src/google-auth.runtime.test.ts index 48fb01d36e11..70174fcd7561 100644 --- a/extensions/googlechat/src/google-auth.runtime.test.ts +++ b/extensions/googlechat/src/google-auth.runtime.test.ts @@ -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({ pull(controller) { if (chunkIndex < chunks.length) { - controller.enqueue(chunks[chunkIndex++]); + controller.enqueue( + expectDefined(chunks[chunkIndex++], `Google auth chunk ${chunkIndex}`), + ); return; } controller.close(); diff --git a/extensions/googlechat/src/monitor.webhook-routing.test.ts b/extensions/googlechat/src/monitor.webhook-routing.test.ts index d7482fa509ce..e0b9d5506275 100644 --- a/extensions/googlechat/src/monitor.webhook-routing.test.ts +++ b/extensions/googlechat/src/monitor.webhook-routing.test.ts @@ -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() { diff --git a/extensions/huggingface/models.test.ts b/extensions/huggingface/models.test.ts index beb2e4ed4451..9a7c12523c2e 100644 --- a/extensions/huggingface/models.test.ts +++ b/extensions/huggingface/models.test.ts @@ -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 () => { diff --git a/extensions/imessage/doctor-contract-api.test.ts b/extensions/imessage/doctor-contract-api.test.ts index a23354b4662a..bfb90e42b6b0 100644 --- a/extensions/imessage/doctor-contract-api.test.ts +++ b/extensions/imessage/doctor-contract-api.test.ts @@ -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>).personal; + const personal = expectDefined( + (imessage.accounts as Record>).personal, + "personal iMessage account", + ); expect(personal.streaming).toEqual({ block: { coalesce: { idleMs: 250 } } }); expect(personal.blockStreamingCoalesce).toBeUndefined(); for (const change of [ diff --git a/extensions/inworld/tts.test.ts b/extensions/inworld/tts.test.ts index 28b3895895bf..87cd8704b1dc 100644 --- a/extensions/inworld/tts.test.ts +++ b/extensions/inworld/tts.test.ts @@ -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 () => { diff --git a/extensions/line/src/auto-reply-delivery.test.ts b/extensions/line/src/auto-reply-delivery.test.ts index ec386fed3ce9..1c987b6efbfe 100644 --- a/extensions/line/src/auto-reply-delivery.test.ts +++ b/extensions/line/src/auto-reply-delivery.test.ts @@ -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 () => { diff --git a/extensions/line/src/channel.sendPayload.test.ts b/extensions/line/src/channel.sendPayload.test.ts index 2500f004ea3e..435566e44dcc 100644 --- a/extensions/line/src/channel.sendPayload.test.ts +++ b/extensions/line/src/channel.sendPayload.test.ts @@ -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 () => { diff --git a/extensions/line/src/markdown-to-line.test.ts b/extensions/line/src/markdown-to-line.test.ts index eb757cabfc7b..b81b63290885 100644 --- a/extensions/line/src/markdown-to-line.test.ts +++ b/extensions/line/src/markdown-to-line.test.ts @@ -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(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])|(? { 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); diff --git a/extensions/line/src/rich-menu.test.ts b/extensions/line/src/rich-menu.test.ts index b132c009e71f..85ce06946899 100644 --- a/extensions/line/src/rich-menu.test.ts +++ b/extensions/line/src/rich-menu.test.ts @@ -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", + ]); }); }); diff --git a/extensions/line/src/send.test.ts b/extensions/line/src/send.test.ts index fff00336b80a..a6c5d1a27a19 100644 --- a/extensions/line/src/send.test.ts +++ b/extensions/line/src/send.test.ts @@ -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, + ); }); }); diff --git a/extensions/llama-cpp/index.test.ts b/extensions/llama-cpp/index.test.ts index 08c40011ba53..bcfa5692eac1 100644 --- a/extensions/llama-cpp/index.test.ts +++ b/extensions/llama-cpp/index.test.ts @@ -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, }); diff --git a/extensions/logbook/src/analyze.test.ts b/extensions/logbook/src/analyze.test.ts index 7d1b3d4d5000..8be51d77e71b 100644 --- a/extensions/logbook/src/analyze.test.ts +++ b/extensions/logbook/src/analyze.test.ts @@ -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({ diff --git a/extensions/logbook/src/store.test.ts b/extensions/logbook/src/store.test.ts index 4554e546cd2d..5dee634a6fc1 100644 --- a/extensions/logbook/src/store.test.ts +++ b/extensions/logbook/src/store.test.ts @@ -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", () => { diff --git a/extensions/matrix/src/matrix/client.test.ts b/extensions/matrix/src/matrix/client.test.ts index b9dc65b15a81..275e530a1d57 100644 --- a/extensions/matrix/src/matrix/client.test.ts +++ b/extensions/matrix/src/matrix/client.test.ts @@ -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"); }); diff --git a/extensions/matrix/src/matrix/monitor/events.test.ts b/extensions/matrix/src/matrix/monitor/events.test.ts index 5e218d38ef03..6eafc48bcc69 100644 --- a/extensions/matrix/src/matrix/monitor/events.test.ts +++ b/extensions/matrix/src/matrix/monitor/events.test.ts @@ -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: [ diff --git a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts index c62e6bdd8128..105852fa3a1e 100644 --- a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts +++ b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts @@ -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", + ), ); }); diff --git a/extensions/matrix/src/onboarding.test-harness.ts b/extensions/matrix/src/onboarding.test-harness.ts index 1dddeb0001ab..bdf45475a81c 100644 --- a/extensions/matrix/src/onboarding.test-harness.ts +++ b/extensions/matrix/src/onboarding.test-harness.ts @@ -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> | undefined, ): Promise => { if (values && message in values) { - return values[message]; + return expectDefined(values[message], `${kind} prompt value for ${message}`); } if (fallback) { return await fallback(message); diff --git a/extensions/mattermost/src/mattermost/client.test.ts b/extensions/mattermost/src/mattermost/client.test.ts index bb2cb6aea89f..0f96b7bb997f 100644 --- a/extensions/mattermost/src/mattermost/client.test.ts +++ b/extensions/mattermost/src/mattermost/client.test.ts @@ -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; } +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; @@ -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"); diff --git a/extensions/mattermost/src/mattermost/monitor-websocket.test.ts b/extensions/mattermost/src/mattermost/monitor-websocket.test.ts index 4afd2fb827de..a71a6eabbb3a 100644 --- a/extensions/mattermost/src/mattermost/monitor-websocket.test.ts +++ b/extensions/mattermost/src/mattermost/monitor-websocket.test.ts @@ -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, diff --git a/extensions/memory-core/doctor-contract-api.test.ts b/extensions/memory-core/doctor-contract-api.test.ts index 077d093218cf..1926880f0924 100644 --- a/extensions/memory-core/doctor-contract-api.test.ts +++ b/extensions/memory-core/doctor-contract-api.test.ts @@ -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(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([ diff --git a/extensions/memory-core/src/dreaming-phases.test.ts b/extensions/memory-core/src/dreaming-phases.test.ts index 6ba79ad46a53..15f1c1c3df68 100644 --- a/extensions/memory-core/src/dreaming-phases.test.ts +++ b/extensions/memory-core/src/dreaming-phases.test.ts @@ -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); }); diff --git a/extensions/memory-core/src/dreaming.test.ts b/extensions/memory-core/src/dreaming.test.ts index 2183e9488928..b0bcd1c23800 100644 --- a/extensions/memory-core/src/dreaming.test.ts +++ b/extensions/memory-core/src/dreaming.test.ts @@ -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 } : {}), diff --git a/extensions/memory-core/src/memory/manager-search.test.ts b/extensions/memory-core/src/memory/manager-search.test.ts index 25a7cf77455d..4ed74ebe3d56 100644 --- a/extensions/memory-core/src/memory/manager-search.test.ts +++ b/extensions/memory-core/src/memory/manager-search.test.ts @@ -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)); } diff --git a/extensions/memory-core/src/memory/mmr.test.ts b/extensions/memory-core/src/memory/mmr.test.ts index 7e6801d58ca9..9f5ba51d65fe 100644 --- a/extensions/memory-core/src/memory/mmr.test.ts +++ b/extensions/memory-core/src/memory/mmr.test.ts @@ -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(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", () => { diff --git a/extensions/memory-core/src/memory/qmd-manager.slugified-paths.test.ts b/extensions/memory-core/src/memory/qmd-manager.slugified-paths.test.ts index 44e4240664cc..1ed2c0122806 100644 --- a/extensions/memory-core/src/memory/qmd-manager.slugified-paths.test.ts +++ b/extensions/memory-core/src/memory/qmd-manager.slugified-paths.test.ts @@ -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, diff --git a/extensions/memory-core/src/memory/qmd-manager.test.ts b/extensions/memory-core/src/memory/qmd-manager.test.ts index 30deb8db2625..923fa258270c 100644 --- a/extensions/memory-core/src/memory/qmd-manager.test.ts +++ b/extensions/memory-core/src/memory/qmd-manager.test.ts @@ -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", diff --git a/extensions/memory-core/src/short-term-promotion.test.ts b/extensions/memory-core/src/short-term-promotion.test.ts index 6941b8b00a64..80e4421879e4 100644 --- a/extensions/memory-core/src/short-term-promotion.test.ts +++ b/extensions/memory-core/src/short-term-promotion.test.ts @@ -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); }); }); diff --git a/extensions/memory-lancedb/index.test.ts b/extensions/memory-lancedb/index.test.ts index c7b209e93ced..3a5c6d689683 100644 --- a/extensions/memory-lancedb/index.test.ts +++ b/extensions/memory-lancedb/index.test.ts @@ -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", diff --git a/extensions/memory-wiki/doctor-contract-api.test.ts b/extensions/memory-wiki/doctor-contract-api.test.ts index 08b07daf7734..2eaad2362cf9 100644 --- a/extensions/memory-wiki/doctor-contract-api.test.ts +++ b/extensions/memory-wiki/doctor-contract-api.test.ts @@ -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 { @@ -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: [], }); diff --git a/extensions/memory-wiki/src/cli.test.ts b/extensions/memory-wiki/src/cli.test.ts index 5b4228b35721..67d6ad570861 100644 --- a/extensions/memory-wiki/src/cli.test.ts +++ b/extensions/memory-wiki/src/cli.test.ts @@ -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 >; - 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({ diff --git a/extensions/memory-wiki/src/prompt-section.test.ts b/extensions/memory-wiki/src/prompt-section.test.ts index ea131ab9c98a..1d43dfdf0d33 100644 --- a/extensions/memory-wiki/src/prompt-section.test.ts +++ b/extensions/memory-wiki/src/prompt-section.test.ts @@ -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, ], }; diff --git a/extensions/memory-wiki/src/wiki-notes-read-retry.test.ts b/extensions/memory-wiki/src/wiki-notes-read-retry.test.ts index 38b32723162f..5920d90bc993 100644 --- a/extensions/memory-wiki/src/wiki-notes-read-retry.test.ts +++ b/extensions/memory-wiki/src/wiki-notes-read-retry.test.ts @@ -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"), + ), }; } diff --git a/extensions/msteams/src/attachments/bot-framework.test.ts b/extensions/msteams/src/attachments/bot-framework.test.ts index 5b8326210780..16df0361eabe 100644 --- a/extensions/msteams/src/attachments/bot-framework.test.ts +++ b/extensions/msteams/src/attachments/bot-framework.test.ts @@ -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(); diff --git a/extensions/msteams/src/file-consent-helpers.test.ts b/extensions/msteams/src/file-consent-helpers.test.ts index a10bbfb9564b..8aaf979ec64a 100644 --- a/extensions/msteams/src/file-consent-helpers.test.ts +++ b/extensions/msteams/src/file-consent-helpers.test.ts @@ -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); }); diff --git a/extensions/msteams/src/monitor-handler/inbound-media.test.ts b/extensions/msteams/src/monitor-handler/inbound-media.test.ts index 61d5b02e7058..78a9c01affb8 100644 --- a/extensions/msteams/src/monitor-handler/inbound-media.test.ts +++ b/extensions/msteams/src/monitor-handler/inbound-media.test.ts @@ -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 () => { diff --git a/extensions/nextcloud-talk/src/setup.test.ts b/extensions/nextcloud-talk/src/setup.test.ts index 669b72101526..9fa54c1bcc6c 100644 --- a/extensions/nextcloud-talk/src/setup.test.ts +++ b/extensions/nextcloud-talk/src/setup.test.ts @@ -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: { diff --git a/extensions/nostr/doctor-contract-api.test.ts b/extensions/nostr/doctor-contract-api.test.ts index 88cbebb7caa8..37510b21778f 100644 --- a/extensions/nostr/doctor-contract-api.test.ts +++ b/extensions/nostr/doctor-contract-api.test.ts @@ -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(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, diff --git a/extensions/nostr/src/nostr-bus.integration.test.ts b/extensions/nostr/src/nostr-bus.integration.test.ts index dc328cd86a8f..a4ef46bbe2c6 100644 --- a/extensions/nostr/src/nostr-bus.integration.test.ts +++ b/extensions/nostr/src/nostr-bus.integration.test.ts @@ -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(entries: Record, 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); }); }); diff --git a/extensions/nostr/src/nostr-profile-http.test.ts b/extensions/nostr/src/nostr-profile-http.test.ts index 3dd2811f3ca2..e4c0d43545e8 100644 --- a/extensions/nostr/src/nostr-profile-http.test.ts +++ b/extensions/nostr/src/nostr-profile-http.test.ts @@ -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(); diff --git a/extensions/oc-path/src/oc-path/tests/find.test.ts b/extensions/oc-path/src/oc-path/tests/find.test.ts index fb730070c140..76828facebc6 100644 --- a/extensions/oc-path/src/oc-path/tests/find.test.ts +++ b/extensions/oc-path/src/oc-path/tests/find.test.ts @@ -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(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", () => { diff --git a/extensions/oc-path/src/oc-path/tests/scenarios/roundtrip-property.test.ts b/extensions/oc-path/src/oc-path/tests/scenarios/roundtrip-property.test.ts index 31adc3ee4155..5b0077a6f8ac 100644 --- a/extensions/oc-path/src/oc-path/tests/scenarios/roundtrip-property.test.ts +++ b/extensions/oc-path/src/oc-path/tests/scenarios/roundtrip-property.test.ts @@ -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 = (arr: readonly T[]): T => arr[Math.floor(rand() * arr.length)]; + const choose = (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"]; diff --git a/extensions/oc-path/src/oc-path/tests/universal.test.ts b/extensions/oc-path/src/oc-path/tests/universal.test.ts index a24c35838037..2fc1ecf6251e 100644 --- a/extensions/oc-path/src/oc-path/tests/universal.test.ts +++ b/extensions/oc-path/src/oc-path/tests/universal.test.ts @@ -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[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[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[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, + }); } }); diff --git a/extensions/ollama/index.test.ts b/extensions/ollama/index.test.ts index 6eb744268f42..3a788e68f018 100644 --- a/extensions/ollama/index.test.ts +++ b/extensions/ollama/index.test.ts @@ -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); diff --git a/extensions/ollama/ollama.live.test.ts b/extensions/ollama/ollama.live.test.ts index c762e2b00541..ff31919db779 100644 --- a/extensions/ollama/ollama.live.test.ts +++ b/extensions/ollama/ollama.live.test.ts @@ -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, ); diff --git a/extensions/ollama/src/discovery-shared.test.ts b/extensions/ollama/src/discovery-shared.test.ts index d2290091cbd4..be21f4c0b3e3 100644 --- a/extensions/ollama/src/discovery-shared.test.ts +++ b/extensions/ollama/src/discovery-shared.test.ts @@ -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 () => { diff --git a/extensions/ollama/src/provider-models.test.ts b/extensions/ollama/src/provider-models.test.ts index 387a0cf2e35d..fd0910890441 100644 --- a/extensions/ollama/src/provider-models.test.ts +++ b/extensions/ollama/src/provider-models.test.ts @@ -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); }); diff --git a/extensions/ollama/src/stream-runtime.test.ts b/extensions/ollama/src/stream-runtime.test.ts index fd8ad1baaea0..f39c2ae67247 100644 --- a/extensions/ollama/src/stream-runtime.test.ts +++ b/extensions/ollama/src/stream-runtime.test.ts @@ -1,4 +1,5 @@ // Ollama tests cover stream runtime plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, describe, expect, it, vi } from "vitest"; const { fetchWithSsrFGuardMock, ollamaStreamWarnMock } = vi.hoisted(() => ({ @@ -39,6 +40,10 @@ type GuardedFetchCall = { auditContext?: string; }; +function requireEntry(entries: readonly T[], index: number, context: string): T { + return expectDefined(entries[index], context); +} + function requireRecord(value: unknown, label: string): Record { if (!value || typeof value !== "object") { throw new Error(`expected ${label}`); @@ -768,8 +773,14 @@ describe("convertToOllamaMessages", () => { it("prepends system message when provided", () => { const messages = [{ role: "user", content: "hello" }]; const result = convertToOllamaMessages(messages, "You are helpful."); - expect(result[0]).toEqual({ role: "system", content: "You are helpful." }); - expect(result[1]).toEqual({ role: "user", content: "hello" }); + expect(requireEntry(result, 0, "first converted Ollama message")).toEqual({ + role: "system", + content: "You are helpful.", + }); + expect(requireEntry(result, 1, "second converted Ollama message")).toEqual({ + role: "user", + content: "hello", + }); }); it("converts assistant messages with toolCall content blocks", () => { @@ -783,9 +794,9 @@ describe("convertToOllamaMessages", () => { }, ]; const result = convertToOllamaMessages(messages); - expect(result[0].role).toBe("assistant"); - expect(result[0].content).toBe("Let me check."); - expect(result[0].tool_calls).toEqual([ + expect(requireEntry(result, 0, "first converted Ollama message").role).toBe("assistant"); + expect(requireEntry(result, 0, "first converted Ollama message").content).toBe("Let me check."); + expect(requireEntry(result, 0, "first converted Ollama message").tool_calls).toEqual([ { id: "call_1", function: { name: "bash", arguments: { command: "ls" } } }, ]); }); @@ -805,7 +816,7 @@ describe("convertToOllamaMessages", () => { }, ]; const result = convertToOllamaMessages(messages); - expect(result[0].tool_calls).toEqual([ + expect(requireEntry(result, 0, "first converted Ollama message").tool_calls).toEqual([ { id: "fc_ollama_123", function: { name: "bash", arguments: { command: "pwd" } } }, ]); }); @@ -821,7 +832,7 @@ describe("convertToOllamaMessages", () => { }, ]; const result = convertToOllamaMessages(messages); - expect(result[0].tool_calls).toEqual([ + expect(requireEntry(result, 0, "first converted Ollama message").tool_calls).toEqual([ { id: "call_1", function: { name: "exec", arguments: { command: "pwd" } } }, { id: "call_2", function: { name: "read", arguments: { path: "README.md" } } }, ]); @@ -841,7 +852,7 @@ describe("convertToOllamaMessages", () => { const result = convertToOllamaMessages(messages, undefined, { availableToolNames: new Set(["tool_a", "tools_invoke_test", "function-run"]), }); - expect(result[0].tool_calls).toEqual([ + expect(requireEntry(result, 0, "first converted Ollama message").tool_calls).toEqual([ { id: "call_1", function: { name: "tool_a", arguments: { value: 1 } } }, { id: "call_2", function: { name: "tools_invoke_test", arguments: { value: 2 } } }, { id: "call_3", function: { name: "function-run", arguments: { value: 3 } } }, @@ -862,7 +873,7 @@ describe("convertToOllamaMessages", () => { const result = convertToOllamaMessages(messages, undefined, { availableToolNames: new Set(["exec", "read"]), }); - expect(result[0].tool_calls).toEqual([ + expect(requireEntry(result, 0, "first converted Ollama message").tool_calls).toEqual([ { id: "call_1", function: { name: "exec", arguments: { command: "pwd" } } }, { id: "call_2", function: { name: "read", arguments: { path: "." } } }, { id: "call_3", function: { name: "tool_missing", arguments: {} } }, @@ -882,7 +893,7 @@ describe("convertToOllamaMessages", () => { }, ]; const result = convertToOllamaMessages(messages); - expect(result[0].tool_calls).toEqual([ + expect(requireEntry(result, 0, "first converted Ollama message").tool_calls).toEqual([ { id: "call_1", function: { name: "functionshell", arguments: {} } }, { id: "call_2", function: { name: "tooling", arguments: {} } }, { id: "call_3", function: { name: "tools", arguments: {} } }, @@ -907,7 +918,7 @@ describe("convertToOllamaMessages", () => { }, ]; const result = convertToOllamaMessages(messages); - expect(result[0].tool_calls).toEqual([ + expect(requireEntry(result, 0, "first converted Ollama message").tool_calls).toEqual([ { id: "call_2", function: { name: "Read", arguments: { file_path: "/tmp/test.txt" } } }, ]); }); @@ -922,7 +933,7 @@ describe("convertToOllamaMessages", () => { }, ]; const result = convertToOllamaMessages(messages); - expect(result[0].tool_calls).toEqual([ + expect(requireEntry(result, 0, "first converted Ollama message").tool_calls).toEqual([ { id: "toolu_1", function: { name: "exec", arguments: { command: "echo hello" } } }, ]); }); @@ -942,7 +953,7 @@ describe("convertToOllamaMessages", () => { }, ]; const result = convertToOllamaMessages(messages); - expect(result[0].tool_calls).toEqual([ + expect(requireEntry(result, 0, "first converted Ollama message").tool_calls).toEqual([ { id: "call_3", function: { @@ -977,7 +988,9 @@ describe("convertToOllamaMessages", () => { const messages = [{ role: "toolResult", content: "output" }]; const result = convertToOllamaMessages(messages); expect(result).toEqual([{ role: "tool", content: "output" }]); - expect(result[0]).not.toHaveProperty("tool_name"); + expect(requireEntry(result, 0, "first converted Ollama message")).not.toHaveProperty( + "tool_name", + ); }); it("handles empty messages array", () => { @@ -1239,8 +1252,8 @@ describe("buildAssistantMessage", () => { const result = buildAssistantMessage(response, modelInfo); expect(result.stopReason).toBe("toolUse"); expect(result.content.length).toBe(1); // toolCall only (empty content is skipped) - expect(result.content[0].type).toBe("toolCall"); - const toolCall = result.content[0] as { + expect(requireEntry(result.content, 0, "Ollama tool-call content").type).toBe("toolCall"); + const toolCall = requireEntry(result.content, 0, "Ollama tool-call content") as { type: "toolCall"; id: string; name: string; @@ -1265,8 +1278,13 @@ describe("buildAssistantMessage", () => { done: true, }; const result = buildAssistantMessage(response, modelInfo); - expectToolCallContent(result.content[0], { name: "bash", arguments: { command: "pwd" } }); - expect((result.content[0] as { id?: string }).id).toBe("fc_ollama_real_1"); + expectToolCallContent(requireEntry(result.content, 0, "Ollama tool-call content"), { + name: "bash", + arguments: { command: "pwd" }, + }); + expect( + (requireEntry(result.content, 0, "Ollama tool-call content") as { id?: string }).id, + ).toBe("fc_ollama_real_1"); }); it("preserves parallel Ollama response tool-call ids independently", () => { @@ -1288,7 +1306,10 @@ describe("buildAssistantMessage", () => { "fc_ollama_real_1", "fc_ollama_real_2", ]); - expectToolCallContent(result.content[0], { name: "read", arguments: { path: "a.txt" } }); + expectToolCallContent(requireEntry(result.content, 0, "Ollama tool-call content"), { + name: "read", + arguments: { path: "a.txt" }, + }); expectToolCallContent(result.content[1], { name: "exec", arguments: { command: "date" } }); }); @@ -1308,7 +1329,10 @@ describe("buildAssistantMessage", () => { }; const result = buildAssistantMessage(response, modelInfo); expect(result.content).toHaveLength(2); - expectToolCallContent(result.content[0], { name: "exec", arguments: { command: "pwd" } }); + expectToolCallContent(requireEntry(result.content, 0, "Ollama tool-call content"), { + name: "exec", + arguments: { command: "pwd" }, + }); expectToolCallContent(result.content[1], { name: "read", arguments: { path: "README.md" } }); }); @@ -1331,7 +1355,10 @@ describe("buildAssistantMessage", () => { availableToolNames: new Set(["tool_a", "tools_invoke_test", "function-run"]), }); expect(result.content).toHaveLength(3); - expectToolCallContent(result.content[0], { name: "tool_a", arguments: { value: 1 } }); + expectToolCallContent(requireEntry(result.content, 0, "Ollama tool-call content"), { + name: "tool_a", + arguments: { value: 1 }, + }); expectToolCallContent(result.content[1], { name: "tools_invoke_test", arguments: { value: 2 }, @@ -1357,7 +1384,10 @@ describe("buildAssistantMessage", () => { }; const result = buildAssistantMessage(response, modelInfo); expect(result.content).toHaveLength(4); - expectToolCallContent(result.content[0], { name: "functionshell", arguments: {} }); + expectToolCallContent(requireEntry(result.content, 0, "Ollama tool-call content"), { + name: "functionshell", + arguments: {}, + }); expectToolCallContent(result.content[1], { name: "tooling", arguments: {} }); expectToolCallContent(result.content[2], { name: "tools", arguments: {} }); expectToolCallContent(result.content[3], { name: "tool_a", arguments: {} }); @@ -1375,7 +1405,7 @@ describe("buildAssistantMessage", () => { done: true, }; const result = buildAssistantMessage(response, modelInfo); - expectToolCallContent(result.content[0], { + expectToolCallContent(requireEntry(result.content, 0, "Ollama tool-call content"), { name: "bash", arguments: { command: "ls", path: "/tmp" }, }); @@ -1400,7 +1430,7 @@ describe("buildAssistantMessage", () => { done: true, }; const result = buildAssistantMessage(response, modelInfo); - expectToolCallContent(result.content[0], { + expectToolCallContent(requireEntry(result.content, 0, "Ollama tool-call content"), { name: "send", arguments: { target: "9223372036854775807", @@ -1421,7 +1451,10 @@ describe("buildAssistantMessage", () => { done: true, }; const result = buildAssistantMessage(response, modelInfo); - expectToolCallContent(result.content[0], { name: "bash", arguments: {} }); + expectToolCallContent(requireEntry(result.content, 0, "Ollama tool-call content"), { + name: "bash", + arguments: {}, + }); }); it("sets all costs to zero for local models", () => { @@ -1520,9 +1553,9 @@ describe("parseNdjsonStream", () => { chunks.push(chunk); } expect(chunks).toHaveLength(3); - expect(chunks[0].message.content).toBe("Hello"); - expect(chunks[1].message.content).toBe(" world"); - expect(chunks[2].done).toBe(true); + expect(requireEntry(chunks, 0, "first parsed Ollama chunk").message.content).toBe("Hello"); + expect(requireEntry(chunks, 1, "second parsed Ollama chunk").message.content).toBe(" world"); + expect(requireEntry(chunks, 2, "final parsed Ollama chunk").done).toBe(true); }); it("parses tool_calls from intermediate chunk (not final)", async () => { @@ -1536,11 +1569,19 @@ describe("parseNdjsonStream", () => { chunks.push(chunk); } expect(chunks).toHaveLength(2); - expect(chunks[0].done).toBe(false); - expect(chunks[0].message.tool_calls).toHaveLength(1); - expect(chunks[0].message.tool_calls![0].function.name).toBe("bash"); - expect(chunks[1].done).toBe(true); - expect(chunks[1].message.tool_calls).toBeUndefined(); + const firstChunk = requireEntry(chunks, 0, "first parsed Ollama chunk"); + const firstToolCall = requireEntry( + expectDefined(firstChunk.message.tool_calls, "first parsed Ollama chunk tool calls"), + 0, + "first parsed Ollama tool call", + ); + expect(firstChunk.done).toBe(false); + expect(firstChunk.message.tool_calls).toHaveLength(1); + expect(firstToolCall.function.name).toBe("bash"); + expect(requireEntry(chunks, 1, "second parsed Ollama chunk").done).toBe(true); + expect( + requireEntry(chunks, 1, "second parsed Ollama chunk").message.tool_calls, + ).toBeUndefined(); }); it("accumulates tool_calls across multiple intermediate chunks", async () => { @@ -1562,10 +1603,14 @@ describe("parseNdjsonStream", () => { } } expect(accumulatedToolCalls).toHaveLength(2); - expect(accumulatedToolCalls[0].function.name).toBe("read"); - expect(accumulatedToolCalls[1].function.name).toBe("bash"); + expect(requireEntry(accumulatedToolCalls, 0, "first accumulated tool call").function.name).toBe( + "read", + ); + expect( + requireEntry(accumulatedToolCalls, 1, "second accumulated tool call").function.name, + ).toBe("bash"); // Final done:true chunk has no tool_calls - expect(chunks[2].message.tool_calls).toBeUndefined(); + expect(requireEntry(chunks, 2, "final parsed Ollama chunk").message.tool_calls).toBeUndefined(); }); it("preserves unsafe integer tool arguments as exact strings", async () => { @@ -1804,7 +1849,7 @@ describe("createOllamaStreamFn streaming events", () => { // No text content means no start/text_start/text_delta/text_end events const types = events.map((e) => e.type); expect(types).toEqual(["done"]); - const doneEvent = events[0]; + const doneEvent = requireEntry(events, 0, "tool-call-only done event"); if (doneEvent.type === "done") { expect(doneEvent.reason).toBe("toolUse"); } diff --git a/extensions/ollama/src/stream.test.ts b/extensions/ollama/src/stream.test.ts index f2a51f9c0d74..4459ab5ef36a 100644 --- a/extensions/ollama/src/stream.test.ts +++ b/extensions/ollama/src/stream.test.ts @@ -1,4 +1,5 @@ // Ollama tests cover stream plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({ @@ -209,8 +210,10 @@ describe("createOllamaStreamFn thinking events", () => { const thinkingDeltas = events.filter((e) => e.type === "thinking_delta"); expect(thinkingDeltas).toHaveLength(2); - expect(thinkingDeltas[0].delta).toBe("Step 1"); - expect(thinkingDeltas[1].delta).toBe(" and step 2"); + expect(expectDefined(thinkingDeltas[0], "first Ollama thinking delta").delta).toBe("Step 1"); + expect(expectDefined(thinkingDeltas[1], "second Ollama thinking delta").delta).toBe( + " and step 2", + ); const thinkingStart = events.find((e) => e.type === "thinking_start"); expect(thinkingStart?.contentIndex).toBe(0); diff --git a/extensions/ollama/src/wsl2-crash-loop-check.test.ts b/extensions/ollama/src/wsl2-crash-loop-check.test.ts index 593029b76bb2..1f5e08af3ad0 100644 --- a/extensions/ollama/src/wsl2-crash-loop-check.test.ts +++ b/extensions/ollama/src/wsl2-crash-loop-check.test.ts @@ -1,5 +1,6 @@ // Ollama tests cover wsl2 crash loop check plugin behavior. import { promisify } from "node:util"; +import { expectDefined } from "@openclaw/normalization-core"; import { beforeEach, describe, expect, it, vi } from "vitest"; const { isWSL2SyncMock } = vi.hoisted(() => ({ @@ -35,7 +36,9 @@ const accessMock = vi.mocked(access); const execFileMock = execFile as unknown as ReturnType & { [key: symbol]: ReturnType; }; -const execFilePromiseMock = vi.mocked(execFileMock[promisify.custom]); +const execFilePromiseMock = vi.mocked( + expectDefined(execFileMock[promisify.custom], "promisified execFile mock"), +); function createLogger() { return { diff --git a/extensions/openai/index.test.ts b/extensions/openai/index.test.ts index b53f9b8b2e4b..2a9394fc0148 100644 --- a/extensions/openai/index.test.ts +++ b/extensions/openai/index.test.ts @@ -1,4 +1,5 @@ // Openai tests cover index plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { requireRegisteredProvider } from "openclaw/plugin-sdk/plugin-test-runtime"; @@ -307,8 +308,16 @@ describe("openai plugin", () => { expect(runtimeMocks.ensureGlobalUndiciEnvProxyDispatcher).toHaveBeenCalledOnce(); expect(runtimeMocks.refreshOpenAICodexToken).toHaveBeenCalledOnce(); expect( - runtimeMocks.ensureGlobalUndiciEnvProxyDispatcher.mock.invocationCallOrder[0], - ).toBeLessThan(runtimeMocks.refreshOpenAICodexToken.mock.invocationCallOrder[0]); + expectDefined( + runtimeMocks.ensureGlobalUndiciEnvProxyDispatcher.mock.invocationCallOrder[0], + "OpenAI proxy dispatcher invocation", + ), + ).toBeLessThan( + expectDefined( + runtimeMocks.refreshOpenAICodexToken.mock.invocationCallOrder[0], + "OpenAI token refresh invocation", + ), + ); }); it("registers provider-owned OpenAI tool compat hooks for API and Codex transports", async () => { diff --git a/extensions/openshell/src/backend.e2e.test.ts b/extensions/openshell/src/backend.e2e.test.ts index b4f6dc051902..9d97c13371e1 100644 --- a/extensions/openshell/src/backend.e2e.test.ts +++ b/extensions/openshell/src/backend.e2e.test.ts @@ -4,6 +4,7 @@ import fs from "node:fs/promises"; import net from "node:net"; import os from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { createSandboxTestContext } from "openclaw/plugin-sdk/test-fixtures"; import { createSandboxBrowserConfig, @@ -147,9 +148,10 @@ async function activeOpenShellGateway( for (const line of output.split(/\r?\n/u)) { const match = line.match(/\*\s+(\S+)/u); if (match) { + const gateway = expectDefined(match[1], "OpenShell gateway name"); const info = await runCommand({ command, - args: ["gateway", "info", "--gateway", match[1]], + args: ["gateway", "info", "--gateway", gateway], env, allowFailure: true, timeoutMs: 20_000, @@ -164,12 +166,12 @@ async function activeOpenShellGateway( ) { const status = await runCommand({ command, - args: ["--gateway", match[1], "sandbox", "list"], + args: ["--gateway", gateway, "sandbox", "list"], env, allowFailure: true, timeoutMs: 20_000, }); - return status.code === 0 ? match[1] : null; + return status.code === 0 ? gateway : null; } return null; } diff --git a/extensions/openshell/src/openshell-core.test.ts b/extensions/openshell/src/openshell-core.test.ts index 2c6faacf761d..5ddd2c3ffa7e 100644 --- a/extensions/openshell/src/openshell-core.test.ts +++ b/extensions/openshell/src/openshell-core.test.ts @@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import type { CreateSandboxBackendParams } from "openclaw/plugin-sdk/sandbox"; import { createSandboxBrowserConfig, @@ -385,7 +386,7 @@ describe("openshell backend manager", () => { }); cliMocks.runOpenShellCli.mockImplementation(async ({ args }: { args: string[] }) => { if (args[0] === "sandbox" && args[1] === "download") { - const tmpDir = args[4]; + const tmpDir = expectDefined(args[4], "OpenShell download destination"); await fs.writeFile(path.join(tmpDir, "from-remote.txt"), "remote", "utf8"); await fs.mkdir(path.join(tmpDir, ".openclaw", "sandbox-skills", "skills"), { recursive: true, @@ -438,7 +439,7 @@ describe("openshell backend manager", () => { const workspaceDir = await makeTempDir("openclaw-openshell-workspace-"); cliMocks.runOpenShellCli.mockImplementation(async ({ args }: { args: string[] }) => { if (args[0] === "sandbox" && args[1] === "download") { - const tmpDir = args[4]; + const tmpDir = expectDefined(args[4], "OpenShell download destination"); await fs.writeFile(path.join(tmpDir, "from-remote.txt"), "remote", "utf8"); await fs.mkdir(path.join(tmpDir, ".openclaw"), { recursive: true }); await fs.writeFile(path.join(tmpDir, ".openclaw", "sandbox-skills"), "poison", "utf8"); @@ -480,7 +481,7 @@ describe("openshell backend manager", () => { await fs.writeFile(shadowFile, "local shadow", "utf8"); cliMocks.runOpenShellCli.mockImplementation(async ({ args }: { args: string[] }) => { if (args[0] === "sandbox" && args[1] === "download") { - const tmpDir = args[4]; + const tmpDir = expectDefined(args[4], "OpenShell download destination"); await fs.writeFile(path.join(tmpDir, "from-remote.txt"), "remote", "utf8"); await fs.writeFile(path.join(tmpDir, ".openclaw"), "poison", "utf8"); } diff --git a/extensions/parallel/src/parallel-free-web-search-provider.test.ts b/extensions/parallel/src/parallel-free-web-search-provider.test.ts index a1a24604ec0c..f85723c36edb 100644 --- a/extensions/parallel/src/parallel-free-web-search-provider.test.ts +++ b/extensions/parallel/src/parallel-free-web-search-provider.test.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; import { beforeEach, describe, expect, it, vi } from "vitest"; type EndpointCall = { @@ -11,6 +12,10 @@ const endpointMockState = vi.hoisted(() => ({ responses: [] as Response[], })); +function requireEndpointCall(index: number): EndpointCall { + return expectDefined(endpointMockState.calls[index], `Parallel endpoint call ${index}`); +} + vi.mock("openclaw/plugin-sdk/provider-web-search", async (importOriginal) => { const actual = await importOriginal(); const runEndpoint = async ( @@ -80,7 +85,7 @@ describe("parallel-free web search provider", () => { const sessionIdParam = ( tool.parameters as { properties: Record } ).properties.session_id; - expect(sessionIdParam.maxLength).toBe(100); + expect(expectDefined(sessionIdParam, "Parallel session_id parameter").maxLength).toBe(100); }); it("searches via the free MCP and brands the result, with no API key", async () => { @@ -109,11 +114,10 @@ describe("parallel-free web search provider", () => { // Three MCP calls (initialize -> notifications -> tools/call) to the free MCP. expect(endpointMockState.calls).toHaveLength(3); - expect(endpointMockState.calls[0].url).toBe("https://search.parallel.ai/mcp"); + const firstCall = requireEndpointCall(0); + expect(firstCall.url).toBe("https://search.parallel.ai/mcp"); // No bearer token on the anonymous free path. - expect( - (endpointMockState.calls[0].init.headers as Record).Authorization, - ).toBeUndefined(); + expect((firstCall.init.headers as Record).Authorization).toBeUndefined(); expect(result).toMatchObject({ provider: "parallel-free" }); expect(Array.isArray(result.results)).toBe(true); expect((result.results as unknown[]).length).toBe(1); @@ -134,7 +138,7 @@ describe("parallel-free web search provider", () => { }); const toolsCallArgs = ( - JSON.parse(endpointMockState.calls[2].init.body as string).params as Record + JSON.parse(requireEndpointCall(2).init.body as string).params as Record ).arguments as Record; const sentSessionId = toolsCallArgs.session_id as string; // The 150-char caller id is out-of-contract for the free MCP; it is dropped diff --git a/extensions/parallel/src/parallel-mcp-search.runtime.test.ts b/extensions/parallel/src/parallel-mcp-search.runtime.test.ts index 4d458dd20c35..9cf450264310 100644 --- a/extensions/parallel/src/parallel-mcp-search.runtime.test.ts +++ b/extensions/parallel/src/parallel-mcp-search.runtime.test.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createStreamingResponse } from "../../test-support/streaming-error-response.js"; @@ -78,6 +79,10 @@ function headerOf(call: EndpointCall, name: string): string | undefined { return (call.init.headers as Record)[name]; } +function requireEndpointCall(index: number): EndpointCall { + return expectDefined(endpointMockState.calls[index], `Parallel MCP endpoint call ${index}`); +} + function boundaryJsonPayload(base: Record): { payload: Record; truncatedJson: string; @@ -247,17 +252,17 @@ describe("runParallelMcpSearch", () => { "tools/call", ]); // Server session id + a negotiated protocol version are echoed post-init. - expect(headerOf(endpointMockState.calls[1], "Mcp-Session-Id")).toBe("server-session-1"); - expect(headerOf(endpointMockState.calls[2], "Mcp-Session-Id")).toBe("server-session-1"); - expect(headerOf(endpointMockState.calls[2], "MCP-Protocol-Version")).toBe("2025-06-18"); + expect(headerOf(requireEndpointCall(1), "Mcp-Session-Id")).toBe("server-session-1"); + expect(headerOf(requireEndpointCall(2), "Mcp-Session-Id")).toBe("server-session-1"); + expect(headerOf(requireEndpointCall(2), "MCP-Protocol-Version")).toBe("2025-06-18"); // No bearer token on the anonymous free path. - expect(headerOf(endpointMockState.calls[0], "Authorization")).toBeUndefined(); + expect(headerOf(requireEndpointCall(0), "Authorization")).toBeUndefined(); // Every call identifies OpenClaw at the HTTP layer (not just node). for (const call of endpointMockState.calls) { expect(headerOf(call, "User-Agent")).toMatch(/^openclaw-parallel\//); } // tools/call carries the documented web_search args. - const callArgs = (readBody(endpointMockState.calls[2]).params as Record) + const callArgs = (readBody(requireEndpointCall(2)).params as Record) .arguments as Record; expect(callArgs).toMatchObject({ objective: "find examples", @@ -285,7 +290,7 @@ describe("runParallelMcpSearch", () => { await runParallelMcpSearch({ searchQueries: ["alpha", "beta"], maxResults: 5 }); - const callArgs = (readBody(endpointMockState.calls[2]).params as Record) + const callArgs = (readBody(requireEndpointCall(2)).params as Record) .arguments as Record; expect(callArgs.objective).toBe("alpha beta"); }); @@ -309,7 +314,7 @@ describe("runParallelMcpSearch", () => { maxResults: 5, sessionId: callerSessionId, }); - const callArgs = (readBody(endpointMockState.calls[2]).params as Record) + const callArgs = (readBody(requireEndpointCall(2)).params as Record) .arguments as Record; expect(callArgs.session_id).toBe(callerSessionId); expect(response.session_id).toBe(callerSessionId); @@ -339,8 +344,8 @@ describe("runParallelMcpSearch", () => { "initialize", "notifications/initialized", ]); - expect(headerOf(endpointMockState.calls[1], "Mcp-Session-Id")).toBe("server-session-1"); - expect(headerOf(endpointMockState.calls[1], "MCP-Protocol-Version")).toBe("2025-06-18"); + expect(headerOf(requireEndpointCall(1), "Mcp-Session-Id")).toBe("server-session-1"); + expect(headerOf(requireEndpointCall(1), "MCP-Protocol-Version")).toBe("2025-06-18"); }); it("bounds initialize error bodies without using response.text()", async () => { diff --git a/extensions/parallel/src/parallel-web-search-provider.test.ts b/extensions/parallel/src/parallel-web-search-provider.test.ts index 233495b5ab28..26fb158b8e62 100644 --- a/extensions/parallel/src/parallel-web-search-provider.test.ts +++ b/extensions/parallel/src/parallel-web-search-provider.test.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createStreamingResponse } from "../../test-support/streaming-error-response.js"; @@ -476,7 +477,7 @@ describe("parallel web search provider", () => { }); expect(endpointMockState.calls).toHaveLength(1); - const [call] = endpointMockState.calls; + const call = expectDefined(endpointMockState.calls[0], "Parallel search endpoint call"); expect(call.url).toBe("https://api.parallel.ai/v1/search"); expect(call.timeoutSeconds).toBe(5); expect(readMockedBody(call)).toEqual({ diff --git a/extensions/phone-control/doctor-contract-api.test.ts b/extensions/phone-control/doctor-contract-api.test.ts index 071f80ff9c75..563ad82cfd32 100644 --- a/extensions/phone-control/doctor-contract-api.test.ts +++ b/extensions/phone-control/doctor-contract-api.test.ts @@ -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, @@ -52,7 +53,7 @@ describe("phone-control doctor state migration", () => { await fs.mkdir(path.dirname(sourcePath), { recursive: true }); await fs.writeFile(sourcePath, JSON.stringify(legacyState)); - const migration = stateMigrations[0]; + const migration = expectDefined(stateMigrations[0], "phone-control state migration"); await expect( migration.detectLegacyState({ config: {}, diff --git a/extensions/qa-lab/src/ci-smoke-plan.test.ts b/extensions/qa-lab/src/ci-smoke-plan.test.ts index d9c56d2ffd40..ae1d48ec004d 100644 --- a/extensions/qa-lab/src/ci-smoke-plan.test.ts +++ b/extensions/qa-lab/src/ci-smoke-plan.test.ts @@ -1,5 +1,6 @@ // Qa Lab tests cover bounded CI smoke profile planning. import { OPENCLAW_CRABLINE_DEFAULT_CHANNEL } from "@openclaw/crabline"; +import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it } from "vitest"; import { createQaSmokeCiPart } from "./ci-smoke-plan.js"; import { readQaScenarioPack } from "./scenario-catalog.js"; @@ -69,9 +70,9 @@ describe("createQaSmokeCiPart", () => { ids.map((scenarioId) => estimateScenarioCost(scenarioById.get(scenarioId))), ), ); - expect(Math.abs(primaryRunCosts[0] - primaryRunCosts[1])).toBeLessThanOrEqual( - largestScenarioCost, - ); + const firstRunCost = expectDefined(primaryRunCosts[0], "first QA smoke run cost"); + const secondRunCost = expectDefined(primaryRunCosts[1], "second QA smoke run cost"); + expect(Math.abs(firstRunCost - secondRunCost)).toBeLessThanOrEqual(largestScenarioCost); }); it("rejects undeclared profile parts", () => { diff --git a/extensions/qa-lab/src/cli.test.ts b/extensions/qa-lab/src/cli.test.ts index f4c8d45777a8..bc319880b9b1 100644 --- a/extensions/qa-lab/src/cli.test.ts +++ b/extensions/qa-lab/src/cli.test.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Qa Lab tests cover cli plugin behavior. import { Command } from "commander"; import type { QaRunnerCliContribution } from "openclaw/plugin-sdk/qa-runner-runtime"; @@ -736,7 +737,12 @@ describe("qa cli registration", () => { }); it("delegates discovered qa runner registration through the generic host seam", () => { - const [{ registration }] = listQaRunnerCliContributions.mock.results[0].value; + const mockResult = expectDefined( + listQaRunnerCliContributions.mock.results[0], + "QA runner contribution result", + ); + const contribution = expectDefined(mockResult.value[0], "QA runner contribution"); + const { registration } = contribution; expect(registration.register).toHaveBeenCalledTimes(1); }); diff --git a/extensions/qa-lab/src/coverage-report.test.ts b/extensions/qa-lab/src/coverage-report.test.ts index 616b2ad00669..8f5da7b0cbee 100644 --- a/extensions/qa-lab/src/coverage-report.test.ts +++ b/extensions/qa-lab/src/coverage-report.test.ts @@ -1,4 +1,5 @@ // Qa Lab tests cover coverage report plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it } from "vitest"; import { buildQaCoverageInventory, @@ -244,8 +245,12 @@ describe("qa coverage report", () => { expect(observabilityPack?.missingScenarioIds).toStrictEqual([]); expect(observabilityPack?.scenarioIds).toEqual(["otel-trace-smoke", "docker-prometheus-smoke"]); expect(observabilityPack?.coverageIds).toContain("telemetry.prometheus"); - expect(inventory.byTheme.memory.map((coverage) => coverage.id)).toContain("memory.recall"); - expect(inventory.bySurface.memory.map((coverage) => coverage.id)).toContain("memory.recall"); + expect( + expectDefined(inventory.byTheme.memory, "memory QA theme").map((coverage) => coverage.id), + ).toContain("memory.recall"); + expect( + expectDefined(inventory.bySurface.memory, "memory QA surface").map((coverage) => coverage.id), + ).toContain("memory.recall"); }); it("rejects duplicate ownership across YAML and non-YAML catalogs", () => { diff --git a/extensions/qa-lab/src/evidence-gallery.test.ts b/extensions/qa-lab/src/evidence-gallery.test.ts index f47efec8f7de..4a1309a21054 100644 --- a/extensions/qa-lab/src/evidence-gallery.test.ts +++ b/extensions/qa-lab/src/evidence-gallery.test.ts @@ -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 { describe, expect, it } from "vitest"; import { buildQaEvidenceGalleryModel, @@ -118,10 +119,15 @@ describe("evidence gallery", () => { }, ], }); + const noArtifactsEntry = expectDefined(evidence.entries[1], "no-artifacts evidence entry"); + const noArtifactsExecution = expectDefined( + noArtifactsEntry.execution, + "no-artifacts evidence execution", + ); evidence.entries[1] = { - ...evidence.entries[1], + ...noArtifactsEntry, execution: { - ...evidence.entries[1].execution!, + ...noArtifactsExecution, artifacts: [], }, }; @@ -171,8 +177,9 @@ describe("evidence gallery", () => { title: "Failure path evidence", artifact: { kind: "log", path: "missing.log" }, }); + const failureEntry = expectDefined(evidence.entries[0], "failure evidence entry"); evidence.entries[0] = { - ...evidence.entries[0], + ...failureEntry, result: { status: "blocked", failure: { @@ -188,7 +195,8 @@ describe("evidence gallery", () => { repoRoot, }); - expect(model.entries[0].failureReason).toBe( + const failureModelEntry = expectDefined(model.entries[0], "failure gallery entry"); + expect(failureModelEntry.failureReason).toBe( "Command failed at /openclaw.mjs and file:///trace.log", ); expect(JSON.stringify(model)).not.toContain(repoRoot); @@ -214,14 +222,23 @@ describe("evidence gallery", () => { artifact: { kind: "log", path: artifactPath }, }); evidence.profile = `${repoRoot}/qa-profile`; + const absoluteEntry = expectDefined(evidence.entries[0], "absolute-path evidence entry"); + const absoluteExecution = expectDefined( + absoluteEntry.execution, + "absolute-path evidence execution", + ); + const absoluteArtifact = expectDefined( + absoluteExecution.artifacts[0], + "absolute-path evidence artifact", + ); evidence.entries[0] = { - ...evidence.entries[0], + ...absoluteEntry, coverage: [{ id: `${repoRoot}/coverage`, role: `${repoRoot}/role` }], execution: { - ...evidence.entries[0].execution!, + ...absoluteExecution, artifacts: [ { - ...evidence.entries[0].execution!.artifacts[0], + ...absoluteArtifact, kind: `${repoRoot}/log`, source: `${repoRoot}/vitest`, }, @@ -233,7 +250,7 @@ describe("evidence gallery", () => { ], }, test: { - ...evidence.entries[0].test, + ...absoluteEntry.test, id: `${repoRoot}/qa-lab.absolute-artifact-path`, kind: `${repoRoot}/vitest-test`, source: { path: path.join(repoRoot, "extensions/qa-lab/src/absolute.test.ts") }, @@ -650,7 +667,8 @@ describe("evidence gallery", () => { ).resolves.toBe(await fs.realpath(path.join(outputDir, "runner", "result.json"))); await fs.rm(path.join(outputDir, "runner", "result.json")); const missingBundleModel = await buildQaEvidenceGalleryModel({ evidencePath, repoRoot }); - expect(missingBundleModel.entries[0].artifacts[0]).toMatchObject({ + const missingEntry = expectDefined(missingBundleModel.entries[0], "missing-bundle entry"); + expect(missingEntry.artifacts[0]).toMatchObject({ exists: false, error: "Evidence artifact not found.", preview: null, @@ -675,13 +693,18 @@ describe("evidence gallery", () => { const outsideArtifact = path.join(outsideDir, "artifact.log"); await fs.writeFile(outsideArtifact, "outside secret\n", "utf8"); await fs.symlink(outsideArtifact, path.join(outputDir, "escape.log")); + const collisionEntry = expectDefined(collisionEvidence.entries[0], "collision evidence entry"); + const collisionExecution = expectDefined( + collisionEntry.execution, + "collision evidence execution", + ); await writeJson(evidencePath, { ...collisionEvidence, entries: [ { - ...collisionEvidence.entries[0], + ...collisionEntry, execution: { - ...collisionEvidence.entries[0].execution, + ...collisionExecution, artifacts: [{ kind: "log", path: "escape.log", source: "vitest" }], }, }, diff --git a/extensions/qa-lab/src/evidence-summary.test.ts b/extensions/qa-lab/src/evidence-summary.test.ts index 3004ca8c58a8..f3f60e5ca051 100644 --- a/extensions/qa-lab/src/evidence-summary.test.ts +++ b/extensions/qa-lab/src/evidence-summary.test.ts @@ -1,5 +1,6 @@ // Qa Lab tests cover QA evidence summary behavior. import { execFileSync } from "node:child_process"; +import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it } from "vitest"; import { QA_EVIDENCE_SUMMARY_KIND, @@ -495,7 +496,9 @@ describe("evidence summary", () => { expect(validateQaEvidenceSummaryJson(evidence)).toEqual(evidence); expect(evidence.evidenceMode).toBe(expectedMode); - expect("execution" in evidence.entries[0]).toBe(hasExecution); + expect("execution" in expectDefined(evidence.entries[0], "QA evidence entry")).toBe( + hasExecution, + ); }, ); diff --git a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts index cf4d9dadafc4..682b307fcfbc 100644 --- a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts +++ b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts @@ -1,4 +1,5 @@ // Qa Lab tests cover telegram live plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime"; import { afterEach, describe, expect, it, vi } from "vitest"; @@ -666,10 +667,13 @@ describe("telegram live qa runtime", () => { ).steps[0]; expect(otherBotStep?.expectReply).toBe(false); expect(otherBotStep?.input).toBe("/status@OpenClawQaOtherBot"); + const mentionedReplyRun = requireScenario( + scenarios, + "telegram-mentioned-message-reply", + ).buildRun("sut_bot"); expect( - scenarios - .find((scenario) => scenario.id === "telegram-mentioned-message-reply") - ?.buildRun("sut_bot").steps[0].replyToLatestSutMessage, + expectDefined(mentionedReplyRun.steps[0], "mentioned-message reply step") + .replyToLatestSutMessage, ).toBe(true); expect( scenarios.find((scenario) => scenario.id === "telegram-mentioned-message-reply") diff --git a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.test.ts b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.test.ts index d772f20d7570..c6111eee6091 100644 --- a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.test.ts +++ b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.test.ts @@ -4,6 +4,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; +import { expectDefined } from "@openclaw/normalization-core"; import type { WhatsAppQaDriverObservedMessage, WhatsAppQaDriverSession, @@ -135,6 +136,21 @@ function buildWhatsAppQaConfigFixture( } type WhatsAppScenarioIdFilter = NonNullable[0]>[number]; + +function findWhatsAppScenario(id: WhatsAppScenarioIdFilter) { + return expectDefined(testing.findScenarios([id])[0], `WhatsApp QA scenario ${id}`); +} + +function updateObservedMessage( + messages: WhatsAppQaDriverObservedMessage[], + index: number, + patch: Partial, +): void { + messages[index] = { + ...expectDefined(messages[index], `WhatsApp observed message ${index}`), + ...patch, + }; +} const DIRECT_GATEWAY_SCENARIO_IDS = [ "whatsapp-outbound-media-matrix", "whatsapp-outbound-document-preserves-filename", @@ -671,7 +687,7 @@ describe("WhatsApp QA live runtime", () => { }); it("defines the user-path WhatsApp agent reaction scenario as mock-backed", () => { - const [scenario] = testing.findScenarios(["whatsapp-agent-message-action-react"]); + const scenario = findWhatsAppScenario("whatsapp-agent-message-action-react"); const run = scenario.buildRun(); if (run.kind === "approval") { throw new Error("whatsapp-agent-message-action-react unexpectedly built approval run"); @@ -692,7 +708,7 @@ describe("WhatsApp QA live runtime", () => { }); it("observes the native WhatsApp reaction for the user-path agent action scenario", async () => { - const [scenario] = testing.findScenarios(["whatsapp-agent-message-action-react"]); + const scenario = findWhatsAppScenario("whatsapp-agent-message-action-react"); const run = scenario.buildRun(); if (run.kind === "approval" || !run.afterSend) { throw new Error("whatsapp-agent-message-action-react unexpectedly omitted afterSend"); @@ -794,7 +810,7 @@ describe("WhatsApp QA live runtime", () => { }); it("observes native WhatsApp group reactions for the user-path action scenario", async () => { - const [scenario] = testing.findScenarios(["whatsapp-group-agent-message-action-react"]); + const scenario = findWhatsAppScenario("whatsapp-group-agent-message-action-react"); const run = scenario.buildRun(); if (run.kind === "approval" || !run.afterSend) { throw new Error("whatsapp-group-agent-message-action-react unexpectedly omitted afterSend"); @@ -946,40 +962,35 @@ describe("WhatsApp QA live runtime", () => { ? (payload as { question: string }).question : undefined; if (question) { - observedMessages[4] = { - ...observedMessages[4], + updateObservedMessage(observedMessages, 4, { observedAt: new Date().toISOString(), poll: { options: ["alpha", "beta"], question }, - }; + }); } const message = typeof (payload as { message?: unknown }).message === "string" ? (payload as { message: string }).message : undefined; if (message?.endsWith("_IMAGE")) { - observedMessages[0] = { - ...observedMessages[0], + updateObservedMessage(observedMessages, 0, { observedAt: new Date().toISOString(), text: message, - }; + }); } if (message?.endsWith("_DOCUMENT")) { - observedMessages[1] = { - ...observedMessages[1], + updateObservedMessage(observedMessages, 1, { observedAt: new Date().toISOString(), text: message, - }; + }); } if (message?.endsWith("_AUDIO")) { - observedMessages[2] = { - ...observedMessages[2], + updateObservedMessage(observedMessages, 2, { observedAt: new Date().toISOString(), - }; - observedMessages[3] = { - ...observedMessages[3], + }); + updateObservedMessage(observedMessages, 3, { observedAt: new Date().toISOString(), text: message, - }; + }); } return {}; }, @@ -2031,7 +2042,7 @@ describe("WhatsApp QA live runtime", () => { }); it("seeds the structured-message location check through text context", () => { - const [scenario] = testing.findScenarios(["whatsapp-inbound-structured-messages"]); + const scenario = findWhatsAppScenario("whatsapp-inbound-structured-messages"); if (!scenario) { throw new Error("missing structured WhatsApp scenario"); } @@ -2211,7 +2222,7 @@ describe("WhatsApp QA live runtime", () => { }); it("formats per-scenario progress lines for live lane visibility", () => { - const [scenario] = testing.findScenarios(["whatsapp-inbound-structured-messages"]); + const scenario = findWhatsAppScenario("whatsapp-inbound-structured-messages"); if (!scenario) { throw new Error("missing structured WhatsApp scenario"); } @@ -2266,7 +2277,7 @@ describe("WhatsApp QA live runtime", () => { }); it("defines WhatsApp final-message accounting as a settled two-chunk assertion", () => { - const [scenario] = testing.findScenarios(["whatsapp-stream-final-message-accounting"]); + const scenario = findWhatsAppScenario("whatsapp-stream-final-message-accounting"); const run = scenario.buildRun(); if (run.kind === "approval") { throw new Error("whatsapp-stream-final-message-accounting unexpectedly built approval run"); @@ -2284,7 +2295,7 @@ describe("WhatsApp QA live runtime", () => { }); it("requires the long-reply delivery-shape tail marker in the second chunk", async () => { - const [scenario] = testing.findScenarios(["whatsapp-reply-delivery-shape"]); + const scenario = findWhatsAppScenario("whatsapp-reply-delivery-shape"); const run = scenario.buildRun(); if (run.kind === "approval" || !run.afterReply) { throw new Error("whatsapp-reply-delivery-shape unexpectedly omitted afterReply"); @@ -2397,7 +2408,7 @@ describe("WhatsApp QA live runtime", () => { }); it("targets group approval reactions at the approval prompt participant", async () => { - const [scenario] = testing.findScenarios(["whatsapp-approval-exec-group-reaction-native"]); + const scenario = findWhatsAppScenario("whatsapp-approval-exec-group-reaction-native"); const run = scenario.buildRun(); if (run.kind !== "approval") { throw new Error("expected approval scenario run"); @@ -2469,7 +2480,7 @@ describe("WhatsApp QA live runtime", () => { }); it("targets DM approval reactions at the approval prompt message", async () => { - const [scenario] = testing.findScenarios(["whatsapp-approval-exec-reaction-native"]); + const scenario = findWhatsAppScenario("whatsapp-approval-exec-reaction-native"); const run = scenario.buildRun(); if (run.kind !== "approval") { throw new Error("expected approval scenario run"); @@ -2586,7 +2597,7 @@ describe("WhatsApp QA live runtime", () => { }); it("enables WhatsApp action discovery for the user-path agent reaction scenario", () => { - const [scenario] = testing.findScenarios(["whatsapp-agent-message-action-react"]); + const scenario = findWhatsAppScenario("whatsapp-agent-message-action-react"); const cfg = buildWhatsAppQaConfigFixture({ overrides: scenario.configOverrides, }); @@ -2597,7 +2608,7 @@ describe("WhatsApp QA live runtime", () => { }); it("defines the WhatsApp audio preflight scenario as mock-backed audio media", () => { - const [scenario] = testing.findScenarios(["whatsapp-audio-preflight"]); + const scenario = findWhatsAppScenario("whatsapp-audio-preflight"); const scenarioRun = scenario.buildRun(); if (scenarioRun.kind === "approval") { throw new Error("whatsapp-audio-preflight unexpectedly built an approval scenario run"); @@ -2618,7 +2629,7 @@ describe("WhatsApp QA live runtime", () => { }); it("defines group audio gating as captionless audio driven by mock transcription sentinel", () => { - const [scenario] = testing.findScenarios(["whatsapp-group-audio-gating"]); + const scenario = findWhatsAppScenario("whatsapp-group-audio-gating"); const scenarioRun = scenario.buildRun(); if (scenarioRun.kind === "approval") { throw new Error("whatsapp-group-audio-gating unexpectedly built an approval scenario run"); @@ -2851,7 +2862,7 @@ describe("WhatsApp QA live runtime", () => { }); it("uses automatic visible replies for WhatsApp group mention gating", () => { - const [scenario] = testing.findScenarios(["whatsapp-mention-gating"]); + const scenario = findWhatsAppScenario("whatsapp-mention-gating"); const scenarioRun = scenario.buildRun(); if (scenarioRun.kind === "approval") { throw new Error("whatsapp-mention-gating unexpectedly built an approval scenario run"); @@ -2867,7 +2878,7 @@ describe("WhatsApp QA live runtime", () => { }); it("fails explicitly requested group scenarios when group credentials are missing", () => { - const [scenario] = testing.findScenarios(["whatsapp-mention-gating"]); + const scenario = findWhatsAppScenario("whatsapp-mention-gating"); const implicitResult = testing.createMissingGroupJidScenarioResult({ explicitScenarioSelection: false, diff --git a/extensions/qa-lab/src/mantis/run.runtime.test.ts b/extensions/qa-lab/src/mantis/run.runtime.test.ts index 89a2e9ee2dd4..6f6c8b5fa29a 100644 --- a/extensions/qa-lab/src/mantis/run.runtime.test.ts +++ b/extensions/qa-lab/src/mantis/run.runtime.test.ts @@ -2,10 +2,19 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { QA_EVIDENCE_FILENAME, buildLiveTransportEvidenceSummary } from "../evidence-summary.js"; import { runMantisBeforeAfter } from "./run.runtime.js"; +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`); +} + describe("mantis before/after runtime", () => { let repoRoot: string; @@ -24,8 +33,8 @@ describe("mantis before/after runtime", () => { if (command !== "pnpm" || !args.includes("openclaw")) { return; } - const repoRootArg = args[args.indexOf("--repo-root") + 1]; - const outputDirArg = args[args.indexOf("--output-dir") + 1]; + const repoRootArg = requireArgAfter(args, "--repo-root"); + const outputDirArg = requireArgAfter(args, "--output-dir"); const lane = outputDirArg.endsWith("baseline") ? "baseline" : "candidate"; const outputDir = path.join(repoRootArg, outputDirArg); await fs.mkdir(outputDir, { recursive: true }); @@ -126,8 +135,8 @@ describe("mantis before/after runtime", () => { if (command !== "pnpm" || !args.includes("openclaw")) { return; } - const repoRootArg = args[args.indexOf("--repo-root") + 1]; - const outputDirArg = args[args.indexOf("--output-dir") + 1]; + const repoRootArg = requireArgAfter(args, "--repo-root"); + const outputDirArg = requireArgAfter(args, "--output-dir"); const lane = outputDirArg.endsWith("baseline") ? "baseline" : "candidate"; const outputDir = path.join(repoRootArg, outputDirArg); await fs.mkdir(outputDir, { recursive: true }); diff --git a/extensions/qa-lab/src/mantis/visual-task.runtime.test.ts b/extensions/qa-lab/src/mantis/visual-task.runtime.test.ts index 87db24772aaa..909580cb3d43 100644 --- a/extensions/qa-lab/src/mantis/visual-task.runtime.test.ts +++ b/extensions/qa-lab/src/mantis/visual-task.runtime.test.ts @@ -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 { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { runMantisVisualDriver, runMantisVisualTask } from "./visual-task.runtime.js"; @@ -22,6 +23,14 @@ function expectArgsContainSequence(args: readonly string[], expected: readonly s expect(startIndex).toBeGreaterThanOrEqual(0); } +function requireArgAfter(args: readonly string[], flag: string): string { + const flagIndex = args.indexOf(flag); + if (flagIndex < 0) { + throw new Error(`Expected ${flag} argument`); + } + return expectDefined(args[flagIndex + 1], `${flag} argument value`); +} + describe("mantis visual task runtime", () => { let repoRoot: string; @@ -56,8 +65,8 @@ describe("mantis visual task runtime", () => { }; } if (command === "/tmp/crabbox" && args[0] === "record") { - const outputPath = args[args.indexOf("--output") + 1]; - const outputDir = args[args.indexOf("--output-dir") + 1]; + const outputPath = requireArgAfter(args, "--output"); + const outputDir = requireArgAfter(args, "--output-dir"); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, "mp4"); await fs.writeFile(path.join(outputDir, "visual-task.png"), "png"); @@ -157,7 +166,7 @@ describe("mantis visual task runtime", () => { }; } if (command === "/tmp/crabbox" && args[0] === "record") { - const outputDir = args[args.indexOf("--output-dir") + 1]; + const outputDir = requireArgAfter(args, "--output-dir"); await fs.mkdir(outputDir, { recursive: true }); await fs.writeFile(path.join(outputDir, "visual-task.png"), "png"); await fs.writeFile( @@ -230,8 +239,8 @@ describe("mantis visual task runtime", () => { }; } if (command === "/tmp/crabbox" && args[0] === "record") { - const outputPath = args[args.indexOf("--output") + 1]; - const outputDir = args[args.indexOf("--output-dir") + 1]; + const outputPath = requireArgAfter(args, "--output"); + const outputDir = requireArgAfter(args, "--output-dir"); stagedVideoPath = outputPath; await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, "mp4"); @@ -296,7 +305,7 @@ describe("mantis visual task runtime", () => { const runner = vi.fn(async (command: string, args: readonly string[]) => { commands.push({ command, args }); if (command === "/tmp/crabbox" && args[0] === "screenshot") { - const outputPath = args[args.indexOf("--output") + 1]; + const outputPath = requireArgAfter(args, "--output"); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, "png"); } @@ -359,10 +368,10 @@ describe("mantis visual task runtime", () => { ]); const promptIndex = visionArgs.indexOf("--prompt"); expect(promptIndex).toBeGreaterThanOrEqual(0); - expect(visionArgs[promptIndex + 1]).toContain("return only valid JSON"); + expect(requireArgAfter(visionArgs, "--prompt")).toContain("return only valid JSON"); const modelIndex = visionArgs.indexOf("--model"); expect(modelIndex).toBeGreaterThanOrEqual(0); - expect(visionArgs[modelIndex + 1]).toBe("openai/gpt-5.4"); + expect(requireArgAfter(visionArgs, "--model")).toBe("openai/gpt-5.4"); expect(result.vision.assertion?.evidence).toBe('The page heading reads "Example Domain".'); expect(result.vision.assertion?.matched).toBe(true); expect(result.vision.assertion?.visible).toBe(true); @@ -371,7 +380,7 @@ describe("mantis visual task runtime", () => { it("fails image-describe text checks when the model gives negative evidence that quotes the target", async () => { const runner = vi.fn(async (command: string, args: readonly string[]) => { if (command === "/tmp/crabbox" && args[0] === "screenshot") { - const outputPath = args[args.indexOf("--output") + 1]; + const outputPath = requireArgAfter(args, "--output"); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, "png"); } @@ -414,7 +423,7 @@ describe("mantis visual task runtime", () => { it("fails metadata mode when text evidence is requested", async () => { const runner = vi.fn(async (command: string, args: readonly string[]) => { if (command === "/tmp/crabbox" && args[0] === "screenshot") { - const outputPath = args[args.indexOf("--output") + 1]; + const outputPath = requireArgAfter(args, "--output"); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, "png"); } diff --git a/extensions/qa-matrix/src/runners/contract/scenarios.test.ts b/extensions/qa-matrix/src/runners/contract/scenarios.test.ts index 9ed42a2a55bb..3b562c9773d0 100644 --- a/extensions/qa-matrix/src/runners/contract/scenarios.test.ts +++ b/extensions/qa-matrix/src/runners/contract/scenarios.test.ts @@ -3,6 +3,7 @@ import { createHash } from "node:crypto"; import { mkdir, mkdtemp, readFile, readdir, rm, stat, 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, beforeEach, vi } from "vitest"; const { createMatrixQaClient } = vi.hoisted(() => ({ createMatrixQaClient: vi.fn(), @@ -939,7 +940,7 @@ describe("matrix live qa scenarios", () => { scenarioTesting.buildMatrixQaTopologyForScenarios({ defaultRoomName: "OpenClaw Matrix QA run", scenarios: [ - MATRIX_QA_SCENARIOS[0], + expectDefined(MATRIX_QA_SCENARIOS[0], "first Matrix QA scenario"), { id: "matrix-secondary-room-open-trigger", standardId: "canary", @@ -3926,7 +3927,7 @@ describe("matrix live qa scenarios", () => { const waitForRoomEvent = vi.fn().mockImplementation(async () => { const callIndex = waitForRoomEvent.mock.calls.length - 1; const mediaCaseIndex = Math.floor(callIndex / 2); - const mediaCase = mediaCases[mediaCaseIndex]; + const mediaCase = expectDefined(mediaCases[mediaCaseIndex], `media case ${mediaCaseIndex}`); const sendOpts = sendMediaMessage.mock.calls[mediaCaseIndex]?.[0]; if (callIndex % 2 === 0) { return { diff --git a/extensions/qqbot/src/engine/engine-import-boundary.test.ts b/extensions/qqbot/src/engine/engine-import-boundary.test.ts index 31b7c7b04ab4..7bcb98e7c5e2 100644 --- a/extensions/qqbot/src/engine/engine-import-boundary.test.ts +++ b/extensions/qqbot/src/engine/engine-import-boundary.test.ts @@ -7,6 +7,7 @@ import fs from "node:fs"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it } from "vitest"; const ENGINE_DIR = path.resolve(import.meta.dirname); @@ -41,7 +42,7 @@ function findOpenclawImports(source: string): string[] { return [ ...source.matchAll(/from\s+["'](openclaw\/[^"']+)["']/g), ...source.matchAll(/import\(\s*["'](openclaw\/[^"']+)["']\s*\)/g), - ].map((match) => match[1]); + ].map((match) => expectDefined(match[1], "OpenClaw import specifier")); } /** Check if an import specifier is an allowed openclaw/plugin-sdk subpath. */ diff --git a/extensions/qqbot/src/engine/gateway/active-cfg.test.ts b/extensions/qqbot/src/engine/gateway/active-cfg.test.ts index 6412fc312124..6ec331da5f07 100644 --- a/extensions/qqbot/src/engine/gateway/active-cfg.test.ts +++ b/extensions/qqbot/src/engine/gateway/active-cfg.test.ts @@ -1,4 +1,5 @@ // Qqbot tests cover active cfg plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { describe, expect, it, vi } from "vitest"; import { createActiveCfgProvider, resolveActiveCfg, type GatewayCfgLoader } from "./active-cfg.js"; @@ -59,7 +60,7 @@ describe("createActiveCfgProvider", () => { let index = 0; const provider = createActiveCfgProvider({ fallback, - load: () => calls[index++], + load: () => expectDefined(calls[index++], `QQBot config load ${index}`), }); expect(provider.getActiveCfg()).toBe(calls[0]); diff --git a/extensions/qqbot/src/engine/gateway/gateway-connection.test.ts b/extensions/qqbot/src/engine/gateway/gateway-connection.test.ts index 55388c3c8c6d..17374b8c9792 100644 --- a/extensions/qqbot/src/engine/gateway/gateway-connection.test.ts +++ b/extensions/qqbot/src/engine/gateway/gateway-connection.test.ts @@ -1,5 +1,6 @@ // Qqbot tests cover gateway connection close/disconnect status behavior. import { EventEmitter } from "node:events"; +import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { EngineAdapters } from "../adapter/index.js"; import { MAX_RECONNECT_ATTEMPTS } from "./constants.js"; @@ -130,13 +131,21 @@ describe("GatewayConnection disconnect status", () => { }); for (let attempt = 0; attempt < MAX_RECONNECT_ATTEMPTS; attempt++) { - sockets[attempt].emit("close", 1006, Buffer.from("")); + expectDefined(sockets[attempt], `QQBot socket ${attempt}`).emit( + "close", + 1006, + Buffer.from(""), + ); await vi.runOnlyPendingTimersAsync(); await vi.waitFor(() => { expect(createQQWSClientMock).toHaveBeenCalledTimes(attempt + 2); }); } - sockets[MAX_RECONNECT_ATTEMPTS].emit("close", 1006, Buffer.from("")); + expectDefined(sockets[MAX_RECONNECT_ATTEMPTS], "final QQBot socket").emit( + "close", + 1006, + Buffer.from(""), + ); expect(onDisconnected).toHaveBeenCalledWith({ reason: "reconnect attempts exhausted", diff --git a/extensions/qqbot/src/engine/utils/stt.test.ts b/extensions/qqbot/src/engine/utils/stt.test.ts index d23f6c5e2ea5..738a43e0badf 100644 --- a/extensions/qqbot/src/engine/utils/stt.test.ts +++ b/extensions/qqbot/src/engine/utils/stt.test.ts @@ -1,6 +1,7 @@ // Qqbot tests cover stt plugin behavior. import * as fs from "node:fs"; import * as path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { withTempDir } from "openclaw/plugin-sdk/test-env"; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -159,7 +160,9 @@ describe("engine/utils/stt", () => { timeoutMs: 90_000, }); - Object.assign(cfg.tools.media.audio.models[0], { timeoutSeconds: 75 }); + Object.assign(expectDefined(cfg.tools.media.audio.models[0], "QQBot STT model"), { + timeoutSeconds: 75, + }); expect(resolveSTTConfig(cfg)?.timeoutMs).toBe(75_000); }); diff --git a/extensions/qqbot/src/state-migrations.test.ts b/extensions/qqbot/src/state-migrations.test.ts index 83507090c7f0..3c9d0a91b603 100644 --- a/extensions/qqbot/src/state-migrations.test.ts +++ b/extensions/qqbot/src/state-migrations.test.ts @@ -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 { createPluginStateKeyedStoreForTests, resetPluginStateStoreForTests, @@ -14,6 +15,10 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { stateMigrations } from "../doctor-contract-api.js"; import { buildQQBotStateKey } from "./engine/utils/state-keys.js"; +function requireStateMigration(index: number) { + return expectDefined(stateMigrations[index], `QQBot state migration ${index}`); +} + type CredentialBackup = { accountId: string; appId: string; @@ -127,7 +132,7 @@ describe("qqbot doctor state migration", () => { }; await writeJson(sourcePath, backup); - const migration = stateMigrations[0]; + const migration = requireStateMigration(0); await expect(migration.detectLegacyState(migrationParams())).resolves.toMatchObject({ preview: [expect.stringContaining("QQBot credential backups: 1 file")], }); @@ -171,7 +176,7 @@ describe("qqbot doctor state migration", () => { savedAt: "2026-06-02T00:00:00.000Z", }); - const result = await stateMigrations[0].migrateLegacyState(migrationParams()); + const result = await requireStateMigration(0).migrateLegacyState(migrationParams()); expect(result.warnings).toEqual([]); await expect( @@ -194,7 +199,7 @@ describe("qqbot doctor state migration", () => { savedAt: "2026-06-02T00:00:00.000Z", }); - await expect(stateMigrations[0].detectLegacyState(migrationParams())).resolves.toBeNull(); + await expect(requireStateMigration(0).detectLegacyState(migrationParams())).resolves.toBeNull(); }); it("does not scan credential backups outside the active state directory", async () => { @@ -210,7 +215,7 @@ describe("qqbot doctor state migration", () => { }, ); - await expect(stateMigrations[0].detectLegacyState(migrationParams())).resolves.toBeNull(); + await expect(requireStateMigration(0).detectLegacyState(migrationParams())).resolves.toBeNull(); }); it("restores credential state and preserves sources when plugin capacity evicts a row", async () => { @@ -233,7 +238,7 @@ describe("qqbot doctor state migration", () => { const params = migrationParams(); params.context = createEvictingDoctorContext({ values, evictedKey: existingKey }); - const result = await stateMigrations[0].migrateLegacyState(params); + const result = await requireStateMigration(0).migrateLegacyState(params); expect(result.changes).toEqual([]); expect(result.warnings).toEqual([expect.stringContaining("plugin state capacity evicted")]); @@ -250,6 +255,6 @@ describe("qqbot doctor state migration", () => { await writeJson(path.join(stateDir, "qqbot", "data", "known-users.json"), []); await fs.writeFile(path.join(stateDir, "qqbot", "data", "ref-index.jsonl"), "{}\n"); - await expect(stateMigrations[0].detectLegacyState(migrationParams())).resolves.toBeNull(); + await expect(requireStateMigration(0).detectLegacyState(migrationParams())).resolves.toBeNull(); }); }); diff --git a/extensions/searxng/src/searxng-client.test.ts b/extensions/searxng/src/searxng-client.test.ts index e608e375ffc3..9c40ff0bc648 100644 --- a/extensions/searxng/src/searxng-client.test.ts +++ b/extensions/searxng/src/searxng-client.test.ts @@ -1,4 +1,5 @@ // Searxng tests cover searxng client plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import type { LookupFn } from "openclaw/plugin-sdk/ssrf-runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -97,8 +98,10 @@ describe("searxng client", () => { }); expect(endpointMockState.calls).toHaveLength(2); - expect(new URL(endpointMockState.calls[0].url).searchParams.get("categories")).toBe("weather"); - expect(new URL(endpointMockState.calls[1].url).searchParams.get("categories")).toBe("general"); + const firstCall = expectDefined(endpointMockState.calls[0], "first SearXNG endpoint call"); + const secondCall = expectDefined(endpointMockState.calls[1], "second SearXNG endpoint call"); + expect(new URL(firstCall.url).searchParams.get("categories")).toBe("weather"); + expect(new URL(secondCall.url).searchParams.get("categories")).toBe("general"); expect(result.provider).toBe("searxng"); expect(result.query).toBe("beijing hourly weather"); expect(result.count).toBe(1); diff --git a/extensions/signal/src/format.chunking.test.ts b/extensions/signal/src/format.chunking.test.ts index cf729de5371f..690bc883e1fe 100644 --- a/extensions/signal/src/format.chunking.test.ts +++ b/extensions/signal/src/format.chunking.test.ts @@ -1,4 +1,5 @@ // Signal tests cover format.chunking plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it } from "vitest"; import { markdownToSignalTextChunks } from "./format.js"; @@ -15,6 +16,10 @@ function expectChunkStyleRangesInBounds(chunks: ReturnType[number]; type SignalTextStyle = SignalTextChunk["styles"][number]; +function requireFirstChunk(chunks: ReturnType): SignalTextChunk { + return expectDefined(chunks[0], "first Signal text chunk"); +} + function requireChunkWithStyle( chunks: ReturnType, styleName: SignalTextStyle["style"], @@ -71,7 +76,7 @@ describe("splitSignalFormattedText", () => { const chunks = markdownToSignalTextChunks(markdown, 100); expect(chunks).toHaveLength(1); - expect(chunks[0].text).toBe("short text"); + expect(requireFirstChunk(chunks).text).toBe("short text"); }); }); @@ -84,7 +89,7 @@ describe("splitSignalFormattedText", () => { expect(chunks.length).toBeGreaterThan(1); // First chunk should contain the bold style - const firstChunk = chunks[0]; + const firstChunk = requireFirstChunk(chunks); expect(firstChunk.text).toContain("bold"); expect(firstChunk.styles.map((style) => style.style)).toContain("BOLD"); // The bold style should start at position 0 in the first chunk @@ -161,7 +166,7 @@ describe("splitSignalFormattedText", () => { const chunks = markdownToSignalTextChunks(markdown, limit); // First chunk should have the complete bold style - const firstChunk = chunks[0]; + const firstChunk = requireFirstChunk(chunks); if (firstChunk.text.includes("bold")) { const boldStyle = requireStyle(firstChunk, "BOLD"); expect(boldStyle.start + boldStyle.length).toBeLessThanOrEqual(firstChunk.text.length); @@ -201,7 +206,7 @@ describe("splitSignalFormattedText", () => { const chunks = markdownToSignalTextChunks(markdown, limit); expect(chunks).toHaveLength(1); - expect(chunks[0].text).toBe("1234567890"); + expect(requireFirstChunk(chunks).text).toBe("1234567890"); }); it("preserves style through whitespace trimming", () => { @@ -210,7 +215,7 @@ describe("splitSignalFormattedText", () => { const chunks = markdownToSignalTextChunks(markdown, limit); // Bold should be preserved in first chunk - const firstChunk = chunks[0]; + const firstChunk = requireFirstChunk(chunks); if (firstChunk.text.includes("bold")) { expect(firstChunk.styles.map((style) => style.style)).toContain("BOLD"); } @@ -302,8 +307,8 @@ describe("markdownToSignalTextChunks", () => { const chunks = markdownToSignalTextChunks(markdown, Number.POSITIVE_INFINITY); expect(chunks).toHaveLength(1); - expect(chunks[0]?.text).toBe("Here's another photo from today's walk."); - expect(chunks[0]?.styles.map((style) => style.style)).toContain("BOLD"); + expect(requireFirstChunk(chunks)?.text).toBe("Here's another photo from today's walk."); + expect(requireFirstChunk(chunks)?.styles.map((style) => style.style)).toContain("BOLD"); }); describe("link expansion chunk limit", () => { diff --git a/extensions/signal/src/monitor/event-handler.mention-gating.test.ts b/extensions/signal/src/monitor/event-handler.mention-gating.test.ts index e34aa49d3902..429f00be065c 100644 --- a/extensions/signal/src/monitor/event-handler.mention-gating.test.ts +++ b/extensions/signal/src/monitor/event-handler.mention-gating.test.ts @@ -1,4 +1,5 @@ // Signal tests cover event handler.mention gating plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { buildDispatchInboundCaptureMock } from "openclaw/plugin-sdk/channel-contract-testing"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime"; @@ -117,7 +118,7 @@ async function expectSkippedGroupHistory(opts: GroupEventOpts, expectedBody: str expect(capturedCtx).toBeUndefined(); const entries = getGroupHistoryEntries(groupHistories); expect(entries).toHaveLength(1); - expect(entries[0].body).toBe(expectedBody); + expect(expectDefined(entries[0], "Signal group history entry").body).toBe(expectedBody); } describe("signal mention gating", () => { @@ -177,8 +178,9 @@ describe("signal mention gating", () => { expect(capturedCtx).toBeUndefined(); const entries = getGroupHistoryEntries(groupHistories); expect(entries).toHaveLength(1); - expect(entries[0].sender).toBe("Alice"); - expect(entries[0].body).toBe("hello from alice"); + const entry = expectDefined(entries[0], "Signal group history entry"); + expect(entry.sender).toBe("Alice"); + expect(entry.body).toBe("hello from alice"); }); it("records attachment placeholder in pending history for skipped attachment-only group messages", async () => { @@ -209,7 +211,7 @@ describe("signal mention gating", () => { expect(capturedCtx).toBeUndefined(); const entries = getGroupHistoryEntries(groupHistories); expect(entries).toHaveLength(1); - expect(entries[0].body).toBe(""); + expect(expectDefined(entries[0], "Signal audio history entry").body).toBe(""); }); it("summarizes multiple skipped attachments with stable file count wording", async () => { @@ -236,7 +238,9 @@ describe("signal mention gating", () => { expect(capturedCtx).toBeUndefined(); const entries = getGroupHistoryEntries(groupHistories); expect(entries).toHaveLength(1); - expect(entries[0].body).toBe("[2 files attached]"); + expect(expectDefined(entries[0], "Signal attachment history entry").body).toBe( + "[2 files attached]", + ); }); it("records quote text in pending history for skipped quote-only group messages", async () => { diff --git a/extensions/slack/src/channel-migration.test.ts b/extensions/slack/src/channel-migration.test.ts index a8a81d06aa5b..a803d58e771e 100644 --- a/extensions/slack/src/channel-migration.test.ts +++ b/extensions/slack/src/channel-migration.test.ts @@ -1,4 +1,5 @@ // Slack tests cover channel migration plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it } from "vitest"; import { migrateSlackChannelConfig, migrateSlackChannelsInPlace } from "./channel-migration.js"; @@ -62,7 +63,9 @@ describe("migrateSlackChannelConfig", () => { expect(result.migrated).toBe(true); expect(result.scopes).toEqual(["account"]); - expect(cfg.channels.slack.accounts.primary.channels).toEqual({ + expect( + expectDefined(cfg.channels.slack.accounts.primary, "primary Slack account").channels, + ).toEqual({ C999: { requireMention: true }, }); }); @@ -80,7 +83,9 @@ describe("migrateSlackChannelConfig", () => { }); expect(result.migrated).toBe(true); - expect(cfg.channels.slack.accounts.Primary.channels).toEqual({ + expect( + expectDefined(cfg.channels.slack.accounts.Primary, "Primary Slack account").channels, + ).toEqual({ C999: {}, }); }); diff --git a/extensions/slack/src/monitor/media.test.ts b/extensions/slack/src/monitor/media.test.ts index b5edb5bc76ad..99132acb3d2a 100644 --- a/extensions/slack/src/monitor/media.test.ts +++ b/extensions/slack/src/monitor/media.test.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Slack tests cover media plugin behavior. import type { WebClient } from "@slack/web-api"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -694,10 +695,12 @@ describe("resolveSlackMedia", () => { const media = expectSlackMediaResult(result); expect(media).toHaveLength(2); - expect(media[0].path).toBe("/tmp/a.jpg"); - expect(media[0].placeholder).toBe("[Slack file: a.jpg (fileId: FA)]"); - expect(media[1].path).toBe("/tmp/b.png"); - expect(media[1].placeholder).toBe("[Slack file: b.png (fileId: FB)]"); + const first = expectDefined(media[0], "first Slack media result"); + const second = expectDefined(media[1], "second Slack media result"); + expect(first.path).toBe("/tmp/a.jpg"); + expect(first.placeholder).toBe("[Slack file: a.jpg (fileId: FA)]"); + expect(second.path).toBe("/tmp/b.png"); + expect(second.placeholder).toBe("[Slack file: b.png (fileId: FB)]"); }); it("caps downloads to 8 files for large multi-attachment messages", async () => { diff --git a/extensions/slack/src/monitor/message-handler/prepare.test.ts b/extensions/slack/src/monitor/message-handler/prepare.test.ts index 93f5ad6baac1..5b1efc36ecfa 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test.ts @@ -1,5 +1,6 @@ // Slack tests cover prepare plugin behavior. import fs from "node:fs/promises"; +import { expectDefined } from "@openclaw/normalization-core"; import type { App } from "@slack/bolt"; import { expectChannelInboundContextContract as expectInboundContextContract } from "openclaw/plugin-sdk/channel-contract-testing"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; @@ -1882,7 +1883,7 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`; message: createMainScopedDmMessage({ thread_ts: "1.000" }), }, ]; - delete cases[2].message.channel_type; + delete expectDefined(cases[2], "missing-channel-type Slack case").message.channel_type; for (const testCase of cases) { const prepared = await prepareMessageWith( diff --git a/extensions/slack/src/send.reconcile.test.ts b/extensions/slack/src/send.reconcile.test.ts index 72a8c9d2be87..ae30b898eaaa 100644 --- a/extensions/slack/src/send.reconcile.test.ts +++ b/extensions/slack/src/send.reconcile.test.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Slack tests cover exact delivery-queue reconciliation through message metadata. import type { MessageMetadata } from "@slack/types"; import type { ChatPostMessageArguments, WebClient } from "@slack/web-api"; @@ -570,11 +571,12 @@ describe("reconcileSlackUnknownSend", () => { } const forgedMetadata: MessageMetadata[] = []; + const firstMetadata = expectDefined(postedMetadata[0], "first posted Slack metadata"); for (const partIndex of [1, 2]) { forgedMetadata.push({ - ...postedMetadata[0], + ...firstMetadata, event_payload: { - ...postedMetadata[0]?.event_payload, + ...firstMetadata.event_payload, openclaw_delivery_part_index: partIndex, }, }); diff --git a/extensions/sms/src/inbound.test.ts b/extensions/sms/src/inbound.test.ts index fc88a19b485f..45838fff21c8 100644 --- a/extensions/sms/src/inbound.test.ts +++ b/extensions/sms/src/inbound.test.ts @@ -1,4 +1,5 @@ // Sms tests cover inbound plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it, vi } from "vitest"; import { dispatchSmsInboundEvent, type SmsChannelRuntime } from "./inbound.js"; import type { sendSmsViaTwilio as sendSmsViaTwilioType } from "./twilio.js"; @@ -144,7 +145,7 @@ describe("dispatchSmsInboundEvent", () => { }, }); - const runParams = run.mock.calls[0]?.[0]; + const runParams = expectDefined(run.mock.calls[0]?.[0], "SMS inbound run parameters"); const ingested = runParams.adapter.ingest({ from: "+15551234567", to: "+15557654321", diff --git a/extensions/synology-chat/src/webhook-handler.test.ts b/extensions/synology-chat/src/webhook-handler.test.ts index 1a62c01d05f6..b4bb5f5d6aec 100644 --- a/extensions/synology-chat/src/webhook-handler.test.ts +++ b/extensions/synology-chat/src/webhook-handler.test.ts @@ -1,4 +1,5 @@ // Synology Chat tests cover webhook handler plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { makeFormBody, makeReq, makeRes, makeStalledReq } from "./test-http-utils.js"; import type { ResolvedSynologyChatAccount } from "./types.js"; @@ -267,7 +268,9 @@ describe("createWebhookHandler", () => { return req; }); const responses = requests.map(() => makeRes()); - const runs = requests.map((req, index) => handler(req, responses[index])); + const runs = requests.map((req, index) => + handler(req, expectDefined(responses[index], `Synology response ${index}`)), + ); // Default maxInFlightPerKey is 8; 12 total requests leaves 4 rejected with 429. expect(countMatching(responses, (res) => res.status === 0)).toBe(8); diff --git a/extensions/telegram/src/bot-message-context.typing.test.ts b/extensions/telegram/src/bot-message-context.typing.test.ts index c7912ff7c710..f80d66c34bd3 100644 --- a/extensions/telegram/src/bot-message-context.typing.test.ts +++ b/extensions/telegram/src/bot-message-context.typing.test.ts @@ -1,9 +1,14 @@ // Telegram tests cover bot message context.typing plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { buildChannelInboundEventContext } from "openclaw/plugin-sdk/channel-inbound"; import { describe, expect, it, vi } from "vitest"; import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; import type { TelegramSendChatActionHandler } from "./sendchataction-401-backoff.js"; +function requireInvocationOrder(mock: { invocationCallOrder: number[] }, context: string): number { + return expectDefined(mock.invocationCallOrder[0], context); +} + function createSendChatActionHandler( sendChatAction = vi.fn(async () => undefined), ): TelegramSendChatActionHandler & { sendChatAction: typeof sendChatAction } { @@ -38,9 +43,9 @@ describe("buildTelegramMessageContext typing", () => { ).resolves.not.toBeNull(); expect(sendChatActionHandler.sendChatAction).toHaveBeenCalledWith(42, "typing", undefined); - expect(sendChatActionHandler.sendChatAction.mock.invocationCallOrder[0]).toBeLessThan( - buildInboundContext.mock.invocationCallOrder[0], - ); + expect( + requireInvocationOrder(sendChatActionHandler.sendChatAction.mock, "send typing invocation"), + ).toBeLessThan(requireInvocationOrder(buildInboundContext.mock, "inbound context invocation")); }); it("does not send direct typing when there is no replyable body", async () => { @@ -114,9 +119,9 @@ describe("buildTelegramMessageContext typing", () => { expect(sendChatActionHandler.sendChatAction).toHaveBeenCalledWith(-1001234567890, "typing", { message_thread_id: 99, }); - expect(sendChatActionHandler.sendChatAction.mock.invocationCallOrder[0]).toBeLessThan( - buildInboundContext.mock.invocationCallOrder[0], - ); + expect( + requireInvocationOrder(sendChatActionHandler.sendChatAction.mock, "send typing invocation"), + ).toBeLessThan(requireInvocationOrder(buildInboundContext.mock, "inbound context invocation")); }); it("does not send forum topic typing for room events", async () => { diff --git a/extensions/telegram/src/bot-message-dispatch.test.ts b/extensions/telegram/src/bot-message-dispatch.test.ts index 0cc8d5904d90..c92d0e4d9faa 100644 --- a/extensions/telegram/src/bot-message-dispatch.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.test.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Telegram tests cover bot message dispatch plugin behavior. import type { Bot } from "grammy"; import { @@ -30,6 +31,14 @@ type DispatchReplyWithBufferedBlockDispatcherArgs = Parameters< TelegramBotDeps["dispatchReplyWithBufferedBlockDispatcher"] >[0]; +function requireInvocationOrder( + mock: { mock: { invocationCallOrder: number[] } }, + index: number, + context: string, +): number { + return expectDefined(mock.mock.invocationCallOrder[index], context); +} + const createTelegramDraftStream = vi.hoisted(() => vi.fn()); const dispatchReplyWithBufferedBlockDispatcher = vi.hoisted(() => vi.fn<(params: DispatchReplyWithBufferedBlockDispatcherArgs) => Promise>(), @@ -424,8 +433,8 @@ describe("dispatchTelegramMessage draft streaming", () => { ) { const calls = stream.finalizeToPreview.mock.calls; expect(calls.length).toBeGreaterThan(0); - const preview = calls[calls.length - 1][0] as { text?: string }; - expect(preview.text).toBe(barText); + const preview = calls.at(-1)?.[0] as { text?: string } | undefined; + expect(preview?.text).toBe(barText); } function createContext(overrides?: Partial): TelegramMessageContext { @@ -1888,7 +1897,10 @@ describe("dispatchTelegramMessage draft streaming", () => { }); const fallback = expectDeliverRepliesParams({ replyToMode: expectedFallbackMode }); - const [fallbackPayload] = fallback.replies as Array>; + const fallbackPayload = expectDefined( + (fallback.replies as Array>)[0], + "unsent suffix fallback payload", + ); expect(fallbackPayload.text).toBe("unsent suffix"); if (keepsReply) { expect(fallbackPayload.replyToId).toBe("1001"); @@ -1929,7 +1941,10 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(answerDraftStream.clear).toHaveBeenCalled(); const fallback = expectDeliverRepliesParams({ replyToMode: expectedFallbackMode }); - const [fallbackPayload] = fallback.replies as Array>; + const fallbackPayload = expectDefined( + (fallback.replies as Array>)[0], + "accepted draft fallback payload", + ); expect(fallbackPayload.text).toBe("final replacement"); if (keepsReply) { expect(fallbackPayload.replyToId).toBe("1001"); @@ -1967,7 +1982,10 @@ describe("dispatchTelegramMessage draft streaming", () => { }); const media = expectDeliverRepliesParams({ replyToMode: expectedMediaMode }); - const [mediaPayload] = media.replies as Array>; + const mediaPayload = expectDefined( + (media.replies as Array>)[0], + "accepted draft media payload", + ); expect(mediaPayload.mediaUrl).toBe("https://example.com/a.png"); if (keepsReply) { expect(mediaPayload.replyToId).toBe("1001"); @@ -3150,8 +3168,10 @@ describe("dispatchTelegramMessage draft streaming", () => { [trailingFinalStatusText], ]); expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1); - expect(answerDraftStream.forceNewMessage.mock.invocationCallOrder[0]).toBeLessThan( - answerDraftStream.update.mock.invocationCallOrder[1], + expect( + requireInvocationOrder(answerDraftStream.forceNewMessage, 0, "first answer draft rotation"), + ).toBeLessThan( + requireInvocationOrder(answerDraftStream.update, 1, "second answer draft update"), ); expect(deliverReplies).not.toHaveBeenCalled(); }); @@ -3373,8 +3393,16 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(answerDraftStream.update).toHaveBeenNthCalledWith(2, "Site B shows Y."); expect(answerDraftStream.update).toHaveBeenNthCalledWith(3, "Final answer"); expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1); - const rotationOrder = answerDraftStream.forceNewMessage.mock.invocationCallOrder[0]; - const secondBlockUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[1]; + const rotationOrder = requireInvocationOrder( + answerDraftStream.forceNewMessage, + 0, + "first answer draft rotation", + ); + const secondBlockUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 1, + "second answer draft update", + ); expect(rotationOrder).toBeLessThan(secondBlockUpdateOrder); expect(deliverReplies).not.toHaveBeenCalled(); }); @@ -3417,10 +3445,22 @@ describe("dispatchTelegramMessage draft streaming", () => { }; expect(fallbackDelivery.replies?.[0]?.text).toBe("Site A shows X."); expect(fallbackDelivery.transcriptMirror).toBeUndefined(); - const clearOrder = answerDraftStream.clear.mock.invocationCallOrder[0]; - const fallbackDeliveryOrder = deliverReplies.mock.invocationCallOrder[0]; - const rotationOrder = answerDraftStream.forceNewMessage.mock.invocationCallOrder[0]; - const secondBlockUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[2]; + const clearOrder = requireInvocationOrder( + answerDraftStream.clear, + 0, + "first answer draft clear", + ); + const fallbackDeliveryOrder = requireInvocationOrder(deliverReplies, 0, "first reply delivery"); + const rotationOrder = requireInvocationOrder( + answerDraftStream.forceNewMessage, + 0, + "first answer draft rotation", + ); + const secondBlockUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 2, + "third answer draft update", + ); expect(clearOrder).toBeLessThan(fallbackDeliveryOrder); expect(fallbackDeliveryOrder).toBeLessThan(rotationOrder); expect(rotationOrder).toBeLessThan(secondBlockUpdateOrder); @@ -3455,9 +3495,21 @@ describe("dispatchTelegramMessage draft streaming", () => { ["Final answer"], ]); expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1); - const firstBlockFlushOrder = answerDraftStream.flush.mock.invocationCallOrder[0]; - const rotationOrder = answerDraftStream.forceNewMessage.mock.invocationCallOrder[0]; - const secondBlockUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[1]; + const firstBlockFlushOrder = requireInvocationOrder( + answerDraftStream.flush, + 0, + "first answer draft flush", + ); + const rotationOrder = requireInvocationOrder( + answerDraftStream.forceNewMessage, + 0, + "first answer draft rotation", + ); + const secondBlockUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 1, + "second answer draft update", + ); expect(firstBlockFlushOrder).toBeLessThan(rotationOrder); expect(rotationOrder).toBeLessThan(secondBlockUpdateOrder); expect(deliverReplies).not.toHaveBeenCalled(); @@ -3482,9 +3534,21 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(answerDraftStream.update).toHaveBeenNthCalledWith(2, "PFX Original block text"); expect(answerDraftStream.update).toHaveBeenNthCalledWith(3, "Final answer"); expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1); - const blockUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[1]; - const rotationOrder = answerDraftStream.forceNewMessage.mock.invocationCallOrder[0]; - const finalUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[2]; + const blockUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 1, + "second answer draft update", + ); + const rotationOrder = requireInvocationOrder( + answerDraftStream.forceNewMessage, + 0, + "first answer draft rotation", + ); + const finalUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 2, + "third answer draft update", + ); expect(blockUpdateOrder).toBeLessThan(rotationOrder); expect(rotationOrder).toBeLessThan(finalUpdateOrder); expect(deliverReplies).not.toHaveBeenCalled(); @@ -3517,9 +3581,21 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, "Site A partial"); expect(answerDraftStream.update).toHaveBeenNthCalledWith(2, "Site B final"); expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1); - const firstPartialUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[0]; - const rotationOrder = answerDraftStream.forceNewMessage.mock.invocationCallOrder[0]; - const visibleBlockUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[1]; + const firstPartialUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 0, + "first answer draft update", + ); + const rotationOrder = requireInvocationOrder( + answerDraftStream.forceNewMessage, + 0, + "first answer draft rotation", + ); + const visibleBlockUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 1, + "second answer draft update", + ); expect(firstPartialUpdateOrder).toBeLessThan(rotationOrder); expect(rotationOrder).toBeLessThan(visibleBlockUpdateOrder); expect(deliverReplies).not.toHaveBeenCalled(); @@ -3544,8 +3620,16 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, "Site A shows X."); expect(answerDraftStream.update).toHaveBeenNthCalledWith(2, "Site B shows Y."); expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1); - const rotationOrder = answerDraftStream.forceNewMessage.mock.invocationCallOrder[0]; - const secondPartialUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[1]; + const rotationOrder = requireInvocationOrder( + answerDraftStream.forceNewMessage, + 0, + "first answer draft rotation", + ); + const secondPartialUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 1, + "second answer draft update", + ); expect(rotationOrder).toBeLessThan(secondPartialUpdateOrder); expect(deliverReplies).not.toHaveBeenCalled(); }); @@ -3581,8 +3665,16 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, "Site A shows X."); expect(answerDraftStream.update).toHaveBeenNthCalledWith(2, "Site B shows Y."); expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1); - const rotationOrder = answerDraftStream.forceNewMessage.mock.invocationCallOrder[0]; - const visibleBlockUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[1]; + const rotationOrder = requireInvocationOrder( + answerDraftStream.forceNewMessage, + 0, + "first answer draft rotation", + ); + const visibleBlockUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 1, + "second answer draft update", + ); expect(rotationOrder).toBeLessThan(visibleBlockUpdateOrder); expect(deliverReplies).not.toHaveBeenCalled(); }); @@ -3615,9 +3707,21 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, "Site A partial"); expect(answerDraftStream.update).toHaveBeenNthCalledWith(2, "Site B partial"); expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1); - const firstPartialUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[0]; - const rotationOrder = answerDraftStream.forceNewMessage.mock.invocationCallOrder[0]; - const nextPartialUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[1]; + const firstPartialUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 0, + "first answer draft update", + ); + const rotationOrder = requireInvocationOrder( + answerDraftStream.forceNewMessage, + 0, + "first answer draft rotation", + ); + const nextPartialUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 1, + "second answer draft update", + ); expect(firstPartialUpdateOrder).toBeLessThan(rotationOrder); expect(rotationOrder).toBeLessThan(nextPartialUpdateOrder); expect(deliverReplies).toHaveBeenCalledTimes(1); @@ -3737,11 +3841,25 @@ describe("dispatchTelegramMessage draft streaming", () => { // so that clear finds no live message id and never deletes the window. if (answerDraftStream.clear.mock.invocationCallOrder.length > 0) { expect( - answerDraftStream.rotateToNewMessageDeferringDelete.mock.invocationCallOrder[0], - ).toBeLessThan(answerDraftStream.clear.mock.invocationCallOrder[0]); + requireInvocationOrder( + answerDraftStream.rotateToNewMessageDeferringDelete, + 0, + "first deferred answer draft rotation", + ), + ).toBeLessThan( + requireInvocationOrder(answerDraftStream.clear, 0, "first answer draft clear"), + ); } - const progressResetOrder = answerDraftStream.forceNewMessage.mock.invocationCallOrder[0]; - const progressUpdateOrder = answerDraftStream.updatePreview.mock.invocationCallOrder[0]; + const progressResetOrder = requireInvocationOrder( + answerDraftStream.forceNewMessage, + 0, + "first answer draft rotation", + ); + const progressUpdateOrder = requireInvocationOrder( + answerDraftStream.updatePreview, + 0, + "first answer preview update", + ); expect(progressResetOrder).toBeLessThan(progressUpdateOrder); expect(deliverReplies).not.toHaveBeenCalled(); }); @@ -3773,8 +3891,14 @@ describe("dispatchTelegramMessage draft streaming", () => { // so that clear finds no live message id and never deletes the window. if (answerDraftStream.clear.mock.invocationCallOrder.length > 0) { expect( - answerDraftStream.rotateToNewMessageDeferringDelete.mock.invocationCallOrder[0], - ).toBeLessThan(answerDraftStream.clear.mock.invocationCallOrder[0]); + requireInvocationOrder( + answerDraftStream.rotateToNewMessageDeferringDelete, + 0, + "first deferred answer draft rotation", + ), + ).toBeLessThan( + requireInvocationOrder(answerDraftStream.clear, 0, "first answer draft clear"), + ); } expect(deliverReplies).not.toHaveBeenCalled(); }); @@ -3823,12 +3947,25 @@ describe("dispatchTelegramMessage draft streaming", () => { // so that clear finds no live message id and never deletes the window. if (answerDraftStream.clear.mock.invocationCallOrder.length > 0) { expect( - answerDraftStream.rotateToNewMessageDeferringDelete.mock.invocationCallOrder[0], - ).toBeLessThan(answerDraftStream.clear.mock.invocationCallOrder[0]); + requireInvocationOrder( + answerDraftStream.rotateToNewMessageDeferringDelete, + 0, + "first deferred answer draft rotation", + ), + ).toBeLessThan( + requireInvocationOrder(answerDraftStream.clear, 0, "first answer draft clear"), + ); } - const rotationOrder = - answerDraftStream.rotateToNewMessageDeferringDelete.mock.invocationCallOrder[0]; - const finalUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[0]; + const rotationOrder = requireInvocationOrder( + answerDraftStream.rotateToNewMessageDeferringDelete, + 0, + "first deferred answer draft rotation", + ); + const finalUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 0, + "first answer draft update", + ); expect(rotationOrder).toBeLessThan(finalUpdateOrder); }); @@ -3856,12 +3993,25 @@ describe("dispatchTelegramMessage draft streaming", () => { // so that clear finds no live message id and never deletes the window. if (answerDraftStream.clear.mock.invocationCallOrder.length > 0) { expect( - answerDraftStream.rotateToNewMessageDeferringDelete.mock.invocationCallOrder[0], - ).toBeLessThan(answerDraftStream.clear.mock.invocationCallOrder[0]); + requireInvocationOrder( + answerDraftStream.rotateToNewMessageDeferringDelete, + 0, + "first deferred answer draft rotation", + ), + ).toBeLessThan( + requireInvocationOrder(answerDraftStream.clear, 0, "first answer draft clear"), + ); } - const rotationOrder = - answerDraftStream.rotateToNewMessageDeferringDelete.mock.invocationCallOrder[0]; - const finalUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[0]; + const rotationOrder = requireInvocationOrder( + answerDraftStream.rotateToNewMessageDeferringDelete, + 0, + "first deferred answer draft rotation", + ); + const finalUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 0, + "first answer draft update", + ); expect(rotationOrder).toBeLessThan(finalUpdateOrder); }); @@ -3884,12 +4034,25 @@ describe("dispatchTelegramMessage draft streaming", () => { // so that clear finds no live message id and never deletes the window. if (answerDraftStream.clear.mock.invocationCallOrder.length > 0) { expect( - answerDraftStream.rotateToNewMessageDeferringDelete.mock.invocationCallOrder[0], - ).toBeLessThan(answerDraftStream.clear.mock.invocationCallOrder[0]); + requireInvocationOrder( + answerDraftStream.rotateToNewMessageDeferringDelete, + 0, + "first deferred answer draft rotation", + ), + ).toBeLessThan( + requireInvocationOrder(answerDraftStream.clear, 0, "first answer draft clear"), + ); } - const rotationOrder = - answerDraftStream.rotateToNewMessageDeferringDelete.mock.invocationCallOrder[0]; - const finalUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[1]; + const rotationOrder = requireInvocationOrder( + answerDraftStream.rotateToNewMessageDeferringDelete, + 0, + "first deferred answer draft rotation", + ); + const finalUpdateOrder = requireInvocationOrder( + answerDraftStream.update, + 1, + "second answer draft update", + ); expect(rotationOrder).toBeLessThan(finalUpdateOrder); }); @@ -3932,8 +4095,12 @@ describe("dispatchTelegramMessage draft streaming", () => { // The final answer is SENT before the window collapses into the bar: sending // first keeps the final at the bottom of the anchored viewport, so shrinking // the tall window above it never drops the final off screen. - expect(deliverReplies.mock.invocationCallOrder[0]).toBeLessThan( - answerDraftStream.finalizeToPreview.mock.invocationCallOrder[0], + expect(requireInvocationOrder(deliverReplies, 0, "first reply delivery")).toBeLessThan( + requireInvocationOrder( + answerDraftStream.finalizeToPreview, + 0, + "first answer draft finalization", + ), ); expect(editMessageTelegram).not.toHaveBeenCalled(); }); @@ -3983,8 +4150,12 @@ describe("dispatchTelegramMessage draft streaming", () => { ); expectDeliveredReply(0, { text: "Terminal block after tool" }); expectWindowCollapsedTo(answerDraftStream, "🛠️ 1 tool call · ⏱️ 1s"); - expect(deliverReplies.mock.invocationCallOrder[0]).toBeLessThan( - answerDraftStream.finalizeToPreview.mock.invocationCallOrder[0], + expect(requireInvocationOrder(deliverReplies, 0, "first reply delivery")).toBeLessThan( + requireInvocationOrder( + answerDraftStream.finalizeToPreview, + 0, + "first answer draft finalization", + ), ); }); @@ -4019,8 +4190,12 @@ describe("dispatchTelegramMessage draft streaming", () => { // the collapse edit. expectDeliveredReply(0, { text: "All done" }); expectWindowCollapsedTo(answerDraftStream, "🛠️ 1 tool call · ⏱️ 1s"); - expect(deliverReplies.mock.invocationCallOrder[0]).toBeLessThan( - answerDraftStream.finalizeToPreview.mock.invocationCallOrder[0], + expect(requireInvocationOrder(deliverReplies, 0, "first reply delivery")).toBeLessThan( + requireInvocationOrder( + answerDraftStream.finalizeToPreview, + 0, + "first answer draft finalization", + ), ); // The bar counters are snapshotted before the final send, so the count is // stable (one tool call — the final's own delivery does not perturb it). @@ -4402,8 +4577,12 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(answerDraftStream.finalizeToPreview).toHaveBeenCalledTimes(1); expectWindowCollapsedTo(answerDraftStream, "💬 2 notes · 🛠️ 2 tool calls · ⏱️ 1s"); expectDeliveredReply(0, { text: "Final answer" }); - expect(deliverReplies.mock.invocationCallOrder[0]).toBeLessThan( - answerDraftStream.finalizeToPreview.mock.invocationCallOrder[0], + expect(requireInvocationOrder(deliverReplies, 0, "first reply delivery")).toBeLessThan( + requireInvocationOrder( + answerDraftStream.finalizeToPreview, + 0, + "first answer draft finalization", + ), ); }); @@ -4626,8 +4805,10 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(answerDraftStream.update).toHaveBeenCalledTimes(1); expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, trailingFinalStatusText); expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(2); - expect(answerDraftStream.forceNewMessage.mock.invocationCallOrder[1]).toBeLessThan( - answerDraftStream.update.mock.invocationCallOrder[0], + expect( + requireInvocationOrder(answerDraftStream.forceNewMessage, 1, "second answer draft rotation"), + ).toBeLessThan( + requireInvocationOrder(answerDraftStream.update, 0, "first answer draft update"), ); // Window collapses in place into the summary bar; the final answer posts // fresh below it. @@ -5715,10 +5896,18 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(reasoningDraftStream.forceNewMessage).not.toHaveBeenCalled(); expect(reasoningDraftStream.clear).toHaveBeenCalledTimes(1); expect( - reasoningDraftStream.rotateToNewMessageDeferringDelete.mock.invocationCallOrder[0], - ).toBeLessThan(reasoningDraftStream.update.mock.invocationCallOrder[1]); - expect(reasoningDraftStream.update.mock.invocationCallOrder[1]).toBeLessThan( - reasoningDraftStream.clear.mock.invocationCallOrder[0], + requireInvocationOrder( + reasoningDraftStream.rotateToNewMessageDeferringDelete, + 0, + "first deferred reasoning draft rotation", + ), + ).toBeLessThan( + requireInvocationOrder(reasoningDraftStream.update, 1, "second reasoning draft update"), + ); + expect( + requireInvocationOrder(reasoningDraftStream.update, 1, "second reasoning draft update"), + ).toBeLessThan( + requireInvocationOrder(reasoningDraftStream.clear, 0, "first reasoning draft clear"), ); expect(replacementMessageId).toBe(3002); }); @@ -7588,11 +7777,31 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(statusReactionController.setCompacting).toHaveBeenCalledTimes(1); expect(statusReactionController.cancelPending).toHaveBeenCalledTimes(1); expect(statusReactionController.setThinking).toHaveBeenCalledTimes(2); - expect(statusReactionController.setCompacting.mock.invocationCallOrder[0]).toBeLessThan( - statusReactionController.cancelPending.mock.invocationCallOrder[0], + expect( + requireInvocationOrder( + statusReactionController.setCompacting, + 0, + "first compacting status reaction", + ), + ).toBeLessThan( + requireInvocationOrder( + statusReactionController.cancelPending, + 0, + "first pending status reaction cancellation", + ), ); - expect(statusReactionController.cancelPending.mock.invocationCallOrder[0]).toBeLessThan( - statusReactionController.setThinking.mock.invocationCallOrder[1], + expect( + requireInvocationOrder( + statusReactionController.cancelPending, + 0, + "first pending status reaction cancellation", + ), + ).toBeLessThan( + requireInvocationOrder( + statusReactionController.setThinking, + 1, + "second thinking status reaction", + ), ); }); diff --git a/extensions/telegram/src/bot-message.test.ts b/extensions/telegram/src/bot-message.test.ts index 739462e4a09c..f77bc1d09070 100644 --- a/extensions/telegram/src/bot-message.test.ts +++ b/extensions/telegram/src/bot-message.test.ts @@ -1,4 +1,5 @@ // Telegram tests cover bot message plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { TelegramBotDeps } from "./bot-deps.js"; import type { TelegramMessageProcessingResult } from "./bot-processing-outcome.js"; @@ -13,6 +14,10 @@ const upsertChannelPairingRequest = vi.hoisted(() => vi.fn(async () => ({ code: "PAIRCODE", created: true })), ); +function requireInvocationOrder(mock: { invocationCallOrder: number[] }, context: string): number { + return expectDefined(mock.invocationCallOrder[0], context); +} + vi.mock("openclaw/plugin-sdk/runtime-env", () => ({ createSubsystemLogger: () => ({ child: () => ({ @@ -163,8 +168,8 @@ describe("telegram bot message processor", () => { expect(sendTyping).toHaveBeenCalledTimes(1); expect(dispatchTelegramMessage).toHaveBeenCalledTimes(1); - expect(sendTyping.mock.invocationCallOrder[0]).toBeLessThan( - dispatchTelegramMessage.mock.invocationCallOrder[0], + expect(requireInvocationOrder(sendTyping.mock, "send typing invocation")).toBeLessThan( + requireInvocationOrder(dispatchTelegramMessage.mock, "message dispatch invocation"), ); expect(telegramInboundInfo).toHaveBeenCalledWith( "Inbound message telegram:123 -> @openclaw_bot (direct, 11 chars)", @@ -225,11 +230,11 @@ describe("telegram bot message processor", () => { expect(sendTyping).toHaveBeenCalledTimes(1); expect(onDispatchStart).toHaveBeenCalledTimes(1); expect(dispatchTelegramMessage).toHaveBeenCalledTimes(1); - expect(sendTyping.mock.invocationCallOrder[0]).toBeLessThan( - onDispatchStart.mock.invocationCallOrder[0], + expect(requireInvocationOrder(sendTyping.mock, "send typing invocation")).toBeLessThan( + requireInvocationOrder(onDispatchStart.mock, "dispatch-start invocation"), ); - expect(onDispatchStart.mock.invocationCallOrder[0]).toBeLessThan( - dispatchTelegramMessage.mock.invocationCallOrder[0], + expect(requireInvocationOrder(onDispatchStart.mock, "dispatch-start invocation")).toBeLessThan( + requireInvocationOrder(dispatchTelegramMessage.mock, "message dispatch invocation"), ); }); diff --git a/extensions/telegram/src/bot-native-command-menu.test.ts b/extensions/telegram/src/bot-native-command-menu.test.ts index 1451073dd16c..4d13a5c35277 100644 --- a/extensions/telegram/src/bot-native-command-menu.test.ts +++ b/extensions/telegram/src/bot-native-command-menu.test.ts @@ -1,4 +1,5 @@ // Telegram tests cover bot native command menu plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it, vi } from "vitest"; import { buildCappedTelegramMenuCommands, @@ -81,7 +82,7 @@ describe("bot-native-command-menu", () => { const allCommands = [ ...canonicalCommands.slice(0, 99), { command: "side", description: "Alias", isAlias: true }, - canonicalCommands[99], + expectDefined(canonicalCommands[99], "last canonical Telegram command"), ]; const result = buildCappedTelegramMenuCommands({ allCommands }); diff --git a/extensions/telegram/src/bot-native-commands.test.ts b/extensions/telegram/src/bot-native-commands.test.ts index cc38436846ad..5fba7f814a9e 100644 --- a/extensions/telegram/src/bot-native-commands.test.ts +++ b/extensions/telegram/src/bot-native-commands.test.ts @@ -1,4 +1,5 @@ // Telegram tests cover bot native commands plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import type { OpenClawConfig, TelegramAccountConfig } from "openclaw/plugin-sdk/config-contracts"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -291,9 +292,10 @@ describe("registerTelegramNativeCommands", () => { const registeredCommands = await waitForRegisteredCommands(setMyCommands); expect(registeredCommands).toHaveLength(92); + const firstCustomCommand = expectDefined(customCommands[0], "first custom Telegram command"); expect( registeredCommands.some( - (entry) => entry.description.length < customCommands[0].description.length, + (entry) => entry.description.length < firstCustomCommand.description.length, ), ).toBe(true); expect(runtimeLog).toHaveBeenCalledWith( diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index ba872e0a17b6..01edfc9ebf4e 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -2,6 +2,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { escapeRegExp, formatEnvelopeTimestamp } from "openclaw/plugin-sdk/channel-test-helpers"; import type { TelegramGroupConfig } from "openclaw/plugin-sdk/config-contracts"; import type { GetReplyOptions, MsgContext } from "openclaw/plugin-sdk/reply-runtime"; @@ -2081,7 +2082,7 @@ describe("createTelegramBot", () => { Math.min(pairingUpsertCall, testCase.pairingUpsertResults.length - 1) ]; pairingUpsertCall += 1; - return result; + return expectDefined(result, `pairing upsert result ${pairingUpsertCall}`); }); createTelegramBot({ token: "tok" }); diff --git a/extensions/telegram/src/fetch.test.ts b/extensions/telegram/src/fetch.test.ts index 15dc6dfd9a98..b5dd99fd0d39 100644 --- a/extensions/telegram/src/fetch.test.ts +++ b/extensions/telegram/src/fetch.test.ts @@ -2,6 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { resolveFetch } from "openclaw/plugin-sdk/fetch-runtime"; import { MAX_DATE_TIMESTAMP_MS } from "openclaw/plugin-sdk/number-runtime"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -655,9 +656,12 @@ describe("resolveTelegramFetch", () => { expect(transport.fetch).not.toBe(transport.sourceFetch); expect(transport.dispatcherAttempts).toHaveLength(3); - const [defaultAttempt, ipv4Attempt, pinnedAttempt] = transport.dispatcherAttempts as Array<{ + const attempts = transport.dispatcherAttempts as Array<{ dispatcherPolicy?: DirectTelegramDispatcherPolicy; }>; + const defaultAttempt = expectDefined(attempts[0], "default Telegram dispatcher attempt"); + const ipv4Attempt = expectDefined(attempts[1], "IPv4 Telegram dispatcher attempt"); + const pinnedAttempt = expectDefined(attempts[2], "pinned Telegram dispatcher attempt"); const defaultPolicy = defaultAttempt.dispatcherPolicy; const ipv4Policy = ipv4Attempt.dispatcherPolicy; @@ -694,9 +698,10 @@ describe("resolveTelegramFetch", () => { ).resolves.toEqual({ ok: true }); // Only the default dispatcher — no IPv4 fallback or pinned IP attempts expect(transport.dispatcherAttempts).toHaveLength(1); - const [defaultAttempt] = transport.dispatcherAttempts as Array<{ + const attempts = transport.dispatcherAttempts as Array<{ dispatcherPolicy?: DirectTelegramDispatcherPolicy; }>; + const defaultAttempt = expectDefined(attempts[0], "default Telegram dispatcher attempt"); expect(defaultAttempt.dispatcherPolicy?.mode).toBe("direct"); expect(defaultAttempt.dispatcherPolicy?.connect?.autoSelectFamily).toBe(true); }); @@ -1330,7 +1335,7 @@ describe("resolveTelegramFetch", () => { dnsResultOrder: "ipv4first", }, }); - const instance = AgentCtor.mock.instances[0]; + const instance = expectDefined(AgentCtor.mock.instances[0], "Telegram dispatcher instance"); await transport.close(); await transport.close(); @@ -1346,7 +1351,7 @@ describe("resolveTelegramFetch", () => { dnsResultOrder: "ipv4first", }, }); - const instance = AgentCtor.mock.instances[0]; + const instance = expectDefined(AgentCtor.mock.instances[0], "Telegram dispatcher instance"); instance.destroy.mockRejectedValueOnce(new Error("already destroyed")); await expect(transport.close()).resolves.toBeUndefined(); diff --git a/extensions/telegram/src/group-migration.test.ts b/extensions/telegram/src/group-migration.test.ts index 40a4f06a3976..4fa1254f4f5c 100644 --- a/extensions/telegram/src/group-migration.test.ts +++ b/extensions/telegram/src/group-migration.test.ts @@ -1,4 +1,5 @@ // Telegram tests cover group migration plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it } from "vitest"; import { migrateTelegramGroupConfig, migrateTelegramGroupsInPlace } from "./group-migration.js"; @@ -62,7 +63,9 @@ describe("migrateTelegramGroupConfig", () => { expect(result.migrated).toBe(true); expect(result.scopes).toEqual(["account"]); - expect(cfg.channels.telegram.accounts.primary.groups).toEqual({ + expect( + expectDefined(cfg.channels.telegram.accounts.primary, "primary Telegram account").groups, + ).toEqual({ "-100123": { requireMention: true }, }); }); @@ -80,7 +83,9 @@ describe("migrateTelegramGroupConfig", () => { }); expect(result.migrated).toBe(true); - expect(cfg.channels.telegram.accounts.Primary.groups).toEqual({ + expect( + expectDefined(cfg.channels.telegram.accounts.Primary, "Primary Telegram account").groups, + ).toEqual({ "-100123": {}, }); }); diff --git a/extensions/telegram/src/outbound-adapter.test.ts b/extensions/telegram/src/outbound-adapter.test.ts index 5596af2ee1b0..ab0616f5592d 100644 --- a/extensions/telegram/src/outbound-adapter.test.ts +++ b/extensions/telegram/src/outbound-adapter.test.ts @@ -1,4 +1,5 @@ // Telegram tests cover outbound adapter plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { verifyDurableFinalCapabilityProofs } from "openclaw/plugin-sdk/channel-outbound"; import { adaptMessagePresentationForChannel } from "openclaw/plugin-sdk/interactive-runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -354,8 +355,16 @@ describe("telegramOutbound", () => { "Done", expect.objectContaining({ accountId: "ops", replyToMessageId: 777 }), ); - expect(reactMessageTelegramMock.mock.invocationCallOrder[0]).toBeLessThan( - sendMessageTelegramMock.mock.invocationCallOrder[0], + expect( + expectDefined( + reactMessageTelegramMock.mock.invocationCallOrder[0], + "Telegram reaction invocation", + ), + ).toBeLessThan( + expectDefined( + sendMessageTelegramMock.mock.invocationCallOrder[0], + "Telegram send invocation", + ), ); }); diff --git a/extensions/telegram/src/polling-session.test.ts b/extensions/telegram/src/polling-session.test.ts index 290ea9ad41bc..20c398a9e3b6 100644 --- a/extensions/telegram/src/polling-session.test.ts +++ b/extensions/telegram/src/polling-session.test.ts @@ -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 { ChannelAccountSnapshot } from "openclaw/plugin-sdk/channel-contract"; import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime"; import { @@ -216,13 +217,15 @@ function installPollingStallWatchdogHarness(dateNowSequence: readonly number[] = watchdogs.push(watchdog); resolveWatchdog?.(watchdog); for (let index = watchdogWaiters.length - 1; index >= 0; index -= 1) { - const waiter = watchdogWaiters[index]; + const waiter = expectDefined(watchdogWaiters[index], `watchdog waiter ${index}`); if (watchdogs.length < waiter.count) { continue; } realClearTimeout(waiter.timeout); watchdogWaiters.splice(index, 1); - waiter.resolve(watchdogs[waiter.count - 1]); + waiter.resolve( + expectDefined(watchdogs[waiter.count - 1], `watchdog callback ${waiter.count}`), + ); } } return 1 as unknown as ReturnType; diff --git a/extensions/telegram/src/telegram-ingress-worker.runtime.test.ts b/extensions/telegram/src/telegram-ingress-worker.runtime.test.ts index 301411307466..65baf6d7908c 100644 --- a/extensions/telegram/src/telegram-ingress-worker.runtime.test.ts +++ b/extensions/telegram/src/telegram-ingress-worker.runtime.test.ts @@ -1,4 +1,5 @@ // Telegram tests cover ingress worker runtime behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { TelegramIngressWorkerCommand, @@ -49,7 +50,8 @@ function createRuntime(responses: Response[]): { }; const fetchImpl: typeof fetch = async () => { calls.push(Date.now()); - return responses[Math.min(calls.length - 1, responses.length - 1)]; + const responseIndex = Math.min(calls.length - 1, responses.length - 1); + return expectDefined(responses[responseIndex], `Telegram response ${responseIndex}`); }; const done = runTelegramIngressWorkerRuntime({ options: { @@ -102,7 +104,9 @@ describe("telegram ingress worker retry policy", () => { await runtime.done; expect(runtime.calls).toHaveLength(2); - expect(runtime.calls[1] - runtime.calls[0]).toBe(50); + const secondCall = expectDefined(runtime.calls[1], "second Telegram poll call"); + const firstCall = expectDefined(runtime.calls[0], "first Telegram poll call"); + expect(secondCall - firstCall).toBe(50); expect(runtime.messages).toContainEqual( expect.objectContaining({ type: "poll-success", count: 0 }), ); @@ -131,7 +135,9 @@ describe("telegram ingress worker retry policy", () => { await runtime.done; expect(runtime.calls).toHaveLength(2); - expect(runtime.calls[1] - runtime.calls[0]).toBe(1000); + const secondCall = expectDefined(runtime.calls[1], "second Telegram poll call"); + const firstCall = expectDefined(runtime.calls[0], "first Telegram poll call"); + expect(secondCall - firstCall).toBe(1000); }); it("retries a non-json getUpdates 502 response as a server error", async () => { diff --git a/extensions/thread-ownership/index.test.ts b/extensions/thread-ownership/index.test.ts index 3804f65d5e64..8cb49f32a709 100644 --- a/extensions/thread-ownership/index.test.ts +++ b/extensions/thread-ownership/index.test.ts @@ -1,10 +1,13 @@ // Thread Ownership tests cover index plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawPluginApi } from "./api.js"; import register from "./index.js"; describe("thread-ownership plugin", () => { const hooks: Record = {}; + const requireHook = (name: string): Function => + expectDefined(hooks[name], `thread-ownership ${name} hook registration`); const fetchMock = vi.fn() as unknown as typeof globalThis.fetch; let configFile: Record = {}; const originalSlackForwarderUrl = process.env.SLACK_FORWARDER_URL; @@ -85,14 +88,14 @@ describe("thread-ownership plugin", () => { }); async function sendSlackThreadMessage() { - return await hooks.message_sending( + return await requireHook("message_sending")( { content: "hello", replyToId: "1234.5678", metadata: { channelId: "C123" }, to: "C123" }, { channelId: "slack", conversationId: "C123" }, ); } it("allows non-slack channels", async () => { - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "hello", replyToId: "1234.5678", metadata: { channelId: "C123" }, to: "C123" }, { channelId: "discord", conversationId: "C123" }, ); @@ -102,7 +105,7 @@ describe("thread-ownership plugin", () => { }); it("allows top-level messages (no threadTs)", async () => { - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "hello", metadata: {}, to: "C123" }, { channelId: "slack", conversationId: "C123" }, ); @@ -112,7 +115,7 @@ describe("thread-ownership plugin", () => { }); it("fails open when Slack thread routing has no canonical conversation id", async () => { - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "hello", replyToId: "1234.5678", metadata: {}, to: "" }, { channelId: "slack", conversationId: "" }, ); @@ -141,7 +144,7 @@ describe("thread-ownership plugin", () => { new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }), ); - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "hello", replyToId: "1234.5678", @@ -163,7 +166,7 @@ describe("thread-ownership plugin", () => { new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }), ); - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "hello", replyToId: "1234.5678", @@ -187,7 +190,7 @@ describe("thread-ownership plugin", () => { new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }), ); - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "hello", replyToId: "1234.5678", @@ -220,7 +223,7 @@ describe("thread-ownership plugin", () => { }; register.register(api as unknown as OpenClawPluginApi); - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "hello", replyToId: "1234.5678", @@ -240,7 +243,7 @@ describe("thread-ownership plugin", () => { new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }), ); - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "hello", replyToId: "1234.5678", @@ -287,7 +290,7 @@ describe("thread-ownership plugin", () => { it("tracks @-mentions and skips ownership check for mentioned threads", async () => { // Simulate receiving a message that @-mentions the agent. - await hooks.message_received( + await requireHook("message_received")( { content: "Hey @TestBot help me", threadId: "9999.0001", @@ -297,7 +300,7 @@ describe("thread-ownership plugin", () => { ); // Now send in the same thread -- should skip the ownership HTTP call. - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "Sure!", replyToId: "9999.0001", metadata: { channelId: "C456" }, to: "C456" }, { channelId: "slack", conversationId: "C456" }, ); @@ -307,7 +310,7 @@ describe("thread-ownership plugin", () => { }); it("tracks mentions under the shared conversationId when inbound metadata is non-canonical", async () => { - await hooks.message_received( + await requireHook("message_received")( { content: "Hey @TestBot help me", threadId: "9999.0002", @@ -316,7 +319,7 @@ describe("thread-ownership plugin", () => { { channelId: "slack", conversationId: "C456" }, ); - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "Sure!", replyToId: "9999.0002", @@ -330,7 +333,7 @@ describe("thread-ownership plugin", () => { }); it("canonicalizes inbound non-canonical metadata without shared conversation context", async () => { - await hooks.message_received( + await requireHook("message_received")( { content: "Hey @TestBot help me", threadId: "9999.0003", @@ -339,7 +342,7 @@ describe("thread-ownership plugin", () => { { channelId: "slack", conversationId: "" }, ); - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "Sure!", replyToId: "9999.0003", @@ -354,7 +357,7 @@ describe("thread-ownership plugin", () => { it("ignores @-mentions on non-slack channels", async () => { // Use a unique thread key so module-level state from other tests doesn't interfere. - await hooks.message_received( + await requireHook("message_received")( { content: "Hey @TestBot", threadId: "7777.0001", metadata: { channelId: "C999" } }, { channelId: "discord", conversationId: "C999" }, ); @@ -364,7 +367,7 @@ describe("thread-ownership plugin", () => { new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }), ); - await hooks.message_sending( + await requireHook("message_sending")( { content: "Sure!", replyToId: "7777.0001", metadata: { channelId: "C999" }, to: "C999" }, { channelId: "slack", conversationId: "C999" }, ); @@ -373,7 +376,7 @@ describe("thread-ownership plugin", () => { }); it("tracks bot user ID mentions via <@U999> syntax", async () => { - await hooks.message_received( + await requireHook("message_received")( { content: "Hey <@U999> help", threadId: "8888.0001", @@ -382,7 +385,7 @@ describe("thread-ownership plugin", () => { { channelId: "slack", conversationId: "C789" }, ); - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "On it!", replyToId: "8888.0001", metadata: { channelId: "C789" }, to: "C789" }, { channelId: "slack", conversationId: "C789" }, ); @@ -392,7 +395,7 @@ describe("thread-ownership plugin", () => { }); it("tracks agent-name mentions case-insensitively", async () => { - await hooks.message_received( + await requireHook("message_received")( { content: "hey @testbot help", threadId: "8888.0002", @@ -401,7 +404,7 @@ describe("thread-ownership plugin", () => { { channelId: "slack", conversationId: "C789" }, ); - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "On it!", replyToId: "8888.0002", metadata: { channelId: "C789" }, to: "C789" }, { channelId: "slack", conversationId: "C789" }, ); @@ -421,7 +424,7 @@ describe("thread-ownership plugin", () => { new Response(JSON.stringify({ owner: "live-agent" }), { status: 200 }), ); - await hooks.message_sending( + await requireHook("message_sending")( { content: "On it!", replyToId: "8888.0005", metadata: { channelId: "C789" }, to: "C789" }, { channelId: "slack", conversationId: "C789" }, ); @@ -441,7 +444,7 @@ describe("thread-ownership plugin", () => { }, }; - await hooks.message_received( + await requireHook("message_received")( { content: "hey @LiveBot help", threadId: "8888.0006", @@ -450,7 +453,7 @@ describe("thread-ownership plugin", () => { { channelId: "slack", conversationId: "C789" }, ); - const result = await hooks.message_sending( + const result = await requireHook("message_sending")( { content: "On it!", replyToId: "8888.0006", metadata: { channelId: "C789" }, to: "C789" }, { channelId: "slack", conversationId: "C789" }, ); @@ -460,7 +463,7 @@ describe("thread-ownership plugin", () => { }); it("does not treat superset handles as agent-name mentions", async () => { - await hooks.message_received( + await requireHook("message_received")( { content: "hey @testbot2 help", threadId: "8888.0003", @@ -473,7 +476,7 @@ describe("thread-ownership plugin", () => { new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }), ); - await hooks.message_sending( + await requireHook("message_sending")( { content: "On it!", replyToId: "8888.0003", metadata: { channelId: "C789" }, to: "C789" }, { channelId: "slack", conversationId: "C789" }, ); @@ -482,7 +485,7 @@ describe("thread-ownership plugin", () => { }); it("does not treat email-like text as an agent-name mention", async () => { - await hooks.message_received( + await requireHook("message_received")( { content: "send mail to foo@testbot.com", threadId: "8888.0004", @@ -495,7 +498,7 @@ describe("thread-ownership plugin", () => { new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }), ); - await hooks.message_sending( + await requireHook("message_sending")( { content: "On it!", replyToId: "8888.0004", metadata: { channelId: "C789" }, to: "C789" }, { channelId: "slack", conversationId: "C789" }, ); diff --git a/extensions/tlon/src/outbound-tool-trace-sanitize.test.ts b/extensions/tlon/src/outbound-tool-trace-sanitize.test.ts index 954ee83b7873..fbdcbe13ba3c 100644 --- a/extensions/tlon/src/outbound-tool-trace-sanitize.test.ts +++ b/extensions/tlon/src/outbound-tool-trace-sanitize.test.ts @@ -2,6 +2,7 @@ // shared delivery hook, target routing, Markdown rendering, and media captions // cannot drift apart unnoticed. import http from "node:http"; +import { expectDefined } from "@openclaw/normalization-core"; import { createTestRegistry, deliverOutboundPayloads, @@ -65,7 +66,7 @@ describe("tlon outbound assistant-visible sanitization", () => { expect(request.headers.cookie).toBe("urbauth-~zod=test-cookie"); const payload = JSON.parse(body) as CapturedPoke[]; expect(payload).toHaveLength(1); - pokes.push(payload[0]); + pokes.push(expectDefined(payload[0], "Urbit HTTP poke")); response.writeHead(204, { Connection: "close" }); response.end(); return; @@ -153,18 +154,18 @@ describe("tlon outbound assistant-visible sanitization", () => { { app: "chat", mark: "chat-dm-action" }, ]); - const dmJson = JSON.stringify(pokes[0].json); + const dmJson = JSON.stringify(expectDefined(pokes[0], "DM poke").json); expect(dmJson).toContain('"bold":["Done."]'); expect(dmJson).not.toContain("search private repos"); expect(dmJson).toContain("documented example (agent)"); - const groupJson = JSON.stringify(pokes[1].json); + const groupJson = JSON.stringify(expectDefined(pokes[1], "group poke").json); expect(groupJson).toContain('"tag":"h2"'); expect(groupJson).toContain("All good."); expect(groupJson).not.toContain("tool_call"); expect(groupJson).not.toContain("private_exec"); - const mediaJson = JSON.stringify(pokes[2].json); + const mediaJson = JSON.stringify(expectDefined(pokes[2], "media poke").json); expect(mediaJson).toContain('"bold":["Caption."]'); expect(mediaJson).toContain('"src":"https://media.example/image.png"'); expect(mediaJson).not.toContain("read private file"); diff --git a/extensions/twitch/src/twitch-client.test.ts b/extensions/twitch/src/twitch-client.test.ts index 41c40b2f8c8a..1addc8990262 100644 --- a/extensions/twitch/src/twitch-client.test.ts +++ b/extensions/twitch/src/twitch-client.test.ts @@ -9,6 +9,7 @@ * - Error handling and edge cases */ +import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { resolveTwitchToken } from "./token.js"; import { TwitchClientManager } from "./twitch-client.js"; @@ -644,7 +645,7 @@ describe("TwitchClientManager", () => { await manager.getClient(testAccount); // Get the onMessage callback - const onMessageCallback = messageHandlers[0]; + const onMessageCallback = expectDefined(messageHandlers[0], "Twitch message handler"); if (!onMessageCallback) { throw new Error("onMessageCallback not found"); } @@ -674,7 +675,7 @@ describe("TwitchClientManager", () => { it("should normalize channel names without # prefix", async () => { await manager.getClient(testAccount); - const onMessageCallback = messageHandlers[0]; + const onMessageCallback = expectDefined(messageHandlers[0], "Twitch message handler"); onMessageCallback("testchannel", "testuser", "Test", { userInfo: { @@ -695,7 +696,7 @@ describe("TwitchClientManager", () => { it("should include user role flags in message", async () => { await manager.getClient(testAccount); - const onMessageCallback = messageHandlers[0]; + const onMessageCallback = expectDefined(messageHandlers[0], "Twitch message handler"); onMessageCallback("#testchannel", "moduser", "Test", { userInfo: { @@ -719,7 +720,7 @@ describe("TwitchClientManager", () => { it("should handle broadcaster messages", async () => { await manager.getClient(testAccount); - const onMessageCallback = messageHandlers[0]; + const onMessageCallback = expectDefined(messageHandlers[0], "Twitch message handler"); onMessageCallback("#testchannel", "broadcaster", "Test", { userInfo: { diff --git a/extensions/venice/models.test.ts b/extensions/venice/models.test.ts index a01948b89d92..9d12c2d1a65c 100644 --- a/extensions/venice/models.test.ts +++ b/extensions/venice/models.test.ts @@ -1,4 +1,5 @@ // Venice tests cover models plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { clearLiveCatalogCacheForTests } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; import { afterEach, describe, expect, it, vi } from "vitest"; import { @@ -118,7 +119,7 @@ describe("venice-models", () => { }); it("buildVeniceModelDefinition returns config with required fields", () => { - const entry = VENICE_MODEL_CATALOG[0]; + const entry = expectDefined(VENICE_MODEL_CATALOG[0], "first Venice catalog model"); const def = buildVeniceModelDefinition(entry); expect(def.id).toBe(entry.id); expect(def.name).toBe(entry.name); diff --git a/extensions/voice-call/doctor-contract-api.test.ts b/extensions/voice-call/doctor-contract-api.test.ts index 9cc2469d99ac..47cecf086d05 100644 --- a/extensions/voice-call/doctor-contract-api.test.ts +++ b/extensions/voice-call/doctor-contract-api.test.ts @@ -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 { createPluginStateKeyedStoreForTests, createPluginStateSyncKeyedStoreForTests, @@ -88,7 +89,10 @@ describe("voice-call doctor state migration", () => { }, }, }; - const result = await stateMigrations[0].migrateLegacyState({ + const result = await expectDefined( + stateMigrations[0], + "voice-call state migration", + ).migrateLegacyState({ config, env: warmEnv, stateDir: warmStateDir, @@ -179,7 +183,7 @@ describe("voice-call doctor state migration", () => { }, ]); - const migration = stateMigrations[0]; + const migration = expectDefined(stateMigrations[0], "voice-call state migration"); const config = { plugins: { entries: { @@ -256,7 +260,7 @@ describe("voice-call doctor state migration", () => { } finally { db.close(); } - const migration = stateMigrations[0]; + const migration = expectDefined(stateMigrations[0], "voice-call state migration"); const config = { plugins: { entries: { @@ -323,7 +327,10 @@ describe("voice-call doctor state migration", () => { }, }, }; - const result = await stateMigrations[0].migrateLegacyState({ + const result = await expectDefined( + stateMigrations[0], + "voice-call state migration", + ).migrateLegacyState({ config, env, stateDir, diff --git a/extensions/voice-call/src/webhook/realtime-handler.test.ts b/extensions/voice-call/src/webhook/realtime-handler.test.ts index f70c0466b0b9..ace5805c2d04 100644 --- a/extensions/voice-call/src/webhook/realtime-handler.test.ts +++ b/extensions/voice-call/src/webhook/realtime-handler.test.ts @@ -1,5 +1,6 @@ // Voice Call tests cover realtime handler plugin behavior. import http from "node:http"; +import { expectDefined } from "@openclaw/normalization-core"; import type { RealtimeVoiceBridge, RealtimeVoiceForcedConsultCoordinator, @@ -144,7 +145,7 @@ const startRealtimeServer = async ( } return await startUpgradeWsServer({ - urlPath: match[1], + urlPath: expectDefined(match[1], "realtime stream path"), onUpgrade: (request, socket, head) => { handler.handleWebSocketUpgrade(request, socket, head); }, @@ -369,7 +370,7 @@ describe("RealtimeCallHandler path routing", () => { throw new Error("Failed to extract realtime stream path"); } const server = await startUpgradeWsServer({ - urlPath: match[1], + urlPath: expectDefined(match[1], "realtime stream path"), onUpgrade: (request, socket, head) => { handler.handleWebSocketUpgrade(request, socket, head); }, diff --git a/extensions/whatsapp/src/auto-reply/monitor/process-message.test.ts b/extensions/whatsapp/src/auto-reply/monitor/process-message.test.ts index 8f463364dc81..f54e13be39d2 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/process-message.test.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/process-message.test.ts @@ -316,7 +316,7 @@ describe("processMessage group system prompt wiring", () => { expect(shouldComputeCommandAuthorizedMock).toHaveBeenCalledWith("/status", {}); expect(isControlCommandMessageMock).toHaveBeenCalledWith("/status", {}); - expect(buildContextMock.mock.calls[0][0]).toMatchObject({ + expect(mockCallArg(buildContextMock, "buildWhatsAppInboundContext")).toMatchObject({ commandBody: "/status", commandAuthorized: true, commandTurn: { @@ -343,7 +343,7 @@ describe("processMessage group system prompt wiring", () => { expect(shouldComputeCommandAuthorizedMock).toHaveBeenCalledWith("/reset", {}); expect(isControlCommandMessageMock).toHaveBeenCalledWith("/reset", {}); - expect(buildContextMock.mock.calls[0][0]).toMatchObject({ + expect(mockCallArg(buildContextMock, "buildWhatsAppInboundContext")).toMatchObject({ bodyForAgent: "/reset\n\n[whatsapp attachment unavailable]", commandBody: "/reset", rawBody: "/reset", @@ -359,7 +359,7 @@ describe("processMessage group system prompt wiring", () => { msg: makeBaseMsg({ body: "please inspect `/tmp/foo`" }), }); - expect(buildContextMock.mock.calls[0][0]).toMatchObject({ + expect(mockCallArg(buildContextMock, "buildWhatsAppInboundContext")).toMatchObject({ commandBody: "please inspect `/tmp/foo`", commandAuthorized: true, commandTurn: { @@ -370,7 +370,9 @@ describe("processMessage group system prompt wiring", () => { }, rawBody: "please inspect `/tmp/foo`", }); - expect(buildContextMock.mock.calls[0][0].commandSource).toBeUndefined(); + expect( + mockCallArg(buildContextMock, "buildWhatsAppInboundContext").commandSource, + ).toBeUndefined(); }); it("passes pending group history from the history window into inbound context", async () => { @@ -392,7 +394,7 @@ describe("processMessage group system prompt wiring", () => { await callProcessMessage({ groupHistories }); - expect(buildContextMock.mock.calls[0][0]).toMatchObject({ + expect(mockCallArg(buildContextMock, "buildWhatsAppInboundContext")).toMatchObject({ groupHistory: [ { sender: "Alice (+15550002222)", diff --git a/extensions/workboard/doctor-contract-api.test.ts b/extensions/workboard/doctor-contract-api.test.ts index 327bfbf59b6f..222d7fa963d6 100644 --- a/extensions/workboard/doctor-contract-api.test.ts +++ b/extensions/workboard/doctor-contract-api.test.ts @@ -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 { createPluginStateKeyedStoreForTests as createPluginStateKeyedStore } from "openclaw/plugin-sdk/plugin-state-test-runtime"; import type { OpenKeyedStoreOptions, @@ -93,7 +94,7 @@ describe("workboard doctor contract", () => { contentBase64: Buffer.from("ok").toString("base64"), }); - const migration = stateMigrations[0]; + const migration = expectDefined(stateMigrations[0], "workboard state migration"); await expect( migration.detectLegacyState({ config: {}, @@ -202,7 +203,10 @@ describe("workboard doctor contract", () => { }); sqlite.close(); - const result = await stateMigrations[0].migrateLegacyState({ + const result = await expectDefined( + stateMigrations[0], + "workboard state migration", + ).migrateLegacyState({ config: {}, env, stateDir, @@ -275,7 +279,10 @@ describe("workboard doctor contract", () => { }); sqlite.close(); - const result = await stateMigrations[0].migrateLegacyState({ + const result = await expectDefined( + stateMigrations[0], + "workboard state migration", + ).migrateLegacyState({ config: {}, env, stateDir, @@ -340,7 +347,10 @@ describe("workboard doctor contract", () => { contentBase64: Buffer.from("ok").toString("base64"), }); - const result = await stateMigrations[0].migrateLegacyState({ + const result = await expectDefined( + stateMigrations[0], + "workboard state migration", + ).migrateLegacyState({ config: {}, env, stateDir, @@ -420,7 +430,10 @@ describe("workboard doctor contract", () => { }); sqlite.close(); - const result = await stateMigrations[0].migrateLegacyState({ + const result = await expectDefined( + stateMigrations[0], + "workboard state migration", + ).migrateLegacyState({ config: {}, env, stateDir, diff --git a/extensions/workboard/src/command.test.ts b/extensions/workboard/src/command.test.ts index ae16297b693e..790a7450d35e 100644 --- a/extensions/workboard/src/command.test.ts +++ b/extensions/workboard/src/command.test.ts @@ -1,4 +1,5 @@ // Workboard tests cover command plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it, vi } from "vitest"; import { handleWorkboardCommand } from "./command.js"; import type { WorkboardSubagentRuntime, WorkboardWorktreeRuntime } from "./dispatcher.js"; @@ -63,7 +64,7 @@ describe("handleWorkboardCommand", () => { senderIsOwner: true, }), ).resolves.toEqual(expect.objectContaining({ text: expect.stringContaining("Ship CLI") })); - const card = (await store.list())[0]; + const card = expectDefined((await store.list())[0], "created workboard card"); expect(card).toMatchObject({ title: "Ship CLI" }); await expect(handleWorkboardCommand({ api, store, args: "list" })).resolves.toEqual( diff --git a/extensions/workboard/src/tools.test.ts b/extensions/workboard/src/tools.test.ts index 9f7de46c0408..3195cea782ba 100644 --- a/extensions/workboard/src/tools.test.ts +++ b/extensions/workboard/src/tools.test.ts @@ -1,4 +1,5 @@ // Workboard tests cover tools plugin behavior. +import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it, vi } from "vitest"; import type { OpenClawPluginApi } from "../api.js"; import { WorkboardStore, type PersistedWorkboardCard, type WorkboardKeyedStore } from "./store.js"; @@ -453,8 +454,9 @@ describe("workboard tools", () => { expect(attached.card).toMatchObject({ metadata: { attachments: [expect.objectContaining({ fileName: "result.txt" })] }, }); - const attachmentId = (attached.card as { metadata: { attachments: Array<{ id: string }> } }) - .metadata.attachments[0].id; + const attachments = (attached.card as { metadata: { attachments: Array<{ id: string }> } }) + .metadata.attachments; + const attachmentId = expectDefined(attachments[0], "workboard attachment").id; const attachment = readPayload( await tools.get("workboard_attachment_read")?.execute("call-attachment-read", { id: attachmentId, diff --git a/extensions/workspaces/src/default-workspace.test.ts b/extensions/workspaces/src/default-workspace.test.ts index 8c9c0d33700a..2f4135f5b1ef 100644 --- a/extensions/workspaces/src/default-workspace.test.ts +++ b/extensions/workspaces/src/default-workspace.test.ts @@ -1,8 +1,13 @@ +import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it } from "vitest"; import { DATA_READ_RPC_ALLOWLIST } from "./binding-contract.js"; import { DEFAULT_WORKSPACE } from "./default-workspace.js"; import { validateWorkspaceDoc } from "./schema.js"; +function mainTab() { + return expectDefined(DEFAULT_WORKSPACE.tabs[0], "default main workspace tab"); +} + describe("default Workspaces document", () => { it("passes write-time schema validation (seeds cleanly on fresh state)", () => { expect(() => validateWorkspaceDoc(DEFAULT_WORKSPACE)).not.toThrow(); @@ -10,14 +15,14 @@ describe("default Workspaces document", () => { it("ships a single curated system Overview tab", () => { expect(DEFAULT_WORKSPACE.tabs).toHaveLength(1); - const [main] = DEFAULT_WORKSPACE.tabs; + const main = mainTab(); expect(main.slug).toBe("main"); expect(main.createdBy).toBe("system"); expect(DEFAULT_WORKSPACE.prefs.tabOrder).toEqual(["main"]); }); it("matches the spec layout (usage cards + instances, sessions + cron, activity)", () => { - const kinds = DEFAULT_WORKSPACE.tabs[0].widgets.map((w) => w.kind); + const kinds = mainTab().widgets.map((w) => w.kind); expect(kinds).toEqual([ "builtin:stat-card", "builtin:stat-card", @@ -27,12 +32,12 @@ describe("default Workspaces document", () => { "builtin:activity", ]); // Activity spans the full width, taller (row 3). - const activity = DEFAULT_WORKSPACE.tabs[0].widgets.find((w) => w.id === "activity"); + const activity = mainTab().widgets.find((w) => w.id === "activity"); expect(activity?.grid).toMatchObject({ x: 0, w: 12 }); }); it("scopes the Today cards to one calendar day", () => { - const widgets = DEFAULT_WORKSPACE.tabs[0].widgets; + const widgets = mainTab().widgets; for (const id of ["cost-today", "tokens-today"]) { expect(widgets.find((widget) => widget.id === id)?.bindings?.value).toEqual({ source: "rpc", diff --git a/extensions/zalo/src/monitor.polling.media-reply.test.ts b/extensions/zalo/src/monitor.polling.media-reply.test.ts index 953de8361af0..2104d27ca20c 100644 --- a/extensions/zalo/src/monitor.polling.media-reply.test.ts +++ b/extensions/zalo/src/monitor.polling.media-reply.test.ts @@ -1,5 +1,6 @@ // Zalo tests cover monitor.polling.media reply plugin behavior. import type { ServerResponse } from "node:http"; +import { expectDefined } from "@openclaw/normalization-core"; import type { OpenKeyedStoreOptions } from "openclaw/plugin-sdk/plugin-state-runtime"; import { createPluginStateKeyedStoreForTests, @@ -315,7 +316,7 @@ describe("Zalo polling media replies", () => { (route) => route.source === "zalo-hosted-media", ); expect(firstHostedMediaRoutes).toHaveLength(1); - const hostedMediaRoute = firstHostedMediaRoutes[0]; + const hostedMediaRoute = expectDefined(firstHostedMediaRoutes[0], "Zalo hosted-media route"); expect(hostedMediaRoute?.path).toBe("/hooks/zalo/media"); expect(hostedMediaRoute?.pluginId).toBe("zalo"); expect(hostedMediaRoute?.source).toBe("zalo-hosted-media"); diff --git a/test/tsconfig/tsconfig.extensions.test.json b/test/tsconfig/tsconfig.extensions.test.json index 5e7960efec78..ba6a6163fcdb 100644 --- a/test/tsconfig/tsconfig.extensions.test.json +++ b/test/tsconfig/tsconfig.extensions.test.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.test.json", "compilerOptions": { + "noUncheckedIndexedAccess": true, "tsBuildInfoFile": "../../.artifacts/tsgo-cache/extensions-test.tsbuildinfo" }, "include": [