Compare commits

..

17 Commits

Author SHA1 Message Date
Peter Steinberger
e7aeb7c3e9 test: cover text-mode CLI session IDs (#52712) (thanks @kenantan32) 2026-03-26 23:33:54 +00:00
kenantan32
7c8d75e3d6 fix: preserve CLI session ID in text output mode
When the CLI backend output mode is "text", sessionId was hardcoded to
undefined. This caused the fallback chain to store the OpenClaw internal
UUID as the CLI session ID. On resume, --resume was called with the
wrong UUID, resulting in "No conversation found with session ID".

Return resolvedSessionId instead of undefined so the correct CLI session
ID is persisted and resume works correctly.
2026-03-26 23:30:41 +00:00
dhi13man
9f8c4efa9b fix(agents): use claude-cli backend in tools-disabled regression test
codex-cli lacks systemPromptArg, so the system prompt is never
serialized into argv — making the not-toContain assertion pass
vacuously even on pre-fix code. Switch to claude-cli which defines
systemPromptArg ("--append-system-prompt") and add a positive
assertion that the user-supplied prompt IS present in argv.

Co-Authored-By: Dhiman's Agentic Suite <dhiman.seal@hotmail.com>
2026-03-26 16:30:19 -07:00
dhi13man
99f0ea8d43 fix(agents): remove unconditional "Tools are disabled" prompt injection in CLI runner
`runCliAgent()` unconditionally appended "Tools are disabled in this
session. Do not call tools." to `extraSystemPrompt` for every CLI
backend session. The intent was to prevent the LLM from calling
OpenClaw's embedded API tools (since CLI backends manage their own
tools natively). However, CLI agents like Claude Code interpret this
text as a blanket prohibition on ALL tools, including their own native
Bash, Read, and Write tools.

This caused silent failures across cron jobs, group chats, and DM
sessions when using any CLI backend: the agent would see the injected
text in the system prompt and refuse to execute tools, returning text
responses instead. Cron jobs reported `lastStatus: "ok"` despite the
agent failing to run scripts.

The fix removes the hardcoded string entirely. CLI backends already
receive `tools: []` (no OpenClaw embedded tools in the API call), so
the text was redundant at best.

Closes #44135

Co-Authored-By: Dhiman's Agentic Suite <dhiman.seal@hotmail.com>
2026-03-26 16:30:19 -07:00
Peter Steinberger
16565020a1 refactor: finish browser test path cleanup 2026-03-26 23:28:46 +00:00
Peter Steinberger
0ef2a9c8b5 refactor: remove core browser test duplicates 2026-03-26 23:28:34 +00:00
Peter Steinberger
9a7ceceffa refactor: move browser tests into plugin 2026-03-26 23:26:37 +00:00
Peter Steinberger
22348914cf refactor: centralize discord gateway ownership 2026-03-26 23:25:27 +00:00
Peter Steinberger
01bcbcf8d5 refactor: require legacy config migration on read 2026-03-26 23:23:47 +00:00
Peter Steinberger
cad83db8b2 refactor: move memory engine into memory plugin 2026-03-26 23:20:35 +00:00
Peter Steinberger
0e182dd3e1 refactor: share top-level setup dm policies 2026-03-26 23:20:26 +00:00
Peter Steinberger
abf95c5f99 refactor: share build copy script helpers 2026-03-26 23:20:26 +00:00
Peter Steinberger
0106b0488a refactor: share config section card rendering 2026-03-26 23:20:26 +00:00
Peter Steinberger
4890656d9d refactor: share matrix state file path helper 2026-03-26 23:20:26 +00:00
Peter Steinberger
bfad32aa16 refactor: share directory config listers 2026-03-26 23:20:26 +00:00
Peter Steinberger
4151b48d6c style(browser): format profiles service test 2026-03-26 23:18:57 +00:00
Peter Steinberger
8eeccb116d test(planner): refresh extension batch expectations 2026-03-26 23:16:22 +00:00
181 changed files with 1614 additions and 2004 deletions

View File

@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
- Agents/compaction: surface safeguard-specific cancel reasons and relabel benign manual `/compact` no-op cases as skipped instead of failed. (#51072) Thanks @afurm.
- Plugins/CLI backends: move bundled Claude CLI, Codex CLI, and Gemini CLI inference defaults onto the plugin surface, add bundled Gemini CLI backend support, and replace `gateway run --claude-cli-logs` with generic `--cli-backend-logs` while keeping the old flag as a compatibility alias.
- Plugins/startup: auto-load bundled provider and CLI-backend plugins from explicit config refs, so bundled Claude CLI, Codex CLI, and Gemini CLI message-provider setups no longer need manual `plugins.allow` entries.
- Config/TTS: auto-migrate legacy speech config on normal reads and secret resolution, keep legacy diagnostics for Doctor, and remove regular-mode runtime fallback for old bundled `tts.<provider>` API-key shapes.
### Fixes
@@ -61,6 +62,8 @@ Docs: https://docs.openclaw.ai
- Agents/failover: classify Codex accountId token extraction failures as auth errors so model fallback continues to the next configured candidate. (#55206) Thanks @cosmicnet.
- Talk/macOS: stop direct system-voice failures from replaying system speech, use app-locale fallback for shared watchdog timing, and add regression coverage for the macOS fallback route and language-aware timeout policy. (#53511) thanks @hongsw.
- Discord/gateway cleanup: keep late Carbon reconnect-exhausted errors suppressed through startup/dispose cleanup so Discord monitor shutdown no longer crashes on late gateway close events. (#55373) Thanks @Takhoffman.
- CLI/agents: stop injecting a fake "Tools are disabled" system-prompt line into CLI backend runs, so Claude CLI native tools can decide tool usage themselves. (#46214) Thanks @Dhi13man.
- CLI/agents: preserve backend session IDs for text-output CLI runs so resume uses the real CLI session handle instead of the OpenClaw session UUID. (#52712) Thanks @kenantan32.
## 2026.3.24

View File

@@ -244,7 +244,7 @@
"exportName": "CliBackendPlugin",
"kind": "type",
"source": {
"line": 1346,
"line": 1316,
"path": "src/plugins/types.ts"
}
},
@@ -406,7 +406,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1390,
"line": 1360,
"path": "src/plugins/types.ts"
}
},
@@ -3423,7 +3423,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1390,
"line": 1360,
"path": "src/plugins/types.ts"
}
},
@@ -3432,7 +3432,7 @@
"exportName": "OpenClawPluginCommandDefinition",
"kind": "type",
"source": {
"line": 1108,
"line": 1086,
"path": "src/plugins/types.ts"
}
},
@@ -3450,7 +3450,7 @@
"exportName": "OpenClawPluginDefinition",
"kind": "type",
"source": {
"line": 1372,
"line": 1342,
"path": "src/plugins/types.ts"
}
},
@@ -3459,7 +3459,7 @@
"exportName": "OpenClawPluginService",
"kind": "type",
"source": {
"line": 1339,
"line": 1309,
"path": "src/plugins/types.ts"
}
},
@@ -3468,7 +3468,7 @@
"exportName": "OpenClawPluginServiceContext",
"kind": "type",
"source": {
"line": 1331,
"line": 1301,
"path": "src/plugins/types.ts"
}
},
@@ -3504,7 +3504,7 @@
"exportName": "PluginInteractiveTelegramHandlerContext",
"kind": "type",
"source": {
"line": 1145,
"line": 1115,
"path": "src/plugins/types.ts"
}
},
@@ -3929,7 +3929,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1390,
"line": 1360,
"path": "src/plugins/types.ts"
}
},
@@ -3938,7 +3938,7 @@
"exportName": "OpenClawPluginCommandDefinition",
"kind": "type",
"source": {
"line": 1108,
"line": 1086,
"path": "src/plugins/types.ts"
}
},
@@ -3956,7 +3956,7 @@
"exportName": "OpenClawPluginDefinition",
"kind": "type",
"source": {
"line": 1372,
"line": 1342,
"path": "src/plugins/types.ts"
}
},
@@ -3965,7 +3965,7 @@
"exportName": "OpenClawPluginService",
"kind": "type",
"source": {
"line": 1339,
"line": 1309,
"path": "src/plugins/types.ts"
}
},
@@ -3974,7 +3974,7 @@
"exportName": "OpenClawPluginServiceContext",
"kind": "type",
"source": {
"line": 1331,
"line": 1301,
"path": "src/plugins/types.ts"
}
},
@@ -4010,7 +4010,7 @@
"exportName": "PluginInteractiveTelegramHandlerContext",
"kind": "type",
"source": {
"line": 1145,
"line": 1115,
"path": "src/plugins/types.ts"
}
},

View File

