diff --git a/extensions/google-gemini-cli-auth/oauth.test.ts b/extensions/google-gemini-cli-auth/oauth.test.ts index 5831b8b1e..334e29701 100644 --- a/extensions/google-gemini-cli-auth/oauth.test.ts +++ b/extensions/google-gemini-cli-auth/oauth.test.ts @@ -1,6 +1,10 @@ import { join, parse } from "node:path"; import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +vi.mock("openclaw/plugin-sdk", () => ({ + isWSL2Sync: () => false, +})); + // Mock fs module before importing the module under test const mockExistsSync = vi.fn(); const mockReadFileSync = vi.fn(); @@ -32,7 +36,6 @@ describe("extractGeminiCliCredentials", () => { let originalPath: string | undefined; beforeEach(async () => { - vi.resetModules(); vi.clearAllMocks(); originalPath = process.env.PATH; }); diff --git a/extensions/twitch/src/onboarding.test.ts b/extensions/twitch/src/onboarding.test.ts index 20b6920b5..d57e2e2de 100644 --- a/extensions/twitch/src/onboarding.test.ts +++ b/extensions/twitch/src/onboarding.test.ts @@ -15,6 +15,11 @@ import type { WizardPrompter } from "openclaw/plugin-sdk"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { TwitchAccountConfig } from "./types.js"; +vi.mock("openclaw/plugin-sdk", () => ({ + formatDocsLink: (url: string, fallback: string) => fallback || url, + promptChannelAccessConfig: vi.fn(async () => null), +})); + // Mock the helpers we're testing const mockPromptText = vi.fn(); const mockPromptConfirm = vi.fn(); diff --git a/src/agents/agent-paths.test.ts b/src/agents/agent-paths.e2e.test.ts similarity index 100% rename from src/agents/agent-paths.test.ts rename to src/agents/agent-paths.e2e.test.ts diff --git a/src/agents/agent-scope.test.ts b/src/agents/agent-scope.e2e.test.ts similarity index 100% rename from src/agents/agent-scope.test.ts rename to src/agents/agent-scope.e2e.test.ts diff --git a/src/agents/apply-patch.test.ts b/src/agents/apply-patch.e2e.test.ts similarity index 100% rename from src/agents/apply-patch.test.ts rename to src/agents/apply-patch.e2e.test.ts diff --git a/src/agents/auth-health.test.ts b/src/agents/auth-health.e2e.test.ts similarity index 100% rename from src/agents/auth-health.test.ts rename to src/agents/auth-health.e2e.test.ts diff --git a/src/agents/auth-profiles.auth-profile-cooldowns.test.ts b/src/agents/auth-profiles.auth-profile-cooldowns.e2e.test.ts similarity index 100% rename from src/agents/auth-profiles.auth-profile-cooldowns.test.ts rename to src/agents/auth-profiles.auth-profile-cooldowns.e2e.test.ts diff --git a/src/agents/auth-profiles.chutes.test.ts b/src/agents/auth-profiles.chutes.e2e.test.ts similarity index 100% rename from src/agents/auth-profiles.chutes.test.ts rename to src/agents/auth-profiles.chutes.e2e.test.ts diff --git a/src/agents/auth-profiles.ensureauthprofilestore.test.ts b/src/agents/auth-profiles.ensureauthprofilestore.e2e.test.ts similarity index 100% rename from src/agents/auth-profiles.ensureauthprofilestore.test.ts rename to src/agents/auth-profiles.ensureauthprofilestore.e2e.test.ts diff --git a/src/agents/auth-profiles.markauthprofilefailure.test.ts b/src/agents/auth-profiles.markauthprofilefailure.e2e.test.ts similarity index 100% rename from src/agents/auth-profiles.markauthprofilefailure.test.ts rename to src/agents/auth-profiles.markauthprofilefailure.e2e.test.ts diff --git a/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts b/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.e2e.test.ts similarity index 100% rename from src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts rename to src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.e2e.test.ts diff --git a/src/agents/auth-profiles.resolve-auth-profile-order.normalizes-z-ai-aliases-auth-order.test.ts b/src/agents/auth-profiles.resolve-auth-profile-order.normalizes-z-ai-aliases-auth-order.e2e.test.ts similarity index 100% rename from src/agents/auth-profiles.resolve-auth-profile-order.normalizes-z-ai-aliases-auth-order.test.ts rename to src/agents/auth-profiles.resolve-auth-profile-order.normalizes-z-ai-aliases-auth-order.e2e.test.ts diff --git a/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts b/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.e2e.test.ts similarity index 100% rename from src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts rename to src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.e2e.test.ts diff --git a/src/agents/auth-profiles.resolve-auth-profile-order.uses-stored-profiles-no-config-exists.test.ts b/src/agents/auth-profiles.resolve-auth-profile-order.uses-stored-profiles-no-config-exists.e2e.test.ts similarity index 100% rename from src/agents/auth-profiles.resolve-auth-profile-order.uses-stored-profiles-no-config-exists.test.ts rename to src/agents/auth-profiles.resolve-auth-profile-order.uses-stored-profiles-no-config-exists.e2e.test.ts diff --git a/src/agents/auth-profiles/oauth.fallback-to-main-agent.test.ts b/src/agents/auth-profiles/oauth.fallback-to-main-agent.e2e.test.ts similarity index 100% rename from src/agents/auth-profiles/oauth.fallback-to-main-agent.test.ts rename to src/agents/auth-profiles/oauth.fallback-to-main-agent.e2e.test.ts diff --git a/src/agents/auth-profiles/session-override.test.ts b/src/agents/auth-profiles/session-override.e2e.test.ts similarity index 100% rename from src/agents/auth-profiles/session-override.test.ts rename to src/agents/auth-profiles/session-override.e2e.test.ts diff --git a/src/agents/bash-process-registry.test.ts b/src/agents/bash-process-registry.e2e.test.ts similarity index 100% rename from src/agents/bash-process-registry.test.ts rename to src/agents/bash-process-registry.e2e.test.ts diff --git a/src/agents/bash-tools.test.ts b/src/agents/bash-tools.e2e.test.ts similarity index 100% rename from src/agents/bash-tools.test.ts rename to src/agents/bash-tools.e2e.test.ts diff --git a/src/agents/bash-tools.exec.approval-id.test.ts b/src/agents/bash-tools.exec.approval-id.e2e.test.ts similarity index 100% rename from src/agents/bash-tools.exec.approval-id.test.ts rename to src/agents/bash-tools.exec.approval-id.e2e.test.ts diff --git a/src/agents/bash-tools.exec.background-abort.test.ts b/src/agents/bash-tools.exec.background-abort.e2e.test.ts similarity index 100% rename from src/agents/bash-tools.exec.background-abort.test.ts rename to src/agents/bash-tools.exec.background-abort.e2e.test.ts diff --git a/src/agents/bash-tools.exec.path.test.ts b/src/agents/bash-tools.exec.path.e2e.test.ts similarity index 100% rename from src/agents/bash-tools.exec.path.test.ts rename to src/agents/bash-tools.exec.path.e2e.test.ts diff --git a/src/agents/bash-tools.exec.pty-fallback.test.ts b/src/agents/bash-tools.exec.pty-fallback.e2e.test.ts similarity index 100% rename from src/agents/bash-tools.exec.pty-fallback.test.ts rename to src/agents/bash-tools.exec.pty-fallback.e2e.test.ts diff --git a/src/agents/bash-tools.exec.pty.test.ts b/src/agents/bash-tools.exec.pty.e2e.test.ts similarity index 100% rename from src/agents/bash-tools.exec.pty.test.ts rename to src/agents/bash-tools.exec.pty.e2e.test.ts diff --git a/src/agents/bash-tools.process.send-keys.test.ts b/src/agents/bash-tools.process.send-keys.e2e.test.ts similarity index 100% rename from src/agents/bash-tools.process.send-keys.test.ts rename to src/agents/bash-tools.process.send-keys.e2e.test.ts diff --git a/src/agents/bedrock-discovery.test.ts b/src/agents/bedrock-discovery.e2e.test.ts similarity index 100% rename from src/agents/bedrock-discovery.test.ts rename to src/agents/bedrock-discovery.e2e.test.ts diff --git a/src/agents/bootstrap-files.test.ts b/src/agents/bootstrap-files.e2e.test.ts similarity index 100% rename from src/agents/bootstrap-files.test.ts rename to src/agents/bootstrap-files.e2e.test.ts diff --git a/src/agents/bootstrap-hooks.test.ts b/src/agents/bootstrap-hooks.e2e.test.ts similarity index 100% rename from src/agents/bootstrap-hooks.test.ts rename to src/agents/bootstrap-hooks.e2e.test.ts diff --git a/src/agents/cache-trace.test.ts b/src/agents/cache-trace.e2e.test.ts similarity index 100% rename from src/agents/cache-trace.test.ts rename to src/agents/cache-trace.e2e.test.ts diff --git a/src/agents/channel-tools.test.ts b/src/agents/channel-tools.e2e.test.ts similarity index 100% rename from src/agents/channel-tools.test.ts rename to src/agents/channel-tools.e2e.test.ts diff --git a/src/agents/chutes-oauth.test.ts b/src/agents/chutes-oauth.e2e.test.ts similarity index 100% rename from src/agents/chutes-oauth.test.ts rename to src/agents/chutes-oauth.e2e.test.ts diff --git a/src/agents/claude-cli-runner.test.ts b/src/agents/claude-cli-runner.e2e.test.ts similarity index 100% rename from src/agents/claude-cli-runner.test.ts rename to src/agents/claude-cli-runner.e2e.test.ts diff --git a/src/agents/cli-credentials.test.ts b/src/agents/cli-credentials.e2e.test.ts similarity index 100% rename from src/agents/cli-credentials.test.ts rename to src/agents/cli-credentials.e2e.test.ts diff --git a/src/agents/cli-runner.test.ts b/src/agents/cli-runner.e2e.test.ts similarity index 100% rename from src/agents/cli-runner.test.ts rename to src/agents/cli-runner.e2e.test.ts diff --git a/src/agents/compaction.test.ts b/src/agents/compaction.e2e.test.ts similarity index 100% rename from src/agents/compaction.test.ts rename to src/agents/compaction.e2e.test.ts diff --git a/src/agents/compaction.tool-result-details.test.ts b/src/agents/compaction.tool-result-details.e2e.test.ts similarity index 100% rename from src/agents/compaction.tool-result-details.test.ts rename to src/agents/compaction.tool-result-details.e2e.test.ts diff --git a/src/agents/context-window-guard.test.ts b/src/agents/context-window-guard.e2e.test.ts similarity index 100% rename from src/agents/context-window-guard.test.ts rename to src/agents/context-window-guard.e2e.test.ts diff --git a/src/agents/failover-error.test.ts b/src/agents/failover-error.e2e.test.ts similarity index 100% rename from src/agents/failover-error.test.ts rename to src/agents/failover-error.e2e.test.ts diff --git a/src/agents/identity-avatar.test.ts b/src/agents/identity-avatar.e2e.test.ts similarity index 100% rename from src/agents/identity-avatar.test.ts rename to src/agents/identity-avatar.e2e.test.ts diff --git a/src/agents/identity-file.test.ts b/src/agents/identity-file.e2e.test.ts similarity index 100% rename from src/agents/identity-file.test.ts rename to src/agents/identity-file.e2e.test.ts diff --git a/src/agents/identity.test.ts b/src/agents/identity.e2e.test.ts similarity index 100% rename from src/agents/identity.test.ts rename to src/agents/identity.e2e.test.ts diff --git a/src/agents/identity.per-channel-prefix.test.ts b/src/agents/identity.per-channel-prefix.e2e.test.ts similarity index 100% rename from src/agents/identity.per-channel-prefix.test.ts rename to src/agents/identity.per-channel-prefix.e2e.test.ts diff --git a/src/agents/live-auth-keys.test.ts b/src/agents/live-auth-keys.e2e.test.ts similarity index 100% rename from src/agents/live-auth-keys.test.ts rename to src/agents/live-auth-keys.e2e.test.ts diff --git a/src/agents/memory-search.test.ts b/src/agents/memory-search.e2e.test.ts similarity index 100% rename from src/agents/memory-search.test.ts rename to src/agents/memory-search.e2e.test.ts diff --git a/src/agents/minimax-vlm.normalizes-api-key.test.ts b/src/agents/minimax-vlm.normalizes-api-key.e2e.test.ts similarity index 100% rename from src/agents/minimax-vlm.normalizes-api-key.test.ts rename to src/agents/minimax-vlm.normalizes-api-key.e2e.test.ts diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.e2e.test.ts similarity index 100% rename from src/agents/model-auth.test.ts rename to src/agents/model-auth.e2e.test.ts diff --git a/src/agents/model-catalog.e2e.test.ts b/src/agents/model-catalog.e2e.test.ts new file mode 100644 index 000000000..3e90d8ee4 --- /dev/null +++ b/src/agents/model-catalog.e2e.test.ts @@ -0,0 +1,87 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { + __setModelCatalogImportForTest, + loadModelCatalog, + resetModelCatalogCacheForTest, +} from "./model-catalog.js"; + +type PiSdkModule = typeof import("./pi-model-discovery.js"); + +vi.mock("./models-config.js", () => ({ + ensureOpenClawModelsJson: vi.fn().mockResolvedValue({ agentDir: "/tmp", wrote: false }), +})); + +vi.mock("./agent-paths.js", () => ({ + resolveOpenClawAgentDir: () => "/tmp/openclaw", +})); + +describe("loadModelCatalog", () => { + beforeEach(() => { + resetModelCatalogCacheForTest(); + }); + + afterEach(() => { + __setModelCatalogImportForTest(); + resetModelCatalogCacheForTest(); + vi.restoreAllMocks(); + }); + + it("retries after import failure without poisoning the cache", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + let call = 0; + + __setModelCatalogImportForTest(async () => { + call += 1; + if (call === 1) { + throw new Error("boom"); + } + return { + AuthStorage: class {}, + ModelRegistry: class { + getAll() { + return [{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }]; + } + }, + } as unknown as PiSdkModule; + }); + + const cfg = {} as OpenClawConfig; + const first = await loadModelCatalog({ config: cfg }); + expect(first).toEqual([]); + + const second = await loadModelCatalog({ config: cfg }); + expect(second).toEqual([{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }]); + expect(call).toBe(2); + expect(warnSpy).toHaveBeenCalledTimes(1); + }); + + it("returns partial results on discovery errors", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + __setModelCatalogImportForTest( + async () => + ({ + AuthStorage: class {}, + ModelRegistry: class { + getAll() { + return [ + { id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }, + { + get id() { + throw new Error("boom"); + }, + provider: "openai", + name: "bad", + }, + ]; + } + }, + }) as unknown as PiSdkModule, + ); + + const result = await loadModelCatalog({ config: {} as OpenClawConfig }); + expect(result).toEqual([{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }]); + expect(warnSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/agents/model-compat.test.ts b/src/agents/model-compat.e2e.test.ts similarity index 100% rename from src/agents/model-compat.test.ts rename to src/agents/model-compat.e2e.test.ts diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.e2e.test.ts similarity index 100% rename from src/agents/model-fallback.test.ts rename to src/agents/model-fallback.e2e.test.ts diff --git a/src/agents/model-scan.test.ts b/src/agents/model-scan.e2e.test.ts similarity index 100% rename from src/agents/model-scan.test.ts rename to src/agents/model-scan.e2e.test.ts diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.e2e.test.ts similarity index 100% rename from src/agents/model-selection.test.ts rename to src/agents/model-selection.e2e.test.ts diff --git a/src/agents/models-config.auto-injects-github-copilot-provider-token-is.test.ts b/src/agents/models-config.auto-injects-github-copilot-provider-token-is.e2e.test.ts similarity index 100% rename from src/agents/models-config.auto-injects-github-copilot-provider-token-is.test.ts rename to src/agents/models-config.auto-injects-github-copilot-provider-token-is.e2e.test.ts diff --git a/src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.test.ts b/src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.e2e.test.ts similarity index 100% rename from src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.test.ts rename to src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.e2e.test.ts diff --git a/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts b/src/agents/models-config.fills-missing-provider-apikey-from-env-var.e2e.test.ts similarity index 100% rename from src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts rename to src/agents/models-config.fills-missing-provider-apikey-from-env-var.e2e.test.ts diff --git a/src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.test.ts b/src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.e2e.test.ts similarity index 100% rename from src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.test.ts rename to src/agents/models-config.normalizes-gemini-3-ids-preview-google-providers.e2e.test.ts diff --git a/src/agents/models-config.providers.ollama.test.ts b/src/agents/models-config.providers.ollama.e2e.test.ts similarity index 100% rename from src/agents/models-config.providers.ollama.test.ts rename to src/agents/models-config.providers.ollama.e2e.test.ts diff --git a/src/agents/models-config.providers.qianfan.test.ts b/src/agents/models-config.providers.qianfan.e2e.test.ts similarity index 100% rename from src/agents/models-config.providers.qianfan.test.ts rename to src/agents/models-config.providers.qianfan.e2e.test.ts diff --git a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts b/src/agents/models-config.skips-writing-models-json-no-env-token.e2e.test.ts similarity index 100% rename from src/agents/models-config.skips-writing-models-json-no-env-token.test.ts rename to src/agents/models-config.skips-writing-models-json-no-env-token.e2e.test.ts diff --git a/src/agents/models-config.uses-first-github-copilot-profile-env-tokens.test.ts b/src/agents/models-config.uses-first-github-copilot-profile-env-tokens.e2e.test.ts similarity index 100% rename from src/agents/models-config.uses-first-github-copilot-profile-env-tokens.test.ts rename to src/agents/models-config.uses-first-github-copilot-profile-env-tokens.e2e.test.ts diff --git a/src/agents/openai-responses.reasoning-replay.test.ts b/src/agents/openai-responses.reasoning-replay.e2e.test.ts similarity index 100% rename from src/agents/openai-responses.reasoning-replay.test.ts rename to src/agents/openai-responses.reasoning-replay.e2e.test.ts diff --git a/src/agents/openclaw-gateway-tool.test.ts b/src/agents/openclaw-gateway-tool.e2e.test.ts similarity index 100% rename from src/agents/openclaw-gateway-tool.test.ts rename to src/agents/openclaw-gateway-tool.e2e.test.ts diff --git a/src/agents/openclaw-tools.agents.test.ts b/src/agents/openclaw-tools.agents.e2e.test.ts similarity index 100% rename from src/agents/openclaw-tools.agents.test.ts rename to src/agents/openclaw-tools.agents.e2e.test.ts diff --git a/src/agents/openclaw-tools.camera.test.ts b/src/agents/openclaw-tools.camera.e2e.test.ts similarity index 100% rename from src/agents/openclaw-tools.camera.test.ts rename to src/agents/openclaw-tools.camera.e2e.test.ts diff --git a/src/agents/openclaw-tools.session-status.test.ts b/src/agents/openclaw-tools.session-status.e2e.test.ts similarity index 100% rename from src/agents/openclaw-tools.session-status.test.ts rename to src/agents/openclaw-tools.session-status.e2e.test.ts diff --git a/src/agents/openclaw-tools.sessions.test.ts b/src/agents/openclaw-tools.sessions.e2e.test.ts similarity index 100% rename from src/agents/openclaw-tools.sessions.test.ts rename to src/agents/openclaw-tools.sessions.e2e.test.ts diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-allows-cross-agent-spawning-configured.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn-allows-cross-agent-spawning-configured.e2e.test.ts similarity index 100% rename from src/agents/openclaw-tools.subagents.sessions-spawn-allows-cross-agent-spawning-configured.test.ts rename to src/agents/openclaw-tools.subagents.sessions-spawn-allows-cross-agent-spawning-configured.e2e.test.ts diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-announces-agent-wait-lifecycle-events.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn-announces-agent-wait-lifecycle-events.e2e.test.ts similarity index 100% rename from src/agents/openclaw-tools.subagents.sessions-spawn-announces-agent-wait-lifecycle-events.test.ts rename to src/agents/openclaw-tools.subagents.sessions-spawn-announces-agent-wait-lifecycle-events.e2e.test.ts diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-applies-model-child-session.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn-applies-model-child-session.e2e.test.ts similarity index 100% rename from src/agents/openclaw-tools.subagents.sessions-spawn-applies-model-child-session.test.ts rename to src/agents/openclaw-tools.subagents.sessions-spawn-applies-model-child-session.e2e.test.ts diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-applies-thinking-default.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn-applies-thinking-default.e2e.test.ts similarity index 100% rename from src/agents/openclaw-tools.subagents.sessions-spawn-applies-thinking-default.test.ts rename to src/agents/openclaw-tools.subagents.sessions-spawn-applies-thinking-default.e2e.test.ts diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-normalizes-allowlisted-agent-ids.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn-normalizes-allowlisted-agent-ids.e2e.test.ts similarity index 100% rename from src/agents/openclaw-tools.subagents.sessions-spawn-normalizes-allowlisted-agent-ids.test.ts rename to src/agents/openclaw-tools.subagents.sessions-spawn-normalizes-allowlisted-agent-ids.e2e.test.ts diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-prefers-per-agent-subagent-model.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn-prefers-per-agent-subagent-model.e2e.test.ts similarity index 100% rename from src/agents/openclaw-tools.subagents.sessions-spawn-prefers-per-agent-subagent-model.test.ts rename to src/agents/openclaw-tools.subagents.sessions-spawn-prefers-per-agent-subagent-model.e2e.test.ts diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-resolves-main-announce-target-from.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn-resolves-main-announce-target-from.e2e.test.ts similarity index 100% rename from src/agents/openclaw-tools.subagents.sessions-spawn-resolves-main-announce-target-from.test.ts rename to src/agents/openclaw-tools.subagents.sessions-spawn-resolves-main-announce-target-from.e2e.test.ts diff --git a/src/agents/opencode-zen-models.test.ts b/src/agents/opencode-zen-models.e2e.test.ts similarity index 100% rename from src/agents/opencode-zen-models.test.ts rename to src/agents/opencode-zen-models.e2e.test.ts diff --git a/src/agents/pi-embedded-block-chunker.test.ts b/src/agents/pi-embedded-block-chunker.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-block-chunker.test.ts rename to src/agents/pi-embedded-block-chunker.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.test.ts b/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.test.ts rename to src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts b/src/agents/pi-embedded-helpers.classifyfailoverreason.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts rename to src/agents/pi-embedded-helpers.classifyfailoverreason.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.downgradeopenai-reasoning.test.ts b/src/agents/pi-embedded-helpers.downgradeopenai-reasoning.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.downgradeopenai-reasoning.test.ts rename to src/agents/pi-embedded-helpers.downgradeopenai-reasoning.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts b/src/agents/pi-embedded-helpers.formatassistanterrortext.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts rename to src/agents/pi-embedded-helpers.formatassistanterrortext.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.formatrawassistanterrorforui.test.ts b/src/agents/pi-embedded-helpers.formatrawassistanterrorforui.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.formatrawassistanterrorforui.test.ts rename to src/agents/pi-embedded-helpers.formatrawassistanterrorforui.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.image-dimension-error.test.ts b/src/agents/pi-embedded-helpers.image-dimension-error.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.image-dimension-error.test.ts rename to src/agents/pi-embedded-helpers.image-dimension-error.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.image-size-error.test.ts b/src/agents/pi-embedded-helpers.image-size-error.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.image-size-error.test.ts rename to src/agents/pi-embedded-helpers.image-size-error.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.isautherrormessage.test.ts b/src/agents/pi-embedded-helpers.isautherrormessage.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.isautherrormessage.test.ts rename to src/agents/pi-embedded-helpers.isautherrormessage.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts rename to src/agents/pi-embedded-helpers.isbillingerrormessage.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.iscloudcodeassistformaterror.test.ts b/src/agents/pi-embedded-helpers.iscloudcodeassistformaterror.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.iscloudcodeassistformaterror.test.ts rename to src/agents/pi-embedded-helpers.iscloudcodeassistformaterror.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.iscloudflareorhtmlerrorpage.test.ts b/src/agents/pi-embedded-helpers.iscloudflareorhtmlerrorpage.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.iscloudflareorhtmlerrorpage.test.ts rename to src/agents/pi-embedded-helpers.iscloudflareorhtmlerrorpage.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.iscompactionfailureerror.test.ts b/src/agents/pi-embedded-helpers.iscompactionfailureerror.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.iscompactionfailureerror.test.ts rename to src/agents/pi-embedded-helpers.iscompactionfailureerror.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.iscontextoverflowerror.test.ts b/src/agents/pi-embedded-helpers.iscontextoverflowerror.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.iscontextoverflowerror.test.ts rename to src/agents/pi-embedded-helpers.iscontextoverflowerror.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.isfailovererrormessage.test.ts b/src/agents/pi-embedded-helpers.isfailovererrormessage.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.isfailovererrormessage.test.ts rename to src/agents/pi-embedded-helpers.isfailovererrormessage.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.islikelycontextoverflowerror.test.ts b/src/agents/pi-embedded-helpers.islikelycontextoverflowerror.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.islikelycontextoverflowerror.test.ts rename to src/agents/pi-embedded-helpers.islikelycontextoverflowerror.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.ismessagingtoolduplicate.test.ts b/src/agents/pi-embedded-helpers.ismessagingtoolduplicate.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.ismessagingtoolduplicate.test.ts rename to src/agents/pi-embedded-helpers.ismessagingtoolduplicate.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.istransienthttperror.test.ts b/src/agents/pi-embedded-helpers.istransienthttperror.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.istransienthttperror.test.ts rename to src/agents/pi-embedded-helpers.istransienthttperror.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.messaging-duplicate.test.ts b/src/agents/pi-embedded-helpers.messaging-duplicate.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.messaging-duplicate.test.ts rename to src/agents/pi-embedded-helpers.messaging-duplicate.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.normalizetextforcomparison.test.ts b/src/agents/pi-embedded-helpers.normalizetextforcomparison.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.normalizetextforcomparison.test.ts rename to src/agents/pi-embedded-helpers.normalizetextforcomparison.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.resolvebootstrapmaxchars.test.ts b/src/agents/pi-embedded-helpers.resolvebootstrapmaxchars.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.resolvebootstrapmaxchars.test.ts rename to src/agents/pi-embedded-helpers.resolvebootstrapmaxchars.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.sanitize-session-messages-images.keeps-tool-call-tool-result-ids-unchanged.test.ts b/src/agents/pi-embedded-helpers.sanitize-session-messages-images.keeps-tool-call-tool-result-ids-unchanged.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.sanitize-session-messages-images.keeps-tool-call-tool-result-ids-unchanged.test.ts rename to src/agents/pi-embedded-helpers.sanitize-session-messages-images.keeps-tool-call-tool-result-ids-unchanged.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts b/src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts rename to src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.sanitizegoogleturnordering.test.ts b/src/agents/pi-embedded-helpers.sanitizegoogleturnordering.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.sanitizegoogleturnordering.test.ts rename to src/agents/pi-embedded-helpers.sanitizegoogleturnordering.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.sanitizesessionmessagesimages-thought-signature-stripping.test.ts b/src/agents/pi-embedded-helpers.sanitizesessionmessagesimages-thought-signature-stripping.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.sanitizesessionmessagesimages-thought-signature-stripping.test.ts rename to src/agents/pi-embedded-helpers.sanitizesessionmessagesimages-thought-signature-stripping.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.sanitizetoolcallid.test.ts b/src/agents/pi-embedded-helpers.sanitizetoolcallid.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.sanitizetoolcallid.test.ts rename to src/agents/pi-embedded-helpers.sanitizetoolcallid.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts rename to src/agents/pi-embedded-helpers.sanitizeuserfacingtext.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.stripthoughtsignatures.test.ts b/src/agents/pi-embedded-helpers.stripthoughtsignatures.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.stripthoughtsignatures.test.ts rename to src/agents/pi-embedded-helpers.stripthoughtsignatures.e2e.test.ts diff --git a/src/agents/pi-embedded-helpers.validate-turns.test.ts b/src/agents/pi-embedded-helpers.validate-turns.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers.validate-turns.test.ts rename to src/agents/pi-embedded-helpers.validate-turns.e2e.test.ts diff --git a/src/agents/pi-embedded-runner-extraparams.test.ts b/src/agents/pi-embedded-runner-extraparams.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner-extraparams.test.ts rename to src/agents/pi-embedded-runner-extraparams.e2e.test.ts diff --git a/src/agents/pi-embedded-runner.applygoogleturnorderingfix.test.ts b/src/agents/pi-embedded-runner.applygoogleturnorderingfix.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner.applygoogleturnorderingfix.test.ts rename to src/agents/pi-embedded-runner.applygoogleturnorderingfix.e2e.test.ts diff --git a/src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts b/src/agents/pi-embedded-runner.buildembeddedsandboxinfo.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts rename to src/agents/pi-embedded-runner.buildembeddedsandboxinfo.e2e.test.ts diff --git a/src/agents/pi-embedded-runner.createsystempromptoverride.test.ts b/src/agents/pi-embedded-runner.createsystempromptoverride.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner.createsystempromptoverride.test.ts rename to src/agents/pi-embedded-runner.createsystempromptoverride.e2e.test.ts diff --git a/src/agents/pi-embedded-runner.test.ts b/src/agents/pi-embedded-runner.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner.test.ts rename to src/agents/pi-embedded-runner.e2e.test.ts diff --git a/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.test.ts b/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.test.ts rename to src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.e2e.test.ts diff --git a/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.test.ts b/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.test.ts rename to src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.e2e.test.ts diff --git a/src/agents/pi-embedded-runner.google-sanitize-thinking.test.ts b/src/agents/pi-embedded-runner.google-sanitize-thinking.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner.google-sanitize-thinking.test.ts rename to src/agents/pi-embedded-runner.google-sanitize-thinking.e2e.test.ts diff --git a/src/agents/pi-embedded-runner.guard.test.ts b/src/agents/pi-embedded-runner.guard.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner.guard.test.ts rename to src/agents/pi-embedded-runner.guard.e2e.test.ts diff --git a/src/agents/pi-embedded-runner.limithistoryturns.test.ts b/src/agents/pi-embedded-runner.limithistoryturns.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner.limithistoryturns.test.ts rename to src/agents/pi-embedded-runner.limithistoryturns.e2e.test.ts diff --git a/src/agents/pi-embedded-runner.resolvesessionagentids.test.ts b/src/agents/pi-embedded-runner.resolvesessionagentids.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner.resolvesessionagentids.test.ts rename to src/agents/pi-embedded-runner.resolvesessionagentids.e2e.test.ts diff --git a/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.test.ts b/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.test.ts rename to src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts diff --git a/src/agents/pi-embedded-runner.sanitize-session-history.e2e.test.ts b/src/agents/pi-embedded-runner.sanitize-session-history.e2e.test.ts new file mode 100644 index 000000000..791525a64 --- /dev/null +++ b/src/agents/pi-embedded-runner.sanitize-session-history.e2e.test.ts @@ -0,0 +1,302 @@ +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import type { SessionManager } from "@mariozechner/pi-coding-agent"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import * as helpers from "./pi-embedded-helpers.js"; + +type SanitizeSessionHistory = + typeof import("./pi-embedded-runner/google.js").sanitizeSessionHistory; +let sanitizeSessionHistory: SanitizeSessionHistory; + +// Mock dependencies +vi.mock("./pi-embedded-helpers.js", async () => { + const actual = await vi.importActual("./pi-embedded-helpers.js"); + return { + ...actual, + isGoogleModelApi: vi.fn(), + sanitizeSessionMessagesImages: vi.fn().mockImplementation(async (msgs) => msgs), + }; +}); + +// We don't mock session-transcript-repair.js as it is a pure function and complicates mocking. +// We rely on the real implementation which should pass through our simple messages. + +describe("sanitizeSessionHistory", () => { + const mockSessionManager = { + getEntries: vi.fn().mockReturnValue([]), + appendCustomEntry: vi.fn(), + } as unknown as SessionManager; + + const mockMessages: AgentMessage[] = [{ role: "user", content: "hello" }]; + + beforeEach(async () => { + vi.resetAllMocks(); + vi.mocked(helpers.sanitizeSessionMessagesImages).mockImplementation(async (msgs) => msgs); + vi.resetModules(); + ({ sanitizeSessionHistory } = await import("./pi-embedded-runner/google.js")); + }); + + it("sanitizes tool call ids for Google model APIs", async () => { + vi.mocked(helpers.isGoogleModelApi).mockReturnValue(true); + + await sanitizeSessionHistory({ + messages: mockMessages, + modelApi: "google-generative-ai", + provider: "google-vertex", + sessionManager: mockSessionManager, + sessionId: "test-session", + }); + + expect(helpers.sanitizeSessionMessagesImages).toHaveBeenCalledWith( + mockMessages, + "session:history", + expect.objectContaining({ sanitizeMode: "full", sanitizeToolCallIds: true }), + ); + }); + + it("sanitizes tool call ids with strict9 for Mistral models", async () => { + vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false); + + await sanitizeSessionHistory({ + messages: mockMessages, + modelApi: "openai-responses", + provider: "openrouter", + modelId: "mistralai/devstral-2512:free", + sessionManager: mockSessionManager, + sessionId: "test-session", + }); + + expect(helpers.sanitizeSessionMessagesImages).toHaveBeenCalledWith( + mockMessages, + "session:history", + expect.objectContaining({ + sanitizeMode: "full", + sanitizeToolCallIds: true, + toolCallIdMode: "strict9", + }), + ); + }); + + it("sanitizes tool call ids for Anthropic APIs", async () => { + vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false); + + await sanitizeSessionHistory({ + messages: mockMessages, + modelApi: "anthropic-messages", + provider: "anthropic", + sessionManager: mockSessionManager, + sessionId: "test-session", + }); + + expect(helpers.sanitizeSessionMessagesImages).toHaveBeenCalledWith( + mockMessages, + "session:history", + expect.objectContaining({ sanitizeMode: "full", sanitizeToolCallIds: true }), + ); + }); + + it("does not sanitize tool call ids for openai-responses", async () => { + vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false); + + await sanitizeSessionHistory({ + messages: mockMessages, + modelApi: "openai-responses", + provider: "openai", + sessionManager: mockSessionManager, + sessionId: "test-session", + }); + + expect(helpers.sanitizeSessionMessagesImages).toHaveBeenCalledWith( + mockMessages, + "session:history", + expect.objectContaining({ sanitizeMode: "images-only", sanitizeToolCallIds: false }), + ); + }); + + it("annotates inter-session user messages before context sanitization", async () => { + vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false); + + const messages: AgentMessage[] = [ + { + role: "user", + content: "forwarded instruction", + provenance: { + kind: "inter_session", + sourceSessionKey: "agent:main:req", + sourceTool: "sessions_send", + }, + } as unknown as AgentMessage, + ]; + + const result = await sanitizeSessionHistory({ + messages, + modelApi: "openai-responses", + provider: "openai", + sessionManager: mockSessionManager, + sessionId: "test-session", + }); + + const first = result[0] as Extract; + expect(first.role).toBe("user"); + expect(typeof first.content).toBe("string"); + expect(first.content as string).toContain("[Inter-session message]"); + expect(first.content as string).toContain("sourceSession=agent:main:req"); + }); + + it("keeps reasoning-only assistant messages for openai-responses", async () => { + vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false); + + const messages: AgentMessage[] = [ + { role: "user", content: "hello" }, + { + role: "assistant", + stopReason: "aborted", + content: [ + { + type: "thinking", + thinking: "reasoning", + thinkingSignature: "sig", + }, + ], + }, + ]; + + const result = await sanitizeSessionHistory({ + messages, + modelApi: "openai-responses", + provider: "openai", + sessionManager: mockSessionManager, + sessionId: "test-session", + }); + + expect(result).toHaveLength(2); + expect(result[1]?.role).toBe("assistant"); + }); + + it("does not synthesize tool results for openai-responses", async () => { + const messages: AgentMessage[] = [ + { + role: "assistant", + content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }], + }, + ]; + + const result = await sanitizeSessionHistory({ + messages, + modelApi: "openai-responses", + provider: "openai", + sessionManager: mockSessionManager, + sessionId: "test-session", + }); + + expect(result).toHaveLength(1); + expect(result[0]?.role).toBe("assistant"); + }); + + it("drops malformed tool calls missing input or arguments", async () => { + const messages: AgentMessage[] = [ + { + role: "assistant", + content: [{ type: "toolCall", id: "call_1", name: "read" }], + }, + { role: "user", content: "hello" }, + ]; + + const result = await sanitizeSessionHistory({ + messages, + modelApi: "openai-responses", + provider: "openai", + sessionManager: mockSessionManager, + sessionId: "test-session", + }); + + expect(result.map((msg) => msg.role)).toEqual(["user"]); + }); + + it("does not downgrade openai reasoning when the model has not changed", async () => { + const sessionEntries: Array<{ type: string; customType: string; data: unknown }> = [ + { + type: "custom", + customType: "model-snapshot", + data: { + timestamp: Date.now(), + provider: "openai", + modelApi: "openai-responses", + modelId: "gpt-5.2-codex", + }, + }, + ]; + const sessionManager = { + getEntries: vi.fn(() => sessionEntries), + appendCustomEntry: vi.fn((customType: string, data: unknown) => { + sessionEntries.push({ type: "custom", customType, data }); + }), + } as unknown as SessionManager; + const messages: AgentMessage[] = [ + { + role: "assistant", + content: [ + { + type: "thinking", + thinking: "reasoning", + thinkingSignature: JSON.stringify({ id: "rs_test", type: "reasoning" }), + }, + ], + }, + ]; + + const result = await sanitizeSessionHistory({ + messages, + modelApi: "openai-responses", + provider: "openai", + modelId: "gpt-5.2-codex", + sessionManager, + sessionId: "test-session", + }); + + expect(result).toEqual(messages); + }); + + it("downgrades openai reasoning only when the model changes", async () => { + const sessionEntries: Array<{ type: string; customType: string; data: unknown }> = [ + { + type: "custom", + customType: "model-snapshot", + data: { + timestamp: Date.now(), + provider: "anthropic", + modelApi: "anthropic-messages", + modelId: "claude-3-7", + }, + }, + ]; + const sessionManager = { + getEntries: vi.fn(() => sessionEntries), + appendCustomEntry: vi.fn((customType: string, data: unknown) => { + sessionEntries.push({ type: "custom", customType, data }); + }), + } as unknown as SessionManager; + const messages: AgentMessage[] = [ + { + role: "assistant", + content: [ + { + type: "thinking", + thinking: "reasoning", + thinkingSignature: { id: "rs_test", type: "reasoning" }, + }, + ], + }, + ]; + + const result = await sanitizeSessionHistory({ + messages, + modelApi: "openai-responses", + provider: "openai", + modelId: "gpt-5.2-codex", + sessionManager, + sessionId: "test-session", + }); + + expect(result).toEqual([]); + }); +}); diff --git a/src/agents/pi-embedded-runner.splitsdktools.test.ts b/src/agents/pi-embedded-runner.splitsdktools.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner.splitsdktools.test.ts rename to src/agents/pi-embedded-runner.splitsdktools.e2e.test.ts diff --git a/src/agents/pi-embedded-runner/google.test.ts b/src/agents/pi-embedded-runner/google.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/google.test.ts rename to src/agents/pi-embedded-runner/google.e2e.test.ts diff --git a/src/agents/pi-embedded-runner/model.e2e.test.ts b/src/agents/pi-embedded-runner/model.e2e.test.ts new file mode 100644 index 000000000..5f9ba96a6 --- /dev/null +++ b/src/agents/pi-embedded-runner/model.e2e.test.ts @@ -0,0 +1,312 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../pi-model-discovery.js", () => ({ + discoverAuthStorage: vi.fn(() => ({ mocked: true })), + discoverModels: vi.fn(() => ({ find: vi.fn(() => null) })), +})); + +import type { OpenClawConfig } from "../../config/config.js"; +import { discoverModels } from "../pi-model-discovery.js"; +import { buildInlineProviderModels, resolveModel } from "./model.js"; + +const makeModel = (id: string) => ({ + id, + name: id, + reasoning: false, + input: ["text"] as const, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1, + maxTokens: 1, +}); + +beforeEach(() => { + vi.mocked(discoverModels).mockReturnValue({ + find: vi.fn(() => null), + } as unknown as ReturnType); +}); + +describe("buildInlineProviderModels", () => { + it("attaches provider ids to inline models", () => { + const providers = { + " alpha ": { baseUrl: "http://alpha.local", models: [makeModel("alpha-model")] }, + beta: { baseUrl: "http://beta.local", models: [makeModel("beta-model")] }, + }; + + const result = buildInlineProviderModels(providers); + + expect(result).toEqual([ + { + ...makeModel("alpha-model"), + provider: "alpha", + baseUrl: "http://alpha.local", + api: undefined, + }, + { + ...makeModel("beta-model"), + provider: "beta", + baseUrl: "http://beta.local", + api: undefined, + }, + ]); + }); + + it("inherits baseUrl from provider when model does not specify it", () => { + const providers = { + custom: { + baseUrl: "http://localhost:8000", + models: [makeModel("custom-model")], + }, + }; + + const result = buildInlineProviderModels(providers); + + expect(result).toHaveLength(1); + expect(result[0].baseUrl).toBe("http://localhost:8000"); + }); + + it("inherits api from provider when model does not specify it", () => { + const providers = { + custom: { + baseUrl: "http://localhost:8000", + api: "anthropic-messages", + models: [makeModel("custom-model")], + }, + }; + + const result = buildInlineProviderModels(providers); + + expect(result).toHaveLength(1); + expect(result[0].api).toBe("anthropic-messages"); + }); + + it("model-level api takes precedence over provider-level api", () => { + const providers = { + custom: { + baseUrl: "http://localhost:8000", + api: "openai-responses", + models: [{ ...makeModel("custom-model"), api: "anthropic-messages" as const }], + }, + }; + + const result = buildInlineProviderModels(providers); + + expect(result).toHaveLength(1); + expect(result[0].api).toBe("anthropic-messages"); + }); + + it("inherits both baseUrl and api from provider config", () => { + const providers = { + custom: { + baseUrl: "http://localhost:10000", + api: "anthropic-messages", + models: [makeModel("claude-opus-4.5")], + }, + }; + + const result = buildInlineProviderModels(providers); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + provider: "custom", + baseUrl: "http://localhost:10000", + api: "anthropic-messages", + name: "claude-opus-4.5", + }); + }); +}); + +describe("resolveModel", () => { + it("includes provider baseUrl in fallback model", () => { + const cfg = { + models: { + providers: { + custom: { + baseUrl: "http://localhost:9000", + models: [], + }, + }, + }, + } as OpenClawConfig; + + const result = resolveModel("custom", "missing-model", "/tmp/agent", cfg); + + expect(result.model?.baseUrl).toBe("http://localhost:9000"); + expect(result.model?.provider).toBe("custom"); + expect(result.model?.id).toBe("missing-model"); + }); + + it("builds an openai-codex fallback for gpt-5.3-codex", () => { + const templateModel = { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + provider: "openai-codex", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + input: ["text", "image"] as const, + cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }, + contextWindow: 272000, + maxTokens: 128000, + }; + + vi.mocked(discoverModels).mockReturnValue({ + find: vi.fn((provider: string, modelId: string) => { + if (provider === "openai-codex" && modelId === "gpt-5.2-codex") { + return templateModel; + } + return null; + }), + } as unknown as ReturnType); + + const result = resolveModel("openai-codex", "gpt-5.3-codex", "/tmp/agent"); + + expect(result.error).toBeUndefined(); + expect(result.model).toMatchObject({ + provider: "openai-codex", + id: "gpt-5.3-codex", + api: "openai-codex-responses", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + contextWindow: 272000, + maxTokens: 128000, + }); + }); + + it("builds an anthropic forward-compat fallback for claude-opus-4-6", () => { + const templateModel = { + id: "claude-opus-4-5", + name: "Claude Opus 4.5", + provider: "anthropic", + api: "anthropic-messages", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"] as const, + cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, + contextWindow: 200000, + maxTokens: 64000, + }; + + vi.mocked(discoverModels).mockReturnValue({ + find: vi.fn((provider: string, modelId: string) => { + if (provider === "anthropic" && modelId === "claude-opus-4-5") { + return templateModel; + } + return null; + }), + } as unknown as ReturnType); + + const result = resolveModel("anthropic", "claude-opus-4-6", "/tmp/agent"); + + expect(result.error).toBeUndefined(); + expect(result.model).toMatchObject({ + provider: "anthropic", + id: "claude-opus-4-6", + api: "anthropic-messages", + baseUrl: "https://api.anthropic.com", + reasoning: true, + }); + }); + + it("builds a google-antigravity forward-compat fallback for claude-opus-4-6-thinking", () => { + const templateModel = { + id: "claude-opus-4-5-thinking", + name: "Claude Opus 4.5 Thinking", + provider: "google-antigravity", + api: "google-gemini-cli", + baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com", + reasoning: true, + input: ["text", "image"] as const, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1000000, + maxTokens: 64000, + }; + + vi.mocked(discoverModels).mockReturnValue({ + find: vi.fn((provider: string, modelId: string) => { + if (provider === "google-antigravity" && modelId === "claude-opus-4-5-thinking") { + return templateModel; + } + return null; + }), + } as unknown as ReturnType); + + const result = resolveModel("google-antigravity", "claude-opus-4-6-thinking", "/tmp/agent"); + + expect(result.error).toBeUndefined(); + expect(result.model).toMatchObject({ + provider: "google-antigravity", + id: "claude-opus-4-6-thinking", + api: "google-gemini-cli", + baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com", + reasoning: true, + }); + }); + + it("builds a zai forward-compat fallback for glm-5", () => { + const templateModel = { + id: "glm-4.7", + name: "GLM-4.7", + provider: "zai", + api: "openai-completions", + baseUrl: "https://api.z.ai/api/paas/v4", + reasoning: true, + input: ["text"] as const, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200000, + maxTokens: 131072, + }; + + vi.mocked(discoverModels).mockReturnValue({ + find: vi.fn((provider: string, modelId: string) => { + if (provider === "zai" && modelId === "glm-4.7") { + return templateModel; + } + return null; + }), + } as unknown as ReturnType); + + const result = resolveModel("zai", "glm-5", "/tmp/agent"); + + expect(result.error).toBeUndefined(); + expect(result.model).toMatchObject({ + provider: "zai", + id: "glm-5", + api: "openai-completions", + baseUrl: "https://api.z.ai/api/paas/v4", + reasoning: true, + }); + }); + + it("keeps unknown-model errors for non-gpt-5 openai-codex ids", () => { + const result = resolveModel("openai-codex", "gpt-4.1-mini", "/tmp/agent"); + expect(result.model).toBeUndefined(); + expect(result.error).toBe("Unknown model: openai-codex/gpt-4.1-mini"); + }); + + it("uses codex fallback even when openai-codex provider is configured", () => { + // This test verifies the ordering: codex fallback must fire BEFORE the generic providerCfg fallback. + // If ordering is wrong, the generic fallback would use api: "openai-responses" (the default) + // instead of "openai-codex-responses". + const cfg: OpenClawConfig = { + models: { + providers: { + "openai-codex": { + baseUrl: "https://custom.example.com", + // No models array, or models without gpt-5.3-codex + }, + }, + }, + } as OpenClawConfig; + + vi.mocked(discoverModels).mockReturnValue({ + find: vi.fn(() => null), + } as unknown as ReturnType); + + const result = resolveModel("openai-codex", "gpt-5.3-codex", "/tmp/agent", cfg); + + expect(result.error).toBeUndefined(); + expect(result.model?.api).toBe("openai-codex-responses"); + expect(result.model?.id).toBe("gpt-5.3-codex"); + expect(result.model?.provider).toBe("openai-codex"); + }); +}); diff --git a/src/agents/pi-embedded-runner/run.overflow-compaction.test.ts b/src/agents/pi-embedded-runner/run.overflow-compaction.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run.overflow-compaction.test.ts rename to src/agents/pi-embedded-runner/run.overflow-compaction.e2e.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.test.ts rename to src/agents/pi-embedded-runner/run/attempt.e2e.test.ts diff --git a/src/agents/pi-embedded-runner/run/images.test.ts b/src/agents/pi-embedded-runner/run/images.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/images.test.ts rename to src/agents/pi-embedded-runner/run/images.e2e.test.ts diff --git a/src/agents/pi-embedded-runner/run/payloads.test.ts b/src/agents/pi-embedded-runner/run/payloads.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/payloads.test.ts rename to src/agents/pi-embedded-runner/run/payloads.e2e.test.ts diff --git a/src/agents/pi-embedded-runner/sanitize-session-history.tool-result-details.test.ts b/src/agents/pi-embedded-runner/sanitize-session-history.tool-result-details.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/sanitize-session-history.tool-result-details.test.ts rename to src/agents/pi-embedded-runner/sanitize-session-history.tool-result-details.e2e.test.ts diff --git a/src/agents/pi-embedded-runner/tool-result-truncation.test.ts b/src/agents/pi-embedded-runner/tool-result-truncation.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/tool-result-truncation.test.ts rename to src/agents/pi-embedded-runner/tool-result-truncation.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.code-span-awareness.test.ts b/src/agents/pi-embedded-subscribe.code-span-awareness.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.code-span-awareness.test.ts rename to src/agents/pi-embedded-subscribe.code-span-awareness.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.reply-tags.test.ts b/src/agents/pi-embedded-subscribe.reply-tags.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.reply-tags.test.ts rename to src/agents/pi-embedded-subscribe.reply-tags.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.calls-onblockreplyflush-before-tool-execution-start-preserve.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.calls-onblockreplyflush-before-tool-execution-start-preserve.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-call-onblockreplyflush-callback-is-not.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-call-onblockreplyflush-callback-is-not.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-call-onblockreplyflush-callback-is-not.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-call-onblockreplyflush-callback-is-not.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-emit-duplicate-block-replies-text.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-emit-duplicate-block-replies-text.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-emit-duplicate-block-replies-text.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-emit-duplicate-block-replies-text.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-reasoning-as-separate-message-enabled.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-reasoning-as-separate-message-enabled.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-reasoning-as-separate-message-enabled.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-reasoning-as-separate-message-enabled.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.filters-final-suppresses-output-without-start-tag.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.filters-final-suppresses-output-without-start-tag.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.filters-final-suppresses-output-without-start-tag.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.filters-final-suppresses-output-without-start-tag.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.includes-canvas-action-metadata-tool-summaries.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.includes-canvas-action-metadata-tool-summaries.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.includes-canvas-action-metadata-tool-summaries.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.includes-canvas-action-metadata-tool-summaries.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-assistanttexts-final-answer-block-replies-are.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-assistanttexts-final-answer-block-replies-are.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-indented-fenced-blocks-intact.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-indented-fenced-blocks-intact.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-indented-fenced-blocks-intact.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-indented-fenced-blocks-intact.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.reopens-fenced-blocks-splitting-inside-them.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.reopens-fenced-blocks-splitting-inside-them.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.reopens-fenced-blocks-splitting-inside-them.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.reopens-fenced-blocks-splitting-inside-them.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.splits-long-single-line-fenced-blocks-reopen.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.splits-long-single-line-fenced-blocks-reopen.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.splits-long-single-line-fenced-blocks-reopen.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.splits-long-single-line-fenced-blocks-reopen.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.streams-soft-chunks-paragraph-preference.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.streams-soft-chunks-paragraph-preference.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.streams-soft-chunks-paragraph-preference.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.streams-soft-chunks-paragraph-preference.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts rename to src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.e2e.test.ts diff --git a/src/agents/pi-embedded-subscribe.tools.test.ts b/src/agents/pi-embedded-subscribe.tools.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.tools.test.ts rename to src/agents/pi-embedded-subscribe.tools.e2e.test.ts diff --git a/src/agents/pi-embedded-utils.test.ts b/src/agents/pi-embedded-utils.e2e.test.ts similarity index 100% rename from src/agents/pi-embedded-utils.test.ts rename to src/agents/pi-embedded-utils.e2e.test.ts diff --git a/src/agents/pi-extensions/compaction-safeguard.test.ts b/src/agents/pi-extensions/compaction-safeguard.e2e.test.ts similarity index 100% rename from src/agents/pi-extensions/compaction-safeguard.test.ts rename to src/agents/pi-extensions/compaction-safeguard.e2e.test.ts diff --git a/src/agents/pi-extensions/context-pruning.test.ts b/src/agents/pi-extensions/context-pruning.e2e.test.ts similarity index 100% rename from src/agents/pi-extensions/context-pruning.test.ts rename to src/agents/pi-extensions/context-pruning.e2e.test.ts diff --git a/src/agents/pi-settings.test.ts b/src/agents/pi-settings.e2e.test.ts similarity index 100% rename from src/agents/pi-settings.test.ts rename to src/agents/pi-settings.e2e.test.ts diff --git a/src/agents/pi-tool-definition-adapter.after-tool-call.test.ts b/src/agents/pi-tool-definition-adapter.after-tool-call.e2e.test.ts similarity index 100% rename from src/agents/pi-tool-definition-adapter.after-tool-call.test.ts rename to src/agents/pi-tool-definition-adapter.after-tool-call.e2e.test.ts diff --git a/src/agents/pi-tool-definition-adapter.test.ts b/src/agents/pi-tool-definition-adapter.e2e.test.ts similarity index 100% rename from src/agents/pi-tool-definition-adapter.test.ts rename to src/agents/pi-tool-definition-adapter.e2e.test.ts diff --git a/src/agents/pi-tools-agent-config.test.ts b/src/agents/pi-tools-agent-config.e2e.test.ts similarity index 100% rename from src/agents/pi-tools-agent-config.test.ts rename to src/agents/pi-tools-agent-config.e2e.test.ts diff --git a/src/agents/pi-tools.before-tool-call.test.ts b/src/agents/pi-tools.before-tool-call.e2e.test.ts similarity index 100% rename from src/agents/pi-tools.before-tool-call.test.ts rename to src/agents/pi-tools.before-tool-call.e2e.test.ts diff --git a/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-b.test.ts b/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-b.e2e.test.ts similarity index 100% rename from src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-b.test.ts rename to src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-b.e2e.test.ts diff --git a/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-d.test.ts b/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-d.e2e.test.ts similarity index 100% rename from src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-d.test.ts rename to src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-d.e2e.test.ts diff --git a/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-f.test.ts b/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-f.e2e.test.ts similarity index 100% rename from src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-f.test.ts rename to src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-f.e2e.test.ts diff --git a/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts b/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping.e2e.test.ts similarity index 100% rename from src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts rename to src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping.e2e.test.ts diff --git a/src/agents/pi-tools.policy.test.ts b/src/agents/pi-tools.policy.e2e.test.ts similarity index 100% rename from src/agents/pi-tools.policy.test.ts rename to src/agents/pi-tools.policy.e2e.test.ts diff --git a/src/agents/pi-tools.safe-bins.test.ts b/src/agents/pi-tools.safe-bins.e2e.test.ts similarity index 100% rename from src/agents/pi-tools.safe-bins.test.ts rename to src/agents/pi-tools.safe-bins.e2e.test.ts diff --git a/src/agents/pi-tools.whatsapp-login-gating.test.ts b/src/agents/pi-tools.whatsapp-login-gating.e2e.test.ts similarity index 100% rename from src/agents/pi-tools.whatsapp-login-gating.test.ts rename to src/agents/pi-tools.whatsapp-login-gating.e2e.test.ts diff --git a/src/agents/pi-tools.workspace-paths.test.ts b/src/agents/pi-tools.workspace-paths.e2e.test.ts similarity index 100% rename from src/agents/pi-tools.workspace-paths.test.ts rename to src/agents/pi-tools.workspace-paths.e2e.test.ts diff --git a/src/agents/pty-dsr.test.ts b/src/agents/pty-dsr.e2e.test.ts similarity index 100% rename from src/agents/pty-dsr.test.ts rename to src/agents/pty-dsr.e2e.test.ts diff --git a/src/agents/pty-keys.test.ts b/src/agents/pty-keys.e2e.test.ts similarity index 100% rename from src/agents/pty-keys.test.ts rename to src/agents/pty-keys.e2e.test.ts diff --git a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.e2e.test.ts b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.e2e.test.ts new file mode 100644 index 000000000..b6a751fa5 --- /dev/null +++ b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.e2e.test.ts @@ -0,0 +1,525 @@ +import { EventEmitter } from "node:events"; +import path from "node:path"; +import { Readable } from "node:stream"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; + +type SpawnCall = { + command: string; + args: string[]; +}; + +const spawnCalls: SpawnCall[] = []; + +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawn: (command: string, args: string[]) => { + spawnCalls.push({ command, args }); + const child = new EventEmitter() as { + stdout?: Readable; + stderr?: Readable; + on: (event: string, cb: (...args: unknown[]) => void) => void; + }; + child.stdout = new Readable({ read() {} }); + child.stderr = new Readable({ read() {} }); + + const dockerArgs = command === "docker" ? args : []; + const shouldFailContainerInspect = + dockerArgs[0] === "inspect" && + dockerArgs[1] === "-f" && + dockerArgs[2] === "{{.State.Running}}"; + const shouldSucceedImageInspect = dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; + + queueMicrotask(() => + child.emit("close", shouldFailContainerInspect && !shouldSucceedImageInspect ? 1 : 0), + ); + return child; + }, + }; +}); + +vi.mock("../skills.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + syncSkillsToWorkspace: vi.fn(async () => undefined), + }; +}); + +describe("Agent-specific sandbox config", () => { + beforeEach(() => { + spawnCalls.length = 0; + vi.resetModules(); + }); + + it("should use agent-specific workspaceRoot", async () => { + const { resolveSandboxContext } = await import("./sandbox.js"); + + const cfg: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "all", + scope: "agent", + workspaceRoot: "~/.openclaw/sandboxes", + }, + }, + list: [ + { + id: "isolated", + workspace: "~/openclaw-isolated", + sandbox: { + mode: "all", + scope: "agent", + workspaceRoot: "/tmp/isolated-sandboxes", + }, + }, + ], + }, + }; + + const context = await resolveSandboxContext({ + config: cfg, + sessionKey: "agent:isolated:main", + workspaceDir: "/tmp/test-isolated", + }); + + expect(context).toBeDefined(); + expect(context?.workspaceDir).toContain(path.resolve("/tmp/isolated-sandboxes")); + }); + + it("should prefer agent config over global for multiple agents", async () => { + const { resolveSandboxContext } = await import("./sandbox.js"); + + const cfg: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "non-main", + scope: "session", + }, + }, + list: [ + { + id: "main", + workspace: "~/openclaw", + sandbox: { + mode: "off", + }, + }, + { + id: "family", + workspace: "~/openclaw-family", + sandbox: { + mode: "all", + scope: "agent", + }, + }, + ], + }, + }; + + const mainContext = await resolveSandboxContext({ + config: cfg, + sessionKey: "agent:main:telegram:group:789", + workspaceDir: "/tmp/test-main", + }); + expect(mainContext).toBeNull(); + + const familyContext = await resolveSandboxContext({ + config: cfg, + sessionKey: "agent:family:whatsapp:group:123", + workspaceDir: "/tmp/test-family", + }); + expect(familyContext).toBeDefined(); + expect(familyContext?.enabled).toBe(true); + }); + + it("should prefer agent-specific sandbox tool policy", async () => { + const { resolveSandboxContext } = await import("./sandbox.js"); + + const cfg: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "all", + scope: "agent", + }, + }, + list: [ + { + id: "restricted", + workspace: "~/openclaw-restricted", + sandbox: { + mode: "all", + scope: "agent", + }, + tools: { + sandbox: { + tools: { + allow: ["read", "write"], + deny: ["edit"], + }, + }, + }, + }, + ], + }, + tools: { + sandbox: { + tools: { + allow: ["read"], + deny: ["exec"], + }, + }, + }, + }; + + const context = await resolveSandboxContext({ + config: cfg, + sessionKey: "agent:restricted:main", + workspaceDir: "/tmp/test-restricted", + }); + + expect(context).toBeDefined(); + expect(context?.tools).toEqual({ + allow: ["read", "write", "image"], + deny: ["edit"], + }); + }); + + it("should use global sandbox config when no agent-specific config exists", async () => { + const { resolveSandboxContext } = await import("./sandbox.js"); + + const cfg: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "all", + scope: "agent", + }, + }, + list: [ + { + id: "main", + workspace: "~/openclaw", + }, + ], + }, + }; + + const context = await resolveSandboxContext({ + config: cfg, + sessionKey: "agent:main:main", + workspaceDir: "/tmp/test", + }); + + expect(context).toBeDefined(); + expect(context?.enabled).toBe(true); + }); + + it("should allow agent-specific docker setupCommand overrides", async () => { + const { resolveSandboxContext } = await import("./sandbox.js"); + + const cfg: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "all", + scope: "agent", + docker: { + setupCommand: "echo global", + }, + }, + }, + list: [ + { + id: "work", + workspace: "~/openclaw-work", + sandbox: { + mode: "all", + scope: "agent", + docker: { + setupCommand: "echo work", + }, + }, + }, + ], + }, + }; + + const context = await resolveSandboxContext({ + config: cfg, + sessionKey: "agent:work:main", + workspaceDir: "/tmp/test-work", + }); + + expect(context).toBeDefined(); + expect(context?.docker.setupCommand).toBe("echo work"); + expect( + spawnCalls.some( + (call) => + call.command === "docker" && + call.args[0] === "exec" && + call.args.includes("-lc") && + call.args.includes("echo work"), + ), + ).toBe(true); + }); + + it("should ignore agent-specific docker overrides when scope is shared", async () => { + const { resolveSandboxContext } = await import("./sandbox.js"); + + const cfg: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "all", + scope: "shared", + docker: { + setupCommand: "echo global", + }, + }, + }, + list: [ + { + id: "work", + workspace: "~/openclaw-work", + sandbox: { + mode: "all", + scope: "shared", + docker: { + setupCommand: "echo work", + }, + }, + }, + ], + }, + }; + + const context = await resolveSandboxContext({ + config: cfg, + sessionKey: "agent:work:main", + workspaceDir: "/tmp/test-work", + }); + + expect(context).toBeDefined(); + expect(context?.docker.setupCommand).toBe("echo global"); + expect(context?.containerName).toContain("shared"); + expect( + spawnCalls.some( + (call) => + call.command === "docker" && + call.args[0] === "exec" && + call.args.includes("-lc") && + call.args.includes("echo global"), + ), + ).toBe(true); + }); + + it("should allow agent-specific docker settings beyond setupCommand", async () => { + const { resolveSandboxContext } = await import("./sandbox.js"); + + const cfg: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "all", + scope: "agent", + docker: { + image: "global-image", + network: "none", + }, + }, + }, + list: [ + { + id: "work", + workspace: "~/openclaw-work", + sandbox: { + mode: "all", + scope: "agent", + docker: { + image: "work-image", + network: "bridge", + }, + }, + }, + ], + }, + }; + + const context = await resolveSandboxContext({ + config: cfg, + sessionKey: "agent:work:main", + workspaceDir: "/tmp/test-work", + }); + + expect(context).toBeDefined(); + expect(context?.docker.image).toBe("work-image"); + expect(context?.docker.network).toBe("bridge"); + }); + + it("should override with agent-specific sandbox mode 'off'", async () => { + const { resolveSandboxContext } = await import("./sandbox.js"); + + const cfg: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "all", + scope: "agent", + }, + }, + list: [ + { + id: "main", + workspace: "~/openclaw", + sandbox: { + mode: "off", + }, + }, + ], + }, + }; + + const context = await resolveSandboxContext({ + config: cfg, + sessionKey: "agent:main:main", + workspaceDir: "/tmp/test", + }); + + expect(context).toBeNull(); + }); + + it("should use agent-specific sandbox mode 'all'", async () => { + const { resolveSandboxContext } = await import("./sandbox.js"); + + const cfg: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "off", + }, + }, + list: [ + { + id: "family", + workspace: "~/openclaw-family", + sandbox: { + mode: "all", + scope: "agent", + }, + }, + ], + }, + }; + + const context = await resolveSandboxContext({ + config: cfg, + sessionKey: "agent:family:whatsapp:group:123", + workspaceDir: "/tmp/test-family", + }); + + expect(context).toBeDefined(); + expect(context?.enabled).toBe(true); + }); + + it("should use agent-specific scope", async () => { + const { resolveSandboxContext } = await import("./sandbox.js"); + + const cfg: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "all", + scope: "session", + }, + }, + list: [ + { + id: "work", + workspace: "~/openclaw-work", + sandbox: { + mode: "all", + scope: "agent", + }, + }, + ], + }, + }; + + const context = await resolveSandboxContext({ + config: cfg, + sessionKey: "agent:work:slack:channel:456", + workspaceDir: "/tmp/test-work", + }); + + expect(context).toBeDefined(); + expect(context?.containerName).toContain("agent-work"); + }); + + it("includes session_status in default sandbox allowlist", async () => { + const { resolveSandboxConfigForAgent } = await import("./sandbox.js"); + + const cfg: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "all", + scope: "agent", + }, + }, + }, + }; + + const sandbox = resolveSandboxConfigForAgent(cfg, "main"); + expect(sandbox.tools.allow).toContain("session_status"); + }); + + it("includes image in default sandbox allowlist", async () => { + const { resolveSandboxConfigForAgent } = await import("./sandbox.js"); + + const cfg: OpenClawConfig = { + agents: { + defaults: { + sandbox: { + mode: "all", + scope: "agent", + }, + }, + }, + }; + + const sandbox = resolveSandboxConfigForAgent(cfg, "main"); + expect(sandbox.tools.allow).toContain("image"); + }); + + it("injects image into explicit sandbox allowlists", async () => { + const { resolveSandboxConfigForAgent } = await import("./sandbox.js"); + + const cfg: OpenClawConfig = { + tools: { + sandbox: { + tools: { + allow: ["bash", "read"], + deny: [], + }, + }, + }, + agents: { + defaults: { + sandbox: { + mode: "all", + scope: "agent", + }, + }, + }, + }; + + const sandbox = resolveSandboxConfigForAgent(cfg, "main"); + expect(sandbox.tools.allow).toContain("image"); + }); +}); diff --git a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.includes-session-status-default-sandbox-allowlist.test.ts b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.includes-session-status-default-sandbox-allowlist.test.ts deleted file mode 100644 index a816b8208..000000000 --- a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.includes-session-status-default-sandbox-allowlist.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { EventEmitter } from "node:events"; -import { Readable } from "node:stream"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../config/config.js"; - -// We need to test the internal defaultSandboxConfig function, but it's not exported. -// Instead, we test the behavior through resolveSandboxContext which uses it. - -type SpawnCall = { - command: string; - args: string[]; -}; - -const spawnCalls: SpawnCall[] = []; - -vi.mock("node:child_process", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - spawn: (command: string, args: string[]) => { - spawnCalls.push({ command, args }); - const child = new EventEmitter() as { - stdout?: Readable; - stderr?: Readable; - on: (event: string, cb: (...args: unknown[]) => void) => void; - }; - child.stdout = new Readable({ read() {} }); - child.stderr = new Readable({ read() {} }); - - const dockerArgs = command === "docker" ? args : []; - const shouldFailContainerInspect = - dockerArgs[0] === "inspect" && - dockerArgs[1] === "-f" && - dockerArgs[2] === "{{.State.Running}}"; - const shouldSucceedImageInspect = dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; - - const code = shouldFailContainerInspect ? 1 : 0; - if (shouldSucceedImageInspect) { - queueMicrotask(() => child.emit("close", 0)); - } else { - queueMicrotask(() => child.emit("close", code)); - } - return child; - }, - }; -}); - -describe("Agent-specific sandbox config", () => { - beforeEach(() => { - spawnCalls.length = 0; - }); - - it("includes session_status in default sandbox allowlist", async () => { - const { resolveSandboxConfigForAgent } = await import("./sandbox.js"); - - const cfg: OpenClawConfig = { - agents: { - defaults: { - sandbox: { - mode: "all", - scope: "agent", - }, - }, - }, - }; - - const sandbox = resolveSandboxConfigForAgent(cfg, "main"); - expect(sandbox.tools.allow).toContain("session_status"); - }); - it("includes image in default sandbox allowlist", async () => { - const { resolveSandboxConfigForAgent } = await import("./sandbox.js"); - - const cfg: OpenClawConfig = { - agents: { - defaults: { - sandbox: { - mode: "all", - scope: "agent", - }, - }, - }, - }; - - const sandbox = resolveSandboxConfigForAgent(cfg, "main"); - expect(sandbox.tools.allow).toContain("image"); - }); - it("injects image into explicit sandbox allowlists", async () => { - const { resolveSandboxConfigForAgent } = await import("./sandbox.js"); - - const cfg: OpenClawConfig = { - tools: { - sandbox: { - tools: { - allow: ["bash", "read"], - deny: [], - }, - }, - }, - agents: { - defaults: { - sandbox: { - mode: "all", - scope: "agent", - }, - }, - }, - }; - - const sandbox = resolveSandboxConfigForAgent(cfg, "main"); - expect(sandbox.tools.allow).toContain("image"); - }); -}); diff --git a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-allow-agent-specific-docker-settings-beyond.test.ts b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-allow-agent-specific-docker-settings-beyond.test.ts deleted file mode 100644 index bb3137dee..000000000 --- a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-allow-agent-specific-docker-settings-beyond.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { EventEmitter } from "node:events"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { Readable } from "node:stream"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../config/config.js"; - -// We need to test the internal defaultSandboxConfig function, but it's not exported. -// Instead, we test the behavior through resolveSandboxContext which uses it. - -type SpawnCall = { - command: string; - args: string[]; -}; - -const spawnCalls: SpawnCall[] = []; - -vi.mock("node:child_process", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - spawn: (command: string, args: string[]) => { - spawnCalls.push({ command, args }); - const child = new EventEmitter() as { - stdout?: Readable; - stderr?: Readable; - on: (event: string, cb: (...args: unknown[]) => void) => void; - }; - child.stdout = new Readable({ read() {} }); - child.stderr = new Readable({ read() {} }); - - const dockerArgs = command === "docker" ? args : []; - const shouldFailContainerInspect = - dockerArgs[0] === "inspect" && - dockerArgs[1] === "-f" && - dockerArgs[2] === "{{.State.Running}}"; - const shouldSucceedImageInspect = dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; - - const code = shouldFailContainerInspect ? 1 : 0; - if (shouldSucceedImageInspect) { - queueMicrotask(() => child.emit("close", 0)); - } else { - queueMicrotask(() => child.emit("close", code)); - } - return child; - }, - }; -}); - -vi.mock("../skills.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - syncSkillsToWorkspace: vi.fn(async () => undefined), - }; -}); -describe("Agent-specific sandbox config", () => { - let previousStateDir: string | undefined; - let tempStateDir: string | undefined; - - beforeEach(async () => { - spawnCalls.length = 0; - previousStateDir = process.env.MOLTBOT_STATE_DIR; - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-test-state-")); - process.env.MOLTBOT_STATE_DIR = tempStateDir; - vi.resetModules(); - }); - - afterEach(async () => { - if (tempStateDir) { - await fs.rm(tempStateDir, { recursive: true, force: true }); - } - if (previousStateDir === undefined) { - delete process.env.MOLTBOT_STATE_DIR; - } else { - process.env.MOLTBOT_STATE_DIR = previousStateDir; - } - tempStateDir = undefined; - }); - - it("should allow agent-specific docker settings beyond setupCommand", async () => { - const { resolveSandboxContext } = await import("./sandbox.js"); - - const cfg: OpenClawConfig = { - agents: { - defaults: { - sandbox: { - mode: "all", - scope: "agent", - docker: { - image: "global-image", - network: "none", - }, - }, - }, - list: [ - { - id: "work", - workspace: "~/openclaw-work", - sandbox: { - mode: "all", - scope: "agent", - docker: { - image: "work-image", - network: "bridge", - }, - }, - }, - ], - }, - }; - - const context = await resolveSandboxContext({ - config: cfg, - sessionKey: "agent:work:main", - workspaceDir: "/tmp/test-work", - }); - - expect(context).toBeDefined(); - expect(context?.docker.image).toBe("work-image"); - expect(context?.docker.network).toBe("bridge"); - }); - it("should override with agent-specific sandbox mode 'off'", async () => { - const { resolveSandboxContext } = await import("./sandbox.js"); - - const cfg: OpenClawConfig = { - agents: { - defaults: { - sandbox: { - mode: "all", // Global default - scope: "agent", - }, - }, - list: [ - { - id: "main", - workspace: "~/openclaw", - sandbox: { - mode: "off", // Agent override - }, - }, - ], - }, - }; - - const context = await resolveSandboxContext({ - config: cfg, - sessionKey: "agent:main:main", - workspaceDir: "/tmp/test", - }); - - // Should be null because mode is "off" - expect(context).toBeNull(); - }); - it("should use agent-specific sandbox mode 'all'", async () => { - const { resolveSandboxContext } = await import("./sandbox.js"); - - const cfg: OpenClawConfig = { - agents: { - defaults: { - sandbox: { - mode: "off", // Global default - }, - }, - list: [ - { - id: "family", - workspace: "~/openclaw-family", - sandbox: { - mode: "all", // Agent override - scope: "agent", - }, - }, - ], - }, - }; - - const context = await resolveSandboxContext({ - config: cfg, - sessionKey: "agent:family:whatsapp:group:123", - workspaceDir: "/tmp/test-family", - }); - - expect(context).toBeDefined(); - expect(context?.enabled).toBe(true); - }); - it("should use agent-specific scope", async () => { - const { resolveSandboxContext } = await import("./sandbox.js"); - - const cfg: OpenClawConfig = { - agents: { - defaults: { - sandbox: { - mode: "all", - scope: "session", // Global default - }, - }, - list: [ - { - id: "work", - workspace: "~/openclaw-work", - sandbox: { - mode: "all", - scope: "agent", // Agent override - }, - }, - ], - }, - }; - - const context = await resolveSandboxContext({ - config: cfg, - sessionKey: "agent:work:slack:channel:456", - workspaceDir: "/tmp/test-work", - }); - - expect(context).toBeDefined(); - // The container name should use agent scope (agent:work) - expect(context?.containerName).toContain("agent-work"); - }); -}); diff --git a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-agent-specific-workspaceroot.test.ts b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-agent-specific-workspaceroot.test.ts deleted file mode 100644 index f1c106c4e..000000000 --- a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-agent-specific-workspaceroot.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { EventEmitter } from "node:events"; -import path from "node:path"; -import { Readable } from "node:stream"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../config/config.js"; - -// We need to test the internal defaultSandboxConfig function, but it's not exported. -// Instead, we test the behavior through resolveSandboxContext which uses it. - -type SpawnCall = { - command: string; - args: string[]; -}; - -const spawnCalls: SpawnCall[] = []; - -vi.mock("node:child_process", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - spawn: (command: string, args: string[]) => { - spawnCalls.push({ command, args }); - const child = new EventEmitter() as { - stdout?: Readable; - stderr?: Readable; - on: (event: string, cb: (...args: unknown[]) => void) => void; - }; - child.stdout = new Readable({ read() {} }); - child.stderr = new Readable({ read() {} }); - - const dockerArgs = command === "docker" ? args : []; - const shouldFailContainerInspect = - dockerArgs[0] === "inspect" && - dockerArgs[1] === "-f" && - dockerArgs[2] === "{{.State.Running}}"; - const shouldSucceedImageInspect = dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; - - const code = shouldFailContainerInspect ? 1 : 0; - if (shouldSucceedImageInspect) { - queueMicrotask(() => child.emit("close", 0)); - } else { - queueMicrotask(() => child.emit("close", code)); - } - return child; - }, - }; -}); - -vi.mock("../skills.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - syncSkillsToWorkspace: vi.fn(async () => undefined), - }; -}); -describe("Agent-specific sandbox config", () => { - beforeEach(() => { - spawnCalls.length = 0; - vi.resetModules(); - }); - - it("should use agent-specific workspaceRoot", async () => { - const { resolveSandboxContext } = await import("./sandbox.js"); - - const cfg: OpenClawConfig = { - agents: { - defaults: { - sandbox: { - mode: "all", - scope: "agent", - workspaceRoot: "~/.openclaw/sandboxes", // Global default - }, - }, - list: [ - { - id: "isolated", - workspace: "~/openclaw-isolated", - sandbox: { - mode: "all", - scope: "agent", - workspaceRoot: "/tmp/isolated-sandboxes", // Agent override - }, - }, - ], - }, - }; - - const context = await resolveSandboxContext({ - config: cfg, - sessionKey: "agent:isolated:main", - workspaceDir: "/tmp/test-isolated", - }); - - expect(context).toBeDefined(); - expect(context?.workspaceDir).toContain(path.resolve("/tmp/isolated-sandboxes")); - }); - it("should prefer agent config over global for multiple agents", async () => { - const { resolveSandboxContext } = await import("./sandbox.js"); - - const cfg: OpenClawConfig = { - agents: { - defaults: { - sandbox: { - mode: "non-main", - scope: "session", - }, - }, - list: [ - { - id: "main", - workspace: "~/openclaw", - sandbox: { - mode: "off", // main: no sandbox - }, - }, - { - id: "family", - workspace: "~/openclaw-family", - sandbox: { - mode: "all", // family: always sandbox - scope: "agent", - }, - }, - ], - }, - }; - - // main agent should not be sandboxed - const mainContext = await resolveSandboxContext({ - config: cfg, - sessionKey: "agent:main:telegram:group:789", - workspaceDir: "/tmp/test-main", - }); - expect(mainContext).toBeNull(); - - // family agent should be sandboxed - const familyContext = await resolveSandboxContext({ - config: cfg, - sessionKey: "agent:family:whatsapp:group:123", - workspaceDir: "/tmp/test-family", - }); - expect(familyContext).toBeDefined(); - expect(familyContext?.enabled).toBe(true); - }); - it("should prefer agent-specific sandbox tool policy", async () => { - const { resolveSandboxContext } = await import("./sandbox.js"); - - const cfg: OpenClawConfig = { - agents: { - defaults: { - sandbox: { - mode: "all", - scope: "agent", - }, - }, - list: [ - { - id: "restricted", - workspace: "~/openclaw-restricted", - sandbox: { - mode: "all", - scope: "agent", - }, - tools: { - sandbox: { - tools: { - allow: ["read", "write"], - deny: ["edit"], - }, - }, - }, - }, - ], - }, - tools: { - sandbox: { - tools: { - allow: ["read"], - deny: ["exec"], - }, - }, - }, - }; - - const context = await resolveSandboxContext({ - config: cfg, - sessionKey: "agent:restricted:main", - workspaceDir: "/tmp/test-restricted", - }); - - expect(context).toBeDefined(); - expect(context?.tools).toEqual({ - allow: ["read", "write", "image"], - deny: ["edit"], - }); - }); -}); diff --git a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-global-sandbox-config-no-agent.test.ts b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-global-sandbox-config-no-agent.test.ts deleted file mode 100644 index 4cfe48c05..000000000 --- a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-global-sandbox-config-no-agent.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { EventEmitter } from "node:events"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { Readable } from "node:stream"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../config/config.js"; - -// We need to test the internal defaultSandboxConfig function, but it's not exported. -// Instead, we test the behavior through resolveSandboxContext which uses it. - -type SpawnCall = { - command: string; - args: string[]; -}; - -const spawnCalls: SpawnCall[] = []; - -vi.mock("node:child_process", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - spawn: (command: string, args: string[]) => { - spawnCalls.push({ command, args }); - const child = new EventEmitter() as { - stdout?: Readable; - stderr?: Readable; - on: (event: string, cb: (...args: unknown[]) => void) => void; - }; - child.stdout = new Readable({ read() {} }); - child.stderr = new Readable({ read() {} }); - - const dockerArgs = command === "docker" ? args : []; - const shouldFailContainerInspect = - dockerArgs[0] === "inspect" && - dockerArgs[1] === "-f" && - dockerArgs[2] === "{{.State.Running}}"; - const shouldSucceedImageInspect = dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; - - const code = shouldFailContainerInspect ? 1 : 0; - if (shouldSucceedImageInspect) { - queueMicrotask(() => child.emit("close", 0)); - } else { - queueMicrotask(() => child.emit("close", code)); - } - return child; - }, - }; -}); - -describe("Agent-specific sandbox config", () => { - let previousStateDir: string | undefined; - let tempStateDir: string | undefined; - - beforeEach(async () => { - spawnCalls.length = 0; - previousStateDir = process.env.MOLTBOT_STATE_DIR; - tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-test-state-")); - process.env.MOLTBOT_STATE_DIR = tempStateDir; - vi.resetModules(); - }); - - afterEach(async () => { - if (tempStateDir) { - await fs.rm(tempStateDir, { recursive: true, force: true }); - } - if (previousStateDir === undefined) { - delete process.env.MOLTBOT_STATE_DIR; - } else { - process.env.MOLTBOT_STATE_DIR = previousStateDir; - } - tempStateDir = undefined; - }); - - it( - "should use global sandbox config when no agent-specific config exists", - { timeout: 60_000 }, - async () => { - const { resolveSandboxContext } = await import("./sandbox.js"); - - const cfg: OpenClawConfig = { - agents: { - defaults: { - sandbox: { - mode: "all", - scope: "agent", - }, - }, - list: [ - { - id: "main", - workspace: "~/openclaw", - }, - ], - }, - }; - - const context = await resolveSandboxContext({ - config: cfg, - sessionKey: "agent:main:main", - workspaceDir: "/tmp/test", - }); - - expect(context).toBeDefined(); - expect(context?.enabled).toBe(true); - }, - ); - it("should allow agent-specific docker setupCommand overrides", async () => { - const { resolveSandboxContext } = await import("./sandbox.js"); - - const cfg: OpenClawConfig = { - agents: { - defaults: { - sandbox: { - mode: "all", - scope: "agent", - docker: { - setupCommand: "echo global", - }, - }, - }, - list: [ - { - id: "work", - workspace: "~/openclaw-work", - sandbox: { - mode: "all", - scope: "agent", - docker: { - setupCommand: "echo work", - }, - }, - }, - ], - }, - }; - - const context = await resolveSandboxContext({ - config: cfg, - sessionKey: "agent:work:main", - workspaceDir: "/tmp/test-work", - }); - - expect(context).toBeDefined(); - expect(context?.docker.setupCommand).toBe("echo work"); - expect( - spawnCalls.some( - (call) => - call.command === "docker" && - call.args[0] === "exec" && - call.args.includes("-lc") && - call.args.includes("echo work"), - ), - ).toBe(true); - }); - it("should ignore agent-specific docker overrides when scope is shared", async () => { - const { resolveSandboxContext } = await import("./sandbox.js"); - - const cfg: OpenClawConfig = { - agents: { - defaults: { - sandbox: { - mode: "all", - scope: "shared", - docker: { - setupCommand: "echo global", - }, - }, - }, - list: [ - { - id: "work", - workspace: "~/openclaw-work", - sandbox: { - mode: "all", - scope: "shared", - docker: { - setupCommand: "echo work", - }, - }, - }, - ], - }, - }; - - const context = await resolveSandboxContext({ - config: cfg, - sessionKey: "agent:work:main", - workspaceDir: "/tmp/test-work", - }); - - expect(context).toBeDefined(); - expect(context?.docker.setupCommand).toBe("echo global"); - expect(context?.containerName).toContain("shared"); - expect( - spawnCalls.some( - (call) => - call.command === "docker" && - call.args[0] === "exec" && - call.args.includes("-lc") && - call.args.includes("echo global"), - ), - ).toBe(true); - }); -}); diff --git a/src/agents/sandbox-create-args.test.ts b/src/agents/sandbox-create-args.e2e.test.ts similarity index 100% rename from src/agents/sandbox-create-args.test.ts rename to src/agents/sandbox-create-args.e2e.test.ts diff --git a/src/agents/sandbox-explain.test.ts b/src/agents/sandbox-explain.e2e.test.ts similarity index 100% rename from src/agents/sandbox-explain.test.ts rename to src/agents/sandbox-explain.e2e.test.ts diff --git a/src/agents/sandbox-merge.test.ts b/src/agents/sandbox-merge.e2e.test.ts similarity index 100% rename from src/agents/sandbox-merge.test.ts rename to src/agents/sandbox-merge.e2e.test.ts diff --git a/src/agents/sandbox-skills.test.ts b/src/agents/sandbox-skills.e2e.test.ts similarity index 100% rename from src/agents/sandbox-skills.test.ts rename to src/agents/sandbox-skills.e2e.test.ts diff --git a/src/agents/sandbox.resolveSandboxContext.test.ts b/src/agents/sandbox.resolveSandboxContext.e2e.test.ts similarity index 100% rename from src/agents/sandbox.resolveSandboxContext.test.ts rename to src/agents/sandbox.resolveSandboxContext.e2e.test.ts diff --git a/src/agents/sandbox/tool-policy.test.ts b/src/agents/sandbox/tool-policy.e2e.test.ts similarity index 100% rename from src/agents/sandbox/tool-policy.test.ts rename to src/agents/sandbox/tool-policy.e2e.test.ts diff --git a/src/agents/session-file-repair.test.ts b/src/agents/session-file-repair.e2e.test.ts similarity index 100% rename from src/agents/session-file-repair.test.ts rename to src/agents/session-file-repair.e2e.test.ts diff --git a/src/agents/session-slug.test.ts b/src/agents/session-slug.e2e.test.ts similarity index 100% rename from src/agents/session-slug.test.ts rename to src/agents/session-slug.e2e.test.ts diff --git a/src/agents/session-tool-result-guard.test.ts b/src/agents/session-tool-result-guard.e2e.test.ts similarity index 100% rename from src/agents/session-tool-result-guard.test.ts rename to src/agents/session-tool-result-guard.e2e.test.ts diff --git a/src/agents/session-tool-result-guard.tool-result-persist-hook.test.ts b/src/agents/session-tool-result-guard.tool-result-persist-hook.e2e.test.ts similarity index 100% rename from src/agents/session-tool-result-guard.tool-result-persist-hook.test.ts rename to src/agents/session-tool-result-guard.tool-result-persist-hook.e2e.test.ts diff --git a/src/agents/session-transcript-repair.test.ts b/src/agents/session-transcript-repair.e2e.test.ts similarity index 100% rename from src/agents/session-transcript-repair.test.ts rename to src/agents/session-transcript-repair.e2e.test.ts diff --git a/src/agents/session-write-lock.test.ts b/src/agents/session-write-lock.e2e.test.ts similarity index 100% rename from src/agents/session-write-lock.test.ts rename to src/agents/session-write-lock.e2e.test.ts diff --git a/src/agents/sessions-spawn-threadid.test.ts b/src/agents/sessions-spawn-threadid.e2e.test.ts similarity index 100% rename from src/agents/sessions-spawn-threadid.test.ts rename to src/agents/sessions-spawn-threadid.e2e.test.ts diff --git a/src/agents/shell-utils.test.ts b/src/agents/shell-utils.e2e.test.ts similarity index 100% rename from src/agents/shell-utils.test.ts rename to src/agents/shell-utils.e2e.test.ts diff --git a/src/agents/skills-install.test.ts b/src/agents/skills-install.e2e.test.ts similarity index 100% rename from src/agents/skills-install.test.ts rename to src/agents/skills-install.e2e.test.ts diff --git a/src/agents/skills-status.test.ts b/src/agents/skills-status.e2e.test.ts similarity index 100% rename from src/agents/skills-status.test.ts rename to src/agents/skills-status.e2e.test.ts diff --git a/src/agents/skills.agents-skills-directory.test.ts b/src/agents/skills.agents-skills-directory.e2e.test.ts similarity index 100% rename from src/agents/skills.agents-skills-directory.test.ts rename to src/agents/skills.agents-skills-directory.e2e.test.ts diff --git a/src/agents/skills.build-workspace-skills-prompt.applies-bundled-allowlist-without-affecting-workspace-skills.test.ts b/src/agents/skills.build-workspace-skills-prompt.applies-bundled-allowlist-without-affecting-workspace-skills.e2e.test.ts similarity index 100% rename from src/agents/skills.build-workspace-skills-prompt.applies-bundled-allowlist-without-affecting-workspace-skills.test.ts rename to src/agents/skills.build-workspace-skills-prompt.applies-bundled-allowlist-without-affecting-workspace-skills.e2e.test.ts diff --git a/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts b/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.e2e.test.ts similarity index 100% rename from src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts rename to src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.e2e.test.ts diff --git a/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts b/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.e2e.test.ts similarity index 100% rename from src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts rename to src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.e2e.test.ts diff --git a/src/agents/skills.buildworkspaceskillsnapshot.test.ts b/src/agents/skills.buildworkspaceskillsnapshot.e2e.test.ts similarity index 100% rename from src/agents/skills.buildworkspaceskillsnapshot.test.ts rename to src/agents/skills.buildworkspaceskillsnapshot.e2e.test.ts diff --git a/src/agents/skills.buildworkspaceskillstatus.test.ts b/src/agents/skills.buildworkspaceskillstatus.e2e.test.ts similarity index 100% rename from src/agents/skills.buildworkspaceskillstatus.test.ts rename to src/agents/skills.buildworkspaceskillstatus.e2e.test.ts diff --git a/src/agents/skills.test.ts b/src/agents/skills.e2e.test.ts similarity index 100% rename from src/agents/skills.test.ts rename to src/agents/skills.e2e.test.ts diff --git a/src/agents/skills.loadworkspaceskillentries.test.ts b/src/agents/skills.loadworkspaceskillentries.e2e.test.ts similarity index 100% rename from src/agents/skills.loadworkspaceskillentries.test.ts rename to src/agents/skills.loadworkspaceskillentries.e2e.test.ts diff --git a/src/agents/skills.resolveskillspromptforrun.test.ts b/src/agents/skills.resolveskillspromptforrun.e2e.test.ts similarity index 100% rename from src/agents/skills.resolveskillspromptforrun.test.ts rename to src/agents/skills.resolveskillspromptforrun.e2e.test.ts diff --git a/src/agents/skills.summarize-skill-description.test.ts b/src/agents/skills.summarize-skill-description.e2e.test.ts similarity index 100% rename from src/agents/skills.summarize-skill-description.test.ts rename to src/agents/skills.summarize-skill-description.e2e.test.ts diff --git a/src/agents/skills/bundled-dir.test.ts b/src/agents/skills/bundled-dir.e2e.test.ts similarity index 100% rename from src/agents/skills/bundled-dir.test.ts rename to src/agents/skills/bundled-dir.e2e.test.ts diff --git a/src/agents/skills/frontmatter.test.ts b/src/agents/skills/frontmatter.e2e.test.ts similarity index 100% rename from src/agents/skills/frontmatter.test.ts rename to src/agents/skills/frontmatter.e2e.test.ts diff --git a/src/agents/skills/refresh.test.ts b/src/agents/skills/refresh.e2e.test.ts similarity index 100% rename from src/agents/skills/refresh.test.ts rename to src/agents/skills/refresh.e2e.test.ts diff --git a/src/agents/subagent-announce.format.test.ts b/src/agents/subagent-announce.format.e2e.test.ts similarity index 100% rename from src/agents/subagent-announce.format.test.ts rename to src/agents/subagent-announce.format.e2e.test.ts diff --git a/src/agents/subagent-registry.persistence.test.ts b/src/agents/subagent-registry.persistence.e2e.test.ts similarity index 100% rename from src/agents/subagent-registry.persistence.test.ts rename to src/agents/subagent-registry.persistence.e2e.test.ts diff --git a/src/agents/system-prompt-params.test.ts b/src/agents/system-prompt-params.e2e.test.ts similarity index 100% rename from src/agents/system-prompt-params.test.ts rename to src/agents/system-prompt-params.e2e.test.ts diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.e2e.test.ts similarity index 100% rename from src/agents/system-prompt.test.ts rename to src/agents/system-prompt.e2e.test.ts diff --git a/src/agents/timeout.test.ts b/src/agents/timeout.e2e.test.ts similarity index 100% rename from src/agents/timeout.test.ts rename to src/agents/timeout.e2e.test.ts diff --git a/src/agents/tool-call-id.test.ts b/src/agents/tool-call-id.e2e.test.ts similarity index 100% rename from src/agents/tool-call-id.test.ts rename to src/agents/tool-call-id.e2e.test.ts diff --git a/src/agents/tool-display.test.ts b/src/agents/tool-display.e2e.test.ts similarity index 100% rename from src/agents/tool-display.test.ts rename to src/agents/tool-display.e2e.test.ts diff --git a/src/agents/tool-images.test.ts b/src/agents/tool-images.e2e.test.ts similarity index 100% rename from src/agents/tool-images.test.ts rename to src/agents/tool-images.e2e.test.ts diff --git a/src/agents/tool-policy.conformance.test.ts b/src/agents/tool-policy.conformance.e2e.test.ts similarity index 100% rename from src/agents/tool-policy.conformance.test.ts rename to src/agents/tool-policy.conformance.e2e.test.ts diff --git a/src/agents/tool-policy.test.ts b/src/agents/tool-policy.e2e.test.ts similarity index 100% rename from src/agents/tool-policy.test.ts rename to src/agents/tool-policy.e2e.test.ts diff --git a/src/agents/tool-policy.plugin-only-allowlist.test.ts b/src/agents/tool-policy.plugin-only-allowlist.e2e.test.ts similarity index 100% rename from src/agents/tool-policy.plugin-only-allowlist.test.ts rename to src/agents/tool-policy.plugin-only-allowlist.e2e.test.ts diff --git a/src/agents/tools/browser-tool.test.ts b/src/agents/tools/browser-tool.e2e.test.ts similarity index 100% rename from src/agents/tools/browser-tool.test.ts rename to src/agents/tools/browser-tool.e2e.test.ts diff --git a/src/agents/tools/common.test.ts b/src/agents/tools/common.e2e.test.ts similarity index 100% rename from src/agents/tools/common.test.ts rename to src/agents/tools/common.e2e.test.ts diff --git a/src/agents/tools/cron-tool.test.ts b/src/agents/tools/cron-tool.e2e.test.ts similarity index 100% rename from src/agents/tools/cron-tool.test.ts rename to src/agents/tools/cron-tool.e2e.test.ts diff --git a/src/agents/tools/discord-actions-presence.test.ts b/src/agents/tools/discord-actions-presence.e2e.test.ts similarity index 100% rename from src/agents/tools/discord-actions-presence.test.ts rename to src/agents/tools/discord-actions-presence.e2e.test.ts diff --git a/src/agents/tools/discord-actions.test.ts b/src/agents/tools/discord-actions.e2e.test.ts similarity index 100% rename from src/agents/tools/discord-actions.test.ts rename to src/agents/tools/discord-actions.e2e.test.ts diff --git a/src/agents/tools/gateway.test.ts b/src/agents/tools/gateway.e2e.test.ts similarity index 100% rename from src/agents/tools/gateway.test.ts rename to src/agents/tools/gateway.e2e.test.ts diff --git a/src/agents/tools/image-tool.test.ts b/src/agents/tools/image-tool.e2e.test.ts similarity index 100% rename from src/agents/tools/image-tool.test.ts rename to src/agents/tools/image-tool.e2e.test.ts diff --git a/src/agents/tools/memory-tool.citations.test.ts b/src/agents/tools/memory-tool.citations.e2e.test.ts similarity index 100% rename from src/agents/tools/memory-tool.citations.test.ts rename to src/agents/tools/memory-tool.citations.e2e.test.ts diff --git a/src/agents/tools/memory-tool.does-not-crash-on-errors.test.ts b/src/agents/tools/memory-tool.does-not-crash-on-errors.e2e.test.ts similarity index 100% rename from src/agents/tools/memory-tool.does-not-crash-on-errors.test.ts rename to src/agents/tools/memory-tool.does-not-crash-on-errors.e2e.test.ts diff --git a/src/agents/tools/message-tool.test.ts b/src/agents/tools/message-tool.e2e.test.ts similarity index 100% rename from src/agents/tools/message-tool.test.ts rename to src/agents/tools/message-tool.e2e.test.ts diff --git a/src/agents/tools/sessions-announce-target.test.ts b/src/agents/tools/sessions-announce-target.e2e.test.ts similarity index 100% rename from src/agents/tools/sessions-announce-target.test.ts rename to src/agents/tools/sessions-announce-target.e2e.test.ts diff --git a/src/agents/tools/sessions-helpers.test.ts b/src/agents/tools/sessions-helpers.e2e.test.ts similarity index 100% rename from src/agents/tools/sessions-helpers.test.ts rename to src/agents/tools/sessions-helpers.e2e.test.ts diff --git a/src/agents/tools/sessions-list-tool.gating.test.ts b/src/agents/tools/sessions-list-tool.gating.e2e.test.ts similarity index 100% rename from src/agents/tools/sessions-list-tool.gating.test.ts rename to src/agents/tools/sessions-list-tool.gating.e2e.test.ts diff --git a/src/agents/tools/sessions-send-tool.gating.test.ts b/src/agents/tools/sessions-send-tool.gating.e2e.test.ts similarity index 100% rename from src/agents/tools/sessions-send-tool.gating.test.ts rename to src/agents/tools/sessions-send-tool.gating.e2e.test.ts diff --git a/src/agents/tools/slack-actions.test.ts b/src/agents/tools/slack-actions.e2e.test.ts similarity index 100% rename from src/agents/tools/slack-actions.test.ts rename to src/agents/tools/slack-actions.e2e.test.ts diff --git a/src/agents/tools/telegram-actions.test.ts b/src/agents/tools/telegram-actions.e2e.test.ts similarity index 100% rename from src/agents/tools/telegram-actions.test.ts rename to src/agents/tools/telegram-actions.e2e.test.ts diff --git a/src/agents/tools/web-fetch.firecrawl-api-key-normalization.test.ts b/src/agents/tools/web-fetch.firecrawl-api-key-normalization.e2e.test.ts similarity index 100% rename from src/agents/tools/web-fetch.firecrawl-api-key-normalization.test.ts rename to src/agents/tools/web-fetch.firecrawl-api-key-normalization.e2e.test.ts diff --git a/src/agents/tools/web-fetch.ssrf.test.ts b/src/agents/tools/web-fetch.ssrf.e2e.test.ts similarity index 100% rename from src/agents/tools/web-fetch.ssrf.test.ts rename to src/agents/tools/web-fetch.ssrf.e2e.test.ts diff --git a/src/agents/tools/web-search.test.ts b/src/agents/tools/web-search.e2e.test.ts similarity index 100% rename from src/agents/tools/web-search.test.ts rename to src/agents/tools/web-search.e2e.test.ts diff --git a/src/agents/tools/web-tools.enabled-defaults.test.ts b/src/agents/tools/web-tools.enabled-defaults.e2e.test.ts similarity index 100% rename from src/agents/tools/web-tools.enabled-defaults.test.ts rename to src/agents/tools/web-tools.enabled-defaults.e2e.test.ts diff --git a/src/agents/tools/web-tools.fetch.test.ts b/src/agents/tools/web-tools.fetch.e2e.test.ts similarity index 100% rename from src/agents/tools/web-tools.fetch.test.ts rename to src/agents/tools/web-tools.fetch.e2e.test.ts diff --git a/src/agents/tools/web-tools.readability.test.ts b/src/agents/tools/web-tools.readability.e2e.test.ts similarity index 100% rename from src/agents/tools/web-tools.readability.test.ts rename to src/agents/tools/web-tools.readability.e2e.test.ts diff --git a/src/agents/tools/whatsapp-actions.test.ts b/src/agents/tools/whatsapp-actions.e2e.test.ts similarity index 100% rename from src/agents/tools/whatsapp-actions.test.ts rename to src/agents/tools/whatsapp-actions.e2e.test.ts diff --git a/src/agents/transcript-policy.e2e.test.ts b/src/agents/transcript-policy.e2e.test.ts new file mode 100644 index 000000000..48977ec98 --- /dev/null +++ b/src/agents/transcript-policy.e2e.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { resolveTranscriptPolicy } from "./transcript-policy.js"; + +describe("resolveTranscriptPolicy", () => { + it("enables sanitizeToolCallIds for Anthropic provider", () => { + const policy = resolveTranscriptPolicy({ + provider: "anthropic", + modelId: "claude-opus-4-5", + modelApi: "anthropic-messages", + }); + expect(policy.sanitizeToolCallIds).toBe(true); + expect(policy.toolCallIdMode).toBe("strict"); + }); + + it("enables sanitizeToolCallIds for Google provider", () => { + const policy = resolveTranscriptPolicy({ + provider: "google", + modelId: "gemini-2.0-flash", + modelApi: "google-generative-ai", + }); + expect(policy.sanitizeToolCallIds).toBe(true); + }); + + it("enables sanitizeToolCallIds for Mistral provider", () => { + const policy = resolveTranscriptPolicy({ + provider: "mistral", + modelId: "mistral-large-latest", + }); + expect(policy.sanitizeToolCallIds).toBe(true); + expect(policy.toolCallIdMode).toBe("strict9"); + }); + + it("disables sanitizeToolCallIds for OpenAI provider", () => { + const policy = resolveTranscriptPolicy({ + provider: "openai", + modelId: "gpt-4o", + modelApi: "openai", + }); + expect(policy.sanitizeToolCallIds).toBe(false); + }); +}); diff --git a/src/agents/usage.test.ts b/src/agents/usage.e2e.test.ts similarity index 100% rename from src/agents/usage.test.ts rename to src/agents/usage.e2e.test.ts diff --git a/src/agents/workspace-run.test.ts b/src/agents/workspace-run.e2e.test.ts similarity index 100% rename from src/agents/workspace-run.test.ts rename to src/agents/workspace-run.e2e.test.ts diff --git a/src/agents/workspace-templates.test.ts b/src/agents/workspace-templates.e2e.test.ts similarity index 100% rename from src/agents/workspace-templates.test.ts rename to src/agents/workspace-templates.e2e.test.ts diff --git a/src/agents/workspace.defaults.test.ts b/src/agents/workspace.defaults.e2e.test.ts similarity index 100% rename from src/agents/workspace.defaults.test.ts rename to src/agents/workspace.defaults.e2e.test.ts diff --git a/src/agents/workspace.test.ts b/src/agents/workspace.e2e.test.ts similarity index 100% rename from src/agents/workspace.test.ts rename to src/agents/workspace.e2e.test.ts diff --git a/src/browser/screenshot.test.ts b/src/browser/screenshot.e2e.test.ts similarity index 100% rename from src/browser/screenshot.test.ts rename to src/browser/screenshot.e2e.test.ts diff --git a/src/cli/daemon-cli.coverage.test.ts b/src/cli/daemon-cli.coverage.e2e.test.ts similarity index 100% rename from src/cli/daemon-cli.coverage.test.ts rename to src/cli/daemon-cli.coverage.e2e.test.ts diff --git a/src/cli/gateway-cli.coverage.test.ts b/src/cli/gateway-cli.coverage.e2e.test.ts similarity index 98% rename from src/cli/gateway-cli.coverage.test.ts rename to src/cli/gateway-cli.coverage.e2e.test.ts index d70e4aa4d..da8cc5f59 100644 --- a/src/cli/gateway-cli.coverage.test.ts +++ b/src/cli/gateway-cli.coverage.e2e.test.ts @@ -55,14 +55,10 @@ async function withEnvOverride( vi.mock( new URL("../../gateway/call.ts", new URL("./gateway-cli/call.ts", import.meta.url)).href, - async (importOriginal) => { - const mod = await importOriginal(); - return { - ...mod, - callGateway: (opts: unknown) => callGateway(opts), - randomIdempotencyKey: () => "rk_test", - }; - }, + () => ({ + callGateway: (opts: unknown) => callGateway(opts), + randomIdempotencyKey: () => "rk_test", + }), ); vi.mock("../gateway/server.js", () => ({ diff --git a/src/cli/gateway.sigterm.test.ts b/src/cli/gateway.sigterm.e2e.test.ts similarity index 100% rename from src/cli/gateway.sigterm.test.ts rename to src/cli/gateway.sigterm.e2e.test.ts diff --git a/src/cli/models-cli.test.ts b/src/cli/models-cli.test.ts index ddc6db0e2..737500e01 100644 --- a/src/cli/models-cli.test.ts +++ b/src/cli/models-cli.test.ts @@ -2,17 +2,34 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const githubCopilotLoginCommand = vi.fn(); const modelsStatusCommand = vi.fn().mockResolvedValue(undefined); +const noopAsync = vi.fn(async () => undefined); -vi.mock("../commands/models.js", async () => { - const actual = - await vi.importActual("../commands/models.js"); - - return { - ...actual, - githubCopilotLoginCommand, - modelsStatusCommand, - }; -}); +vi.mock("../commands/models.js", () => ({ + githubCopilotLoginCommand, + modelsStatusCommand, + modelsAliasesAddCommand: noopAsync, + modelsAliasesListCommand: noopAsync, + modelsAliasesRemoveCommand: noopAsync, + modelsAuthAddCommand: noopAsync, + modelsAuthLoginCommand: noopAsync, + modelsAuthOrderClearCommand: noopAsync, + modelsAuthOrderGetCommand: noopAsync, + modelsAuthOrderSetCommand: noopAsync, + modelsAuthPasteTokenCommand: noopAsync, + modelsAuthSetupTokenCommand: noopAsync, + modelsFallbacksAddCommand: noopAsync, + modelsFallbacksClearCommand: noopAsync, + modelsFallbacksListCommand: noopAsync, + modelsFallbacksRemoveCommand: noopAsync, + modelsImageFallbacksAddCommand: noopAsync, + modelsImageFallbacksClearCommand: noopAsync, + modelsImageFallbacksListCommand: noopAsync, + modelsImageFallbacksRemoveCommand: noopAsync, + modelsListCommand: noopAsync, + modelsScanCommand: noopAsync, + modelsSetCommand: noopAsync, + modelsSetImageCommand: noopAsync, +})); describe("models cli", () => { beforeEach(() => { diff --git a/src/cli/program.nodes-basic.test.ts b/src/cli/program.nodes-basic.e2e.test.ts similarity index 100% rename from src/cli/program.nodes-basic.test.ts rename to src/cli/program.nodes-basic.e2e.test.ts diff --git a/src/cli/program.nodes-media.test.ts b/src/cli/program.nodes-media.e2e.test.ts similarity index 100% rename from src/cli/program.nodes-media.test.ts rename to src/cli/program.nodes-media.e2e.test.ts diff --git a/src/cli/program.smoke.test.ts b/src/cli/program.smoke.e2e.test.ts similarity index 100% rename from src/cli/program.smoke.test.ts rename to src/cli/program.smoke.e2e.test.ts diff --git a/src/cli/program/register.subclis.test.ts b/src/cli/program/register.subclis.e2e.test.ts similarity index 100% rename from src/cli/program/register.subclis.test.ts rename to src/cli/program/register.subclis.e2e.test.ts diff --git a/src/commands/agent-via-gateway.test.ts b/src/commands/agent-via-gateway.e2e.test.ts similarity index 100% rename from src/commands/agent-via-gateway.test.ts rename to src/commands/agent-via-gateway.e2e.test.ts diff --git a/src/commands/agent.delivery.test.ts b/src/commands/agent.delivery.e2e.test.ts similarity index 100% rename from src/commands/agent.delivery.test.ts rename to src/commands/agent.delivery.e2e.test.ts diff --git a/src/commands/agent.test.ts b/src/commands/agent.e2e.test.ts similarity index 100% rename from src/commands/agent.test.ts rename to src/commands/agent.e2e.test.ts diff --git a/src/commands/agents.add.test.ts b/src/commands/agents.add.e2e.test.ts similarity index 100% rename from src/commands/agents.add.test.ts rename to src/commands/agents.add.e2e.test.ts diff --git a/src/commands/agents.test.ts b/src/commands/agents.e2e.test.ts similarity index 100% rename from src/commands/agents.test.ts rename to src/commands/agents.e2e.test.ts diff --git a/src/commands/agents.identity.test.ts b/src/commands/agents.identity.e2e.test.ts similarity index 100% rename from src/commands/agents.identity.test.ts rename to src/commands/agents.identity.e2e.test.ts diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.e2e.test.ts similarity index 100% rename from src/commands/auth-choice-options.test.ts rename to src/commands/auth-choice-options.e2e.test.ts diff --git a/src/commands/auth-choice.default-model.test.ts b/src/commands/auth-choice.default-model.e2e.test.ts similarity index 100% rename from src/commands/auth-choice.default-model.test.ts rename to src/commands/auth-choice.default-model.e2e.test.ts diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.e2e.test.ts similarity index 100% rename from src/commands/auth-choice.test.ts rename to src/commands/auth-choice.e2e.test.ts diff --git a/src/commands/auth-choice.moonshot.test.ts b/src/commands/auth-choice.moonshot.e2e.test.ts similarity index 100% rename from src/commands/auth-choice.moonshot.test.ts rename to src/commands/auth-choice.moonshot.e2e.test.ts diff --git a/src/commands/channels.adds-non-default-telegram-account.test.ts b/src/commands/channels.adds-non-default-telegram-account.e2e.test.ts similarity index 100% rename from src/commands/channels.adds-non-default-telegram-account.test.ts rename to src/commands/channels.adds-non-default-telegram-account.e2e.test.ts diff --git a/src/commands/channels.surfaces-signal-runtime-errors-channels-status-output.test.ts b/src/commands/channels.surfaces-signal-runtime-errors-channels-status-output.e2e.test.ts similarity index 100% rename from src/commands/channels.surfaces-signal-runtime-errors-channels-status-output.test.ts rename to src/commands/channels.surfaces-signal-runtime-errors-channels-status-output.e2e.test.ts diff --git a/src/commands/channels/capabilities.test.ts b/src/commands/channels/capabilities.e2e.test.ts similarity index 100% rename from src/commands/channels/capabilities.test.ts rename to src/commands/channels/capabilities.e2e.test.ts diff --git a/src/commands/chutes-oauth.test.ts b/src/commands/chutes-oauth.e2e.test.ts similarity index 100% rename from src/commands/chutes-oauth.test.ts rename to src/commands/chutes-oauth.e2e.test.ts diff --git a/src/commands/configure.gateway-auth.test.ts b/src/commands/configure.gateway-auth.e2e.test.ts similarity index 100% rename from src/commands/configure.gateway-auth.test.ts rename to src/commands/configure.gateway-auth.e2e.test.ts diff --git a/src/commands/configure.gateway.test.ts b/src/commands/configure.gateway.e2e.test.ts similarity index 100% rename from src/commands/configure.gateway.test.ts rename to src/commands/configure.gateway.e2e.test.ts diff --git a/src/commands/configure.wizard.test.ts b/src/commands/configure.wizard.e2e.test.ts similarity index 100% rename from src/commands/configure.wizard.test.ts rename to src/commands/configure.wizard.e2e.test.ts diff --git a/src/commands/daemon-install-helpers.test.ts b/src/commands/daemon-install-helpers.e2e.test.ts similarity index 100% rename from src/commands/daemon-install-helpers.test.ts rename to src/commands/daemon-install-helpers.e2e.test.ts diff --git a/src/commands/dashboard.test.ts b/src/commands/dashboard.e2e.test.ts similarity index 100% rename from src/commands/dashboard.test.ts rename to src/commands/dashboard.e2e.test.ts diff --git a/src/commands/doctor-auth.deprecated-cli-profiles.test.ts b/src/commands/doctor-auth.deprecated-cli-profiles.e2e.test.ts similarity index 100% rename from src/commands/doctor-auth.deprecated-cli-profiles.test.ts rename to src/commands/doctor-auth.deprecated-cli-profiles.e2e.test.ts diff --git a/src/commands/doctor-config-flow.test.ts b/src/commands/doctor-config-flow.e2e.test.ts similarity index 100% rename from src/commands/doctor-config-flow.test.ts rename to src/commands/doctor-config-flow.e2e.test.ts diff --git a/src/commands/doctor-legacy-config.test.ts b/src/commands/doctor-legacy-config.e2e.test.ts similarity index 100% rename from src/commands/doctor-legacy-config.test.ts rename to src/commands/doctor-legacy-config.e2e.test.ts diff --git a/src/commands/doctor-platform-notes.launchctl-env-overrides.test.ts b/src/commands/doctor-platform-notes.launchctl-env-overrides.e2e.test.ts similarity index 100% rename from src/commands/doctor-platform-notes.launchctl-env-overrides.test.ts rename to src/commands/doctor-platform-notes.launchctl-env-overrides.e2e.test.ts diff --git a/src/commands/doctor-security.test.ts b/src/commands/doctor-security.e2e.test.ts similarity index 100% rename from src/commands/doctor-security.test.ts rename to src/commands/doctor-security.e2e.test.ts diff --git a/src/commands/doctor-state-migrations.test.ts b/src/commands/doctor-state-migrations.e2e.test.ts similarity index 100% rename from src/commands/doctor-state-migrations.test.ts rename to src/commands/doctor-state-migrations.e2e.test.ts diff --git a/src/commands/doctor-workspace.test.ts b/src/commands/doctor-workspace.e2e.test.ts similarity index 100% rename from src/commands/doctor-workspace.test.ts rename to src/commands/doctor-workspace.e2e.test.ts diff --git a/src/commands/doctor.falls-back-legacy-sandbox-image-missing.test.ts b/src/commands/doctor.falls-back-legacy-sandbox-image-missing.e2e.test.ts similarity index 100% rename from src/commands/doctor.falls-back-legacy-sandbox-image-missing.test.ts rename to src/commands/doctor.falls-back-legacy-sandbox-image-missing.e2e.test.ts diff --git a/src/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.test.ts b/src/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.e2e.test.ts similarity index 100% rename from src/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.test.ts rename to src/commands/doctor.migrates-routing-allowfrom-channels-whatsapp-allowfrom.e2e.test.ts diff --git a/src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.test.ts b/src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.e2e.test.ts similarity index 100% rename from src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.test.ts rename to src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.e2e.test.ts diff --git a/src/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.test.ts b/src/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.e2e.test.ts similarity index 100% rename from src/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.test.ts rename to src/commands/doctor.warns-per-agent-sandbox-docker-browser-prune.e2e.test.ts diff --git a/src/commands/doctor.warns-state-directory-is-missing.test.ts b/src/commands/doctor.warns-state-directory-is-missing.e2e.test.ts similarity index 100% rename from src/commands/doctor.warns-state-directory-is-missing.test.ts rename to src/commands/doctor.warns-state-directory-is-missing.e2e.test.ts diff --git a/src/commands/gateway-status.test.ts b/src/commands/gateway-status.e2e.test.ts similarity index 100% rename from src/commands/gateway-status.test.ts rename to src/commands/gateway-status.e2e.test.ts diff --git a/src/commands/google-gemini-model-default.test.ts b/src/commands/google-gemini-model-default.e2e.test.ts similarity index 100% rename from src/commands/google-gemini-model-default.test.ts rename to src/commands/google-gemini-model-default.e2e.test.ts diff --git a/src/commands/health-format.test.ts b/src/commands/health-format.e2e.test.ts similarity index 100% rename from src/commands/health-format.test.ts rename to src/commands/health-format.e2e.test.ts diff --git a/src/commands/health.command.coverage.test.ts b/src/commands/health.command.coverage.e2e.test.ts similarity index 100% rename from src/commands/health.command.coverage.test.ts rename to src/commands/health.command.coverage.e2e.test.ts diff --git a/src/commands/health.test.ts b/src/commands/health.e2e.test.ts similarity index 100% rename from src/commands/health.test.ts rename to src/commands/health.e2e.test.ts diff --git a/src/commands/health.snapshot.test.ts b/src/commands/health.snapshot.e2e.test.ts similarity index 100% rename from src/commands/health.snapshot.test.ts rename to src/commands/health.snapshot.e2e.test.ts diff --git a/src/commands/message.test.ts b/src/commands/message.e2e.test.ts similarity index 100% rename from src/commands/message.test.ts rename to src/commands/message.e2e.test.ts diff --git a/src/commands/model-picker.test.ts b/src/commands/model-picker.e2e.test.ts similarity index 100% rename from src/commands/model-picker.test.ts rename to src/commands/model-picker.e2e.test.ts diff --git a/src/commands/models.list.test.ts b/src/commands/models.list.e2e.test.ts similarity index 100% rename from src/commands/models.list.test.ts rename to src/commands/models.list.e2e.test.ts diff --git a/src/commands/models.set.test.ts b/src/commands/models.set.e2e.test.ts similarity index 100% rename from src/commands/models.set.test.ts rename to src/commands/models.set.e2e.test.ts diff --git a/src/commands/models/list.status.test.ts b/src/commands/models/list.status.e2e.test.ts similarity index 100% rename from src/commands/models/list.status.test.ts rename to src/commands/models/list.status.e2e.test.ts diff --git a/src/commands/onboard-auth.test.ts b/src/commands/onboard-auth.e2e.test.ts similarity index 100% rename from src/commands/onboard-auth.test.ts rename to src/commands/onboard-auth.e2e.test.ts diff --git a/src/commands/onboard-channels.test.ts b/src/commands/onboard-channels.e2e.test.ts similarity index 100% rename from src/commands/onboard-channels.test.ts rename to src/commands/onboard-channels.e2e.test.ts diff --git a/src/commands/onboard-custom.test.ts b/src/commands/onboard-custom.e2e.test.ts similarity index 100% rename from src/commands/onboard-custom.test.ts rename to src/commands/onboard-custom.e2e.test.ts diff --git a/src/commands/onboard-helpers.test.ts b/src/commands/onboard-helpers.e2e.test.ts similarity index 100% rename from src/commands/onboard-helpers.test.ts rename to src/commands/onboard-helpers.e2e.test.ts diff --git a/src/commands/onboard-hooks.test.ts b/src/commands/onboard-hooks.e2e.test.ts similarity index 100% rename from src/commands/onboard-hooks.test.ts rename to src/commands/onboard-hooks.e2e.test.ts diff --git a/src/commands/onboard-interactive.test.ts b/src/commands/onboard-interactive.e2e.test.ts similarity index 100% rename from src/commands/onboard-interactive.test.ts rename to src/commands/onboard-interactive.e2e.test.ts diff --git a/src/commands/onboard-non-interactive.gateway.test.ts b/src/commands/onboard-non-interactive.gateway.e2e.test.ts similarity index 100% rename from src/commands/onboard-non-interactive.gateway.test.ts rename to src/commands/onboard-non-interactive.gateway.e2e.test.ts diff --git a/src/commands/onboard-non-interactive.litellm.test.ts b/src/commands/onboard-non-interactive.litellm.test.ts deleted file mode 100644 index a6b5170ac..000000000 --- a/src/commands/onboard-non-interactive.litellm.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; - -describe("onboard (non-interactive): LiteLLM", () => { - it("stores the API key and configures the default model", async () => { - const prev = { - home: process.env.HOME, - stateDir: process.env.OPENCLAW_STATE_DIR, - configPath: process.env.OPENCLAW_CONFIG_PATH, - skipChannels: process.env.OPENCLAW_SKIP_CHANNELS, - skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER, - skipCron: process.env.OPENCLAW_SKIP_CRON, - skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST, - token: process.env.OPENCLAW_GATEWAY_TOKEN, - password: process.env.OPENCLAW_GATEWAY_PASSWORD, - }; - - process.env.OPENCLAW_SKIP_CHANNELS = "1"; - process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1"; - process.env.OPENCLAW_SKIP_CRON = "1"; - process.env.OPENCLAW_SKIP_CANVAS_HOST = "1"; - delete process.env.OPENCLAW_GATEWAY_TOKEN; - delete process.env.OPENCLAW_GATEWAY_PASSWORD; - - const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-onboard-litellm-")); - process.env.HOME = tempHome; - process.env.OPENCLAW_STATE_DIR = tempHome; - process.env.OPENCLAW_CONFIG_PATH = path.join(tempHome, "openclaw.json"); - vi.resetModules(); - - const runtime = { - log: () => {}, - error: (msg: string) => { - throw new Error(msg); - }, - exit: (code: number) => { - throw new Error(`exit:${code}`); - }, - }; - - try { - const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js"); - await runNonInteractiveOnboarding( - { - nonInteractive: true, - authChoice: "litellm-api-key", - litellmApiKey: "litellm-test-key", - skipHealth: true, - skipChannels: true, - skipSkills: true, - json: true, - }, - runtime, - ); - - const { CONFIG_PATH } = await import("../config/config.js"); - const cfg = JSON.parse(await fs.readFile(CONFIG_PATH, "utf8")) as { - auth?: { - profiles?: Record; - }; - agents?: { defaults?: { model?: { primary?: string } } }; - }; - - expect(cfg.auth?.profiles?.["litellm:default"]?.provider).toBe("litellm"); - expect(cfg.auth?.profiles?.["litellm:default"]?.mode).toBe("api_key"); - expect(cfg.agents?.defaults?.model?.primary).toBe("litellm/claude-opus-4-6"); - - const { ensureAuthProfileStore } = await import("../agents/auth-profiles.js"); - const store = ensureAuthProfileStore(); - const profile = store.profiles["litellm:default"]; - expect(profile?.type).toBe("api_key"); - if (profile?.type === "api_key") { - expect(profile.provider).toBe("litellm"); - expect(profile.key).toBe("litellm-test-key"); - } - } finally { - await fs.rm(tempHome, { recursive: true, force: true }); - process.env.HOME = prev.home; - process.env.OPENCLAW_STATE_DIR = prev.stateDir; - process.env.OPENCLAW_CONFIG_PATH = prev.configPath; - process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels; - process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail; - process.env.OPENCLAW_SKIP_CRON = prev.skipCron; - process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas; - process.env.OPENCLAW_GATEWAY_TOKEN = prev.token; - process.env.OPENCLAW_GATEWAY_PASSWORD = prev.password; - } - }, 60_000); -}); diff --git a/src/commands/onboard-non-interactive.provider-auth.test.ts b/src/commands/onboard-non-interactive.provider-auth.e2e.test.ts similarity index 94% rename from src/commands/onboard-non-interactive.provider-auth.test.ts rename to src/commands/onboard-non-interactive.provider-auth.e2e.test.ts index 6a88c8668..896020838 100644 --- a/src/commands/onboard-non-interactive.provider-auth.test.ts +++ b/src/commands/onboard-non-interactive.provider-auth.e2e.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { setTimeout as delay } from "node:timers/promises"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { OPENAI_DEFAULT_MODEL } from "./openai-model-default.js"; type RuntimeMock = { @@ -104,7 +104,6 @@ async function withOnboardEnv( process.env.HOME = tempHome; process.env.OPENCLAW_STATE_DIR = tempHome; process.env.OPENCLAW_CONFIG_PATH = configPath; - vi.resetModules(); const runtime: RuntimeMock = { log: () => {}, @@ -331,6 +330,37 @@ describe("onboard (non-interactive): provider auth", () => { }); }, 60_000); + it("stores LiteLLM API key and sets default model", async () => { + await withOnboardEnv("openclaw-onboard-litellm-", async ({ configPath, runtime }) => { + await runNonInteractive( + { + nonInteractive: true, + authChoice: "litellm-api-key", + litellmApiKey: "litellm-test-key", + skipHealth: true, + skipChannels: true, + skipSkills: true, + json: true, + }, + runtime, + ); + + const cfg = await readJsonFile<{ + auth?: { profiles?: Record }; + agents?: { defaults?: { model?: { primary?: string } } }; + }>(configPath); + + expect(cfg.auth?.profiles?.["litellm:default"]?.provider).toBe("litellm"); + expect(cfg.auth?.profiles?.["litellm:default"]?.mode).toBe("api_key"); + expect(cfg.agents?.defaults?.model?.primary).toBe("litellm/claude-opus-4-6"); + await expectApiKeyProfile({ + profileId: "litellm:default", + provider: "litellm", + key: "litellm-test-key", + }); + }); + }, 60_000); + it("stores Cloudflare AI Gateway API key and metadata", async () => { await withOnboardEnv("openclaw-onboard-cf-gateway-", async ({ configPath, runtime }) => { await runNonInteractive( diff --git a/src/commands/onboard-skills.test.ts b/src/commands/onboard-skills.e2e.test.ts similarity index 100% rename from src/commands/onboard-skills.test.ts rename to src/commands/onboard-skills.e2e.test.ts diff --git a/src/commands/onboarding/plugin-install.test.ts b/src/commands/onboarding/plugin-install.e2e.test.ts similarity index 100% rename from src/commands/onboarding/plugin-install.test.ts rename to src/commands/onboarding/plugin-install.e2e.test.ts diff --git a/src/commands/openai-codex-model-default.test.ts b/src/commands/openai-codex-model-default.e2e.test.ts similarity index 100% rename from src/commands/openai-codex-model-default.test.ts rename to src/commands/openai-codex-model-default.e2e.test.ts diff --git a/src/commands/openai-model-default.test.ts b/src/commands/openai-model-default.e2e.test.ts similarity index 100% rename from src/commands/openai-model-default.test.ts rename to src/commands/openai-model-default.e2e.test.ts diff --git a/src/commands/opencode-zen-model-default.test.ts b/src/commands/opencode-zen-model-default.e2e.test.ts similarity index 100% rename from src/commands/opencode-zen-model-default.test.ts rename to src/commands/opencode-zen-model-default.e2e.test.ts diff --git a/src/commands/sandbox-explain.test.ts b/src/commands/sandbox-explain.e2e.test.ts similarity index 100% rename from src/commands/sandbox-explain.test.ts rename to src/commands/sandbox-explain.e2e.test.ts diff --git a/src/commands/sandbox-formatters.test.ts b/src/commands/sandbox-formatters.e2e.test.ts similarity index 100% rename from src/commands/sandbox-formatters.test.ts rename to src/commands/sandbox-formatters.e2e.test.ts diff --git a/src/commands/sandbox.test.ts b/src/commands/sandbox.e2e.test.ts similarity index 100% rename from src/commands/sandbox.test.ts rename to src/commands/sandbox.e2e.test.ts diff --git a/src/commands/sessions.test.ts b/src/commands/sessions.e2e.test.ts similarity index 100% rename from src/commands/sessions.test.ts rename to src/commands/sessions.e2e.test.ts diff --git a/src/commands/status.test.ts b/src/commands/status.e2e.test.ts similarity index 100% rename from src/commands/status.test.ts rename to src/commands/status.e2e.test.ts diff --git a/src/commands/zai-endpoint-detect.test.ts b/src/commands/zai-endpoint-detect.e2e.test.ts similarity index 100% rename from src/commands/zai-endpoint-detect.test.ts rename to src/commands/zai-endpoint-detect.e2e.test.ts diff --git a/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts b/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.e2e.test.ts similarity index 100% rename from src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts rename to src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.e2e.test.ts diff --git a/src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts b/src/config/config.legacy-config-detection.rejects-routing-allowfrom.e2e.test.ts similarity index 100% rename from src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts rename to src/config/config.legacy-config-detection.rejects-routing-allowfrom.e2e.test.ts diff --git a/src/config/config.nix-integration-u3-u5-u9.test.ts b/src/config/config.nix-integration-u3-u5-u9.e2e.test.ts similarity index 100% rename from src/config/config.nix-integration-u3-u5-u9.test.ts rename to src/config/config.nix-integration-u3-u5-u9.e2e.test.ts diff --git a/src/config/config.talk-api-key-fallback.test.ts b/src/config/config.talk-api-key-fallback.test.ts index f8f1d6659..e16526b34 100644 --- a/src/config/config.talk-api-key-fallback.test.ts +++ b/src/config/config.talk-api-key-fallback.test.ts @@ -1,51 +1,45 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { withTempHome } from "./test-helpers.js"; +import type fs from "node:fs"; +import type os from "node:os"; +import type path from "node:path"; +import { describe, expect, it, vi } from "vitest"; +import { resolveTalkApiKey } from "./talk.js"; describe("talk api key fallback", () => { - let previousEnv: string | undefined; + it("reads ELEVENLABS_API_KEY from profile when env is missing", () => { + const existsSync = vi.fn((candidate: string) => candidate.endsWith(".profile")); + const readFileSync = vi.fn(() => "export ELEVENLABS_API_KEY=profile-key\n"); + const homedir = vi.fn(() => "/tmp/home"); - beforeEach(() => { - previousEnv = process.env.ELEVENLABS_API_KEY; - delete process.env.ELEVENLABS_API_KEY; + const value = resolveTalkApiKey( + {}, + { + fs: { existsSync, readFileSync } as unknown as typeof fs, + os: { homedir } as unknown as typeof os, + path: { join: (...parts: string[]) => parts.join("/") } as unknown as typeof path, + }, + ); + + expect(value).toBe("profile-key"); + expect(readFileSync).toHaveBeenCalledOnce(); }); - afterEach(() => { - process.env.ELEVENLABS_API_KEY = previousEnv; - }); - - it("injects talk.apiKey from profile when config is missing", async () => { - await withTempHome(async (home) => { - await fs.writeFile( - path.join(home, ".profile"), - "export ELEVENLABS_API_KEY=profile-key\n", - "utf-8", - ); - - vi.resetModules(); - const { readConfigFileSnapshot } = await import("./config.js"); - const snap = await readConfigFileSnapshot(); - - expect(snap.config?.talk?.apiKey).toBe("profile-key"); - expect(snap.exists).toBe(false); + it("prefers ELEVENLABS_API_KEY env over profile", () => { + const existsSync = vi.fn(() => { + throw new Error("profile should not be read when env key exists"); }); - }); + const readFileSync = vi.fn(() => ""); - it("prefers ELEVENLABS_API_KEY env over profile", async () => { - await withTempHome(async (home) => { - await fs.writeFile( - path.join(home, ".profile"), - "export ELEVENLABS_API_KEY=profile-key\n", - "utf-8", - ); - process.env.ELEVENLABS_API_KEY = "env-key"; + const value = resolveTalkApiKey( + { ELEVENLABS_API_KEY: "env-key" }, + { + fs: { existsSync, readFileSync } as unknown as typeof fs, + os: { homedir: () => "/tmp/home" } as unknown as typeof os, + path: { join: (...parts: string[]) => parts.join("/") } as unknown as typeof path, + }, + ); - vi.resetModules(); - const { readConfigFileSnapshot } = await import("./config.js"); - const snap = await readConfigFileSnapshot(); - - expect(snap.config?.talk?.apiKey).toBe("env-key"); - }); + expect(value).toBe("env-key"); + expect(existsSync).not.toHaveBeenCalled(); + expect(readFileSync).not.toHaveBeenCalled(); }); }); diff --git a/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.ts b/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.e2e.test.ts similarity index 100% rename from src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.ts rename to src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.e2e.test.ts diff --git a/src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts b/src/cron/isolated-agent.uses-last-non-empty-agent-text-as.e2e.test.ts similarity index 100% rename from src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts rename to src/cron/isolated-agent.uses-last-non-empty-agent-text-as.e2e.test.ts diff --git a/src/discord/monitor.slash.test.ts b/src/discord/monitor.slash.test.ts deleted file mode 100644 index 508ee5a93..000000000 --- a/src/discord/monitor.slash.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { createReplyDispatcherWithTyping } from "../auto-reply/reply/reply-dispatcher.js"; - -const dispatchMock = vi.fn(); - -vi.mock("@buape/carbon", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - ChannelType: { DM: "dm", GroupDM: "group" }, - MessageType: { - ChatInputCommand: 1, - ContextMenuCommand: 2, - Default: 0, - }, - Client: class {}, - }; -}); - -vi.mock("../auto-reply/dispatch.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - dispatchInboundMessage: (...args: unknown[]) => dispatchMock(...args), - dispatchInboundMessageWithDispatcher: (...args: unknown[]) => dispatchMock(...args), - dispatchInboundMessageWithBufferedDispatcher: (...args: unknown[]) => dispatchMock(...args), - }; -}); - -beforeEach(() => { - dispatchMock.mockReset().mockImplementation(async (params) => { - if ("dispatcher" in params && params.dispatcher) { - params.dispatcher.sendFinalReply({ text: "final reply" }); - return { queuedFinal: true, counts: { tool: 0, block: 0, final: 1 } }; - } - if ("dispatcherOptions" in params && params.dispatcherOptions) { - const { dispatcher, markDispatchIdle } = createReplyDispatcherWithTyping( - params.dispatcherOptions, - ); - dispatcher.sendFinalReply({ text: "final reply" }); - await dispatcher.waitForIdle(); - markDispatchIdle(); - return { queuedFinal: true, counts: dispatcher.getQueuedCounts() }; - } - return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } }; - }); -}); - -describe("discord native commands", () => { - it("skips tool results for native slash commands", { timeout: 60_000 }, async () => { - const { ChannelType } = await import("@buape/carbon"); - const { createDiscordNativeCommand } = await import("./monitor.js"); - - const cfg = { - agents: { - defaults: { - model: "anthropic/claude-opus-4-5", - humanDelay: { mode: "off" }, - workspace: "/tmp/openclaw", - }, - }, - session: { store: "/tmp/openclaw-sessions.json" }, - discord: { dm: { enabled: true, policy: "open" } }, - } as ReturnType; - - const command = createDiscordNativeCommand({ - command: { - name: "verbose", - description: "Toggle verbose mode.", - acceptsArgs: true, - }, - cfg, - discordConfig: cfg.discord, - accountId: "default", - sessionPrefix: "discord:slash", - ephemeralDefault: true, - }); - - const reply = vi.fn().mockResolvedValue(undefined); - const followUp = vi.fn().mockResolvedValue(undefined); - - await command.run({ - user: { id: "u1", username: "Ada", globalName: "Ada" }, - channel: { type: ChannelType.DM }, - guild: null, - rawData: { id: "i1" }, - options: { getString: vi.fn().mockReturnValue("on") }, - reply, - followUp, - }); - - expect(dispatchMock).toHaveBeenCalledTimes(1); - expect(reply).toHaveBeenCalledTimes(1); - expect(followUp).toHaveBeenCalledTimes(0); - expect(reply.mock.calls[0]?.[0]?.content).toContain("final"); - }); -}); diff --git a/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts b/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.e2e.test.ts similarity index 90% rename from src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts rename to src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.e2e.test.ts index 369ae80b3..e129a5df0 100644 --- a/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts +++ b/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.e2e.test.ts @@ -2,6 +2,7 @@ import type { Client } from "@buape/carbon"; import { ChannelType, MessageType } from "@buape/carbon"; import { Routes } from "discord-api-types/v10"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createReplyDispatcherWithTyping } from "../auto-reply/reply/reply-dispatcher.js"; import { __resetDiscordChannelInfoCacheForTest } from "./monitor/message-utils.js"; const sendMock = vi.fn(); @@ -52,9 +53,34 @@ beforeEach(() => { vi.useRealTimers(); sendMock.mockReset().mockResolvedValue(undefined); updateLastRouteMock.mockReset(); - dispatchMock.mockReset().mockImplementation(async ({ dispatcher }) => { - dispatcher.sendFinalReply({ text: "hi" }); - return { queuedFinal: true, counts: { tool: 0, block: 0, final: 1 } }; + dispatchMock.mockReset().mockImplementation(async (params: unknown) => { + if ( + typeof params === "object" && + params !== null && + "dispatcher" in params && + typeof params.dispatcher === "object" && + params.dispatcher !== null && + "sendFinalReply" in params.dispatcher && + typeof params.dispatcher.sendFinalReply === "function" + ) { + params.dispatcher.sendFinalReply({ text: "hi" }); + return { queuedFinal: true, counts: { tool: 0, block: 0, final: 1 } }; + } + if ( + typeof params === "object" && + params !== null && + "dispatcherOptions" in params && + params.dispatcherOptions + ) { + const { dispatcher, markDispatchIdle } = createReplyDispatcherWithTyping( + params.dispatcherOptions as Parameters[0], + ); + dispatcher.sendFinalReply({ text: "final reply" }); + await dispatcher.waitForIdle(); + markDispatchIdle(); + return { queuedFinal: true, counts: dispatcher.getQueuedCounts() }; + } + return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } }; }); readAllowFromStoreMock.mockReset().mockResolvedValue([]); upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true }); @@ -150,87 +176,54 @@ describe("discord tool result dispatch", () => { ); it( - "accepts guild messages when mentionPatterns match even if another user is mentioned", + "skips tool results for native slash commands", + { timeout: MENTION_PATTERNS_TEST_TIMEOUT_MS }, async () => { - const { createDiscordMessageHandler } = await import("./monitor.js"); + const { createDiscordNativeCommand } = await import("./monitor.js"); const cfg = { agents: { defaults: { model: "anthropic/claude-opus-4-5", + humanDelay: { mode: "off" }, workspace: "/tmp/openclaw", }, }, session: { store: "/tmp/openclaw-sessions.json" }, - channels: { - discord: { - dm: { enabled: true, policy: "open" }, - groupPolicy: "open", - guilds: { "*": { requireMention: true } }, - }, - }, - messages: { - responsePrefix: "PFX", - groupChat: { mentionPatterns: ["\\bopenclaw\\b"] }, - }, + discord: { dm: { enabled: true, policy: "open" } }, } as ReturnType; - const handler = createDiscordMessageHandler({ + const command = createDiscordNativeCommand({ + command: { + name: "verbose", + description: "Toggle verbose mode.", + acceptsArgs: true, + }, cfg, - discordConfig: cfg.channels.discord, + discordConfig: cfg.discord, accountId: "default", token: "token", - runtime: { - log: vi.fn(), - error: vi.fn(), - exit: (code: number): never => { - throw new Error(`exit ${code}`); - }, - }, - botUserId: "bot-id", - guildHistories: new Map(), - historyLimit: 0, - mediaMaxBytes: 10_000, - textLimit: 2000, - replyToMode: "off", - dmEnabled: true, - groupDmEnabled: false, - guildEntries: { "*": { requireMention: true } }, + sessionPrefix: "discord:slash", + ephemeralDefault: true, }); - const client = { - fetchChannel: vi.fn().mockResolvedValue({ - type: ChannelType.GuildText, - name: "general", - }), - } as unknown as Client; + const reply = vi.fn().mockResolvedValue(undefined); + const followUp = vi.fn().mockResolvedValue(undefined); - await handler( - { - message: { - id: "m2", - content: "openclaw: hello", - channelId: "c1", - timestamp: new Date().toISOString(), - type: MessageType.Default, - attachments: [], - embeds: [], - mentionedEveryone: false, - mentionedUsers: [{ id: "u2", bot: false, username: "Bea" }], - mentionedRoles: [], - author: { id: "u1", bot: false, username: "Ada" }, - }, - author: { id: "u1", bot: false, username: "Ada" }, - member: { nickname: "Ada" }, - guild: { id: "g1", name: "Guild" }, - guild_id: "g1", - }, - client, - ); + await command.run({ + user: { id: "u1", username: "Ada", globalName: "Ada" }, + channel: { type: ChannelType.DM }, + guild: null, + rawData: { id: "i1" }, + options: { getString: vi.fn().mockReturnValue("on") }, + reply, + followUp, + }); expect(dispatchMock).toHaveBeenCalledTimes(1); - expect(sendMock).toHaveBeenCalledTimes(1); + expect(reply).toHaveBeenCalledTimes(1); + expect(followUp).toHaveBeenCalledTimes(0); + expect(reply.mock.calls[0]?.[0]?.content).toContain("final"); }, - MENTION_PATTERNS_TEST_TIMEOUT_MS, ); it("accepts guild reply-to-bot messages as implicit mentions", async () => { diff --git a/src/gateway/client.test.ts b/src/gateway/client.e2e.test.ts similarity index 100% rename from src/gateway/client.test.ts rename to src/gateway/client.e2e.test.ts diff --git a/src/gateway/server-methods/chat.inject.parentid.test.ts b/src/gateway/server-methods/chat.inject.parentid.e2e.test.ts similarity index 100% rename from src/gateway/server-methods/chat.inject.parentid.test.ts rename to src/gateway/server-methods/chat.inject.parentid.e2e.test.ts diff --git a/src/infra/outbound/message.test.ts b/src/infra/outbound/message.e2e.test.ts similarity index 100% rename from src/infra/outbound/message.test.ts rename to src/infra/outbound/message.e2e.test.ts diff --git a/src/media-understanding/apply.test.ts b/src/media-understanding/apply.e2e.test.ts similarity index 100% rename from src/media-understanding/apply.test.ts rename to src/media-understanding/apply.e2e.test.ts diff --git a/src/plugins/install.test.ts b/src/plugins/install.e2e.test.ts similarity index 100% rename from src/plugins/install.test.ts rename to src/plugins/install.e2e.test.ts diff --git a/src/plugins/wired-hooks-after-tool-call.test.ts b/src/plugins/wired-hooks-after-tool-call.e2e.test.ts similarity index 96% rename from src/plugins/wired-hooks-after-tool-call.test.ts rename to src/plugins/wired-hooks-after-tool-call.e2e.test.ts index 12bdf7c41..0256f6f3b 100644 --- a/src/plugins/wired-hooks-after-tool-call.test.ts +++ b/src/plugins/wired-hooks-after-tool-call.e2e.test.ts @@ -83,9 +83,7 @@ describe("after_tool_call hook wiring", () => { } as never, ); - await vi.waitFor(() => { - expect(hookMocks.runner.runAfterToolCall).toHaveBeenCalledTimes(1); - }); + expect(hookMocks.runner.runAfterToolCall).toHaveBeenCalledTimes(1); const [event, context] = hookMocks.runner.runAfterToolCall.mock.calls[0]; expect(event.toolName).toBe("read"); @@ -149,9 +147,7 @@ describe("after_tool_call hook wiring", () => { } as never, ); - await vi.waitFor(() => { - expect(hookMocks.runner.runAfterToolCall).toHaveBeenCalledTimes(1); - }); + expect(hookMocks.runner.runAfterToolCall).toHaveBeenCalledTimes(1); const [event] = hookMocks.runner.runAfterToolCall.mock.calls[0]; expect(event.error).toBeDefined(); diff --git a/src/process/command-queue.ts b/src/process/command-queue.ts index 205cb1601..598007584 100644 --- a/src/process/command-queue.ts +++ b/src/process/command-queue.ts @@ -187,7 +187,8 @@ export function getActiveTaskCount(): number { * already executing are waited on. */ export function waitForActiveTasks(timeoutMs: number): Promise<{ drained: boolean }> { - const POLL_INTERVAL_MS = 250; + // Keep shutdown/drain checks responsive without busy looping. + const POLL_INTERVAL_MS = 50; const deadline = Date.now() + timeoutMs; const activeAtStart = new Set(); for (const state of lanes.values()) { diff --git a/src/signal/monitor.event-handler.sender-prefix.test.ts b/src/signal/monitor.event-handler.sender-prefix.e2e.test.ts similarity index 89% rename from src/signal/monitor.event-handler.sender-prefix.test.ts rename to src/signal/monitor.event-handler.sender-prefix.e2e.test.ts index c53340918..0526c0cd1 100644 --- a/src/signal/monitor.event-handler.sender-prefix.test.ts +++ b/src/signal/monitor.event-handler.sender-prefix.e2e.test.ts @@ -3,6 +3,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const dispatchMock = vi.fn(); const readAllowFromMock = vi.fn(); +vi.mock("../auto-reply/dispatch.js", () => ({ + dispatchInboundMessage: (...args: unknown[]) => dispatchMock(...args), + dispatchInboundMessageWithDispatcher: (...args: unknown[]) => dispatchMock(...args), + dispatchInboundMessageWithBufferedDispatcher: (...args: unknown[]) => dispatchMock(...args), +})); + vi.mock("../pairing/pairing-store.js", () => ({ readChannelAllowFromStore: (...args: unknown[]) => readAllowFromMock(...args), upsertChannelPairingRequest: vi.fn(), @@ -19,10 +25,6 @@ describe("signal event handler sender prefix", () => { it("prefixes group bodies with sender label", async () => { let capturedBody = ""; - const dispatchModule = await import("../auto-reply/dispatch.js"); - vi.spyOn(dispatchModule, "dispatchInboundMessage").mockImplementation( - async (...args: unknown[]) => dispatchMock(...args), - ); dispatchMock.mockImplementationOnce(async ({ dispatcher, ctx }) => { capturedBody = ctx.Body ?? ""; dispatcher.sendFinalReply({ text: "ok" }); diff --git a/src/signal/monitor.event-handler.typing-read-receipts.test.ts b/src/signal/monitor.event-handler.typing-read-receipts.e2e.test.ts similarity index 80% rename from src/signal/monitor.event-handler.typing-read-receipts.test.ts rename to src/signal/monitor.event-handler.typing-read-receipts.e2e.test.ts index aa4398e9a..b94cb7886 100644 --- a/src/signal/monitor.event-handler.typing-read-receipts.test.ts +++ b/src/signal/monitor.event-handler.typing-read-receipts.e2e.test.ts @@ -2,6 +2,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const sendTypingMock = vi.fn(); const sendReadReceiptMock = vi.fn(); +const dispatchInboundMessageMock = vi.fn( + async (params: { replyOptions?: { onReplyStart?: () => void } }) => { + await Promise.resolve(params.replyOptions?.onReplyStart?.()); + return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } }; + }, +); vi.mock("./send.js", () => ({ sendMessageSignal: vi.fn(), @@ -9,21 +15,12 @@ vi.mock("./send.js", () => ({ sendReadReceiptSignal: (...args: unknown[]) => sendReadReceiptMock(...args), })); -vi.mock("../auto-reply/dispatch.js", async (importOriginal) => { - const actual = await importOriginal(); - const dispatchInboundMessage = vi.fn( - async (params: { replyOptions?: { onReplyStart?: () => void } }) => { - await Promise.resolve(params.replyOptions?.onReplyStart?.()); - return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } }; - }, - ); - return { - ...actual, - dispatchInboundMessage, - dispatchInboundMessageWithDispatcher: dispatchInboundMessage, - dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessage, - }; -}); +vi.mock("../auto-reply/dispatch.js", () => ({ + dispatchInboundMessage: (...args: unknown[]) => dispatchInboundMessageMock(...args), + dispatchInboundMessageWithDispatcher: (...args: unknown[]) => dispatchInboundMessageMock(...args), + dispatchInboundMessageWithBufferedDispatcher: (...args: unknown[]) => + dispatchInboundMessageMock(...args), +})); vi.mock("../pairing/pairing-store.js", () => ({ readChannelAllowFromStore: vi.fn().mockResolvedValue([]), @@ -35,10 +32,10 @@ describe("signal event handler typing + read receipts", () => { vi.useRealTimers(); sendTypingMock.mockReset().mockResolvedValue(true); sendReadReceiptMock.mockReset().mockResolvedValue(true); + dispatchInboundMessageMock.mockClear(); }); it("sends typing + read receipt for allowed DMs", async () => { - vi.resetModules(); const { createSignalEventHandler } = await import("./monitor/event-handler.js"); const handler = createSignalEventHandler({ // oxlint-disable-next-line typescript/no-explicit-any diff --git a/src/signal/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.test.ts b/src/signal/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.e2e.test.ts similarity index 100% rename from src/signal/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.test.ts rename to src/signal/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.e2e.test.ts diff --git a/src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts b/src/telegram/bot.media.downloads-media-file-path-no-file-download.e2e.test.ts similarity index 100% rename from src/telegram/bot.media.downloads-media-file-path-no-file-download.test.ts rename to src/telegram/bot.media.downloads-media-file-path-no-file-download.e2e.test.ts diff --git a/src/telegram/bot.media.includes-location-text-ctx-fields-pins.test.ts b/src/telegram/bot.media.includes-location-text-ctx-fields-pins.e2e.test.ts similarity index 100% rename from src/telegram/bot.media.includes-location-text-ctx-fields-pins.test.ts rename to src/telegram/bot.media.includes-location-text-ctx-fields-pins.e2e.test.ts diff --git a/src/telegram/fetch.test.ts b/src/telegram/fetch.test.ts index 437359286..c96966f35 100644 --- a/src/telegram/fetch.test.ts +++ b/src/telegram/fetch.test.ts @@ -54,6 +54,7 @@ describe("resolveTelegramFetch", () => { }); it("env disable override wins over config", async () => { + vi.stubEnv("OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY", "0"); vi.stubEnv("OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY", "1"); globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch; const { resolveTelegramFetch, setDefaultAutoSelectFamily } = await loadModule(); diff --git a/src/web/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts b/src/web/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.e2e.test.ts similarity index 100% rename from src/web/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts rename to src/web/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.e2e.test.ts diff --git a/src/web/auto-reply.web-auto-reply.reconnects-after-connection-close.test.ts b/src/web/auto-reply.web-auto-reply.reconnects-after-connection-close.e2e.test.ts similarity index 100% rename from src/web/auto-reply.web-auto-reply.reconnects-after-connection-close.test.ts rename to src/web/auto-reply.web-auto-reply.reconnects-after-connection-close.e2e.test.ts