mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 22:11:38 +08:00
Compare commits
17 Commits
codex/plug
...
fix/text-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7aeb7c3e9 | ||
|
|
7c8d75e3d6 | ||
|
|
9f8c4efa9b | ||
|
|
99f0ea8d43 | ||
|
|
16565020a1 | ||
|
|
0ef2a9c8b5 | ||
|
|
9a7ceceffa | ||
|
|
22348914cf | ||
|
|
01bcbcf8d5 | ||
|
|
cad83db8b2 | ||
|
|
0e182dd3e1 | ||
|
|
abf95c5f99 | ||
|
|
0106b0488a | ||
|
|
4890656d9d | ||
|
|
bfad32aa16 | ||
|
|
4151b48d6c | ||
|
|
8eeccb116d |
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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. Auto‑TTS 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)
|
||||
|
||||
108
docs/tts.md
108
docs/tts.md
@@ -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. Auto‑TTS 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)
|
||||
|
||||
@@ -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([
|
||||
@@ -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 {
|
||||
@@ -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", () => {
|
||||
@@ -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
|
||||
@@ -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", () => {
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
openChromeMcpTab,
|
||||
resetChromeMcpSessionsForTest,
|
||||
setChromeMcpSessionFactoryForTest,
|
||||
} from "../../extensions/browser/src/browser/chrome-mcp.js";
|
||||
} from "./chrome-mcp.js";
|
||||
|
||||
type ToolCall = {
|
||||
name: string;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
@@ -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];
|
||||
|
||||
@@ -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(() => {
|
||||
@@ -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[]) {
|
||||
@@ -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", () => {
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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> {
|
||||
@@ -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 () => {
|
||||
@@ -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;
|
||||
@@ -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-"));
|
||||
@@ -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", () => {
|
||||
@@ -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(() => {
|
||||
@@ -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"])(
|
||||
@@ -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;
|
||||
@@ -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 () => {
|
||||
@@ -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", () => {
|
||||
@@ -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" });
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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(() => {
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
refLocator,
|
||||
rememberRoleRefsForTarget,
|
||||
restoreRoleRefsForTarget,
|
||||
} from "../../extensions/browser/src/browser/pw-session.js";
|
||||
} from "./pw-session.js";
|
||||
|
||||
function fakePage(): {
|
||||
page: Page;
|
||||
@@ -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 () => {
|
||||
@@ -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(() => {
|
||||
@@ -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([
|
||||
@@ -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;
|
||||
@@ -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 () => {
|
||||
@@ -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(() => {
|
||||
@@ -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 () => {
|
||||
@@ -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(() => {
|
||||
@@ -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([
|
||||
@@ -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() {
|
||||
@@ -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 {
|
||||
@@ -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", () => {
|
||||
@@ -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 };
|
||||
|
||||
@@ -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", () => {
|
||||
@@ -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", () => {
|
||||
@@ -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 () => {
|
||||
@@ -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 () => {
|
||||
@@ -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 {
|
||||
@@ -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", () => {
|
||||
@@ -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" },
|
||||
@@ -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;
|
||||
@@ -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(() => {
|
||||
@@ -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"] {
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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 () => {
|
||||
@@ -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;
|
||||
@@ -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 () => {
|
||||
@@ -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();
|
||||
@@ -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(() => {
|
||||
@@ -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", () => {
|
||||
@@ -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([
|
||||
@@ -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(() => {
|
||||
@@ -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 }) {
|
||||
@@ -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(() => {
|
||||
@@ -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(() => {
|
||||
@@ -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", () => {
|
||||
@@ -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;
|
||||
@@ -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 }?];
|
||||
|
||||
@@ -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[] } },
|
||||
11
extensions/browser/test-support.ts
Normal file
11
extensions/browser/test-support.ts
Normal 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";
|
||||
@@ -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)." });
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 }));
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user