@@ -25,7 +25,7 @@
{"declaration":"export type ChannelStatusIssue = ChannelStatusIssue;","entrypoint":"index","exportName":"ChannelStatusIssue","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":100,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"index","exportName":"ClawdbotConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type CliBackendConfig = CliBackendConfig;","entrypoint":"index","exportName":"CliBackendConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":47,"sourcePath":"src/config/types.agent-defaults.ts"}
{"declaration":"export type CliBackendPlugin = CliBackendPlugin;","entrypoint":"index","exportName":"CliBackendPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1346,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type CliBackendPlugin = CliBackendPlugin;","entrypoint":"index","exportName":"CliBackendPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1316,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type CompiledConfiguredBinding = CompiledConfiguredBinding;","entrypoint":"index","exportName":"CompiledConfiguredBinding","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":38,"sourcePath":"src/channels/plugins/binding-types.ts"}
{"declaration":"export type ConfiguredBindingConversation = ConversationRef;","entrypoint":"index","exportName":"ConfiguredBindingConversation","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":13,"sourcePath":"src/channels/plugins/binding-types.ts"}
{"declaration":"export type ConfiguredBindingResolution = ConfiguredBindingResolution;","entrypoint":"index","exportName":"ConfiguredBindingResolution","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":49,"sourcePath":"src/channels/plugins/binding-types.ts"}
@@ -43,7 +43,7 @@
{"declaration":"export type ImageGenerationSourceImage = ImageGenerationSourceImage;","entrypoint":"index","exportName":"ImageGenerationSourceImage","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":14,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"index","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":969,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"index","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"index","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1390,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"index","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1360,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"index","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":95,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"index","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":66,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"index","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":54,"sourcePath":"src/plugins/runtime/types.ts"}
@@ -376,16 +376,16 @@
{"declaration":"export type GatewayRequestHandlerOptions = GatewayRequestHandlerOptions;","entrypoint":"core","exportName":"GatewayRequestHandlerOptions","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":112,"sourcePath":"src/gateway/server-methods/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"core","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":969,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"core","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1390,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1108,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1360,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1086,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":95,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1372,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1339,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"core","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1331,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1342,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1309,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"core","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1301,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"core","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":110,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"core","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":131,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"core","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":984,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"core","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1145,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"core","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1115,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"core","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":66,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"core","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":54,"sourcePath":"src/plugins/runtime/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"core","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":582,"sourcePath":"src/plugins/types.ts"}
@@ -432,16 +432,16 @@
{"declaration":"export type AnyAgentTool = AnyAgentTool;","entrypoint":"plugin-entry","exportName":"AnyAgentTool","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":9,"sourcePath":"src/agents/tools/common.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"plugin-entry","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":969,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"plugin-entry","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1390,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1108,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1360,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1086,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":95,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1372,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"plugin-entry","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1339,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1331,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1342,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"plugin-entry","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1309,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1301,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":110,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"plugin-entry","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":131,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"plugin-entry","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":984,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"plugin-entry","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1145,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"plugin-entry","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1115,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"plugin-entry","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":66,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":582,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":166,"sourcePath":"src/plugins/types.ts"}

View File

@@ -15,7 +15,7 @@ It works anywhere OpenClaw can send audio.
## Supported services
- **ElevenLabs** (primary or fallback provider)
- **Microsoft** (primary or fallback provider; current bundled implementation uses `node-edge-tts`, default when no API keys)
- **Microsoft** (primary or fallback provider; current bundled implementation uses `node-edge-tts`)
- **OpenAI** (primary or fallback provider; also used for summaries)
### Microsoft speech notes
@@ -38,9 +38,7 @@ If you want OpenAI or ElevenLabs:
- `ELEVENLABS_API_KEY` (or `XI_API_KEY`)
- `OPENAI_API_KEY`
Microsoft speech does **not** require an API key. If no API keys are found,
OpenClaw defaults to Microsoft (unless disabled via
`messages.tts.microsoft.enabled=false` or `messages.tts.edge.enabled=false`).
Microsoft speech does **not** require an API key.
If multiple providers are configured, the selected provider is used first and the others are fallback options.
Auto-summary uses the configured `summaryModel` (or `agents.defaults.model.primary`),
@@ -60,8 +58,8 @@ so that provider must also be authenticated if you enable summaries.
No. AutoTTS is **off** by default. Enable it in config with
`messages.tts.auto` or per session with `/tts always` (alias: `/tts on`).
Microsoft speech **is** enabled by default once TTS is on, and is used automatically
when no OpenAI or ElevenLabs API keys are available.
When `messages.tts.provider` is unset, OpenClaw picks the first configured
speech provider in registry auto-select order.
## Config
@@ -93,26 +91,28 @@ Full schema is in [Gateway configuration](/gateway/configuration).
modelOverrides: {
enabled: true,
},
openai: {
apiKey: "openai_api_key",
baseUrl: "https://api.openai.com/v1",
model: "gpt-4o-mini-tts",
voice: "alloy",
},
elevenlabs: {
apiKey: "elevenlabs_api_key",
baseUrl: "https://api.elevenlabs.io",
voiceId: "voice_id",
modelId: "eleven_multilingual_v2",
seed: 42,
applyTextNormalization: "auto",
languageCode: "en",
voiceSettings: {
stability: 0.5,
similarityBoost: 0.75,
style: 0.0,
useSpeakerBoost: true,
speed: 1.0,
providers: {
openai: {
apiKey: "openai_api_key",
baseUrl: "https://api.openai.com/v1",
model: "gpt-4o-mini-tts",
voice: "alloy",
},
elevenlabs: {
apiKey: "elevenlabs_api_key",
baseUrl: "https://api.elevenlabs.io",
voiceId: "voice_id",
modelId: "eleven_multilingual_v2",
seed: 42,
applyTextNormalization: "auto",
languageCode: "en",
voiceSettings: {
stability: 0.5,
similarityBoost: 0.75,
style: 0.0,
useSpeakerBoost: true,
speed: 1.0,
},
},
},
},
@@ -128,13 +128,15 @@ Full schema is in [Gateway configuration](/gateway/configuration).
tts: {
auto: "always",
provider: "microsoft",
microsoft: {
enabled: true,
voice: "en-US-MichelleNeural",
lang: "en-US",
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
rate: "+10%",
pitch: "-5%",
providers: {
microsoft: {
enabled: true,
voice: "en-US-MichelleNeural",
lang: "en-US",
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
rate: "+10%",
pitch: "-5%",
},
},
},
},
@@ -147,8 +149,10 @@ Full schema is in [Gateway configuration](/gateway/configuration).
{
messages: {
tts: {
microsoft: {
enabled: false,
providers: {
microsoft: {
enabled: false,
},
},
},
},
@@ -208,37 +212,37 @@ Then run:
- `enabled`: legacy toggle (doctor migrates this to `auto`).
- `mode`: `"final"` (default) or `"all"` (includes tool/block replies).
- `provider`: speech provider id such as `"elevenlabs"`, `"microsoft"`, or `"openai"` (fallback is automatic).
- If `provider` is **unset**, OpenClaw prefers `openai` (if key), then `elevenlabs` (if key),
otherwise `microsoft`.
- If `provider` is **unset**, OpenClaw uses the first configured speech provider in registry auto-select order.
- Legacy `provider: "edge"` still works and is normalized to `microsoft`.
- `summaryModel`: optional cheap model for auto-summary; defaults to `agents.defaults.model.primary`.
- Accepts `provider/model` or a configured model alias.
- `modelOverrides`: allow the model to emit TTS directives (on by default).
- `allowProvider` defaults to `false` (provider switching is opt-in).
- `providers.<id>`: provider-owned settings keyed by speech provider id.
- `maxTextLength`: hard cap for TTS input (chars). `/tts audio` fails if exceeded.
- `timeoutMs`: request timeout (ms).
- `prefsPath`: override the local prefs JSON path (provider/limit/summary).
- `apiKey` values fall back to env vars (`ELEVENLABS_API_KEY`/`XI_API_KEY`, `OPENAI_API_KEY`).
- `elevenlabs.baseUrl`: override ElevenLabs API base URL.
- `openai.baseUrl`: override the OpenAI TTS endpoint.
- Resolution order: `messages.tts.openai.baseUrl` -> `OPENAI_TTS_BASE_URL` -> `https://api.openai.com/v1`
- `providers.elevenlabs.baseUrl`: override ElevenLabs API base URL.
- `providers.openai.baseUrl`: override the OpenAI TTS endpoint.
- Resolution order: `messages.tts.providers.openai.baseUrl` -> `OPENAI_TTS_BASE_URL` -> `https://api.openai.com/v1`
- Non-default values are treated as OpenAI-compatible TTS endpoints, so custom model and voice names are accepted.
- `elevenlabs.voiceSettings`:
- `providers.elevenlabs.voiceSettings`:
- `stability`, `similarityBoost`, `style`: `0..1`
- `useSpeakerBoost`: `true|false`
- `speed`: `0.5..2.0` (1.0 = normal)
- `elevenlabs.applyTextNormalization`: `auto|on|off`
- `elevenlabs.languageCode`: 2-letter ISO 639-1 (e.g. `en`, `de`)
- `elevenlabs.seed`: integer `0..4294967295` (best-effort determinism)
- `microsoft.enabled`: allow Microsoft speech usage (default `true`; no API key).
- `microsoft.voice`: Microsoft neural voice name (e.g. `en-US-MichelleNeural`).
- `microsoft.lang`: language code (e.g. `en-US`).
- `microsoft.outputFormat`: Microsoft output format (e.g. `audio-24khz-48kbitrate-mono-mp3`).
- `providers.elevenlabs.applyTextNormalization`: `auto|on|off`
- `providers.elevenlabs.languageCode`: 2-letter ISO 639-1 (e.g. `en`, `de`)
- `providers.elevenlabs.seed`: integer `0..4294967295` (best-effort determinism)
- `providers.microsoft.enabled`: allow Microsoft speech usage (default `true`; no API key).
- `providers.microsoft.voice`: Microsoft neural voice name (e.g. `en-US-MichelleNeural`).
- `providers.microsoft.lang`: language code (e.g. `en-US`).
- `providers.microsoft.outputFormat`: Microsoft output format (e.g. `audio-24khz-48kbitrate-mono-mp3`).
- See Microsoft Speech output formats for valid values; not all formats are supported by the bundled Edge-backed transport.
- `microsoft.rate` / `microsoft.pitch` / `microsoft.volume`: percent strings (e.g. `+10%`, `-5%`).
- `microsoft.saveSubtitles`: write JSON subtitles alongside the audio file.
- `microsoft.proxy`: proxy URL for Microsoft speech requests.
- `microsoft.timeoutMs`: request timeout override (ms).
- `providers.microsoft.rate` / `providers.microsoft.pitch` / `providers.microsoft.volume`: percent strings (e.g. `+10%`, `-5%`).
- `providers.microsoft.saveSubtitles`: write JSON subtitles alongside the audio file.
- `providers.microsoft.proxy`: proxy URL for Microsoft speech requests.
- `providers.microsoft.timeoutMs`: request timeout override (ms).
- `edge.*`: legacy alias for the same Microsoft settings.
## Model-driven overrides (default on)

View File

@@ -15,7 +15,7 @@ It works anywhere OpenClaw can send audio.
## Supported services
- **ElevenLabs** (primary or fallback provider)
- **Microsoft** (primary or fallback provider; current bundled implementation uses `node-edge-tts`, default when no API keys)
- **Microsoft** (primary or fallback provider; current bundled implementation uses `node-edge-tts`)
- **OpenAI** (primary or fallback provider; also used for summaries)
### Microsoft speech notes
@@ -38,9 +38,7 @@ If you want OpenAI or ElevenLabs:
- `ELEVENLABS_API_KEY` (or `XI_API_KEY`)
- `OPENAI_API_KEY`
Microsoft speech does **not** require an API key. If no API keys are found,
OpenClaw defaults to Microsoft (unless disabled via
`messages.tts.microsoft.enabled=false` or `messages.tts.edge.enabled=false`).
Microsoft speech does **not** require an API key.
If multiple providers are configured, the selected provider is used first and the others are fallback options.
Auto-summary uses the configured `summaryModel` (or `agents.defaults.model.primary`),
@@ -60,8 +58,8 @@ so that provider must also be authenticated if you enable summaries.
No. AutoTTS is **off** by default. Enable it in config with
`messages.tts.auto` or per session with `/tts always` (alias: `/tts on`).
Microsoft speech **is** enabled by default once TTS is on, and is used automatically
when no OpenAI or ElevenLabs API keys are available.
When `messages.tts.provider` is unset, OpenClaw picks the first configured
speech provider in registry auto-select order.
## Config
@@ -93,26 +91,28 @@ Full schema is in [Gateway configuration](/gateway/configuration).
modelOverrides: {
enabled: true,
},
openai: {
apiKey: "openai_api_key",
baseUrl: "https://api.openai.com/v1",
model: "gpt-4o-mini-tts",
voice: "alloy",
},
elevenlabs: {
apiKey: "elevenlabs_api_key",
baseUrl: "https://api.elevenlabs.io",
voiceId: "voice_id",
modelId: "eleven_multilingual_v2",
seed: 42,
applyTextNormalization: "auto",
languageCode: "en",
voiceSettings: {
stability: 0.5,
similarityBoost: 0.75,
style: 0.0,
useSpeakerBoost: true,
speed: 1.0,
providers: {
openai: {
apiKey: "openai_api_key",
baseUrl: "https://api.openai.com/v1",
model: "gpt-4o-mini-tts",
voice: "alloy",
},
elevenlabs: {
apiKey: "elevenlabs_api_key",
baseUrl: "https://api.elevenlabs.io",
voiceId: "voice_id",
modelId: "eleven_multilingual_v2",
seed: 42,
applyTextNormalization: "auto",
languageCode: "en",
voiceSettings: {
stability: 0.5,
similarityBoost: 0.75,
style: 0.0,
useSpeakerBoost: true,
speed: 1.0,
},
},
},
},
@@ -128,13 +128,15 @@ Full schema is in [Gateway configuration](/gateway/configuration).
tts: {
auto: "always",
provider: "microsoft",
microsoft: {
enabled: true,
voice: "en-US-MichelleNeural",
lang: "en-US",
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
rate: "+10%",
pitch: "-5%",
providers: {
microsoft: {
enabled: true,
voice: "en-US-MichelleNeural",
lang: "en-US",
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
rate: "+10%",
pitch: "-5%",
},
},
},
},
@@ -147,8 +149,10 @@ Full schema is in [Gateway configuration](/gateway/configuration).
{
messages: {
tts: {
microsoft: {
enabled: false,
providers: {
microsoft: {
enabled: false,
},
},
},
},
@@ -208,37 +212,37 @@ Then run:
- `enabled`: legacy toggle (doctor migrates this to `auto`).
- `mode`: `"final"` (default) or `"all"` (includes tool/block replies).
- `provider`: speech provider id such as `"elevenlabs"`, `"microsoft"`, or `"openai"` (fallback is automatic).
- If `provider` is **unset**, OpenClaw prefers `openai` (if key), then `elevenlabs` (if key),
otherwise `microsoft`.
- If `provider` is **unset**, OpenClaw uses the first configured speech provider in registry auto-select order.
- Legacy `provider: "edge"` still works and is normalized to `microsoft`.
- `summaryModel`: optional cheap model for auto-summary; defaults to `agents.defaults.model.primary`.
- Accepts `provider/model` or a configured model alias.
- `modelOverrides`: allow the model to emit TTS directives (on by default).
- `allowProvider` defaults to `false` (provider switching is opt-in).
- `providers.<id>`: provider-owned settings keyed by speech provider id.
- `maxTextLength`: hard cap for TTS input (chars). `/tts audio` fails if exceeded.
- `timeoutMs`: request timeout (ms).
- `prefsPath`: override the local prefs JSON path (provider/limit/summary).
- `apiKey` values fall back to env vars (`ELEVENLABS_API_KEY`/`XI_API_KEY`, `OPENAI_API_KEY`).
- `elevenlabs.baseUrl`: override ElevenLabs API base URL.
- `openai.baseUrl`: override the OpenAI TTS endpoint.
- Resolution order: `messages.tts.openai.baseUrl` -> `OPENAI_TTS_BASE_URL` -> `https://api.openai.com/v1`
- `providers.elevenlabs.baseUrl`: override ElevenLabs API base URL.
- `providers.openai.baseUrl`: override the OpenAI TTS endpoint.
- Resolution order: `messages.tts.providers.openai.baseUrl` -> `OPENAI_TTS_BASE_URL` -> `https://api.openai.com/v1`
- Non-default values are treated as OpenAI-compatible TTS endpoints, so custom model and voice names are accepted.
- `elevenlabs.voiceSettings`:
- `providers.elevenlabs.voiceSettings`:
- `stability`, `similarityBoost`, `style`: `0..1`
- `useSpeakerBoost`: `true|false`
- `speed`: `0.5..2.0` (1.0 = normal)
- `elevenlabs.applyTextNormalization`: `auto|on|off`
- `elevenlabs.languageCode`: 2-letter ISO 639-1 (e.g. `en`, `de`)
- `elevenlabs.seed`: integer `0..4294967295` (best-effort determinism)
- `microsoft.enabled`: allow Microsoft speech usage (default `true`; no API key).
- `microsoft.voice`: Microsoft neural voice name (e.g. `en-US-MichelleNeural`).
- `microsoft.lang`: language code (e.g. `en-US`).
- `microsoft.outputFormat`: Microsoft output format (e.g. `audio-24khz-48kbitrate-mono-mp3`).
- `providers.elevenlabs.applyTextNormalization`: `auto|on|off`
- `providers.elevenlabs.languageCode`: 2-letter ISO 639-1 (e.g. `en`, `de`)
- `providers.elevenlabs.seed`: integer `0..4294967295` (best-effort determinism)
- `providers.microsoft.enabled`: allow Microsoft speech usage (default `true`; no API key).
- `providers.microsoft.voice`: Microsoft neural voice name (e.g. `en-US-MichelleNeural`).
- `providers.microsoft.lang`: language code (e.g. `en-US`).
- `providers.microsoft.outputFormat`: Microsoft output format (e.g. `audio-24khz-48kbitrate-mono-mp3`).
- See Microsoft Speech output formats for valid values; not all formats are supported by the bundled Edge-backed transport.
- `microsoft.rate` / `microsoft.pitch` / `microsoft.volume`: percent strings (e.g. `+10%`, `-5%`).
- `microsoft.saveSubtitles`: write JSON subtitles alongside the audio file.
- `microsoft.proxy`: proxy URL for Microsoft speech requests.
- `microsoft.timeoutMs`: request timeout override (ms).
- `providers.microsoft.rate` / `providers.microsoft.pitch` / `providers.microsoft.volume`: percent strings (e.g. `+10%`, `-5%`).
- `providers.microsoft.saveSubtitles`: write JSON subtitles alongside the audio file.
- `providers.microsoft.proxy`: proxy URL for Microsoft speech requests.
- `providers.microsoft.timeoutMs`: request timeout override (ms).
- `edge.*`: legacy alias for the same Microsoft settings.
## Model-driven overrides (default on)

View File

@@ -27,7 +27,7 @@ const browserClientMocks = vi.hoisted(() => ({
browserStop: vi.fn(async (..._args: unknown[]) => ({})),
browserTabs: vi.fn(async (..._args: unknown[]): Promise<Array<Record<string, unknown>>> => []),
}));
vi.mock("../../../extensions/browser/src/browser/client.js", () => browserClientMocks);
vi.mock("./browser/client.js", () => browserClientMocks);
const browserActionsMocks = vi.hoisted(() => ({
browserAct: vi.fn(async () => ({ ok: true })),
@@ -48,7 +48,7 @@ const browserActionsMocks = vi.hoisted(() => ({
browserPdfSave: vi.fn(async () => ({ ok: true, path: "/tmp/test.pdf" })),
browserScreenshotAction: vi.fn(async () => ({ ok: true, path: "/tmp/test.png" })),
}));
vi.mock("../../../extensions/browser/src/browser/client-actions.js", () => browserActionsMocks);
vi.mock("./browser/client-actions.js", () => browserActionsMocks);
const browserConfigMocks = vi.hoisted(() => ({
resolveBrowserConfig: vi.fn(() => ({
@@ -89,13 +89,15 @@ const browserConfigMocks = vi.hoisted(() => ({
};
}),
}));
vi.mock("../../../extensions/browser/src/browser/config.js", () => browserConfigMocks);
vi.mock("./browser/config.js", () => browserConfigMocks);
const nodesUtilsMocks = vi.hoisted(() => ({
listNodes: vi.fn(async (..._args: unknown[]): Promise<Array<Record<string, unknown>>> => []),
}));
vi.mock("./nodes-utils.js", async () => {
const actual = await vi.importActual<typeof import("./nodes-utils.js")>("./nodes-utils.js");
vi.mock("../../../src/agents/tools/nodes-utils.js", async () => {
const actual = await vi.importActual<typeof import("../../../src/agents/tools/nodes-utils.js")>(
"../../../src/agents/tools/nodes-utils.js",
);
return {
...actual,
listNodes: nodesUtilsMocks.listNodes,
@@ -108,39 +110,35 @@ const gatewayMocks = vi.hoisted(() => ({
payload: { result: { ok: true, running: true } },
})),
}));
vi.mock("./gateway.js", () => gatewayMocks);
vi.mock("../../../src/agents/tools/gateway.js", () => gatewayMocks);
const configMocks = vi.hoisted(() => ({
loadConfig: vi.fn(() => ({ browser: {} })),
}));
vi.mock("../../config/config.js", () => configMocks);
vi.mock("../../../src/config/config.js", () => configMocks);
const sessionTabRegistryMocks = vi.hoisted(() => ({
trackSessionBrowserTab: vi.fn(),
untrackSessionBrowserTab: vi.fn(),
}));
vi.mock(
"../../../extensions/browser/src/browser/session-tab-registry.js",
() => sessionTabRegistryMocks,
);
vi.mock("./browser/session-tab-registry.js", () => sessionTabRegistryMocks);
const toolCommonMocks = vi.hoisted(() => ({
imageResultFromFile: vi.fn(),
}));
vi.mock("./common.js", async () => {
const actual = await vi.importActual<typeof import("./common.js")>("./common.js");
vi.mock("../../../src/agents/tools/common.js", async () => {
const actual = await vi.importActual<typeof import("../../../src/agents/tools/common.js")>(
"../../../src/agents/tools/common.js",
);
return {
...actual,
imageResultFromFile: toolCommonMocks.imageResultFromFile,
};
});
import { __testing as browserToolActionsTesting } from "../../../extensions/browser/src/browser-tool.actions.js";
import {
__testing as browserToolTesting,
createBrowserTool,
} from "../../../extensions/browser/src/browser-tool.js";
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../../extensions/browser/src/browser/constants.js";
import { __testing as browserToolActionsTesting } from "./browser-tool.actions.js";
import { __testing as browserToolTesting, createBrowserTool } from "./browser-tool.js";
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "./browser/constants.js";
function mockSingleBrowserProxyNode() {
nodesUtilsMocks.listNodes.mockResolvedValue([

View File

@@ -1,13 +1,10 @@
import { afterEach, describe, expect, it } from "vitest";
import {
startBrowserBridgeServer,
stopBrowserBridgeServer,
} from "../../extensions/browser/src/browser/bridge-server.js";
import type { ResolvedBrowserConfig } from "../../extensions/browser/src/browser/config.js";
import { startBrowserBridgeServer, stopBrowserBridgeServer } from "./bridge-server.js";
import type { ResolvedBrowserConfig } from "./config.js";
import {
DEFAULT_OPENCLAW_BROWSER_COLOR,
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
} from "../../extensions/browser/src/browser/constants.js";
} from "./constants.js";
function buildResolvedConfig(): ResolvedBrowserConfig {
return {

View File

@@ -3,17 +3,14 @@ import {
appendCdpPath,
getHeadersWithAuth,
normalizeCdpHttpBaseForJsonEndpoints,
} from "../../extensions/browser/src/browser/cdp.helpers.js";
import { __test } from "../../extensions/browser/src/browser/client-fetch.js";
import {
resolveBrowserConfig,
resolveProfile,
} from "../../extensions/browser/src/browser/config.js";
import { shouldRejectBrowserMutation } from "../../extensions/browser/src/browser/csrf.js";
import { toBoolean } from "../../extensions/browser/src/browser/routes/utils.js";
import type { BrowserServerState } from "../../extensions/browser/src/browser/server-context.js";
import { listKnownProfileNames } from "../../extensions/browser/src/browser/server-context.js";
import { resolveTargetIdFromTabs } from "../../extensions/browser/src/browser/target-id.js";
} from "./cdp.helpers.js";
import { __test } from "./client-fetch.js";
import { resolveBrowserConfig, resolveProfile } from "./config.js";
import { shouldRejectBrowserMutation } from "./csrf.js";
import { toBoolean } from "./routes/utils.js";
import type { BrowserServerState } from "./server-context.js";
import { listKnownProfileNames } from "./server-context.js";
import { resolveTargetIdFromTabs } from "./target-id.js";
describe("toBoolean", () => {
it("parses yes/no and 1/0", () => {

View File

@@ -6,7 +6,7 @@ import {
hasProxyEnv,
withNoProxyForCdpUrl,
withNoProxyForLocalhost,
} from "../../extensions/browser/src/browser/cdp-proxy-bypass.js";
} from "./cdp-proxy-bypass.js";
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -202,8 +202,7 @@ describe("cdp-proxy-bypass", () => {
describe("withNoProxyForLocalhost concurrency", () => {
it("does not leak NO_PROXY when called concurrently", async () => {
await withIsolatedNoProxyEnv(async () => {
const { withNoProxyForLocalhost } =
await import("../../extensions/browser/src/browser/cdp-proxy-bypass.js");
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
// Simulate concurrent calls
const callA = withNoProxyForLocalhost(async () => {
@@ -230,8 +229,7 @@ describe("withNoProxyForLocalhost concurrency", () => {
describe("withNoProxyForLocalhost reverse exit order", () => {
it("restores NO_PROXY when first caller exits before second", async () => {
await withIsolatedNoProxyEnv(async () => {
const { withNoProxyForLocalhost } =
await import("../../extensions/browser/src/browser/cdp-proxy-bypass.js");
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
// Call A enters first, exits first (short task)
// Call B enters second, exits last (long task)
@@ -261,8 +259,7 @@ describe("withNoProxyForLocalhost preserves user-configured NO_PROXY", () => {
process.env.HTTP_PROXY = "http://proxy:8080";
try {
const { withNoProxyForLocalhost } =
await import("../../extensions/browser/src/browser/cdp-proxy-bypass.js");
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
await withNoProxyForLocalhost(async () => {
// Should not modify since loopback is already covered

View File

@@ -4,7 +4,7 @@ import {
PROFILE_WS_REACHABILITY_MAX_TIMEOUT_MS,
PROFILE_WS_REACHABILITY_MIN_TIMEOUT_MS,
resolveCdpReachabilityTimeouts,
} from "../../extensions/browser/src/browser/cdp-timeouts.js";
} from "./cdp-timeouts.js";
describe("resolveCdpReachabilityTimeouts", () => {
it("uses loopback defaults when timeout is omitted", () => {

View File

@@ -1,17 +1,12 @@
import { createServer } from "node:http";
import { afterEach, describe, expect, it, vi } from "vitest";
import { type WebSocket, WebSocketServer } from "ws";
import { isWebSocketUrl } from "../../extensions/browser/src/browser/cdp.helpers.js";
import {
createTargetViaCdp,
evaluateJavaScript,
normalizeCdpWsUrl,
snapshotAria,
} from "../../extensions/browser/src/browser/cdp.js";
import { parseHttpUrl } from "../../extensions/browser/src/browser/config.js";
import { InvalidBrowserNavigationUrlError } from "../../extensions/browser/src/browser/navigation-guard.js";
import { SsrFBlockedError } from "../infra/net/ssrf.js";
import { rawDataToString } from "../infra/ws.js";
import { isWebSocketUrl } from "./cdp.helpers.js";
import { createTargetViaCdp, evaluateJavaScript, normalizeCdpWsUrl, snapshotAria } from "./cdp.js";
import { parseHttpUrl } from "./config.js";
import { InvalidBrowserNavigationUrlError } from "./navigation-guard.js";
describe("cdp", () => {
let httpServer: ReturnType<typeof createServer> | null = null;

View File

@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import {
buildAiSnapshotFromChromeMcpSnapshot,
flattenChromeMcpSnapshotToAriaNodes,
} from "../../extensions/browser/src/browser/chrome-mcp.snapshot.js";
} from "./chrome-mcp.snapshot.js";
const snapshot = {
id: "root",

View File

@@ -6,7 +6,7 @@ import {
openChromeMcpTab,
resetChromeMcpSessionsForTest,
setChromeMcpSessionFactoryForTest,
} from "../../extensions/browser/src/browser/chrome-mcp.js";
} from "./chrome-mcp.js";
type ToolCall = {
name: string;

View File

@@ -25,7 +25,7 @@ import * as fs from "node:fs";
import os from "node:os";
async function loadResolveBrowserExecutableForPlatform() {
const mod = await import("../../extensions/browser/src/browser/chrome.executables.js");
const mod = await import("./chrome.executables.js");
return mod.resolveBrowserExecutableForPlatform;
}

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { buildOpenClawChromeLaunchArgs } from "../../extensions/browser/src/browser/chrome.js";
import { buildOpenClawChromeLaunchArgs } from "./chrome.js";
describe("browser chrome launch args", () => {
it("does not force an about:blank tab at startup", () => {

View File

@@ -15,11 +15,11 @@ import {
isChromeReachable,
resolveBrowserExecutableForPlatform,
stopOpenClawChrome,
} from "../../extensions/browser/src/browser/chrome.js";
} from "./chrome.js";
import {
DEFAULT_OPENCLAW_BROWSER_COLOR,
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
} from "../../extensions/browser/src/browser/constants.js";
} from "./constants.js";
type StopChromeTarget = Parameters<typeof stopOpenClawChrome>[0];

View File

@@ -1,5 +1,5 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { BrowserDispatchResponse } from "../../extensions/browser/src/browser/routes/dispatcher.js";
import type { BrowserDispatchResponse } from "./routes/dispatcher.js";
function okDispatchResponse(): BrowserDispatchResponse {
return { status: 200, body: { ok: true } };
@@ -22,35 +22,34 @@ const mocks = vi.hoisted(() => ({
dispatch: vi.fn(async (): Promise<BrowserDispatchResponse> => okDispatchResponse()),
}));
vi.mock("../../extensions/browser/src/config/config.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../extensions/browser/src/config/config.js")>();
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: mocks.loadConfig,
};
});
vi.mock("../../extensions/browser/src/browser/control-service.js", () => ({
vi.mock("./control-service.js", () => ({
createBrowserControlContext: vi.fn(() => ({})),
startBrowserControlServiceFromConfig: mocks.startBrowserControlServiceFromConfig,
}));
vi.mock("../../extensions/browser/src/browser/control-auth.js", () => ({
vi.mock("./control-auth.js", () => ({
resolveBrowserControlAuth: mocks.resolveBrowserControlAuth,
}));
vi.mock("../../extensions/browser/src/browser/bridge-auth-registry.js", () => ({
vi.mock("./bridge-auth-registry.js", () => ({
getBridgeAuthForPort: mocks.getBridgeAuthForPort,
}));
vi.mock("../../extensions/browser/src/browser/routes/dispatcher.js", () => ({
vi.mock("./routes/dispatcher.js", () => ({
createBrowserRouteDispatcher: vi.fn(() => ({
dispatch: mocks.dispatch,
})),
}));
let fetchBrowserJson: typeof import("../../extensions/browser/src/browser/client-fetch.js").fetchBrowserJson;
let fetchBrowserJson: typeof import("./client-fetch.js").fetchBrowserJson;
function stubJsonFetchOk() {
const fetchMock = vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>(
@@ -88,7 +87,7 @@ async function expectThrownBrowserFetchError(
describe("fetchBrowserJson loopback auth", () => {
beforeAll(async () => {
vi.resetModules();
({ fetchBrowserJson } = await import("../../extensions/browser/src/browser/client-fetch.js"));
({ fetchBrowserJson } = await import("./client-fetch.js"));
});
beforeEach(() => {

View File

@@ -7,13 +7,8 @@ import {
browserNavigate,
browserPdfSave,
browserScreenshotAction,
} from "../../extensions/browser/src/browser/client-actions.js";
import {
browserOpenTab,
browserSnapshot,
browserStatus,
browserTabs,
} from "../../extensions/browser/src/browser/client.js";
} from "./client-actions.js";
import { browserOpenTab, browserSnapshot, browserStatus, browserTabs } from "./client.js";
describe("browser client", () => {
function stubSnapshotFetch(calls: string[]) {

View File

@@ -1,12 +1,8 @@
import { describe, expect, it } from "vitest";
import {
resolveBrowserConfig,
resolveProfile,
shouldStartLocalBrowserServer,
} from "../../extensions/browser/src/browser/config.js";
import { getBrowserProfileCapabilities } from "../../extensions/browser/src/browser/profile-capabilities.js";
import { withEnv } from "../test-utils/env.js";
import { withEnv } from "../../test-support.js";
import { resolveUserPath } from "../utils.js";
import { resolveBrowserConfig, resolveProfile, shouldStartLocalBrowserServer } from "./config.js";
import { getBrowserProfileCapabilities } from "./profile-capabilities.js";
describe("browser config", () => {
it("defaults to enabled with loopback defaults and lobster-orange color", () => {

View File

@@ -1,10 +1,28 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { expectGeneratedTokenPersistedToGatewayAuth } from "../../test-support.js";
import type { OpenClawConfig } from "../config/config.js";
import { expectGeneratedTokenPersistedToGatewayAuth } from "../test-utils/auth-token-assertions.js";
const mocks = vi.hoisted(() => ({
loadConfig: vi.fn<() => OpenClawConfig>(),
writeConfigFile: vi.fn(async (_cfg: OpenClawConfig) => {}),
ensureGatewayStartupAuth: vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
cfg: {
...cfg,
gateway: {
...cfg.gateway,
auth: {
...cfg.gateway?.auth,
mode: "token" as const,
token: "a".repeat(48),
},
},
},
auth: {
mode: "token" as const,
token: "a".repeat(48),
},
generatedToken: "a".repeat(48),
persistedGeneratedToken: true,
})),
}));
vi.mock("../config/config.js", async (importOriginal) => {
@@ -12,11 +30,14 @@ vi.mock("../config/config.js", async (importOriginal) => {
return {
...actual,
loadConfig: mocks.loadConfig,
writeConfigFile: mocks.writeConfigFile,
};
});
let ensureBrowserControlAuth: typeof import("../../extensions/browser/src/browser/control-auth.js").ensureBrowserControlAuth;
vi.mock("../gateway/startup-auth.js", () => ({
ensureGatewayStartupAuth: mocks.ensureGatewayStartupAuth,
}));
let ensureBrowserControlAuth: typeof import("./control-auth.js").ensureBrowserControlAuth;
describe("ensureBrowserControlAuth", () => {
const expectExplicitModeSkipsAutoAuth = async (mode: "password" | "none") => {
@@ -32,28 +53,28 @@ describe("ensureBrowserControlAuth", () => {
const result = await ensureBrowserControlAuth({ cfg, env: {} as NodeJS.ProcessEnv });
expect(result).toEqual({ auth: {} });
expect(mocks.loadConfig).not.toHaveBeenCalled();
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
expect(mocks.ensureGatewayStartupAuth).not.toHaveBeenCalled();
};
const expectGeneratedTokenPersisted = (result: {
const expectGeneratedTokenPersisted = async (result: {
generatedToken?: string;
auth: { token?: string };
}) => {
expect(mocks.writeConfigFile).toHaveBeenCalledTimes(1);
expect(mocks.ensureGatewayStartupAuth).toHaveBeenCalledTimes(1);
const ensured = await mocks.ensureGatewayStartupAuth.mock.results[0]?.value;
expectGeneratedTokenPersistedToGatewayAuth({
generatedToken: result.generatedToken,
authToken: result.auth.token,
persistedConfig: mocks.writeConfigFile.mock.calls[0]?.[0],
persistedConfig: ensured?.cfg,
});
};
beforeEach(async () => {
vi.resetModules();
({ ensureBrowserControlAuth } =
await import("../../extensions/browser/src/browser/control-auth.js"));
({ ensureBrowserControlAuth } = await import("./control-auth.js"));
vi.restoreAllMocks();
mocks.loadConfig.mockClear();
mocks.writeConfigFile.mockClear();
mocks.ensureGatewayStartupAuth.mockClear();
});
it("returns existing auth and skips writes", async () => {
@@ -69,7 +90,7 @@ describe("ensureBrowserControlAuth", () => {
expect(result).toEqual({ auth: { token: "already-set" } });
expect(mocks.loadConfig).not.toHaveBeenCalled();
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
expect(mocks.ensureGatewayStartupAuth).not.toHaveBeenCalled();
});
it("auto-generates and persists a token when auth is missing", async () => {
@@ -85,7 +106,7 @@ describe("ensureBrowserControlAuth", () => {
});
const result = await ensureBrowserControlAuth({ cfg, env: {} as NodeJS.ProcessEnv });
expectGeneratedTokenPersisted(result);
await expectGeneratedTokenPersisted(result);
});
it("skips auto-generation in test env", async () => {
@@ -102,7 +123,7 @@ describe("ensureBrowserControlAuth", () => {
expect(result).toEqual({ auth: {} });
expect(mocks.loadConfig).not.toHaveBeenCalled();
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
expect(mocks.ensureGatewayStartupAuth).not.toHaveBeenCalled();
});
it("respects explicit password mode", async () => {
@@ -133,7 +154,7 @@ describe("ensureBrowserControlAuth", () => {
const result = await ensureBrowserControlAuth({ cfg, env: {} as NodeJS.ProcessEnv });
expect(result).toEqual({ auth: { token: "latest-token" } });
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
expect(mocks.ensureGatewayStartupAuth).not.toHaveBeenCalled();
});
it("fails when gateway.auth.token SecretRef is unresolved", async () => {
@@ -154,10 +175,11 @@ describe("ensureBrowserControlAuth", () => {
},
};
mocks.loadConfig.mockReturnValue(cfg);
mocks.ensureGatewayStartupAuth.mockRejectedValueOnce(new Error("MISSING_GW_TOKEN"));
await expect(ensureBrowserControlAuth({ cfg, env: {} as NodeJS.ProcessEnv })).rejects.toThrow(
/MISSING_GW_TOKEN/i,
);
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
expect(mocks.ensureGatewayStartupAuth).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { ensureBrowserControlAuth } from "../../extensions/browser/src/browser/control-auth.js";
import type { OpenClawConfig } from "../config/types.js";
import type { OpenClawConfig } from "../../test-support.js";
import { ensureBrowserControlAuth } from "./control-auth.js";
describe("ensureBrowserControlAuth", () => {
async function expectNoAutoGeneratedAuth(cfg: OpenClawConfig): Promise<void> {

View File

@@ -3,28 +3,29 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
ensureBrowserControlAuth: vi.fn(async () => ({ generatedToken: false })),
createBrowserRuntimeState: vi.fn(async () => ({ ok: true })),
}));
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: () => ({
browser: {
enabled: true,
},
plugins: {
entries: {
browser: {
enabled: false,
},
loadConfig: vi.fn(() => ({
browser: {
enabled: true,
},
plugins: {
entries: {
browser: {
enabled: false,
},
},
}),
},
})),
}));
vi.mock("openclaw/plugin-sdk/browser-support", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/browser-support")>();
return {
...actual,
loadConfig: mocks.loadConfig,
};
});
vi.mock("../../extensions/browser/src/browser/config.js", () => ({
vi.mock("./config.js", () => ({
resolveBrowserConfig: vi.fn(() => ({
enabled: true,
controlPort: 18791,
@@ -32,24 +33,24 @@ vi.mock("../../extensions/browser/src/browser/config.js", () => ({
})),
}));
vi.mock("../../extensions/browser/src/browser/control-auth.js", () => ({
vi.mock("./control-auth.js", () => ({
ensureBrowserControlAuth: mocks.ensureBrowserControlAuth,
}));
vi.mock("../../extensions/browser/src/browser/runtime-lifecycle.js", () => ({
vi.mock("./runtime-lifecycle.js", () => ({
createBrowserRuntimeState: mocks.createBrowserRuntimeState,
stopBrowserRuntime: vi.fn(async () => {}),
}));
let startBrowserControlServiceFromConfig: typeof import("../../extensions/browser/src/browser/control-service.js").startBrowserControlServiceFromConfig;
let startBrowserControlServiceFromConfig: typeof import("../control-service.js").startBrowserControlServiceFromConfig;
describe("startBrowserControlServiceFromConfig", () => {
beforeEach(async () => {
mocks.ensureBrowserControlAuth.mockClear();
mocks.createBrowserRuntimeState.mockClear();
mocks.loadConfig.mockClear();
vi.resetModules();
({ startBrowserControlServiceFromConfig } =
await import("../../extensions/browser/src/browser/control-service.js"));
({ startBrowserControlServiceFromConfig } = await import("../control-service.js"));
});
it("does not start the default service when the browser plugin is disabled", async () => {

View File

@@ -1,12 +1,12 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { SsrFBlockedError, type LookupFn } from "../infra/net/ssrf.js";
import {
assertBrowserNavigationAllowed,
assertBrowserNavigationRedirectChainAllowed,
assertBrowserNavigationResultAllowed,
InvalidBrowserNavigationUrlError,
requiresInspectableBrowserNavigationRedirects,
} from "../../extensions/browser/src/browser/navigation-guard.js";
import { SsrFBlockedError, type LookupFn } from "../infra/net/ssrf.js";
} from "./navigation-guard.js";
function createLookupFn(address: string): LookupFn {
const family = address.includes(":") ? 6 : 4;

View File

@@ -8,7 +8,7 @@ import {
resolvePathWithinRoot,
resolveStrictExistingPathsWithinRoot,
resolveWritablePathWithinRoot,
} from "../../extensions/browser/src/browser/paths.js";
} from "./paths.js";
async function createFixtureRoot(): Promise<{ baseDir: string; uploadsDir: string }> {
const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-browser-paths-"));

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { isDefaultBrowserPluginEnabled } from "../../extensions/browser/src/browser/plugin-enabled.js";
import type { OpenClawConfig } from "../config/config.js";
import { isDefaultBrowserPluginEnabled } from "./plugin-enabled.js";
describe("isDefaultBrowserPluginEnabled", () => {
it("defaults to enabled", () => {

View File

@@ -1,17 +1,13 @@
import fs from "node:fs";
import path from "node:path";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type {
BrowserRouteContext,
BrowserServerState,
} from "../../extensions/browser/src/browser/server-context.js";
import { resolveOpenClawUserDataDir } from "../../extensions/browser/src/browser/chrome.js";
import { movePathToTrash } from "../../extensions/browser/src/browser/trash.js";
import { loadConfig, writeConfigFile } from "../../extensions/browser/src/config/config.js";
import { loadConfig, writeConfigFile } from "../config/config.js";
import { resolveOpenClawUserDataDir } from "./chrome.js";
import type { BrowserRouteContext, BrowserServerState } from "./server-context.js";
import { movePathToTrash } from "./trash.js";
vi.mock("../../extensions/browser/src/config/config.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../extensions/browser/src/config/config.js")>();
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: vi.fn(),
@@ -19,16 +15,16 @@ vi.mock("../../extensions/browser/src/config/config.js", async (importOriginal)
};
});
vi.mock("../../extensions/browser/src/browser/trash.js", () => ({
vi.mock("./trash.js", () => ({
movePathToTrash: vi.fn(async (targetPath: string) => targetPath),
}));
vi.mock("../../extensions/browser/src/browser/chrome.js", () => ({
vi.mock("./chrome.js", () => ({
resolveOpenClawUserDataDir: vi.fn(() => "/tmp/openclaw-test/openclaw/user-data"),
}));
let resolveBrowserConfig: typeof import("../../extensions/browser/src/browser/config.js").resolveBrowserConfig;
let createBrowserProfilesService: typeof import("../../extensions/browser/src/browser/profiles-service.js").createBrowserProfilesService;
let resolveBrowserConfig: typeof import("./config.js").resolveBrowserConfig;
let createBrowserProfilesService: typeof import("./profiles-service.js").createBrowserProfilesService;
function createCtx(resolved: BrowserServerState["resolved"]) {
const state: BrowserServerState = {
@@ -63,9 +59,8 @@ async function createWorkProfileWithConfig(params: {
describe("BrowserProfilesService", () => {
beforeAll(async () => {
vi.resetModules();
({ resolveBrowserConfig } = await import("../../extensions/browser/src/browser/config.js"));
({ createBrowserProfilesService } =
await import("../../extensions/browser/src/browser/profiles-service.js"));
({ resolveBrowserConfig } = await import("./config.js"));
({ createBrowserProfilesService } = await import("./profiles-service.js"));
});
beforeEach(() => {

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { resolveBrowserConfig } from "../../extensions/browser/src/browser/config.js";
import { resolveBrowserConfig } from "./config.js";
import {
allocateCdpPort,
allocateColor,
@@ -9,7 +9,7 @@ import {
getUsedPorts,
isValidProfileName,
PROFILE_COLORS,
} from "../../extensions/browser/src/browser/profiles.js";
} from "./profiles.js";
describe("profile name validation", () => {
it.each(["openclaw", "work", "my-profile", "test123", "a", "a-b-c-1-2-3", "1test"])(

View File

@@ -1,9 +1,9 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { persistBrowserProxyFiles } from "../../extensions/browser/src/browser/proxy-files.js";
import { MEDIA_MAX_BYTES } from "../media/store.js";
import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js";
import { MEDIA_MAX_BYTES } from "../../../../src/media/store.js";
import { createTempHomeEnv, type TempHomeEnv } from "../../test-support.js";
import { persistBrowserProxyFiles } from "./proxy-files.js";
describe("persistBrowserProxyFiles", () => {
let tempHome: TempHomeEnv;

View File

@@ -55,19 +55,16 @@ function createBrowser(pages: unknown[]) {
}
let chromiumMock: typeof import("playwright-core").chromium;
let snapshotAiViaPlaywright: typeof import("../../extensions/browser/src/browser/pw-tools-core.snapshot.js").snapshotAiViaPlaywright;
let clickViaPlaywright: typeof import("../../extensions/browser/src/browser/pw-tools-core.interactions.js").clickViaPlaywright;
let closePlaywrightBrowserConnection: typeof import("../../extensions/browser/src/browser/pw-session.js").closePlaywrightBrowserConnection;
let snapshotAiViaPlaywright: typeof import("./pw-tools-core.snapshot.js").snapshotAiViaPlaywright;
let clickViaPlaywright: typeof import("./pw-tools-core.interactions.js").clickViaPlaywright;
let closePlaywrightBrowserConnection: typeof import("./pw-session.js").closePlaywrightBrowserConnection;
beforeAll(async () => {
const pw = await import("playwright-core");
chromiumMock = pw.chromium;
({ snapshotAiViaPlaywright } =
await import("../../extensions/browser/src/browser/pw-tools-core.snapshot.js"));
({ clickViaPlaywright } =
await import("../../extensions/browser/src/browser/pw-tools-core.interactions.js"));
({ closePlaywrightBrowserConnection } =
await import("../../extensions/browser/src/browser/pw-session.js"));
({ snapshotAiViaPlaywright } = await import("./pw-tools-core.snapshot.js"));
({ clickViaPlaywright } = await import("./pw-tools-core.interactions.js"));
({ closePlaywrightBrowserConnection } = await import("./pw-session.js"));
});
afterEach(async () => {

View File

@@ -4,7 +4,7 @@ import {
buildRoleSnapshotFromAriaSnapshot,
getRoleSnapshotStats,
parseRoleRef,
} from "../../extensions/browser/src/browser/pw-role-snapshot.js";
} from "./pw-role-snapshot.js";
describe("pw-role-snapshot", () => {
it("adds refs for interactive elements", () => {

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { isLiveTestEnabled } from "../agents/live-test-helpers.js";
import { isLiveTestEnabled } from "../../test-support.js";
const LIVE = isLiveTestEnabled();
const CDP_URL = process.env.OPENCLAW_LIVE_BROWSER_CDP_URL?.trim() || "";
@@ -14,7 +14,7 @@ async function waitFor(
describeLive("browser (live): remote CDP tab persistence", () => {
it("creates, lists, focuses, and closes tabs via Playwright", { timeout: 60_000 }, async () => {
const pw = await import("../../extensions/browser/src/browser/pw-ai.js");
const pw = await import("./pw-ai.js");
await pw.closePlaywrightBrowserConnection().catch(() => {});
const created = await pw.createPageViaPlaywright({ cdpUrl: CDP_URL, url: "about:blank" });

View File

@@ -1,10 +1,7 @@
import { chromium } from "playwright-core";
import { afterEach, describe, expect, it, vi } from "vitest";
import * as chromeModule from "../../extensions/browser/src/browser/chrome.js";
import {
closePlaywrightBrowserConnection,
listPagesViaPlaywright,
} from "../../extensions/browser/src/browser/pw-session.js";
import * as chromeModule from "./chrome.js";
import { closePlaywrightBrowserConnection, listPagesViaPlaywright } from "./pw-session.js";
const connectOverCdpSpy = vi.spyOn(chromium, "connectOverCDP");
const getChromeWebSocketUrlSpy = vi.spyOn(chromeModule, "getChromeWebSocketUrl");

View File

@@ -1,12 +1,9 @@
import { chromium } from "playwright-core";
import { afterEach, describe, expect, it, vi } from "vitest";
import * as chromeModule from "../../extensions/browser/src/browser/chrome.js";
import { InvalidBrowserNavigationUrlError } from "../../extensions/browser/src/browser/navigation-guard.js";
import {
closePlaywrightBrowserConnection,
createPageViaPlaywright,
} from "../../extensions/browser/src/browser/pw-session.js";
import { SsrFBlockedError } from "../infra/net/ssrf.js";
import * as chromeModule from "./chrome.js";
import { InvalidBrowserNavigationUrlError } from "./navigation-guard.js";
import { closePlaywrightBrowserConnection, createPageViaPlaywright } from "./pw-session.js";
const connectOverCdpSpy = vi.spyOn(chromium, "connectOverCDP");
const getChromeWebSocketUrlSpy = vi.spyOn(chromeModule, "getChromeWebSocketUrl");

View File

@@ -1,10 +1,7 @@
import { chromium } from "playwright-core";
import { afterEach, describe, expect, it, vi } from "vitest";
import * as chromeModule from "../../extensions/browser/src/browser/chrome.js";
import {
closePlaywrightBrowserConnection,
getPageForTargetId,
} from "../../extensions/browser/src/browser/pw-session.js";
import * as chromeModule from "./chrome.js";
import { closePlaywrightBrowserConnection, getPageForTargetId } from "./pw-session.js";
const connectOverCdpSpy = vi.spyOn(chromium, "connectOverCDP");
const getChromeWebSocketUrlSpy = vi.spyOn(chromeModule, "getChromeWebSocketUrl");

View File

@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { withPageScopedCdpClient } from "../../extensions/browser/src/browser/pw-session.page-cdp.js";
import { withPageScopedCdpClient } from "./pw-session.page-cdp.js";
describe("pw-session page-scoped CDP client", () => {
beforeEach(() => {

View File

@@ -5,7 +5,7 @@ import {
refLocator,
rememberRoleRefsForTarget,
restoreRoleRefsForTarget,
} from "../../extensions/browser/src/browser/pw-session.js";
} from "./pw-session.js";
function fakePage(): {
page: Page;

View File

@@ -3,15 +3,15 @@ import {
installPwToolsCoreTestHooks,
setPwToolsCoreCurrentPage,
setPwToolsCoreCurrentRefLocator,
} from "../../extensions/browser/src/browser/pw-tools-core.test-harness.js";
} from "./pw-tools-core.test-harness.js";
installPwToolsCoreTestHooks();
let mod: typeof import("../../extensions/browser/src/browser/pw-tools-core.js");
let mod: typeof import("./pw-tools-core.js");
describe("pw-tools-core", () => {
beforeAll(async () => {
vi.resetModules();
mod = await import("../../extensions/browser/src/browser/pw-tools-core.js");
mod = await import("./pw-tools-core.js");
});
it("clamps timeoutMs for scrollIntoView", async () => {

View File

@@ -18,7 +18,7 @@ const restoreRoleRefsForTarget = vi.fn(() => {});
const closePageViaPlaywright = vi.fn(async () => {});
const resizeViewportViaPlaywright = vi.fn(async () => {});
vi.mock("../../extensions/browser/src/browser/pw-session.js", () => ({
vi.mock("./pw-session.js", () => ({
ensurePageState,
forceDisconnectPlaywrightForTarget,
getPageForTargetId,
@@ -26,18 +26,17 @@ vi.mock("../../extensions/browser/src/browser/pw-session.js", () => ({
restoreRoleRefsForTarget,
}));
vi.mock("../../extensions/browser/src/browser/pw-tools-core.snapshot.js", () => ({
vi.mock("./pw-tools-core.snapshot.js", () => ({
closePageViaPlaywright,
resizeViewportViaPlaywright,
}));
let batchViaPlaywright: typeof import("../../extensions/browser/src/browser/pw-tools-core.interactions.js").batchViaPlaywright;
let batchViaPlaywright: typeof import("./pw-tools-core.interactions.js").batchViaPlaywright;
describe("batchViaPlaywright", () => {
beforeAll(async () => {
vi.resetModules();
({ batchViaPlaywright } =
await import("../../extensions/browser/src/browser/pw-tools-core.interactions.js"));
({ batchViaPlaywright } = await import("./pw-tools-core.interactions.js"));
});
beforeEach(() => {

View File

@@ -19,7 +19,7 @@ const refLocator = vi.fn(() => {
return locator;
});
vi.mock("../../extensions/browser/src/browser/pw-session.js", () => {
vi.mock("./pw-session.js", () => {
return {
ensurePageState,
forceDisconnectPlaywrightForTarget,
@@ -29,7 +29,7 @@ vi.mock("../../extensions/browser/src/browser/pw-session.js", () => {
};
});
let evaluateViaPlaywright: typeof import("../../extensions/browser/src/browser/pw-tools-core.interactions.js").evaluateViaPlaywright;
let evaluateViaPlaywright: typeof import("./pw-tools-core.interactions.js").evaluateViaPlaywright;
function createPendingEval() {
let evalCalled!: () => void;
@@ -48,8 +48,7 @@ describe("evaluateViaPlaywright (abort)", () => {
vi.clearAllMocks();
page = null;
locator = null;
({ evaluateViaPlaywright } =
await import("../../extensions/browser/src/browser/pw-tools-core.interactions.js"));
({ evaluateViaPlaywright } = await import("./pw-tools-core.interactions.js"));
});
it.each([

View File

@@ -20,11 +20,9 @@ const refLocator = vi.fn(() => {
const forceDisconnectPlaywrightForTarget = vi.fn(async () => {});
const resolveStrictExistingPathsWithinRoot =
vi.fn<
typeof import("../../extensions/browser/src/browser/paths.js").resolveStrictExistingPathsWithinRoot
>();
vi.fn<typeof import("./paths.js").resolveStrictExistingPathsWithinRoot>();
vi.mock("../../extensions/browser/src/browser/pw-session.js", () => {
vi.mock("./pw-session.js", () => {
return {
ensurePageState,
forceDisconnectPlaywrightForTarget,
@@ -34,14 +32,14 @@ vi.mock("../../extensions/browser/src/browser/pw-session.js", () => {
};
});
vi.mock("../../extensions/browser/src/browser/paths.js", () => {
vi.mock("./paths.js", () => {
return {
DEFAULT_UPLOAD_DIR: "/tmp/openclaw/uploads",
resolveStrictExistingPathsWithinRoot,
};
});
let setInputFilesViaPlaywright: typeof import("../../extensions/browser/src/browser/pw-tools-core.interactions.js").setInputFilesViaPlaywright;
let setInputFilesViaPlaywright: typeof import("./pw-tools-core.interactions.js").setInputFilesViaPlaywright;
function seedSingleLocatorPage(): { setInputFiles: ReturnType<typeof vi.fn> } {
const setInputFiles = vi.fn(async () => {});
@@ -58,8 +56,7 @@ function seedSingleLocatorPage(): { setInputFiles: ReturnType<typeof vi.fn> } {
describe("setInputFilesViaPlaywright", () => {
beforeEach(async () => {
vi.resetModules();
({ setInputFilesViaPlaywright } =
await import("../../extensions/browser/src/browser/pw-tools-core.interactions.js"));
({ setInputFilesViaPlaywright } = await import("./pw-tools-core.interactions.js"));
vi.clearAllMocks();
page = null;
locator = null;

View File

@@ -2,19 +2,19 @@ import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { DEFAULT_UPLOAD_DIR } from "../../extensions/browser/src/browser/paths.js";
import { DEFAULT_UPLOAD_DIR } from "./paths.js";
import {
installPwToolsCoreTestHooks,
setPwToolsCoreCurrentPage,
} from "../../extensions/browser/src/browser/pw-tools-core.test-harness.js";
} from "./pw-tools-core.test-harness.js";
installPwToolsCoreTestHooks();
let mod: typeof import("../../extensions/browser/src/browser/pw-tools-core.js");
let mod: typeof import("./pw-tools-core.js");
describe("pw-tools-core", () => {
beforeAll(async () => {
vi.resetModules();
mod = await import("../../extensions/browser/src/browser/pw-tools-core.js");
mod = await import("./pw-tools-core.js");
});
it("last file-chooser arm wins", async () => {

View File

@@ -2,17 +2,17 @@ import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_UPLOAD_DIR } from "../../extensions/browser/src/browser/paths.js";
import { DEFAULT_UPLOAD_DIR } from "./paths.js";
import {
getPwToolsCoreSessionMocks,
installPwToolsCoreTestHooks,
setPwToolsCoreCurrentPage,
setPwToolsCoreCurrentRefLocator,
} from "../../extensions/browser/src/browser/pw-tools-core.test-harness.js";
} from "./pw-tools-core.test-harness.js";
installPwToolsCoreTestHooks();
const sessionMocks = getPwToolsCoreSessionMocks();
let mod: typeof import("../../extensions/browser/src/browser/pw-tools-core.js");
let mod: typeof import("./pw-tools-core.js");
function createFileChooserPageMocks() {
const fileChooser = { setFiles: vi.fn(async () => {}) };
@@ -28,7 +28,7 @@ function createFileChooserPageMocks() {
describe("pw-tools-core", () => {
beforeAll(async () => {
vi.resetModules();
mod = await import("../../extensions/browser/src/browser/pw-tools-core.js");
mod = await import("./pw-tools-core.js");
});
beforeEach(() => {

View File

@@ -1,14 +1,14 @@
import { describe, expect, it, vi } from "vitest";
import { InvalidBrowserNavigationUrlError } from "../../extensions/browser/src/browser/navigation-guard.js";
import { SsrFBlockedError } from "../infra/net/ssrf.js";
import { InvalidBrowserNavigationUrlError } from "./navigation-guard.js";
import {
getPwToolsCoreSessionMocks,
installPwToolsCoreTestHooks,
setPwToolsCoreCurrentPage,
} from "../../extensions/browser/src/browser/pw-tools-core.test-harness.js";
import { SsrFBlockedError } from "../infra/net/ssrf.js";
} from "./pw-tools-core.test-harness.js";
installPwToolsCoreTestHooks();
const mod = await import("../../extensions/browser/src/browser/pw-tools-core.snapshot.js");
const mod = await import("./pw-tools-core.snapshot.js");
describe("pw-tools-core.snapshot navigate guard", () => {
it("blocks unsupported non-network URLs before page lookup", async () => {

View File

@@ -7,7 +7,7 @@ import {
installPwToolsCoreTestHooks,
setPwToolsCoreCurrentPage,
setPwToolsCoreCurrentRefLocator,
} from "../../extensions/browser/src/browser/pw-tools-core.test-harness.js";
} from "./pw-tools-core.test-harness.js";
installPwToolsCoreTestHooks();
const sessionMocks = getPwToolsCoreSessionMocks();
@@ -15,12 +15,12 @@ const tmpDirMocks = vi.hoisted(() => ({
resolvePreferredOpenClawTmpDir: vi.fn(() => "/tmp/openclaw"),
}));
vi.mock("../infra/tmp-openclaw-dir.js", () => tmpDirMocks);
let mod: typeof import("../../extensions/browser/src/browser/pw-tools-core.js");
let mod: typeof import("./pw-tools-core.js");
describe("pw-tools-core", () => {
beforeAll(async () => {
vi.resetModules();
mod = await import("../../extensions/browser/src/browser/pw-tools-core.js");
mod = await import("./pw-tools-core.js");
});
beforeEach(() => {

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { isPersistentBrowserProfileMutation } from "../../extensions/browser/src/browser/request-policy.js";
import { isPersistentBrowserProfileMutation } from "./request-policy.js";
describe("isPersistentBrowserProfileMutation", () => {
it.each([

View File

@@ -1,9 +1,6 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
createBrowserRouteApp,
createBrowserRouteResponse,
} from "../../../extensions/browser/src/browser/routes/test-helpers.js";
import type { BrowserRequest } from "../../../extensions/browser/src/browser/routes/types.js";
import { createBrowserRouteApp, createBrowserRouteResponse } from "./test-helpers.js";
import type { BrowserRequest } from "./types.js";
const routeState = vi.hoisted(() => ({
profileCtx: {
@@ -36,7 +33,7 @@ const chromeMcpMocks = vi.hoisted(() => ({
})),
}));
vi.mock("../../../extensions/browser/src/browser/chrome-mcp.js", () => ({
vi.mock("../chrome-mcp.js", () => ({
clickChromeMcpElement: vi.fn(async () => {}),
closeChromeMcpTab: vi.fn(async () => {}),
dragChromeMcpElement: vi.fn(async () => {}),
@@ -51,18 +48,18 @@ vi.mock("../../../extensions/browser/src/browser/chrome-mcp.js", () => ({
takeChromeMcpSnapshot: chromeMcpMocks.takeChromeMcpSnapshot,
}));
vi.mock("../../../extensions/browser/src/browser/cdp.js", () => ({
vi.mock("../cdp.js", () => ({
captureScreenshot: vi.fn(),
snapshotAria: vi.fn(),
}));
vi.mock("../../../extensions/browser/src/browser/navigation-guard.js", () => ({
vi.mock("../navigation-guard.js", () => ({
assertBrowserNavigationAllowed: vi.fn(async () => {}),
assertBrowserNavigationResultAllowed: vi.fn(async () => {}),
withBrowserNavigationPolicy: vi.fn(() => ({})),
}));
vi.mock("../../../extensions/browser/src/browser/screenshot.js", () => ({
vi.mock("../screenshot.js", () => ({
DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES: 128,
DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE: 64,
normalizeBrowserScreenshot: vi.fn(async (buffer: Buffer) => ({
@@ -76,7 +73,7 @@ vi.mock("../../media/store.js", () => ({
saveMediaBuffer: vi.fn(async () => ({ path: "/tmp/fake.png" })),
}));
vi.mock("../../../extensions/browser/src/browser/routes/agent.shared.js", () => ({
vi.mock("./agent.shared.js", () => ({
getPwAiModule: vi.fn(async () => null),
handleRouteError: vi.fn(),
readBody: vi.fn((req: BrowserRequest) => req.body ?? {}),
@@ -97,15 +94,13 @@ vi.mock("../../../extensions/browser/src/browser/routes/agent.shared.js", () =>
}),
}));
let registerBrowserAgentActRoutes: typeof import("../../../extensions/browser/src/browser/routes/agent.act.js").registerBrowserAgentActRoutes;
let registerBrowserAgentSnapshotRoutes: typeof import("../../../extensions/browser/src/browser/routes/agent.snapshot.js").registerBrowserAgentSnapshotRoutes;
let registerBrowserAgentActRoutes: typeof import("./agent.act.js").registerBrowserAgentActRoutes;
let registerBrowserAgentSnapshotRoutes: typeof import("./agent.snapshot.js").registerBrowserAgentSnapshotRoutes;
beforeAll(async () => {
vi.resetModules();
({ registerBrowserAgentActRoutes } =
await import("../../../extensions/browser/src/browser/routes/agent.act.js"));
({ registerBrowserAgentSnapshotRoutes } =
await import("../../../extensions/browser/src/browser/routes/agent.snapshot.js"));
({ registerBrowserAgentActRoutes } = await import("./agent.act.js"));
({ registerBrowserAgentSnapshotRoutes } = await import("./agent.snapshot.js"));
});
function getSnapshotGetHandler() {

View File

@@ -1,10 +1,6 @@
import { describe, expect, it } from "vitest";
import {
readBody,
resolveTargetIdFromBody,
resolveTargetIdFromQuery,
} from "../../../extensions/browser/src/browser/routes/agent.shared.js";
import type { BrowserRequest } from "../../../extensions/browser/src/browser/routes/types.js";
import { readBody, resolveTargetIdFromBody, resolveTargetIdFromQuery } from "./agent.shared.js";
import type { BrowserRequest } from "./types.js";
function requestWithBody(body: unknown): BrowserRequest {
return {

View File

@@ -1,9 +1,6 @@
import { describe, expect, it } from "vitest";
import {
resolveBrowserConfig,
resolveProfile,
} from "../../../extensions/browser/src/browser/config.js";
import { resolveSnapshotPlan } from "../../../extensions/browser/src/browser/routes/agent.snapshot.plan.js";
import { resolveBrowserConfig, resolveProfile } from "../config.js";
import { resolveSnapshotPlan } from "./agent.snapshot.plan.js";
describe("resolveSnapshotPlan", () => {
it("defaults existing-session snapshots to ai when format is omitted", () => {

View File

@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { resolveTargetIdAfterNavigate } from "../../../extensions/browser/src/browser/routes/agent.snapshot.js";
import { resolveTargetIdAfterNavigate } from "./agent.snapshot.js";
type Tab = { targetId: string; url: string };

View File

@@ -3,7 +3,7 @@ import {
parseRequiredStorageMutationRequest,
parseStorageKind,
parseStorageMutationRequest,
} from "../../../extensions/browser/src/browser/routes/agent.storage.js";
} from "./agent.storage.js";
describe("browser storage route parsing", () => {
describe("parseStorageKind", () => {

View File

@@ -1,15 +1,12 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
createBrowserRouteApp,
createBrowserRouteResponse,
} from "../../../extensions/browser/src/browser/routes/test-helpers.js";
import { createBrowserRouteApp, createBrowserRouteResponse } from "./test-helpers.js";
vi.mock("../../../extensions/browser/src/browser/chrome-mcp.js", () => ({
vi.mock("../chrome-mcp.js", () => ({
getChromeMcpPid: vi.fn(() => 4321),
}));
let registerBrowserBasicRoutes: typeof import("../../../extensions/browser/src/browser/routes/basic.js").registerBrowserBasicRoutes;
let BrowserProfileUnavailableError: typeof import("../../../extensions/browser/src/browser/errors.js").BrowserProfileUnavailableError;
let registerBrowserBasicRoutes: typeof import("./basic.js").registerBrowserBasicRoutes;
let BrowserProfileUnavailableError: typeof import("../errors.js").BrowserProfileUnavailableError;
function createExistingSessionProfileState(params?: { isHttpReachable?: () => Promise<boolean> }) {
return {
@@ -57,10 +54,8 @@ async function callBasicRouteWithState(params: {
beforeEach(async () => {
vi.resetModules();
({ BrowserProfileUnavailableError } =
await import("../../../extensions/browser/src/browser/errors.js"));
({ registerBrowserBasicRoutes } =
await import("../../../extensions/browser/src/browser/routes/basic.js"));
({ BrowserProfileUnavailableError } = await import("../errors.js"));
({ registerBrowserBasicRoutes } = await import("./basic.js"));
});
describe("basic browser routes", () => {

View File

@@ -1,12 +1,12 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { BrowserRouteContext } from "../../../extensions/browser/src/browser/server-context.js";
import type { BrowserRouteContext } from "../server-context.js";
let createBrowserRouteDispatcher: typeof import("../../../extensions/browser/src/browser/routes/dispatcher.js").createBrowserRouteDispatcher;
let createBrowserRouteDispatcher: typeof import("./dispatcher.js").createBrowserRouteDispatcher;
describe("browser route dispatcher (abort)", () => {
beforeEach(async () => {
vi.resetModules();
vi.doMock("../../../extensions/browser/src/browser/routes/index.js", () => {
vi.doMock("./index.js", () => {
return {
registerBrowserRoutes(app: { get: (path: string, handler: unknown) => void }) {
app.get(
@@ -40,8 +40,7 @@ describe("browser route dispatcher (abort)", () => {
},
};
});
({ createBrowserRouteDispatcher } =
await import("../../../extensions/browser/src/browser/routes/dispatcher.js"));
({ createBrowserRouteDispatcher } = await import("./dispatcher.js"));
});
it("propagates AbortSignal and lets handlers observe abort", async () => {

View File

@@ -1,6 +1,6 @@
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { normalizeBrowserScreenshot } from "../../extensions/browser/src/browser/screenshot.js";
import { normalizeBrowserScreenshot } from "./screenshot.js";
describe("browser screenshot normalization", () => {
it("shrinks oversized images to <=2000x2000 and <=5MB", async () => {

View File

@@ -6,15 +6,15 @@ vi.hoisted(() => {
vi.resetModules();
});
import "../../extensions/browser/src/browser/server-context.chrome-test-harness.js";
import "./server-context.chrome-test-harness.js";
import {
PROFILE_ATTACH_RETRY_TIMEOUT_MS,
PROFILE_HTTP_REACHABILITY_TIMEOUT_MS,
} from "../../extensions/browser/src/browser/cdp-timeouts.js";
import * as chromeModule from "../../extensions/browser/src/browser/chrome.js";
import type { RunningChrome } from "../../extensions/browser/src/browser/chrome.js";
import type { BrowserServerState } from "../../extensions/browser/src/browser/server-context.js";
import { createBrowserRouteContext } from "../../extensions/browser/src/browser/server-context.js";
} from "./cdp-timeouts.js";
import * as chromeModule from "./chrome.js";
import type { RunningChrome } from "./chrome.js";
import type { BrowserServerState } from "./server-context.js";
import { createBrowserRouteContext } from "./server-context.js";
function makeBrowserState(): BrowserServerState {
return {

View File

@@ -1,8 +1,8 @@
import fs from "node:fs";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { BrowserServerState } from "../../extensions/browser/src/browser/server-context.js";
import type { BrowserServerState } from "./server-context.js";
vi.mock("../../extensions/browser/src/browser/chrome-mcp.js", () => ({
vi.mock("./chrome-mcp.js", () => ({
closeChromeMcpSession: vi.fn(async () => true),
ensureChromeMcpAvailable: vi.fn(async () => {}),
focusChromeMcpTab: vi.fn(async () => {}),
@@ -19,8 +19,8 @@ vi.mock("../../extensions/browser/src/browser/chrome-mcp.js", () => ({
getChromeMcpPid: vi.fn(() => 4321),
}));
let createBrowserRouteContext: typeof import("../../extensions/browser/src/browser/server-context.js").createBrowserRouteContext;
let chromeMcp: typeof import("../../extensions/browser/src/browser/chrome-mcp.js");
let createBrowserRouteContext: typeof import("./server-context.js").createBrowserRouteContext;
let chromeMcp: typeof import("./chrome-mcp.js");
function makeState(): BrowserServerState {
return {
@@ -64,9 +64,8 @@ afterEach(() => {
beforeEach(async () => {
vi.resetModules();
({ createBrowserRouteContext } =
await import("../../extensions/browser/src/browser/server-context.js"));
chromeMcp = await import("../../extensions/browser/src/browser/chrome-mcp.js");
({ createBrowserRouteContext } = await import("./server-context.js"));
chromeMcp = await import("./chrome-mcp.js");
});
describe("browser server-context existing-session profile", () => {

View File

@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { BrowserServerState } from "../../extensions/browser/src/browser/server-context.types.js";
import type { BrowserServerState } from "./server-context.types.js";
let cfgProfiles: Record<string, { cdpPort?: number; cdpUrl?: string; color?: string }> = {};
@@ -42,18 +42,17 @@ vi.mock("../config/config.js", async (importOriginal) => {
describe("server-context hot-reload profiles", () => {
let loadConfig: typeof import("../config/config.js").loadConfig;
let resolveBrowserConfig: typeof import("../../extensions/browser/src/browser/config.js").resolveBrowserConfig;
let resolveProfile: typeof import("../../extensions/browser/src/browser/config.js").resolveProfile;
let refreshResolvedBrowserConfigFromDisk: typeof import("../../extensions/browser/src/browser/resolved-config-refresh.js").refreshResolvedBrowserConfigFromDisk;
let resolveBrowserProfileWithHotReload: typeof import("../../extensions/browser/src/browser/resolved-config-refresh.js").resolveBrowserProfileWithHotReload;
let resolveBrowserConfig: typeof import("./config.js").resolveBrowserConfig;
let resolveProfile: typeof import("./config.js").resolveProfile;
let refreshResolvedBrowserConfigFromDisk: typeof import("./resolved-config-refresh.js").refreshResolvedBrowserConfigFromDisk;
let resolveBrowserProfileWithHotReload: typeof import("./resolved-config-refresh.js").resolveBrowserProfileWithHotReload;
beforeEach(async () => {
vi.resetModules();
({ loadConfig } = await import("../config/config.js"));
({ resolveBrowserConfig, resolveProfile } =
await import("../../extensions/browser/src/browser/config.js"));
({ resolveBrowserConfig, resolveProfile } = await import("./config.js"));
({ refreshResolvedBrowserConfigFromDisk, resolveBrowserProfileWithHotReload } =
await import("../../extensions/browser/src/browser/resolved-config-refresh.js"));
await import("./resolved-config-refresh.js"));
vi.clearAllMocks();
cfgProfiles = {
openclaw: { cdpPort: 18800, color: "#FF4500" },

View File

@@ -1,11 +1,8 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import * as cdpModule from "../../extensions/browser/src/browser/cdp.js";
import { createBrowserRouteContext } from "../../extensions/browser/src/browser/server-context.js";
import {
makeState,
originalFetch,
} from "../../extensions/browser/src/browser/server-context.remote-tab-ops.harness.js";
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
import { withFetchPreconnect } from "../../test-support.js";
import * as cdpModule from "./cdp.js";
import { createBrowserRouteContext } from "./server-context.js";
import { makeState, originalFetch } from "./server-context.remote-tab-ops.harness.js";
afterEach(() => {
globalThis.fetch = originalFetch;

View File

@@ -2,29 +2,26 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
const originalFetch = globalThis.fetch;
let chromeModule: typeof import("../../extensions/browser/src/browser/chrome.js");
let InvalidBrowserNavigationUrlError: typeof import("../../extensions/browser/src/browser/navigation-guard.js").InvalidBrowserNavigationUrlError;
let pwAiModule: typeof import("../../extensions/browser/src/browser/pw-ai-module.js");
let closePlaywrightBrowserConnection: typeof import("../../extensions/browser/src/browser/pw-session.js").closePlaywrightBrowserConnection;
let createBrowserRouteContext: typeof import("../../extensions/browser/src/browser/server-context.js").createBrowserRouteContext;
let createJsonListFetchMock: typeof import("../../extensions/browser/src/browser/server-context.remote-tab-ops.harness.js").createJsonListFetchMock;
let createRemoteRouteHarness: typeof import("../../extensions/browser/src/browser/server-context.remote-tab-ops.harness.js").createRemoteRouteHarness;
let createSequentialPageLister: typeof import("../../extensions/browser/src/browser/server-context.remote-tab-ops.harness.js").createSequentialPageLister;
let makeState: typeof import("../../extensions/browser/src/browser/server-context.remote-tab-ops.harness.js").makeState;
let chromeModule: typeof import("./chrome.js");
let InvalidBrowserNavigationUrlError: typeof import("./navigation-guard.js").InvalidBrowserNavigationUrlError;
let pwAiModule: typeof import("./pw-ai-module.js");
let closePlaywrightBrowserConnection: typeof import("./pw-session.js").closePlaywrightBrowserConnection;
let createBrowserRouteContext: typeof import("./server-context.js").createBrowserRouteContext;
let createJsonListFetchMock: typeof import("./server-context.remote-tab-ops.harness.js").createJsonListFetchMock;
let createRemoteRouteHarness: typeof import("./server-context.remote-tab-ops.harness.js").createRemoteRouteHarness;
let createSequentialPageLister: typeof import("./server-context.remote-tab-ops.harness.js").createSequentialPageLister;
let makeState: typeof import("./server-context.remote-tab-ops.harness.js").makeState;
beforeAll(async () => {
vi.resetModules();
await import("../../extensions/browser/src/browser/server-context.chrome-test-harness.js");
chromeModule = await import("../../extensions/browser/src/browser/chrome.js");
({ InvalidBrowserNavigationUrlError } =
await import("../../extensions/browser/src/browser/navigation-guard.js"));
pwAiModule = await import("../../extensions/browser/src/browser/pw-ai-module.js");
({ closePlaywrightBrowserConnection } =
await import("../../extensions/browser/src/browser/pw-session.js"));
({ createBrowserRouteContext } =
await import("../../extensions/browser/src/browser/server-context.js"));
await import("./server-context.chrome-test-harness.js");
chromeModule = await import("./chrome.js");
({ InvalidBrowserNavigationUrlError } = await import("./navigation-guard.js"));
pwAiModule = await import("./pw-ai-module.js");
({ closePlaywrightBrowserConnection } = await import("./pw-session.js"));
({ createBrowserRouteContext } = await import("./server-context.js"));
({ createJsonListFetchMock, createRemoteRouteHarness, createSequentialPageLister, makeState } =
await import("../../extensions/browser/src/browser/server-context.remote-tab-ops.harness.js"));
await import("./server-context.remote-tab-ops.harness.js"));
});
beforeEach(() => {

View File

@@ -11,10 +11,10 @@ const pwAiMocks = vi.hoisted(() => ({
closePlaywrightBrowserConnection: vi.fn(async () => {}),
}));
vi.mock("../../extensions/browser/src/browser/trash.js", () => trashMocks);
vi.mock("../../extensions/browser/src/browser/pw-ai.js", () => pwAiMocks);
vi.mock("./trash.js", () => trashMocks);
vi.mock("./pw-ai.js", () => pwAiMocks);
let createProfileResetOps: typeof import("../../extensions/browser/src/browser/server-context.reset.js").createProfileResetOps;
let createProfileResetOps: typeof import("./server-context.reset.js").createProfileResetOps;
afterEach(() => {
vi.clearAllMocks();
@@ -22,8 +22,7 @@ afterEach(() => {
beforeEach(async () => {
vi.resetModules();
({ createProfileResetOps } =
await import("../../extensions/browser/src/browser/server-context.reset.js"));
({ createProfileResetOps } = await import("./server-context.reset.js"));
});
function localOpenClawProfile(): Parameters<typeof createProfileResetOps>[0]["profile"] {

View File

@@ -1,23 +1,22 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
import { withFetchPreconnect } from "../../test-support.js";
vi.hoisted(() => {
vi.resetModules();
});
import "../../extensions/browser/src/browser/server-context.chrome-test-harness.js";
import * as cdpModule from "../../extensions/browser/src/browser/cdp.js";
import { InvalidBrowserNavigationUrlError } from "../../extensions/browser/src/browser/navigation-guard.js";
import { createBrowserRouteContext } from "../../extensions/browser/src/browser/server-context.js";
import "./server-context.chrome-test-harness.js";
import * as cdpModule from "./cdp.js";
import { InvalidBrowserNavigationUrlError } from "./navigation-guard.js";
import { createBrowserRouteContext } from "./server-context.js";
import {
makeManagedTabsWithNew,
makeState,
originalFetch,
} from "../../extensions/browser/src/browser/server-context.remote-tab-ops.harness.js";
} from "./server-context.remote-tab-ops.harness.js";
afterEach(async () => {
const { closePlaywrightBrowserConnection } =
await import("../../extensions/browser/src/browser/pw-session.js");
const { closePlaywrightBrowserConnection } = await import("./pw-session.js");
await closePlaywrightBrowserConnection().catch(() => {});
globalThis.fetch = originalFetch;
vi.restoreAllMocks();

View File

@@ -9,22 +9,22 @@ const { createBrowserRouteContextMock, listKnownProfileNamesMock } = vi.hoisted(
listKnownProfileNamesMock: vi.fn(),
}));
vi.mock("../../extensions/browser/src/browser/chrome.js", () => ({
vi.mock("./chrome.js", () => ({
stopOpenClawChrome: stopOpenClawChromeMock,
}));
vi.mock("../../extensions/browser/src/browser/server-context.js", () => ({
vi.mock("./server-context.js", () => ({
createBrowserRouteContext: createBrowserRouteContextMock,
listKnownProfileNames: listKnownProfileNamesMock,
}));
let ensureExtensionRelayForProfiles: typeof import("../../extensions/browser/src/browser/server-lifecycle.js").ensureExtensionRelayForProfiles;
let stopKnownBrowserProfiles: typeof import("../../extensions/browser/src/browser/server-lifecycle.js").stopKnownBrowserProfiles;
let ensureExtensionRelayForProfiles: typeof import("./server-lifecycle.js").ensureExtensionRelayForProfiles;
let stopKnownBrowserProfiles: typeof import("./server-lifecycle.js").stopKnownBrowserProfiles;
beforeEach(async () => {
vi.resetModules();
({ ensureExtensionRelayForProfiles, stopKnownBrowserProfiles } =
await import("../../extensions/browser/src/browser/server-lifecycle.js"));
await import("./server-lifecycle.js"));
createBrowserRouteContextMock.mockClear();
listKnownProfileNamesMock.mockClear();
stopOpenClawChromeMock.mockClear();

View File

@@ -2,24 +2,17 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
DEFAULT_DOWNLOAD_DIR,
DEFAULT_TRACE_DIR,
DEFAULT_UPLOAD_DIR,
} from "../../extensions/browser/src/browser/paths.js";
import { DEFAULT_DOWNLOAD_DIR, DEFAULT_TRACE_DIR, DEFAULT_UPLOAD_DIR } from "./paths.js";
import {
installAgentContractHooks,
postJson,
startServerAndBase,
} from "../../extensions/browser/src/browser/server.agent-contract.test-harness.js";
} from "./server.agent-contract.test-harness.js";
import {
getBrowserControlServerTestState,
getPwMocks,
} from "../../extensions/browser/src/browser/server.control-server.test-harness.js";
import {
getBrowserTestFetch,
type BrowserTestFetch,
} from "../../extensions/browser/src/browser/test-fetch.js";
} from "./server.control-server.test-harness.js";
import { getBrowserTestFetch, type BrowserTestFetch } from "./test-fetch.js";
const state = getBrowserControlServerTestState();
const pwMocks = getPwMocks();

View File

@@ -1,16 +1,16 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../extensions/browser/src/browser/constants.js";
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "./constants.js";
import {
installAgentContractHooks,
postJson,
startServerAndBase,
} from "../../extensions/browser/src/browser/server.agent-contract.test-harness.js";
} from "./server.agent-contract.test-harness.js";
import {
getBrowserControlServerTestState,
getCdpMocks,
getPwMocks,
} from "../../extensions/browser/src/browser/server.control-server.test-harness.js";
import { getBrowserTestFetch } from "../../extensions/browser/src/browser/test-fetch.js";
} from "./server.control-server.test-harness.js";
import { getBrowserTestFetch } from "./test-fetch.js";
const state = getBrowserControlServerTestState();
const cdpMocks = getCdpMocks();

View File

@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getFreePort } from "../../extensions/browser/src/browser/test-port.js";
import { getFreePort } from "./test-port.js";
const mocks = vi.hoisted(() => ({
controlPort: 0,
@@ -23,9 +23,8 @@ vi.mock("../config/config.js", async (importOriginal) => {
};
});
vi.mock("../../extensions/browser/src/browser/config.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../extensions/browser/src/browser/config.js")>();
vi.mock("./config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./config.js")>();
return {
...actual,
resolveBrowserConfig: vi.fn(() => ({
@@ -35,30 +34,30 @@ vi.mock("../../extensions/browser/src/browser/config.js", async (importOriginal)
};
});
vi.mock("../../extensions/browser/src/browser/control-auth.js", () => ({
vi.mock("./control-auth.js", () => ({
ensureBrowserControlAuth: mocks.ensureBrowserControlAuth,
resolveBrowserControlAuth: mocks.resolveBrowserControlAuth,
}));
vi.mock("../../extensions/browser/src/browser/routes/index.js", () => ({
vi.mock("./routes/index.js", () => ({
registerBrowserRoutes: vi.fn(() => {}),
}));
vi.mock("../../extensions/browser/src/browser/server-context.js", () => ({
vi.mock("./server-context.js", () => ({
createBrowserRouteContext: vi.fn(() => ({})),
}));
vi.mock("../../extensions/browser/src/browser/server-lifecycle.js", () => ({
vi.mock("./server-lifecycle.js", () => ({
ensureExtensionRelayForProfiles: mocks.ensureExtensionRelayForProfiles,
stopKnownBrowserProfiles: vi.fn(async () => {}),
}));
vi.mock("../../extensions/browser/src/browser/pw-ai-state.js", () => ({
vi.mock("./pw-ai-state.js", () => ({
isPwAiLoaded: vi.fn(() => false),
}));
let startBrowserControlServerFromConfig: typeof import("../../extensions/browser/src/browser/server.js").startBrowserControlServerFromConfig;
let stopBrowserControlServer: typeof import("../../extensions/browser/src/browser/server.js").stopBrowserControlServer;
let startBrowserControlServerFromConfig: typeof import("./server.js").startBrowserControlServerFromConfig;
let stopBrowserControlServer: typeof import("./server.js").stopBrowserControlServer;
describe("browser control auth bootstrap failures", () => {
beforeEach(async () => {
@@ -68,7 +67,7 @@ describe("browser control auth bootstrap failures", () => {
mocks.ensureExtensionRelayForProfiles.mockClear();
vi.resetModules();
({ startBrowserControlServerFromConfig, stopBrowserControlServer } =
await import("../../extensions/browser/src/browser/server.js"));
await import("./server.js"));
});
afterEach(async () => {

View File

@@ -1,10 +1,7 @@
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { isAuthorizedBrowserRequest } from "../../extensions/browser/src/browser/http-auth.js";
import {
getBrowserTestFetch,
type BrowserTestFetch,
} from "../../extensions/browser/src/browser/test-fetch.js";
import { isAuthorizedBrowserRequest } from "./http-auth.js";
import { getBrowserTestFetch, type BrowserTestFetch } from "./test-fetch.js";
let server: ReturnType<typeof createServer> | null = null;
let port = 0;

View File

@@ -1,6 +1,6 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { getBrowserTestFetch } from "../../extensions/browser/src/browser/test-fetch.js";
import { getFreePort } from "../../extensions/browser/src/browser/test-port.js";
import { getBrowserTestFetch } from "./test-fetch.js";
import { getFreePort } from "./test-port.js";
let testPort = 0;
let prevGatewayPort: string | undefined;
@@ -53,27 +53,26 @@ vi.mock("../config/config.js", async (importOriginal) => {
};
});
vi.mock("../../extensions/browser/src/browser/pw-ai-module.js", () => ({
vi.mock("./pw-ai-module.js", () => ({
getPwAiModule: vi.fn(async () => pwMocks),
}));
vi.mock("../../extensions/browser/src/browser/server-context.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../extensions/browser/src/browser/server-context.js")>();
vi.mock("./server-context.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./server-context.js")>();
return {
...actual,
createBrowserRouteContext: routeCtxMocks.createBrowserRouteContext,
};
});
let startBrowserControlServerFromConfig: typeof import("../../extensions/browser/src/browser/server.js").startBrowserControlServerFromConfig;
let stopBrowserControlServer: typeof import("../../extensions/browser/src/browser/server.js").stopBrowserControlServer;
let startBrowserControlServerFromConfig: typeof import("./server.js").startBrowserControlServerFromConfig;
let stopBrowserControlServer: typeof import("./server.js").stopBrowserControlServer;
describe("browser control evaluate gating", () => {
beforeAll(async () => {
vi.resetModules();
({ startBrowserControlServerFromConfig, stopBrowserControlServer } =
await import("../../extensions/browser/src/browser/server.js"));
await import("./server.js"));
});
beforeEach(async () => {

View File

@@ -8,8 +8,8 @@ import {
resetBrowserControlServerTestContext,
setBrowserControlServerReachable,
startBrowserControlServerFromConfig,
} from "../../extensions/browser/src/browser/server.control-server.test-harness.js";
import { getBrowserTestFetch } from "../../extensions/browser/src/browser/test-fetch.js";
} from "./server.control-server.test-harness.js";
import { getBrowserTestFetch } from "./test-fetch.js";
describe("browser control server", () => {
installBrowserControlServerHooks();

View File

@@ -5,7 +5,7 @@ import {
closeTrackedBrowserTabsForSessions,
trackSessionBrowserTab,
untrackSessionBrowserTab,
} from "../../extensions/browser/src/browser/session-tab-registry.js";
} from "./session-tab-registry.js";
describe("session tab registry", () => {
beforeEach(() => {

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { matchBrowserUrlPattern } from "../../extensions/browser/src/browser/url-pattern.js";
import { matchBrowserUrlPattern } from "./url-pattern.js";
describe("browser url pattern matching", () => {
it("matches exact URLs", () => {

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { readFields } from "../../../extensions/browser/src/cli/browser-cli-actions-input/shared.js";
import { readFields } from "./shared.js";
describe("readFields", () => {
it.each([

View File

@@ -1,6 +1,6 @@
import { Command } from "commander";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
import { createCliRuntimeCapture } from "../../test-support.js";
const { defaultRuntime: runtime, resetRuntimeCapture } = createCliRuntimeCapture();
@@ -14,7 +14,7 @@ const gatewayMocks = vi.hoisted(() => ({
})),
}));
vi.mock("./gateway-rpc.js", () => ({
vi.mock("../../../../src/cli/gateway-rpc.js", () => ({
callGatewayFromCli: gatewayMocks.callGatewayFromCli,
}));
@@ -46,17 +46,17 @@ const sharedMocks = vi.hoisted(() => ({
},
),
}));
vi.mock("../../extensions/browser/src/cli/browser-cli-shared.js", () => ({
vi.mock("./browser-cli-shared.js", () => ({
callBrowserRequest: sharedMocks.callBrowserRequest,
}));
vi.mock("../../extensions/browser/src/core-api.js", async () => ({
...(await vi.importActual<object>("../../extensions/browser/src/core-api.js")),
vi.mock("../core-api.js", async () => ({
...(await vi.importActual<object>("../core-api.js")),
defaultRuntime: runtime,
loadConfig: configMocks.loadConfig,
}));
let registerBrowserInspectCommands: typeof import("../../extensions/browser/src/cli/browser-cli-inspect.js").registerBrowserInspectCommands;
let registerBrowserInspectCommands: typeof import("./browser-cli-inspect.js").registerBrowserInspectCommands;
type SnapshotDefaultsCase = {
label: string;
@@ -80,8 +80,7 @@ describe("browser cli snapshot defaults", () => {
const runSnapshot = async (args: string[]) => await runBrowserInspect(["snapshot", ...args]);
beforeAll(async () => {
({ registerBrowserInspectCommands } =
await import("../../extensions/browser/src/cli/browser-cli-inspect.js"));
({ registerBrowserInspectCommands } = await import("./browser-cli-inspect.js"));
});
afterEach(() => {

View File

@@ -1,6 +1,6 @@
import { vi } from "vitest";
import { registerBrowserManageCommands } from "../../extensions/browser/src/cli/browser-cli-manage.js";
import { createBrowserProgram } from "./browser-cli-test-helpers.js";
import { registerBrowserManageCommands } from "./browser-cli-manage.js";
import { createBrowserProgram } from "./browser-cli.test-support.js";
type BrowserRequest = { path?: string };
type BrowserRuntimeOptions = { timeoutMs?: number };
@@ -31,14 +31,14 @@ const browserManageMocks = vi.hoisted(() => ({
),
}));
vi.mock("../../extensions/browser/src/cli/browser-cli-shared.js", () => ({
vi.mock("./browser-cli-shared.js", () => ({
callBrowserRequest: browserManageMocks.callBrowserRequest,
}));
vi.mock("../../extensions/browser/src/core-api.js", async () => ({
...(await vi.importActual<object>("../../extensions/browser/src/core-api.js")),
...(await (await import("./browser-cli-test-helpers.js")).createBrowserCliRuntimeMockModule()),
...(await (await import("./browser-cli-test-helpers.js")).createBrowserCliUtilsMockModule()),
vi.mock("../core-api.js", async () => ({
...(await vi.importActual<object>("../core-api.js")),
...(await (await import("./browser-cli.test-support.js")).createBrowserCliRuntimeMockModule()),
...(await (await import("./browser-cli.test-support.js")).createBrowserCliUtilsMockModule()),
}));
export function createBrowserManageProgram(params?: { withParentTimeout?: boolean }) {

View File

@@ -3,7 +3,7 @@ import {
createBrowserManageProgram,
getBrowserManageCallBrowserRequestMock,
} from "./browser-cli-manage.test-helpers.js";
import { getBrowserCliRuntime, getBrowserCliRuntimeCapture } from "./browser-cli-test-helpers.js";
import { getBrowserCliRuntime, getBrowserCliRuntimeCapture } from "./browser-cli.test-support.js";
describe("browser manage output", () => {
beforeEach(() => {

View File

@@ -4,7 +4,7 @@ import {
findBrowserManageCall,
getBrowserManageCallBrowserRequestMock,
} from "./browser-cli-manage.test-helpers.js";
import { getBrowserCliRuntimeCapture } from "./browser-cli-test-helpers.js";
import { getBrowserCliRuntimeCapture } from "./browser-cli.test-support.js";
describe("browser manage start timeout option", () => {
beforeEach(() => {

View File

@@ -1,28 +1,28 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerBrowserStateCommands } from "../../extensions/browser/src/cli/browser-cli-state.js";
import { registerBrowserStateCommands } from "./browser-cli-state.js";
import {
createBrowserProgram as createBrowserProgramShared,
getBrowserCliRuntime,
getBrowserCliRuntimeCapture,
} from "./browser-cli-test-helpers.js";
} from "./browser-cli.test-support.js";
const mocks = vi.hoisted(() => ({
callBrowserRequest: vi.fn(async (..._args: unknown[]) => ({ ok: true })),
runBrowserResizeWithOutput: vi.fn(async (_params: unknown) => {}),
}));
vi.mock("../../extensions/browser/src/cli/browser-cli-shared.js", () => ({
vi.mock("./browser-cli-shared.js", () => ({
callBrowserRequest: mocks.callBrowserRequest,
}));
vi.mock("../../extensions/browser/src/cli/browser-cli-resize.js", () => ({
vi.mock("./browser-cli-resize.js", () => ({
runBrowserResizeWithOutput: mocks.runBrowserResizeWithOutput,
}));
vi.mock("../../extensions/browser/src/core-api.js", async () => ({
...(await vi.importActual<object>("../../extensions/browser/src/core-api.js")),
...(await (await import("./browser-cli-test-helpers.js")).createBrowserCliRuntimeMockModule()),
...(await (await import("./browser-cli-test-helpers.js")).createBrowserCliUtilsMockModule()),
vi.mock("../core-api.js", async () => ({
...(await vi.importActual<object>("../core-api.js")),
...(await (await import("./browser-cli.test-support.js")).createBrowserCliRuntimeMockModule()),
...(await (await import("./browser-cli.test-support.js")).createBrowserCliUtilsMockModule()),
}));
describe("browser state option collisions", () => {

View File

@@ -1,7 +1,7 @@
import { Command } from "commander";
import type { GatewayRpcOpts } from "./gateway-rpc.js";
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
import type { CliRuntimeCapture } from "./test-runtime-capture.js";
import type { GatewayRpcOpts } from "../../../../src/cli/gateway-rpc.js";
import { createCliRuntimeCapture } from "../../test-support.js";
import type { CliRuntimeCapture } from "../../test-support.js";
type BrowserParentOpts = GatewayRpcOpts & {
json?: boolean;

View File

@@ -8,16 +8,16 @@ const { loadConfigMock, isNodeCommandAllowedMock, resolveNodeCommandAllowlistMoc
}),
);
vi.mock("../../config/config.js", () => ({
vi.mock("../../../../src/config/config.js", () => ({
loadConfig: loadConfigMock,
}));
vi.mock("../node-command-policy.js", () => ({
vi.mock("../../../../src/gateway/node-command-policy.js", () => ({
isNodeCommandAllowed: isNodeCommandAllowedMock,
resolveNodeCommandAllowlist: resolveNodeCommandAllowlistMock,
}));
import { browserHandlers } from "../../plugin-sdk/browser.js";
import { browserHandlers } from "./browser-request.js";
type RespondCall = [boolean, unknown?, { code: number; message: string }?];

View File

@@ -26,8 +26,8 @@ const browserConfigMocks = vi.hoisted(() => ({
})),
}));
vi.mock("../../extensions/browser/src/core-api.js", async () => ({
...(await vi.importActual<object>("../../extensions/browser/src/core-api.js")),
vi.mock("../core-api.js", async () => ({
...(await vi.importActual<object>("../core-api.js")),
createBrowserControlContext: controlServiceMocks.createBrowserControlContext,
createBrowserRouteDispatcher: dispatcherMocks.createBrowserRouteDispatcher,
detectMime: vi.fn(async () => "image/png"),
@@ -36,7 +36,7 @@ vi.mock("../../extensions/browser/src/core-api.js", async () => ({
startBrowserControlServiceFromConfig: controlServiceMocks.startBrowserControlServiceFromConfig,
}));
let runBrowserProxyCommand: typeof import("../../extensions/browser/src/node-host/invoke-browser.js").runBrowserProxyCommand;
let runBrowserProxyCommand: typeof import("./invoke-browser.js").runBrowserProxyCommand;
describe("runBrowserProxyCommand", () => {
beforeEach(async () => {
@@ -58,8 +58,7 @@ describe("runBrowserProxyCommand", () => {
enabled: true,
defaultProfile: "openclaw",
});
({ runBrowserProxyCommand } =
await import("../../extensions/browser/src/node-host/invoke-browser.js"));
({ runBrowserProxyCommand } = await import("./invoke-browser.js"));
configMocks.loadConfig.mockReturnValue({
browser: {},
nodeHost: { browserProxy: { enabled: true, allowProfiles: [] as string[] } },

View File

@@ -0,0 +1,11 @@
export { isLiveTestEnabled } from "../../src/agents/live-test-helpers.js";
export {
createCliRuntimeCapture,
type CliMockOutputRuntime,
type CliRuntimeCapture,
} from "../../src/cli/test-runtime-capture.js";
export type { OpenClawConfig } from "openclaw/plugin-sdk/browser-support";
export { expectGeneratedTokenPersistedToGatewayAuth } from "../../test/helpers/extensions/auth-token-assertions.ts";
export { withEnv, withEnvAsync } from "../../test/helpers/extensions/env.ts";
export { withFetchPreconnect, type FetchMock } from "../../test/helpers/extensions/fetch-mock.ts";
export { createTempHomeEnv, type TempHomeEnv } from "../../test/helpers/extensions/temp-home.ts";

View File

@@ -6,7 +6,6 @@ import type {
PluginCommandContext,
} from "openclaw/plugin-sdk/core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { executePluginCommand } from "../../src/plugins/commands.js";
import { createTestPluginApi } from "../../test/helpers/extensions/plugin-api.js";
import type { OpenClawPluginApi } from "./api.js";
import type { PendingPairingRequest } from "./notify.ts";
@@ -121,12 +120,9 @@ function createChannelRuntime(
}
function createCommandContext(params?: Partial<PluginCommandContext>): PluginCommandContext {
const surface = params?.surface ?? params?.channel ?? "webchat";
return {
surface,
channel: surface,
channel: "webchat",
isAuthorizedSender: true,
senderIsOwner: false,
commandBody: "/pair qr",
args: "qr",
config: {},
@@ -450,20 +446,14 @@ describe("device-pair /pair approve", () => {
});
const command = registerPairCommand();
const result = await executePluginCommand({
command: {
...command,
pluginId: "device-pair",
},
senderId: "writer-1",
surface: "webchat",
channel: "webchat",
isAuthorizedSender: true,
gatewayClientScopes: ["operator.write"],
args: "approve latest",
commandBody: "/pair approve latest",
config: {} as never,
});
const result = await command.handler(
createCommandContext({
channel: "webchat",
args: "approve latest",
commandBody: "/pair approve latest",
gatewayClientScopes: ["operator.write"],
}),
);
expect(vi.mocked(approveDevicePairing)).not.toHaveBeenCalled();
expect(result).toEqual({
@@ -511,20 +501,14 @@ describe("device-pair /pair approve", () => {
});
const command = registerPairCommand();
const result = await executePluginCommand({
command: {
...command,
pluginId: "device-pair",
},
senderId: "pairing-1",
surface: "webchat",
channel: "webchat",
isAuthorizedSender: true,
gatewayClientScopes: ["operator.write", "operator.pairing"],
args: "approve latest",
commandBody: "/pair approve latest",
config: {} as never,
});
const result = await command.handler(
createCommandContext({
channel: "webchat",
args: "approve latest",
commandBody: "/pair approve latest",
gatewayClientScopes: ["operator.write", "operator.pairing"],
}),
);
expect(vi.mocked(approveDevicePairing)).toHaveBeenCalledWith("req-1");
expect(result).toEqual({ text: "✅ Paired Victim Phone (ios)." });

View File

@@ -546,14 +546,13 @@ export default definePluginEntry({
name: "pair",
description: "Generate setup codes and approve device pairing requests.",
acceptsArgs: true,
resolveRequiredGatewayScopes: (ctx) => {
const action = ctx.args?.trim().split(/\s+/, 1)[0]?.toLowerCase();
return action === "approve" ? ["operator.pairing"] : undefined;
},
handler: async (ctx) => {
const args = ctx.args?.trim() ?? "";
const tokens = args.split(/\s+/).filter(Boolean);
const action = tokens[0]?.toLowerCase() ?? "";
const gatewayClientScopes = Array.isArray(ctx.gatewayClientScopes)
? ctx.gatewayClientScopes
: null;
api.logger.info?.(
`device-pair: /pair invoked channel=${ctx.channel} sender=${ctx.senderId ?? "unknown"} action=${
action || "new"
@@ -575,6 +574,15 @@ export default definePluginEntry({
}
if (action === "approve") {
if (
gatewayClientScopes &&
!gatewayClientScopes.includes("operator.pairing") &&
!gatewayClientScopes.includes("operator.admin")
) {
return {
text: "⚠️ This command requires operator.pairing for internal gateway callers.",
};
}
const requested = tokens[1]?.trim();
const list = await listDevicePairing();
if (list.pending.length === 0) {

View File

@@ -1,6 +1,6 @@
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import {
listResolvedDirectoryEntriesFromSources,
createResolvedDirectoryEntriesLister,
type DirectoryConfigParams,
} from "openclaw/plugin-sdk/directory-runtime";
import { mergeDiscordAccountConfig, resolveDefaultDiscordAccountId } from "./accounts.js";
@@ -18,38 +18,36 @@ function resolveDiscordDirectoryConfigAccount(
};
}
export async function listDiscordDirectoryPeersFromConfig(params: DirectoryConfigParams) {
return listResolvedDirectoryEntriesFromSources({
...params,
kind: "user",
resolveAccount: (cfg, accountId) => resolveDiscordDirectoryConfigAccount(cfg, accountId),
resolveSources: (account) => {
const allowFrom = account.config.allowFrom ?? account.config.dm?.allowFrom ?? [];
const guildUsers = Object.values(account.config.guilds ?? {}).flatMap((guild) => [
...(guild.users ?? []),
...Object.values(guild.channels ?? {}).flatMap((channel) => channel.users ?? []),
]);
return [allowFrom, Object.keys(account.config.dms ?? {}), guildUsers];
},
normalizeId: (raw) => {
const mention = raw.match(/^<@!?(\d+)>$/);
const cleaned = (mention?.[1] ?? raw).replace(/^(discord|user):/i, "").trim();
return /^\d+$/.test(cleaned) ? `user:${cleaned}` : null;
},
});
}
export const listDiscordDirectoryPeersFromConfig = createResolvedDirectoryEntriesLister<
ReturnType<typeof resolveDiscordDirectoryConfigAccount>
>({
kind: "user",
resolveAccount: (cfg, accountId) => resolveDiscordDirectoryConfigAccount(cfg, accountId),
resolveSources: (account) => {
const allowFrom = account.config.allowFrom ?? account.config.dm?.allowFrom ?? [];
const guildUsers = Object.values(account.config.guilds ?? {}).flatMap((guild) => [
...(guild.users ?? []),
...Object.values(guild.channels ?? {}).flatMap((channel) => channel.users ?? []),
]);
return [allowFrom, Object.keys(account.config.dms ?? {}), guildUsers];
},
normalizeId: (raw) => {
const mention = raw.match(/^<@!?(\d+)>$/);
const cleaned = (mention?.[1] ?? raw).replace(/^(discord|user):/i, "").trim();
return /^\d+$/.test(cleaned) ? `user:${cleaned}` : null;
},
});
export async function listDiscordDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
return listResolvedDirectoryEntriesFromSources({
...params,
kind: "group",
resolveAccount: (cfg, accountId) => resolveDiscordDirectoryConfigAccount(cfg, accountId),
resolveSources: (account) =>
Object.values(account.config.guilds ?? {}).map((guild) => Object.keys(guild.channels ?? {})),
normalizeId: (raw) => {
const mention = raw.match(/^<#(\d+)>$/);
const cleaned = (mention?.[1] ?? raw).replace(/^(discord|channel|group):/i, "").trim();
return /^\d+$/.test(cleaned) ? `channel:${cleaned}` : null;
},
});
}
export const listDiscordDirectoryGroupsFromConfig = createResolvedDirectoryEntriesLister<
ReturnType<typeof resolveDiscordDirectoryConfigAccount>
>({
kind: "group",
resolveAccount: (cfg, accountId) => resolveDiscordDirectoryConfigAccount(cfg, accountId),
resolveSources: (account) =>
Object.values(account.config.guilds ?? {}).map((guild) => Object.keys(guild.channels ?? {})),
normalizeId: (raw) => {
const mention = raw.match(/^<#(\d+)>$/);
const cleaned = (mention?.[1] ?? raw).replace(/^(discord|channel|group):/i, "").trim();
return /^\d+$/.test(cleaned) ? `channel:${cleaned}` : null;
},
});

View File

@@ -40,9 +40,7 @@ describe("createDiscordGatewaySupervisor", () => {
error: vi.fn(),
};
const supervisor = createDiscordGatewaySupervisor({
client: {
getPlugin: vi.fn(() => ({ emitter })),
} as never,
gateway: { emitter },
isDisallowedIntentsError: (err) => String(err).includes("4014"),
runtime: runtime as never,
});
@@ -72,9 +70,7 @@ describe("createDiscordGatewaySupervisor", () => {
it("is idempotent on dispose and noops without an emitter", () => {
const supervisor = createDiscordGatewaySupervisor({
client: {
getPlugin: vi.fn(() => undefined),
} as never,
gateway: undefined,
isDisallowedIntentsError: () => false,
runtime: { error: vi.fn() } as never,
});
@@ -90,9 +86,7 @@ describe("createDiscordGatewaySupervisor", () => {
const emitter = new EventEmitter();
const runtime = { error: vi.fn() };
const supervisor = createDiscordGatewaySupervisor({
client: {
getPlugin: vi.fn(() => ({ emitter })),
} as never,
gateway: { emitter },
isDisallowedIntentsError: () => false,
runtime: runtime as never,
});

View File

@@ -1,5 +1,4 @@
import type { EventEmitter } from "node:events";
import type { Client } from "@buape/carbon";
import { danger } from "openclaw/plugin-sdk/runtime-env";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { getDiscordGatewayEmitter } from "../monitor.gateway.js";
@@ -67,12 +66,11 @@ export function classifyDiscordGatewayEvent(params: {
}
export function createDiscordGatewaySupervisor(params: {
client: Client;
gateway?: unknown;
isDisallowedIntentsError: (err: unknown) => boolean;
runtime: RuntimeEnv;
}): DiscordGatewaySupervisor {
const gateway = params.client.getPlugin("gateway");
const emitter = getDiscordGatewayEmitter(gateway);
const emitter = getDiscordGatewayEmitter(params.gateway);
const pending: DiscordGatewayEvent[] = [];
if (!emitter) {
return {
@@ -86,39 +84,36 @@ export function createDiscordGatewaySupervisor(params: {
let lifecycleHandler: ((event: DiscordGatewayEvent) => void) | undefined;
let phase: GatewaySupervisorPhase = "buffering";
let disposed = false;
const logLateTeardownEvent = (event: DiscordGatewayEvent) => {
params.runtime.error?.(
danger(
`discord: suppressed late gateway ${event.type} error during teardown: ${event.message}`,
),
);
};
const logLateDisposedEvent = (event: DiscordGatewayEvent) => {
params.runtime.error?.(
danger(
`discord: suppressed late gateway ${event.type} error after dispose: ${event.message}`,
),
);
};
const logLateEvent =
(state: Extract<GatewaySupervisorPhase, "disposed" | "teardown">) =>
(event: DiscordGatewayEvent) => {
params.runtime.error?.(
danger(
`discord: suppressed late gateway ${event.type} error ${
state === "disposed" ? "after dispose" : "during teardown"
}: ${event.message}`,
),
);
};
const onGatewayError = (err: unknown) => {
const event = classifyDiscordGatewayEvent({
err,
isDisallowedIntentsError: params.isDisallowedIntentsError,
});
if (phase === "disposed") {
logLateDisposedEvent(event);
return;
switch (phase) {
case "disposed":
logLateEvent("disposed")(event);
return;
case "active":
lifecycleHandler?.(event);
return;
case "teardown":
logLateEvent("teardown")(event);
return;
case "buffering":
pending.push(event);
return;
}
if (phase === "active" && lifecycleHandler) {
lifecycleHandler(event);
return;
}
if (phase === "teardown") {
logLateTeardownEvent(event);
return;
}
pending.push(event);
};
emitter.on("error", onGatewayError);
@@ -146,10 +141,9 @@ export function createDiscordGatewaySupervisor(params: {
return "continue";
},
dispose: () => {
if (disposed) {
if (phase === "disposed") {
return;
}
disposed = true;
lifecycleHandler = undefined;
phase = "disposed";
pending.length = 0;

View File

@@ -1,5 +1,4 @@
import { EventEmitter } from "node:events";
import type { Client } from "@buape/carbon";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../../../../src/runtime.js";
import type { WaitForDiscordGatewayStopParams } from "../monitor.gateway.js";
@@ -43,6 +42,7 @@ vi.mock("./gateway-registry.js", () => ({
describe("runDiscordGatewayLifecycle", () => {
beforeEach(() => {
vi.resetModules();
attachDiscordGatewayLoggingMock.mockClear();
getDiscordGatewayEmitterMock.mockClear();
waitForDiscordGatewayStopMock.mockClear();
@@ -72,6 +72,13 @@ describe("runDiscordGatewayLifecycle", () => {
ws?: EventEmitter & { terminate?: () => void };
};
}) => {
const gateway =
params?.gateway ??
(() => {
const defaultGateway = createGatewayHarness().gateway;
defaultGateway.isConnected = true;
return defaultGateway;
})();
const start = vi.fn(params?.start ?? (async () => undefined));
const stop = vi.fn(params?.stop ?? (async () => undefined));
const threadStop = vi.fn();
@@ -96,7 +103,7 @@ describe("runDiscordGatewayLifecycle", () => {
return "continue";
}),
dispose: vi.fn(),
emitter: params?.gateway?.emitter,
emitter: gateway.emitter,
};
const statusSink = vi.fn();
const runtime: RuntimeEnv = {
@@ -114,9 +121,7 @@ describe("runDiscordGatewayLifecycle", () => {
statusSink,
lifecycleParams: {
accountId: params?.accountId ?? "default",
client: {
getPlugin: vi.fn((name: string) => (name === "gateway" ? params?.gateway : undefined)),
} as unknown as Client,
gateway,
runtime,
isDisallowedIntentsError: params?.isDisallowedIntentsError ?? (() => false),
voiceManager: null,

View File

@@ -1,4 +1,3 @@
import type { Client } from "@buape/carbon";
import type { GatewayPlugin } from "@buape/carbon/gateway";
import { createArmableStallWatchdog } from "openclaw/plugin-sdk/channel-lifecycle";
import { createConnectedChannelStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
@@ -70,7 +69,7 @@ async function waitForDiscordGatewayReady(params: {
export async function runDiscordGatewayLifecycle(params: {
accountId: string;
client: Client;
gateway?: GatewayPlugin;
runtime: RuntimeEnv;
abortSignal?: AbortSignal;
isDisallowedIntentsError: (err: unknown) => boolean;
@@ -85,7 +84,7 @@ export async function runDiscordGatewayLifecycle(params: {
const HELLO_CONNECTED_POLL_MS = 250;
const MAX_CONSECUTIVE_HELLO_STALLS = 3;
const RECONNECT_STALL_TIMEOUT_MS = 5 * 60_000;
const gateway = params.client.getPlugin<GatewayPlugin>("gateway");
const gateway = params.gateway;
if (gateway) {
registerGateway(params.accountId, gateway);
}

View File

@@ -947,15 +947,15 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
},
clientPlugins,
);
lifecycleGateway = client.getPlugin<GatewayPlugin>("gateway");
gatewaySupervisor = (
createDiscordGatewaySupervisorForTesting ?? createDiscordGatewaySupervisor
)({
client,
gateway: lifecycleGateway,
isDisallowedIntentsError: isDiscordDisallowedIntentsError,
runtime,
});
lifecycleGateway = client.getPlugin<GatewayPlugin>("gateway");
earlyGatewayEmitter = gatewaySupervisor.emitter;
onEarlyGatewayDebug = (msg: unknown) => {
if (!(isVerboseForTesting ?? isVerbose)()) {
@@ -1164,7 +1164,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
onEarlyGatewayDebug = undefined;
await (runDiscordGatewayLifecycleForTesting ?? runDiscordGatewayLifecycle)({
accountId: account.accountId,
client,
gateway: lifecycleGateway,
runtime,
abortSignal: opts.abortSignal,
statusSink: opts.setStatus,

View File

@@ -3,17 +3,16 @@ import {
createDelegatedSetupWizardProxy,
createDelegatedTextInputShouldPrompt,
createPatchedAccountSetupAdapter,
createTopLevelChannelDmPolicy,
parseSetupEntriesAllowingWildcard,
promptParsedAllowFromForAccount,
setAccountAllowFromForChannel,
setChannelDmPolicyWithAllowFrom,
setSetupChannelEnabled,
type OpenClawConfig,
type WizardPrompter,
} from "openclaw/plugin-sdk/setup";
import type {
ChannelSetupAdapter,
ChannelSetupDmPolicy,
ChannelSetupWizard,
ChannelSetupWizardTextInput,
} from "openclaw/plugin-sdk/setup";
@@ -106,20 +105,14 @@ export async function promptIMessageAllowFrom(params: {
});
}
export const imessageDmPolicy: ChannelSetupDmPolicy = {
export const imessageDmPolicy = createTopLevelChannelDmPolicy({
label: "iMessage",
channel,
policyKey: "channels.imessage.dmPolicy",
allowFromKey: "channels.imessage.allowFrom",
getCurrent: (cfg: OpenClawConfig) => cfg.channels?.imessage?.dmPolicy ?? "pairing",
setPolicy: (cfg: OpenClawConfig, policy) =>
setChannelDmPolicyWithAllowFrom({
cfg,
channel,
dmPolicy: policy,
}),
promptAllowFrom: promptIMessageAllowFrom,
};
});
function resolveIMessageCliPath(params: { cfg: OpenClawConfig; accountId: string }) {
return resolveIMessageAccount(params).config.cliPath ?? "imsg";

View File

@@ -13,7 +13,7 @@ import {
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
import {
createChannelDirectoryAdapter,
listResolvedDirectoryEntriesFromSources,
createResolvedDirectoryEntriesLister,
} from "openclaw/plugin-sdk/directory-runtime";
import { runStoppablePassiveMonitor } from "openclaw/plugin-sdk/extension-shared";
import {
@@ -61,6 +61,30 @@ function normalizePairingTarget(raw: string): string {
return normalized.split(/[!@]/, 1)[0]?.trim() ?? "";
}
const listIrcDirectoryPeersFromConfig = createResolvedDirectoryEntriesLister<ResolvedIrcAccount>({
kind: "user",
resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount),
resolveSources: (account) => [
account.config.allowFrom ?? [],
account.config.groupAllowFrom ?? [],
...Object.values(account.config.groups ?? {}).map((group) => group.allowFrom ?? []),
],
normalizeId: (entry) => normalizePairingTarget(entry) || null,
});
const listIrcDirectoryGroupsFromConfig = createResolvedDirectoryEntriesLister<ResolvedIrcAccount>({
kind: "group",
resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount),
resolveSources: (account) => [
account.config.channels ?? [],
Object.keys(account.config.groups ?? {}),
],
normalizeId: (entry) => {
const normalized = normalizeIrcMessagingTarget(entry);
return normalized && isChannelTarget(normalized) ? normalized : null;
},
});
const ircConfigAdapter = createScopedChannelConfigAdapter<
ResolvedIrcAccount,
ResolvedIrcAccount,
@@ -227,32 +251,9 @@ export const ircPlugin: ChannelPlugin<ResolvedIrcAccount, IrcProbe> = createChat
},
},
directory: createChannelDirectoryAdapter({
listPeers: async (params) =>
listResolvedDirectoryEntriesFromSources<ResolvedIrcAccount>({
...params,
kind: "user",
resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount),
resolveSources: (account) => [
account.config.allowFrom ?? [],
account.config.groupAllowFrom ?? [],
...Object.values(account.config.groups ?? {}).map((group) => group.allowFrom ?? []),
],
normalizeId: (entry) => normalizePairingTarget(entry) || null,
}),
listPeers: async (params) => listIrcDirectoryPeersFromConfig(params),
listGroups: async (params) => {
const entries = listResolvedDirectoryEntriesFromSources<ResolvedIrcAccount>({
...params,
kind: "group",
resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount),
resolveSources: (account) => [
account.config.channels ?? [],
Object.keys(account.config.groups ?? {}),
],
normalizeId: (entry) => {
const normalized = normalizeIrcMessagingTarget(entry);
return normalized && isChannelTarget(normalized) ? normalized : null;
},
});
const entries = await listIrcDirectoryGroupsFromConfig(params);
return entries.map((entry) => ({ ...entry, name: entry.id }));
},
}),

View File

@@ -16,8 +16,8 @@ import { createScopedAccountReplyToModeResolver } from "openclaw/plugin-sdk/conv
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
import {
createChannelDirectoryAdapter,
createResolvedDirectoryEntriesLister,
createRuntimeDirectoryLiveAdapter,
listResolvedDirectoryEntriesFromSources,
} from "openclaw/plugin-sdk/directory-runtime";
import { buildTrafficStatusSummary } from "openclaw/plugin-sdk/extension-shared";
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
@@ -78,6 +78,46 @@ const meta = {
quickstartAllowFrom: true,
};
const listMatrixDirectoryPeersFromConfig =
createResolvedDirectoryEntriesLister<ResolvedMatrixAccount>({
kind: "user",
resolveAccount: adaptScopedAccountAccessor(resolveMatrixAccount),
resolveSources: (account) => [
account.config.dm?.allowFrom ?? [],
account.config.groupAllowFrom ?? [],
...Object.values(account.config.groups ?? account.config.rooms ?? {}).map(
(room) => room.users ?? [],
),
],
normalizeId: (entry) => {
const raw = entry.replace(/^matrix:/i, "").trim();
if (!raw || raw === "*") {
return null;
}
const lowered = raw.toLowerCase();
const cleaned = lowered.startsWith("user:") ? raw.slice("user:".length).trim() : raw;
return cleaned.startsWith("@") ? `user:${cleaned}` : cleaned;
},
});
const listMatrixDirectoryGroupsFromConfig =
createResolvedDirectoryEntriesLister<ResolvedMatrixAccount>({
kind: "group",
resolveAccount: adaptScopedAccountAccessor(resolveMatrixAccount),
resolveSources: (account) => [Object.keys(account.config.groups ?? account.config.rooms ?? {})],
normalizeId: (entry) => {
const raw = entry.replace(/^matrix:/i, "").trim();
if (!raw || raw === "*") {
return null;
}
const lowered = raw.toLowerCase();
if (lowered.startsWith("room:") || lowered.startsWith("channel:")) {
return raw;
}
return raw.startsWith("!") ? `room:${raw}` : raw;
},
});
const matrixConfigAdapter = createScopedChannelConfigAdapter<
ResolvedMatrixAccount,
ReturnType<typeof resolveMatrixAccountConfig>,
@@ -246,53 +286,14 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount, MatrixProbe> =
},
directory: createChannelDirectoryAdapter({
listPeers: async (params) => {
const entries = listResolvedDirectoryEntriesFromSources<ResolvedMatrixAccount>({
...params,
kind: "user",
resolveAccount: adaptScopedAccountAccessor(resolveMatrixAccount),
resolveSources: (account) => [
account.config.dm?.allowFrom ?? [],
account.config.groupAllowFrom ?? [],
...Object.values(account.config.groups ?? account.config.rooms ?? {}).map(
(room) => room.users ?? [],
),
],
normalizeId: (entry) => {
const raw = entry.replace(/^matrix:/i, "").trim();
if (!raw || raw === "*") {
return null;
}
const lowered = raw.toLowerCase();
const cleaned = lowered.startsWith("user:") ? raw.slice("user:".length).trim() : raw;
return cleaned.startsWith("@") ? `user:${cleaned}` : cleaned;
},
});
const entries = await listMatrixDirectoryPeersFromConfig(params);
return entries.map((entry) => {
const raw = entry.id.startsWith("user:") ? entry.id.slice("user:".length) : entry.id;
const incomplete = !raw.startsWith("@") || !raw.includes(":");
return incomplete ? { ...entry, name: "incomplete id; expected @user:server" } : entry;
});
},
listGroups: async (params) =>
listResolvedDirectoryEntriesFromSources<ResolvedMatrixAccount>({
...params,
kind: "group",
resolveAccount: adaptScopedAccountAccessor(resolveMatrixAccount),
resolveSources: (account) => [
Object.keys(account.config.groups ?? account.config.rooms ?? {}),
],
normalizeId: (entry) => {
const raw = entry.replace(/^matrix:/i, "").trim();
if (!raw || raw === "*") {
return null;
}
const lowered = raw.toLowerCase();
if (lowered.startsWith("room:") || lowered.startsWith("channel:")) {
return raw;
}
return raw.startsWith("!") ? `room:${raw}` : raw;
},
}),
listGroups: async (params) => await listMatrixDirectoryGroupsFromConfig(params),
...createRuntimeDirectoryLiveAdapter({
getRuntime: loadMatrixChannelRuntime,
listPeersLive: (runtime) => runtime.listMatrixDirectoryPeersLive,

View File

@@ -28,6 +28,7 @@ vi.mock("../../../../../src/infra/backup-create.js", async (importOriginal) => {
});
let maybeMigrateLegacyStorage: typeof import("./storage.js").maybeMigrateLegacyStorage;
let resolveMatrixStateFilePath: typeof import("./storage.js").resolveMatrixStateFilePath;
let resolveMatrixStoragePaths: typeof import("./storage.js").resolveMatrixStoragePaths;
describe("matrix client storage paths", () => {
@@ -39,7 +40,8 @@ describe("matrix client storage paths", () => {
};
beforeAll(async () => {
({ maybeMigrateLegacyStorage, resolveMatrixStoragePaths } = await import("./storage.js"));
({ maybeMigrateLegacyStorage, resolveMatrixStateFilePath, resolveMatrixStoragePaths } =
await import("./storage.js"));
});
afterEach(() => {
@@ -111,6 +113,26 @@ describe("matrix client storage paths", () => {
});
}
it("resolves state file paths inside the selected storage root", () => {
setupStateDir();
const filePath = resolveMatrixStateFilePath({
auth: {
...defaultStorageAuth,
accountId: "ops",
deviceId: "DEVICE1",
},
filename: "thread-bindings.json",
env: {},
});
expect(filePath).toBe(
path.join(
resolveDefaultStoragePaths({ accountId: "ops", deviceId: "DEVICE1" }).rootDir,
"thread-bindings.json",
),
);
});
function writeLegacyMatrixStorage(
stateDir: string,
params: {

View File

@@ -11,6 +11,7 @@ import {
resolveMatrixAccountStorageRoot,
resolveMatrixLegacyFlatStoragePaths,
} from "../../storage-paths.js";
import type { MatrixAuth } from "./types.js";
import type { MatrixStoragePaths } from "./types.js";
export const DEFAULT_ACCOUNT_KEY = "default";
@@ -293,6 +294,25 @@ export function resolveMatrixStoragePaths(params: {
};
}
export function resolveMatrixStateFilePath(params: {
auth: MatrixAuth;
filename: string;
accountId?: string | null;
env?: NodeJS.ProcessEnv;
stateDir?: string;
}): string {
const storagePaths = resolveMatrixStoragePaths({
homeserver: params.auth.homeserver,
userId: params.auth.userId,
accessToken: params.auth.accessToken,
accountId: params.accountId ?? params.auth.accountId,
deviceId: params.auth.deviceId,
env: params.env,
stateDir: params.stateDir,
});
return path.join(storagePaths.rootDir, params.filename);
}
export async function maybeMigrateLegacyStorage(params: {
storagePaths: MatrixStoragePaths;
env?: NodeJS.ProcessEnv;

View File

@@ -1,7 +1,6 @@
import path from "node:path";
import { readJsonFileWithFallback, writeJsonFileAtomically } from "../../runtime-api.js";
import { createAsyncLock } from "../async-lock.js";
import { resolveMatrixStoragePaths } from "../client/storage.js";
import { resolveMatrixStateFilePath } from "../client/storage.js";
import type { MatrixAuth } from "../client/types.js";
import { LogService } from "../sdk/logger.js";
@@ -44,16 +43,12 @@ function resolveInboundDedupeStatePath(params: {
env?: NodeJS.ProcessEnv;
stateDir?: string;
}): string {
const storagePaths = resolveMatrixStoragePaths({
homeserver: params.auth.homeserver,
userId: params.auth.userId,
accessToken: params.auth.accessToken,
accountId: params.auth.accountId,
deviceId: params.auth.deviceId,
return resolveMatrixStateFilePath({
auth: params.auth,
env: params.env,
stateDir: params.stateDir,
filename: INBOUND_DEDUPE_FILENAME,
});
return path.join(storagePaths.rootDir, INBOUND_DEDUPE_FILENAME);
}
function normalizeTimestamp(raw: unknown): number | null {

View File

@@ -8,7 +8,7 @@ import {
} from "../../../../src/infra/outbound/session-binding-service.js";
import type { PluginRuntime } from "../../runtime-api.js";
import { setMatrixRuntime } from "../runtime.js";
import { resolveMatrixStoragePaths } from "./client/storage.js";
import { resolveMatrixStateFilePath, resolveMatrixStoragePaths } from "./client/storage.js";
import {
createMatrixThreadBindingManager,
resetMatrixThreadBindingsForTests,
@@ -87,14 +87,12 @@ describe("matrix thread bindings", () => {
}
function resolveBindingsFilePath(customStateDir?: string) {
return path.join(
resolveMatrixStoragePaths({
...auth,
env: process.env,
...(customStateDir ? { stateDir: customStateDir } : {}),
}).rootDir,
"thread-bindings.json",
);
return resolveMatrixStateFilePath({
auth,
env: process.env,
...(customStateDir ? { stateDir: customStateDir } : {}),
filename: "thread-bindings.json",
});
}
async function readPersistedLastActivityAt(bindingsPath: string) {

View File

@@ -1,4 +1,3 @@
import path from "node:path";
import type { SessionBindingAdapter } from "openclaw/plugin-sdk/conversation-runtime";
import {
readJsonFileWithFallback,
@@ -8,7 +7,7 @@ import {
unregisterSessionBindingAdapter,
writeJsonFileAtomically,
} from "../runtime-api.js";
import { resolveMatrixStoragePaths } from "./client/storage.js";
import { resolveMatrixStateFilePath } from "./client/storage.js";
import type { MatrixAuth } from "./client/types.js";
import type { MatrixClient } from "./sdk.js";
import { sendMessageMatrix } from "./send.js";
@@ -62,16 +61,13 @@ function resolveBindingsPath(params: {
env?: NodeJS.ProcessEnv;
stateDir?: string;
}): string {
const storagePaths = resolveMatrixStoragePaths({
homeserver: params.auth.homeserver,
userId: params.auth.userId,
accessToken: params.auth.accessToken,
return resolveMatrixStateFilePath({
auth: params.auth,
accountId: params.accountId,
deviceId: params.auth.deviceId,
env: params.env,
stateDir: params.stateDir,
filename: "thread-bindings.json",
});
return path.join(storagePaths.rootDir, "thread-bindings.json");
}
async function loadBindingsFromDisk(filePath: string, accountId: string) {

View File

@@ -6,7 +6,6 @@ import {
colorize,
defaultRuntime,
formatErrorMessage,
getMemorySearchManager,
isRich,
listMemoryFiles,
loadConfig,
@@ -25,6 +24,7 @@ import {
type OpenClawConfig,
} from "./api.js";
import type { MemoryCommandOptions, MemorySearchCommandOptions } from "./cli.types.js";
import { getMemorySearchManager } from "./runtime-api.js";
type MemoryManager = NonNullable<Awaited<ReturnType<typeof getMemorySearchManager>>["manager"]>;
type MemoryManagerPurpose = Parameters<typeof getMemorySearchManager>[0]["purpose"];

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