mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 13:14:56 +03:00
Compare commits
2 Commits
release/v3
...
refactor/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ced57bc08 | ||
|
|
a459dfd221 |
@@ -368,7 +368,7 @@ const result = await executor.execute({
|
|||||||
});
|
});
|
||||||
````
|
````
|
||||||
|
|
||||||
The factory is generated from `config/providerRegistry.ts` which lists all 338 providers and their executor class.
|
Resolution goes through the `ExecutorRegistry` (`executors/registry.ts`): every specialized executor is declared in the built-in table of `executors/index.ts` and registered via `registerExecutor(alias, instance)` at module load; `getExecutor()` consults the registry and falls back to a memoized `DefaultExecutor` for any provider without a specialized entry. The full alias → executor mapping is characterized by the golden test `tests/unit/executor-map-golden.test.ts`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { SEARCH_PROVIDERS } from "../config/searchRegistry.ts";
|
import { SEARCH_PROVIDERS } from "../config/searchRegistry.ts";
|
||||||
|
import {
|
||||||
|
registerExecutor,
|
||||||
|
getRegisteredExecutor,
|
||||||
|
hasRegisteredExecutor,
|
||||||
|
} from "./registry.ts";
|
||||||
import { AntigravityExecutor } from "./antigravity.ts";
|
import { AntigravityExecutor } from "./antigravity.ts";
|
||||||
import { GithubExecutor } from "./github.ts";
|
import { GithubExecutor } from "./github.ts";
|
||||||
import { GheCopilotExecutor } from "./ghe-copilot.ts";
|
import { GheCopilotExecutor } from "./ghe-copilot.ts";
|
||||||
@@ -78,6 +83,12 @@ import { XaiExecutor } from "./xai.ts";
|
|||||||
import { PromptQlExecutor } from "./promptql.ts";
|
import { PromptQlExecutor } from "./promptql.ts";
|
||||||
import { ConolWebExecutor } from "./conol-web.ts";
|
import { ConolWebExecutor } from "./conol-web.ts";
|
||||||
|
|
||||||
|
// R0.3 — declarative built-in table. The object literal stays as the single
|
||||||
|
// place built-ins are declared (compile-time duplicate-key safety; the
|
||||||
|
// check:known-symbols gate parses this literal from source), but lookup goes
|
||||||
|
// through the ExecutorRegistry (./registry.ts): every entry is registered at
|
||||||
|
// module load below, and getExecutor()/hasSpecializedExecutor() consult the
|
||||||
|
// registry — the literal is never read at request time.
|
||||||
const executors = {
|
const executors = {
|
||||||
antigravity: new AntigravityExecutor(),
|
antigravity: new AntigravityExecutor(),
|
||||||
agy: new AntigravityExecutor(),
|
agy: new AntigravityExecutor(),
|
||||||
@@ -221,6 +232,13 @@ const executors = {
|
|||||||
cnl: new ConolWebExecutor(), // Alias
|
cnl: new ConolWebExecutor(), // Alias
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bootstrap: register every built-in in the ExecutorRegistry. registerExecutor
|
||||||
|
// throws on duplicates, so an alias collision fails at module load, exactly as
|
||||||
|
// loudly as a duplicate object key would have failed at lint time.
|
||||||
|
for (const [alias, executor] of Object.entries(executors)) {
|
||||||
|
registerExecutor(alias, executor);
|
||||||
|
}
|
||||||
|
|
||||||
const defaultCache = new Map();
|
const defaultCache = new Map();
|
||||||
|
|
||||||
// #6699 — providers that exist ONLY as Cloud Agent task-API entries
|
// #6699 — providers that exist ONLY as Cloud Agent task-API entries
|
||||||
@@ -246,7 +264,8 @@ const CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS = new Set(["jules"]);
|
|||||||
const CHAT_UNSUPPORTED_SEARCH_PROVIDERS = new Set(Object.keys(SEARCH_PROVIDERS));
|
const CHAT_UNSUPPORTED_SEARCH_PROVIDERS = new Set(Object.keys(SEARCH_PROVIDERS));
|
||||||
|
|
||||||
export function getExecutor(provider) {
|
export function getExecutor(provider) {
|
||||||
if (executors[provider]) return executors[provider];
|
const registered = getRegisteredExecutor(provider);
|
||||||
|
if (registered) return registered;
|
||||||
if (CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS.has(provider)) {
|
if (CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS.has(provider)) {
|
||||||
const err = new Error(
|
const err = new Error(
|
||||||
`Provider "${provider}" is a cloud-agent provider and does not support direct chat completions; use the Cloud Agents task API instead.`
|
`Provider "${provider}" is a cloud-agent provider and does not support direct chat completions; use the Cloud Agents task API instead.`
|
||||||
@@ -266,9 +285,11 @@ export function getExecutor(provider) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function hasSpecializedExecutor(provider) {
|
export function hasSpecializedExecutor(provider) {
|
||||||
return !!executors[provider];
|
return hasRegisteredExecutor(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { registerExecutor, listExecutorAliases } from "./registry.ts";
|
||||||
|
|
||||||
export { BaseExecutor } from "./base.ts";
|
export { BaseExecutor } from "./base.ts";
|
||||||
export { AntigravityExecutor } from "./antigravity.ts";
|
export { AntigravityExecutor } from "./antigravity.ts";
|
||||||
export { GithubExecutor } from "./github.ts";
|
export { GithubExecutor } from "./github.ts";
|
||||||
|
|||||||
38
open-sse/executors/registry.ts
Normal file
38
open-sse/executors/registry.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { BaseExecutor } from "./base.ts";
|
||||||
|
|
||||||
|
// R0.3 — ExecutorRegistry: runtime registry for provider executors, mirroring
|
||||||
|
// open-sse/translator/registry.ts. Built-ins register at module load from
|
||||||
|
// executors/index.ts; getExecutor() resolves through this map instead of a
|
||||||
|
// hard-coded object literal. This is the seam the v4 plan (M1.6
|
||||||
|
// host.registerProvider) extends — today the surface is internal-only.
|
||||||
|
//
|
||||||
|
// The alias → executor mapping is characterized by
|
||||||
|
// tests/unit/executor-map-golden.test.ts (tests/snapshots/executors/): any
|
||||||
|
// change to keys, classes or instance sharing shows up as a golden diff.
|
||||||
|
|
||||||
|
const registry = new Map<string, BaseExecutor>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an executor under an alias. Aliases are unique: registering the
|
||||||
|
* same alias twice throws, preserving the guarantee the old object literal
|
||||||
|
* gave at compile time (duplicate keys were impossible).
|
||||||
|
*/
|
||||||
|
export function registerExecutor(alias: string, executor: BaseExecutor): void {
|
||||||
|
if (registry.has(alias)) {
|
||||||
|
throw new Error(`executor alias already registered: "${alias}"`);
|
||||||
|
}
|
||||||
|
registry.set(alias, executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRegisteredExecutor(alias: string): BaseExecutor | undefined {
|
||||||
|
return registry.get(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasRegisteredExecutor(alias: string): boolean {
|
||||||
|
return registry.has(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All registered aliases, in registration order. */
|
||||||
|
export function listExecutorAliases(): string[] {
|
||||||
|
return [...registry.keys()];
|
||||||
|
}
|
||||||
86
tests/snapshots/executors/dispatch-rules.json
Normal file
86
tests/snapshots/executors/dispatch-rules.json
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"cloudAgentGuard": {
|
||||||
|
"jules": {
|
||||||
|
"message": "Provider \"jules\" is a cloud-agent provider and does not support direct chat completions; use the Cloud Agents task API instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"className": "DefaultExecutor",
|
||||||
|
"configSource": "openai",
|
||||||
|
"provider": "golden-test-unknown-provider"
|
||||||
|
},
|
||||||
|
"searchGuard": {
|
||||||
|
"brave-search": {
|
||||||
|
"message": "Provider \"brave-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
},
|
||||||
|
"duckduckgo-free": {
|
||||||
|
"message": "Provider \"duckduckgo-free\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
},
|
||||||
|
"exa-search": {
|
||||||
|
"message": "Provider \"exa-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
},
|
||||||
|
"firecrawl": {
|
||||||
|
"message": "Provider \"firecrawl\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
},
|
||||||
|
"google-pse-search": {
|
||||||
|
"message": "Provider \"google-pse-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
},
|
||||||
|
"linkup-search": {
|
||||||
|
"message": "Provider \"linkup-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
},
|
||||||
|
"ollama-search": {
|
||||||
|
"message": "Provider \"ollama-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
},
|
||||||
|
"perplexity-search": {
|
||||||
|
"message": "Provider \"perplexity-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
},
|
||||||
|
"searchapi-search": {
|
||||||
|
"message": "Provider \"searchapi-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
},
|
||||||
|
"searxng-search": {
|
||||||
|
"message": "Provider \"searxng-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
},
|
||||||
|
"serper-search": {
|
||||||
|
"message": "Provider \"serper-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
},
|
||||||
|
"tavily-search": {
|
||||||
|
"message": "Provider \"tavily-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
},
|
||||||
|
"youcom-search": {
|
||||||
|
"message": "Provider \"youcom-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
},
|
||||||
|
"zai-search": {
|
||||||
|
"message": "Provider \"zai-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||||
|
"status": 400,
|
||||||
|
"throws": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
691
tests/snapshots/executors/executor-map.json
Normal file
691
tests/snapshots/executors/executor-map.json
Normal file
@@ -0,0 +1,691 @@
|
|||||||
|
{
|
||||||
|
"entries": {
|
||||||
|
"9router": {
|
||||||
|
"className": "NineRouterExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "9router"
|
||||||
|
},
|
||||||
|
"adapta-web": {
|
||||||
|
"className": "AdaptaWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "adapta-web"
|
||||||
|
},
|
||||||
|
"adobe-firefly": {
|
||||||
|
"className": "AdobeFireflyExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "adobe-firefly"
|
||||||
|
},
|
||||||
|
"adp-web": {
|
||||||
|
"className": "AdaptaWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "adapta-web"
|
||||||
|
},
|
||||||
|
"agy": {
|
||||||
|
"className": "AntigravityExecutor",
|
||||||
|
"configSource": "antigravity",
|
||||||
|
"provider": "antigravity"
|
||||||
|
},
|
||||||
|
"amazon-q": {
|
||||||
|
"className": "KiroExecutor",
|
||||||
|
"configSource": "kiro",
|
||||||
|
"provider": "amazon-q"
|
||||||
|
},
|
||||||
|
"antigravity": {
|
||||||
|
"className": "AntigravityExecutor",
|
||||||
|
"configSource": "antigravity",
|
||||||
|
"provider": "antigravity"
|
||||||
|
},
|
||||||
|
"auggie": {
|
||||||
|
"className": "AuggieExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "auggie"
|
||||||
|
},
|
||||||
|
"azure-ai": {
|
||||||
|
"className": "AzureAiExecutor",
|
||||||
|
"configSource": "openai",
|
||||||
|
"provider": "azure-ai"
|
||||||
|
},
|
||||||
|
"azure-openai": {
|
||||||
|
"className": "AzureOpenAIExecutor",
|
||||||
|
"configSource": "openai",
|
||||||
|
"provider": "azure-openai"
|
||||||
|
},
|
||||||
|
"bb-web": {
|
||||||
|
"className": "BlackboxWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "blackbox-web"
|
||||||
|
},
|
||||||
|
"bedrock": {
|
||||||
|
"className": "BedrockExecutor",
|
||||||
|
"configSource": "bedrock",
|
||||||
|
"provider": "bedrock"
|
||||||
|
},
|
||||||
|
"blackbox-web": {
|
||||||
|
"className": "BlackboxWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "blackbox-web"
|
||||||
|
},
|
||||||
|
"cbcn": {
|
||||||
|
"className": "CodeBuddyCnExecutor",
|
||||||
|
"configSource": "codebuddy-cn",
|
||||||
|
"provider": "codebuddy-cn"
|
||||||
|
},
|
||||||
|
"cf": {
|
||||||
|
"className": "CloudflareAIExecutor",
|
||||||
|
"configSource": "cloudflare-ai",
|
||||||
|
"provider": "cloudflare-ai"
|
||||||
|
},
|
||||||
|
"cgpt-codex": {
|
||||||
|
"className": "ChatGptWebCodexExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "chatgpt-web-codex"
|
||||||
|
},
|
||||||
|
"cgpt-web": {
|
||||||
|
"className": "ChatGptWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "chatgpt-web"
|
||||||
|
},
|
||||||
|
"chatgpt-web": {
|
||||||
|
"className": "ChatGptWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "chatgpt-web"
|
||||||
|
},
|
||||||
|
"chatgpt-web-codex": {
|
||||||
|
"className": "ChatGptWebCodexExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "chatgpt-web-codex"
|
||||||
|
},
|
||||||
|
"cheaperinference": {
|
||||||
|
"className": "CheaperInferenceExecutor",
|
||||||
|
"configSource": "cheaperinference",
|
||||||
|
"provider": "cheaperinference"
|
||||||
|
},
|
||||||
|
"chipotle": {
|
||||||
|
"className": "ChipotleExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "chipotle"
|
||||||
|
},
|
||||||
|
"cinf": {
|
||||||
|
"className": "CheaperInferenceExecutor",
|
||||||
|
"configSource": "cheaperinference",
|
||||||
|
"provider": "cheaperinference"
|
||||||
|
},
|
||||||
|
"claude-web": {
|
||||||
|
"className": "ClaudeWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "claude-web"
|
||||||
|
},
|
||||||
|
"cliproxyapi": {
|
||||||
|
"className": "CliproxyapiExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "cliproxyapi"
|
||||||
|
},
|
||||||
|
"cloudflare-ai": {
|
||||||
|
"className": "CloudflareAIExecutor",
|
||||||
|
"configSource": "cloudflare-ai",
|
||||||
|
"provider": "cloudflare-ai"
|
||||||
|
},
|
||||||
|
"cmd": {
|
||||||
|
"className": "CommandCodeExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "command-code"
|
||||||
|
},
|
||||||
|
"cnl": {
|
||||||
|
"className": "ConolWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "conol-web"
|
||||||
|
},
|
||||||
|
"codebuddy-cn": {
|
||||||
|
"className": "CodeBuddyCnExecutor",
|
||||||
|
"configSource": "codebuddy-cn",
|
||||||
|
"provider": "codebuddy-cn"
|
||||||
|
},
|
||||||
|
"codex": {
|
||||||
|
"className": "CodexExecutor",
|
||||||
|
"configSource": "codex",
|
||||||
|
"provider": "codex"
|
||||||
|
},
|
||||||
|
"command-code": {
|
||||||
|
"className": "CommandCodeExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "command-code"
|
||||||
|
},
|
||||||
|
"conol-web": {
|
||||||
|
"className": "ConolWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "conol-web"
|
||||||
|
},
|
||||||
|
"copilot": {
|
||||||
|
"className": "CopilotWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "copilot-web"
|
||||||
|
},
|
||||||
|
"copilot-m365-web": {
|
||||||
|
"className": "CopilotM365WebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "copilot-m365-web"
|
||||||
|
},
|
||||||
|
"copilot-web": {
|
||||||
|
"className": "CopilotWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "copilot-web"
|
||||||
|
},
|
||||||
|
"cpa": {
|
||||||
|
"className": "CliproxyapiExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "cliproxyapi"
|
||||||
|
},
|
||||||
|
"cu": {
|
||||||
|
"className": "CursorExecutor",
|
||||||
|
"configSource": "cursor",
|
||||||
|
"provider": "cursor"
|
||||||
|
},
|
||||||
|
"cursor": {
|
||||||
|
"className": "CursorExecutor",
|
||||||
|
"configSource": "cursor",
|
||||||
|
"provider": "cursor"
|
||||||
|
},
|
||||||
|
"cw-web": {
|
||||||
|
"className": "ClaudeWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "claude-web"
|
||||||
|
},
|
||||||
|
"dario": {
|
||||||
|
"className": "DarioExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "dario"
|
||||||
|
},
|
||||||
|
"db": {
|
||||||
|
"className": "DoubaoWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "doubao-web"
|
||||||
|
},
|
||||||
|
"ddgw": {
|
||||||
|
"className": "DuckDuckGoWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "duckduckgo-web"
|
||||||
|
},
|
||||||
|
"deepseek-web": {
|
||||||
|
"className": "DeepSeekWebWithAutoRefreshExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "deepseek-web"
|
||||||
|
},
|
||||||
|
"devin": {
|
||||||
|
"className": "DevinCliExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "devin-cli"
|
||||||
|
},
|
||||||
|
"devin-cli": {
|
||||||
|
"className": "DevinCliExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "devin-cli"
|
||||||
|
},
|
||||||
|
"devin-cli-agentic": {
|
||||||
|
"className": "DevinCliAgenticExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "devin-cli-agentic"
|
||||||
|
},
|
||||||
|
"devin-desktop": {
|
||||||
|
"className": "DevinDesktopExecutor",
|
||||||
|
"configSource": "devin-desktop",
|
||||||
|
"provider": "devin-desktop"
|
||||||
|
},
|
||||||
|
"doubao-web": {
|
||||||
|
"className": "DoubaoWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "doubao-web"
|
||||||
|
},
|
||||||
|
"dr": {
|
||||||
|
"className": "DarioExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "dario"
|
||||||
|
},
|
||||||
|
"ds-web": {
|
||||||
|
"className": "DeepSeekWebWithAutoRefreshExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "deepseek-web"
|
||||||
|
},
|
||||||
|
"duckduckgo-web": {
|
||||||
|
"className": "DuckDuckGoWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "duckduckgo-web"
|
||||||
|
},
|
||||||
|
"felo": {
|
||||||
|
"className": "FeloWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "felo-web"
|
||||||
|
},
|
||||||
|
"felo-web": {
|
||||||
|
"className": "FeloWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "felo-web"
|
||||||
|
},
|
||||||
|
"firefly": {
|
||||||
|
"className": "AdobeFireflyExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "adobe-firefly"
|
||||||
|
},
|
||||||
|
"gc": {
|
||||||
|
"className": "GrokCliExecutor",
|
||||||
|
"configSource": "grok-cli",
|
||||||
|
"provider": "grok-cli"
|
||||||
|
},
|
||||||
|
"gembiz": {
|
||||||
|
"className": "GeminiBusinessExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "gemini-business"
|
||||||
|
},
|
||||||
|
"gemini-business": {
|
||||||
|
"className": "GeminiBusinessExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "gemini-business"
|
||||||
|
},
|
||||||
|
"gemini-web": {
|
||||||
|
"className": "GeminiWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "gemini-web"
|
||||||
|
},
|
||||||
|
"ghe-copilot": {
|
||||||
|
"className": "GheCopilotExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "ghe-copilot"
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"className": "GithubExecutor",
|
||||||
|
"configSource": "github",
|
||||||
|
"provider": "github"
|
||||||
|
},
|
||||||
|
"gitlab": {
|
||||||
|
"className": "GitlabExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "gitlab"
|
||||||
|
},
|
||||||
|
"gitlab-duo": {
|
||||||
|
"className": "GitlabExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "gitlab-duo"
|
||||||
|
},
|
||||||
|
"glm": {
|
||||||
|
"className": "GlmExecutor",
|
||||||
|
"configSource": "glm",
|
||||||
|
"provider": "glm"
|
||||||
|
},
|
||||||
|
"glm-cn": {
|
||||||
|
"className": "GlmExecutor",
|
||||||
|
"configSource": "glm-cn",
|
||||||
|
"provider": "glm-cn"
|
||||||
|
},
|
||||||
|
"glmt": {
|
||||||
|
"className": "GlmExecutor",
|
||||||
|
"configSource": "glmt",
|
||||||
|
"provider": "glmt"
|
||||||
|
},
|
||||||
|
"grok-cli": {
|
||||||
|
"className": "GrokCliExecutor",
|
||||||
|
"configSource": "grok-cli",
|
||||||
|
"provider": "grok-cli"
|
||||||
|
},
|
||||||
|
"grok-web": {
|
||||||
|
"className": "GrokWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "grok-web"
|
||||||
|
},
|
||||||
|
"gweb": {
|
||||||
|
"className": "GeminiWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "gemini-web"
|
||||||
|
},
|
||||||
|
"ha": {
|
||||||
|
"className": "HyperAgentExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "hyperagent"
|
||||||
|
},
|
||||||
|
"hailuo-web": {
|
||||||
|
"className": "HailuoWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "hailuo-web"
|
||||||
|
},
|
||||||
|
"hc": {
|
||||||
|
"className": "HuggingChatExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "huggingchat"
|
||||||
|
},
|
||||||
|
"huggingchat": {
|
||||||
|
"className": "HuggingChatExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "huggingchat"
|
||||||
|
},
|
||||||
|
"hyperagent": {
|
||||||
|
"className": "HyperAgentExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "hyperagent"
|
||||||
|
},
|
||||||
|
"in-ai": {
|
||||||
|
"className": "InnerAiExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "inner-ai"
|
||||||
|
},
|
||||||
|
"inner-ai": {
|
||||||
|
"className": "InnerAiExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "inner-ai"
|
||||||
|
},
|
||||||
|
"kimi": {
|
||||||
|
"className": "MoonshotExecutor",
|
||||||
|
"configSource": "kimi",
|
||||||
|
"provider": "kimi"
|
||||||
|
},
|
||||||
|
"kimi-coding": {
|
||||||
|
"className": "KimiExecutor",
|
||||||
|
"configSource": "kimi-coding",
|
||||||
|
"provider": "kimi-coding"
|
||||||
|
},
|
||||||
|
"kimi-coding-apikey": {
|
||||||
|
"className": "KimiExecutor",
|
||||||
|
"configSource": "kimi-coding-apikey",
|
||||||
|
"provider": "kimi-coding-apikey"
|
||||||
|
},
|
||||||
|
"kimi-web": {
|
||||||
|
"className": "KimiWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "kimi-web"
|
||||||
|
},
|
||||||
|
"kiro": {
|
||||||
|
"className": "KiroExecutor",
|
||||||
|
"configSource": "kiro",
|
||||||
|
"provider": "kiro"
|
||||||
|
},
|
||||||
|
"lma": {
|
||||||
|
"className": "LMArenaExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "lmarena"
|
||||||
|
},
|
||||||
|
"lmarena": {
|
||||||
|
"className": "LMArenaExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "lmarena"
|
||||||
|
},
|
||||||
|
"mcode": {
|
||||||
|
"className": "MimocodeExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "mimocode"
|
||||||
|
},
|
||||||
|
"microsoft-designer-web": {
|
||||||
|
"className": "MicrosoftDesignerWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "microsoft-designer-web"
|
||||||
|
},
|
||||||
|
"mimocode": {
|
||||||
|
"className": "MimocodeExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "mimocode"
|
||||||
|
},
|
||||||
|
"moonshot": {
|
||||||
|
"className": "MoonshotExecutor",
|
||||||
|
"configSource": "moonshot",
|
||||||
|
"provider": "moonshot"
|
||||||
|
},
|
||||||
|
"ms-web": {
|
||||||
|
"className": "MuseSparkWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "muse-spark-web"
|
||||||
|
},
|
||||||
|
"msdesigner": {
|
||||||
|
"className": "MicrosoftDesignerWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "microsoft-designer-web"
|
||||||
|
},
|
||||||
|
"muse-spark-web": {
|
||||||
|
"className": "MuseSparkWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "muse-spark-web"
|
||||||
|
},
|
||||||
|
"nlpcloud": {
|
||||||
|
"className": "NlpCloudExecutor",
|
||||||
|
"configSource": "nlpcloud",
|
||||||
|
"provider": "nlpcloud"
|
||||||
|
},
|
||||||
|
"notion-web": {
|
||||||
|
"className": "NotionWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "notion-web"
|
||||||
|
},
|
||||||
|
"nr": {
|
||||||
|
"className": "NineRouterExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "9router"
|
||||||
|
},
|
||||||
|
"nw": {
|
||||||
|
"className": "NotionWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "notion-web"
|
||||||
|
},
|
||||||
|
"opencode": {
|
||||||
|
"className": "OpencodeExecutor",
|
||||||
|
"configSource": "opencode-zen",
|
||||||
|
"provider": "opencode-zen"
|
||||||
|
},
|
||||||
|
"opencode-go": {
|
||||||
|
"className": "OpencodeExecutor",
|
||||||
|
"configSource": "opencode-go",
|
||||||
|
"provider": "opencode-go"
|
||||||
|
},
|
||||||
|
"opencode-zen": {
|
||||||
|
"className": "OpencodeExecutor",
|
||||||
|
"configSource": "opencode-zen",
|
||||||
|
"provider": "opencode-zen"
|
||||||
|
},
|
||||||
|
"pepper": {
|
||||||
|
"className": "ChipotleExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "chipotle"
|
||||||
|
},
|
||||||
|
"perplexity-web": {
|
||||||
|
"className": "PerplexityWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "perplexity-web"
|
||||||
|
},
|
||||||
|
"poe-web": {
|
||||||
|
"className": "PoeWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "poe-web"
|
||||||
|
},
|
||||||
|
"pol": {
|
||||||
|
"className": "PollinationsExecutor",
|
||||||
|
"configSource": "pollinations",
|
||||||
|
"provider": "pollinations"
|
||||||
|
},
|
||||||
|
"pollinations": {
|
||||||
|
"className": "PollinationsExecutor",
|
||||||
|
"configSource": "pollinations",
|
||||||
|
"provider": "pollinations"
|
||||||
|
},
|
||||||
|
"pplx-web": {
|
||||||
|
"className": "PerplexityWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "perplexity-web"
|
||||||
|
},
|
||||||
|
"pql": {
|
||||||
|
"className": "PromptQlExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "promptql"
|
||||||
|
},
|
||||||
|
"promptql": {
|
||||||
|
"className": "PromptQlExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "promptql"
|
||||||
|
},
|
||||||
|
"qoder": {
|
||||||
|
"className": "QoderExecutor",
|
||||||
|
"configSource": "qoder",
|
||||||
|
"provider": "qoder"
|
||||||
|
},
|
||||||
|
"qw": {
|
||||||
|
"className": "QwenWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "qwen-web"
|
||||||
|
},
|
||||||
|
"qwen-web": {
|
||||||
|
"className": "QwenWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "qwen-web"
|
||||||
|
},
|
||||||
|
"raycast": {
|
||||||
|
"className": "RaycastExecutor",
|
||||||
|
"configSource": "raycast",
|
||||||
|
"provider": "raycast"
|
||||||
|
},
|
||||||
|
"rc": {
|
||||||
|
"className": "RaycastExecutor",
|
||||||
|
"configSource": "raycast",
|
||||||
|
"provider": "raycast"
|
||||||
|
},
|
||||||
|
"t3-web": {
|
||||||
|
"className": "T3ChatWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "t3-web"
|
||||||
|
},
|
||||||
|
"t3chat": {
|
||||||
|
"className": "T3ChatWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "t3-web"
|
||||||
|
},
|
||||||
|
"tasw": {
|
||||||
|
"className": "TencentAIStudioWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "tencent-aistudio-web"
|
||||||
|
},
|
||||||
|
"tcw": {
|
||||||
|
"className": "TinyCmsExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "tinycms-web"
|
||||||
|
},
|
||||||
|
"tencent-aistudio-web": {
|
||||||
|
"className": "TencentAIStudioWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "tencent-aistudio-web"
|
||||||
|
},
|
||||||
|
"theoldllm": {
|
||||||
|
"className": "TheOldLlmExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "theoldllm"
|
||||||
|
},
|
||||||
|
"tinycms-web": {
|
||||||
|
"className": "TinyCmsExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "tinycms-web"
|
||||||
|
},
|
||||||
|
"tllm": {
|
||||||
|
"className": "TheOldLlmExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "theoldllm"
|
||||||
|
},
|
||||||
|
"trae": {
|
||||||
|
"className": "TraeExecutor",
|
||||||
|
"configSource": "trae",
|
||||||
|
"provider": "trae"
|
||||||
|
},
|
||||||
|
"v0": {
|
||||||
|
"className": "V0VercelWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "v0-vercel-web"
|
||||||
|
},
|
||||||
|
"v0-vercel-web": {
|
||||||
|
"className": "V0VercelWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "v0-vercel-web"
|
||||||
|
},
|
||||||
|
"ven": {
|
||||||
|
"className": "VeniceWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "venice-web"
|
||||||
|
},
|
||||||
|
"venice-web": {
|
||||||
|
"className": "VeniceWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "venice-web"
|
||||||
|
},
|
||||||
|
"veo-free": {
|
||||||
|
"className": "VeoAIFreeWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "veoaifree-web"
|
||||||
|
},
|
||||||
|
"veoaifree-web": {
|
||||||
|
"className": "VeoAIFreeWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "veoaifree-web"
|
||||||
|
},
|
||||||
|
"vertex": {
|
||||||
|
"className": "VertexExecutor",
|
||||||
|
"configSource": "vertex",
|
||||||
|
"provider": "vertex"
|
||||||
|
},
|
||||||
|
"vertex-partner": {
|
||||||
|
"className": "VertexExecutor",
|
||||||
|
"configSource": "vertex",
|
||||||
|
"provider": "vertex"
|
||||||
|
},
|
||||||
|
"xai": {
|
||||||
|
"className": "XaiExecutor",
|
||||||
|
"configSource": "xai",
|
||||||
|
"provider": "xai"
|
||||||
|
},
|
||||||
|
"xai-oauth": {
|
||||||
|
"className": "XaiExecutor",
|
||||||
|
"configSource": "xai-oauth",
|
||||||
|
"provider": "xai-oauth"
|
||||||
|
},
|
||||||
|
"xao": {
|
||||||
|
"className": "XaiExecutor",
|
||||||
|
"configSource": "xai-oauth",
|
||||||
|
"provider": "xai-oauth"
|
||||||
|
},
|
||||||
|
"ybw": {
|
||||||
|
"className": "YuanbaoWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "yuanbao-web"
|
||||||
|
},
|
||||||
|
"yuanbao-web": {
|
||||||
|
"className": "YuanbaoWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "yuanbao-web"
|
||||||
|
},
|
||||||
|
"zai-web": {
|
||||||
|
"className": "ZaiWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "zai-web"
|
||||||
|
},
|
||||||
|
"zc": {
|
||||||
|
"className": "ZcodeExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "zcode"
|
||||||
|
},
|
||||||
|
"zcode": {
|
||||||
|
"className": "ZcodeExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "zcode"
|
||||||
|
},
|
||||||
|
"zed-hosted": {
|
||||||
|
"className": "ZedHostedExecutor",
|
||||||
|
"configSource": "zed-hosted",
|
||||||
|
"provider": "zed-hosted"
|
||||||
|
},
|
||||||
|
"zenmux-free": {
|
||||||
|
"className": "ZenmuxFreeExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "zenmux-free"
|
||||||
|
},
|
||||||
|
"zmf": {
|
||||||
|
"className": "ZenmuxFreeExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "zenmux-free"
|
||||||
|
},
|
||||||
|
"zw": {
|
||||||
|
"className": "ZaiWebExecutor",
|
||||||
|
"configSource": "<custom-config>",
|
||||||
|
"provider": "zai-web"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keyCount": 137,
|
||||||
|
"sharedInstances": []
|
||||||
|
}
|
||||||
134
tests/unit/executor-map-golden.test.ts
Normal file
134
tests/unit/executor-map-golden.test.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
// R0.3 GOLDEN LOCK (characterization BEFORE the ExecutorRegistry refactor):
|
||||||
|
// freeze the full provider-id → executor mapping of open-sse/executors/index.ts —
|
||||||
|
// every specialized key with its executor class, effective provider identity and
|
||||||
|
// which PROVIDERS config entry backs it — plus the getExecutor() dispatch rules
|
||||||
|
// (specialized hit, DefaultExecutor fallback + cache, cloud-agent guard #6699,
|
||||||
|
// search-provider guard #10274). The registry refactor must keep this snapshot
|
||||||
|
// byte-identical: any drift in keys, classes, provider identity or guard behavior
|
||||||
|
// is a golden diff, not a silent routing change.
|
||||||
|
|
||||||
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-executor-golden-"));
|
||||||
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||||
|
|
||||||
|
// Dynamic imports AFTER DATA_DIR is set so db/core.ts picks up the temp path.
|
||||||
|
const { getExecutor, hasSpecializedExecutor, DefaultExecutor } = await import(
|
||||||
|
"../../open-sse/executors/index.ts"
|
||||||
|
);
|
||||||
|
const { PROVIDERS } = await import("../../open-sse/config/constants.ts");
|
||||||
|
const { SEARCH_PROVIDERS } = await import("../../open-sse/config/searchRegistry.ts");
|
||||||
|
const { goldenSnapshot } = await import("../helpers/goldenSnapshot.ts");
|
||||||
|
|
||||||
|
test.after(() => {
|
||||||
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// The specialized keys are not exported; enumerate them through the public
|
||||||
|
// surface by probing every plausible id source AND the literal keys read from
|
||||||
|
// the module source. Reading the source keeps the golden honest: a key added
|
||||||
|
// to (or removed from) the hard-coded map cannot hide from the snapshot.
|
||||||
|
function readSpecializedKeys(): string[] {
|
||||||
|
const src = fs.readFileSync(
|
||||||
|
path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../open-sse/executors/index.ts"),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
const mapMatch = src.match(/const executors = \{([\s\S]*?)\n\};/);
|
||||||
|
assert.ok(mapMatch, "executors map literal not found in open-sse/executors/index.ts");
|
||||||
|
const keys: string[] = [];
|
||||||
|
for (const line of mapMatch[1].split("\n")) {
|
||||||
|
const m = line.match(/^\s*(?:"([^"]+)"|([A-Za-z0-9_$-]+)):\s*new /);
|
||||||
|
if (m) keys.push(m[1] ?? m[2]);
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map a ProviderConfig object back to its PROVIDERS key by identity, so the
|
||||||
|
// snapshot records WHICH config backs each executor without freezing the whole
|
||||||
|
// (huge, frequently-edited) config content.
|
||||||
|
const providerConfigKeyByRef = new Map<object, string>();
|
||||||
|
for (const [key, cfg] of Object.entries(PROVIDERS)) {
|
||||||
|
if (cfg && typeof cfg === "object" && !providerConfigKeyByRef.has(cfg)) {
|
||||||
|
providerConfigKeyByRef.set(cfg, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeExecutor(instance: unknown): {
|
||||||
|
className: string;
|
||||||
|
provider: string | null;
|
||||||
|
configSource: string | null;
|
||||||
|
} {
|
||||||
|
const inst = instance as { constructor: { name: string }; provider?: string; config?: object };
|
||||||
|
const cfg = inst.config;
|
||||||
|
return {
|
||||||
|
className: inst.constructor.name,
|
||||||
|
provider: typeof inst.provider === "string" ? inst.provider : null,
|
||||||
|
configSource:
|
||||||
|
cfg == null ? null : (providerConfigKeyByRef.get(cfg) ?? "<custom-config>"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const specializedKeys = readSpecializedKeys();
|
||||||
|
|
||||||
|
test("golden: specialized executor map — key → class + provider identity + config source", () => {
|
||||||
|
assert.ok(specializedKeys.length >= 100, `suspiciously few keys: ${specializedKeys.length}`);
|
||||||
|
|
||||||
|
const entries: Record<
|
||||||
|
string,
|
||||||
|
{ className: string; provider: string | null; configSource: string | null }
|
||||||
|
> = {};
|
||||||
|
const byInstance = new Map<unknown, string[]>();
|
||||||
|
|
||||||
|
for (const key of [...specializedKeys].sort()) {
|
||||||
|
assert.equal(hasSpecializedExecutor(key), true, `hasSpecializedExecutor(${key})`);
|
||||||
|
const instance = getExecutor(key);
|
||||||
|
entries[key] = describeExecutor(instance);
|
||||||
|
const group = byInstance.get(instance) ?? [];
|
||||||
|
group.push(key);
|
||||||
|
byInstance.set(instance, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys sharing the SAME instance share per-instance state (session pools,
|
||||||
|
// rotation cooldowns); today every map entry is its own `new X()`. Freeze that.
|
||||||
|
const sharedInstances = [...byInstance.values()]
|
||||||
|
.filter((keys) => keys.length > 1)
|
||||||
|
.map((keys) => keys.sort())
|
||||||
|
.sort((a, b) => a[0].localeCompare(b[0]));
|
||||||
|
|
||||||
|
goldenSnapshot("executors/executor-map", {
|
||||||
|
keyCount: specializedKeys.length,
|
||||||
|
entries,
|
||||||
|
sharedInstances,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("golden: getExecutor dispatch rules — fallback, cache and 400-guards", () => {
|
||||||
|
// 1. Unknown provider → DefaultExecutor for that provider, memoized.
|
||||||
|
const unknown = "golden-test-unknown-provider";
|
||||||
|
assert.equal(hasSpecializedExecutor(unknown), false);
|
||||||
|
const fallback = getExecutor(unknown);
|
||||||
|
assert.ok(fallback instanceof DefaultExecutor, "fallback must be DefaultExecutor");
|
||||||
|
assert.equal(getExecutor(unknown), fallback, "DefaultExecutor fallback must be cached");
|
||||||
|
|
||||||
|
// 2. Cloud-agent guard (#6699) and search guard (#10274) → status-400 throw.
|
||||||
|
const guardOutcome = (provider: string) => {
|
||||||
|
try {
|
||||||
|
getExecutor(provider);
|
||||||
|
return { throws: false as const };
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error & { status?: number };
|
||||||
|
return { throws: true as const, status: e.status ?? null, message: e.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchProviders = Object.keys(SEARCH_PROVIDERS).sort();
|
||||||
|
goldenSnapshot("executors/dispatch-rules", {
|
||||||
|
fallback: describeExecutor(fallback),
|
||||||
|
cloudAgentGuard: { jules: guardOutcome("jules") },
|
||||||
|
searchGuard: Object.fromEntries(searchProviders.map((p) => [p, guardOutcome(p)])),
|
||||||
|
});
|
||||||
|
});
|
||||||
57
tests/unit/executor-registry.test.ts
Normal file
57
tests/unit/executor-registry.test.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
// R0.3 — unit tests for the ExecutorRegistry seam itself (registration
|
||||||
|
// semantics + wiring of the built-ins). Behavior parity of the full map is
|
||||||
|
// covered separately by tests/unit/executor-map-golden.test.ts.
|
||||||
|
|
||||||
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-executor-registry-"));
|
||||||
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||||
|
|
||||||
|
const { registerExecutor, getRegisteredExecutor, hasRegisteredExecutor, listExecutorAliases } =
|
||||||
|
await import("../../open-sse/executors/registry.ts");
|
||||||
|
const { getExecutor, hasSpecializedExecutor, BaseExecutor, DefaultExecutor } = await import(
|
||||||
|
"../../open-sse/executors/index.ts"
|
||||||
|
);
|
||||||
|
|
||||||
|
test.after(() => {
|
||||||
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("built-ins are registered at module load and resolve through the registry", () => {
|
||||||
|
const aliases = listExecutorAliases();
|
||||||
|
assert.ok(aliases.length >= 100, `expected the built-in table, got ${aliases.length} aliases`);
|
||||||
|
for (const alias of ["antigravity", "kiro", "glm", "9router", "conol-web"]) {
|
||||||
|
assert.ok(hasRegisteredExecutor(alias), `missing built-in: ${alias}`);
|
||||||
|
assert.equal(getExecutor(alias), getRegisteredExecutor(alias));
|
||||||
|
assert.ok(getExecutor(alias) instanceof BaseExecutor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("registerExecutor throws on duplicate alias", () => {
|
||||||
|
assert.throws(() => registerExecutor("kiro", getRegisteredExecutor("kiro")!), {
|
||||||
|
message: /already registered: "kiro"/,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("registering a new alias makes it resolvable via getExecutor and hasSpecializedExecutor", () => {
|
||||||
|
const alias = "registry-test-provider";
|
||||||
|
assert.equal(hasSpecializedExecutor(alias), false);
|
||||||
|
const instance = new DefaultExecutor(alias);
|
||||||
|
registerExecutor(alias, instance);
|
||||||
|
assert.equal(hasSpecializedExecutor(alias), true);
|
||||||
|
assert.equal(getExecutor(alias), instance);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("registry lookup is exact — Object.prototype names are not executors", () => {
|
||||||
|
// The old object-literal lookup (`executors[provider]`) leaked prototype
|
||||||
|
// members: getExecutor("constructor") returned Object's constructor. The Map
|
||||||
|
// registry must treat these as unknown providers (DefaultExecutor fallback).
|
||||||
|
for (const name of ["constructor", "toString", "hasOwnProperty", "__proto__"]) {
|
||||||
|
assert.equal(hasSpecializedExecutor(name), false, name);
|
||||||
|
assert.ok(getExecutor(name) instanceof DefaultExecutor, name);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user