From 7cfabfc5c9216eada09dc5b99cec1a4eccd44792 Mon Sep 17 00:00:00 2001 From: Paijo <14921983+oyi77@users.noreply.github.com> Date: Wed, 26 Aug 2026 04:22:46 +0700 Subject: [PATCH] =?UTF-8?q?perf(executors):=20lazy-load=20the=20executor?= =?UTF-8?q?=20registry=20=E2=80=94=20defer=20class=20imports=20+=20constru?= =?UTF-8?q?ction=20to=20first=20use=20(#11220)=20(#11421)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated in a combined 3-PR batch worktree off release/v3.8.51 tip (a sibling PR from the same author, #11495, was held out — a typecheck error in zai-web.ts only reproduced with this PR + #11495 boarded together, and cleared without #11495; isolated this PR alone confirmed clean on its own too, so the interaction belonged to #11495's side — see its comment). - Golden lock: executor-map-golden.test.ts — passes byte-identical (same keys, classes, provider identities, dispatch guards) - Focused tests part of batch's 94/94 node:test run - typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity — all OK - Full-repo lint: 228 pre-existing dashboard react-hooks/* findings, unrelated to this diff Thanks for the measured, careful methodology here — the golden-lock contract plus the isolated DATA_DIR benchmarking make this an easy PR to trust despite the wide surface (72 files). --- open-sse/executors/index.ts | 522 ++++++++---------- open-sse/executors/registry.ts | 43 +- .../handlers/chatCore/cliproxyModelMapping.ts | 4 +- .../chatCore/cliproxyapiCredentials.ts | 4 +- open-sse/handlers/chatCore/executorProxy.ts | 13 +- open-sse/handlers/videoGeneration.ts | 2 +- .../compression/eval/executorModelClient.ts | 3 +- scripts/check/check-known-symbols.ts | 4 +- src/lib/compression/judgeModelClient.ts | 3 +- .../providers/validation/anthropicFormat.ts | 8 +- src/lib/services/quotaAutoPing.ts | 5 +- tests/unit/adobe-firefly.test.ts | 9 +- tests/unit/azure-param-rules.test.ts | 7 +- tests/unit/blackbox-web.test.ts | 6 +- tests/unit/chatcore-executor-proxy.test.ts | 20 +- tests/unit/chatcore-translation-paths.test.ts | 2 +- tests/unit/chatgpt-web.test.ts | 10 +- tests/unit/chipotle-executor.test.ts | 4 +- tests/unit/claude-web.test.ts | 10 +- .../cloudflare-playground-provider.test.ts | 6 +- tests/unit/codebuddy-cn-provider.test.ts | 6 +- ...ex-quota-sync-no-proactive-refresh.test.ts | 4 +- tests/unit/command-code-executor.test.ts | 28 +- ...mmand-code-maxtokens-negative-5166.test.ts | 4 +- .../unit/command-code-user-array-5166.test.ts | 14 +- tests/unit/command-code-vision.test.ts | 30 +- tests/unit/cursor-apikey-provider.test.ts | 12 +- tests/unit/deepseek-web.test.ts | 8 +- tests/unit/devin-providers.test.ts | 29 +- tests/unit/duckduckgo-web-executor.test.ts | 2 +- tests/unit/executor-agy.test.ts | 27 +- tests/unit/executor-gitlab.test.ts | 12 +- tests/unit/executor-kimi.test.ts | 4 +- tests/unit/executor-map-golden.test.ts | 26 +- tests/unit/executor-nlpcloud.test.ts | 4 +- tests/unit/executor-registry.test.ts | 14 +- tests/unit/executor-web-cookie-sweep.test.ts | 4 +- tests/unit/executor-xai.test.ts | 4 +- ...-tier-providers-phase3-integration.test.ts | 4 +- ...e-tier-providers-wave2-integration.test.ts | 4 +- ...e-tier-providers-wave3-integration.test.ts | 5 +- .../unit/free-tier-providers-wave4-a.test.ts | 4 +- .../unit/free-tier-providers-wave4-b.test.ts | 4 +- ...e-tier-providers-wave4-integration.test.ts | 4 +- ...e-tier-providers-wave5-integration.test.ts | 4 +- tests/unit/g4f-space-gateway-6650.test.ts | 4 +- tests/unit/gemini-web.test.ts | 4 +- tests/unit/glm-executor.test.ts | 8 +- tests/unit/grok-web.test.ts | 4 +- tests/unit/kie-executor-routing.test.ts | 4 +- tests/unit/moonshot-k3.test.ts | 13 +- tests/unit/ninerouter-executor.test.ts | 4 +- tests/unit/openference-oauth-provider.test.ts | 5 +- tests/unit/perplexity-web.test.ts | 10 +- .../unit/poe-api-executor-regression.test.ts | 23 +- ...probe-6699-jules-executor-misroute.test.ts | 8 +- ...stoken-fallback-on-refresh-failure.test.ts | 4 +- ...ider-limits-force-refresh-imported.test.ts | 4 +- .../unit/provider-registry-freetheai.test.ts | 4 +- .../provider-request-failure-pipeline.test.ts | 2 +- tests/unit/provider-tinycms-web.test.ts | 17 +- tests/unit/providers-g4f-batch3.test.ts | 4 +- tests/unit/providers-yuanbao-web.test.ts | 6 +- ...pro-9550-amazon-q-alias-resolution.test.ts | 4 +- .../unit/search-providers-chat-guard.test.ts | 8 +- tests/unit/t3-chat-web.test.ts | 8 +- .../tinycms-secure-nonce-randomness.test.ts | 2 +- tests/unit/web-cookie-providers-new.test.ts | 34 +- tests/unit/windsurf-devin-executors.test.ts | 4 +- tests/unit/xai-oauth-provider.test.ts | 6 +- tests/unit/zed-provider.test.ts | 4 +- tests/unit/zenmux-free-provider.test.ts | 8 +- 72 files changed, 562 insertions(+), 590 deletions(-) diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index f0c57270bd..e5b7db3da1 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -1,246 +1,218 @@ import { SEARCH_PROVIDERS } from "../config/searchRegistry.ts"; -import { registerExecutor, getRegisteredExecutor, hasRegisteredExecutor } from "./registry.ts"; +import { + registerLazyExecutor, + loadRegisteredExecutor, + hasRegisteredExecutor, +} from "./registry.ts"; +// Type-only: pulls no runtime code, keeps DefaultExecutor the only eager class. import type { BaseExecutor } from "./base.ts"; -import { AntigravityExecutor } from "./antigravity.ts"; -import { GithubExecutor } from "./github.ts"; -import { GheCopilotExecutor } from "./ghe-copilot.ts"; -import { QoderExecutor } from "./qoder.ts"; -import { KiroExecutor } from "./kiro.ts"; -import { CodexExecutor } from "./codex.ts"; -import { CodexAppServerExecutor } from "./codex-app-server.ts"; -import { CursorExecutor } from "./cursor.ts"; -import { TraeExecutor } from "./trae.ts"; import { DefaultExecutor } from "./default.ts"; -import { BedrockExecutor } from "./bedrock.ts"; -import { GlmExecutor } from "./glm.ts"; -import { PollinationsExecutor } from "./pollinations.ts"; -import { CloudflareAIExecutor } from "./cloudflare-ai.ts"; -import { FreebuffExecutor } from "./freebuff.ts"; -import { OpencodeExecutor } from "./opencode.ts"; -import { VertexExecutor } from "./vertex.ts"; -import { CliproxyapiExecutor } from "./cliproxyapi.ts"; -import { DarioExecutor } from "./dario.ts"; -import { NineRouterExecutor } from "./ninerouter.ts"; -import { PerplexityWebExecutor } from "./perplexity-web.ts"; -import { GrokWebExecutor } from "./grok-web.ts"; -import { GeminiWebExecutor } from "./gemini-web.ts"; -import { TencentAIStudioWebExecutor } from "./tencent-aistudio-web.ts"; -import { GeminiBusinessExecutor } from "./gemini-business.ts"; -import { ChatGptWebExecutor } from "./chatgpt-web.ts"; -import { ChatGptWebCodexExecutor } from "./chatgpt-web-codex.ts"; -import { BlackboxWebExecutor } from "./blackbox-web.ts"; -import { MuseSparkWebExecutor } from "./muse-spark-web.ts"; -import { AzureOpenAIExecutor } from "./azure-openai.ts"; -import { AzureAiExecutor } from "./azure-ai.ts"; -import { CommandCodeExecutor } from "./commandCode.ts"; -import { GitlabExecutor } from "./gitlab.ts"; -import { NlpCloudExecutor } from "./nlpcloud.ts"; -import { DevinDesktopExecutor } from "./devin-desktop.ts"; -import { ZedHostedExecutor } from "./zed-hosted.ts"; -import { DevinCliExecutor } from "./devin-cli.ts"; -import { ZcodeExecutor } from "./zcode.ts"; -import { DevinCliAgenticExecutor } from "./devin-cli-agentic.ts"; -import { AuggieExecutor } from "./auggie.ts"; -import { DeepSeekWebExecutor } from "./deepseek-web.ts"; -import { DeepSeekWebWithAutoRefreshExecutor } from "./deepseek-web-with-auto-refresh.ts"; -import { AdaptaWebExecutor } from "./adapta-web.ts"; -import { CopilotWebExecutor } from "./copilot-web.ts"; -import { CopilotM365WebExecutor } from "./copilot-m365-web.ts"; -import { MicrosoftDesignerWebExecutor } from "./microsoft-designer-web.ts"; -import { AdobeFireflyExecutor } from "./adobe-firefly.ts"; -import { VeoAIFreeWebExecutor } from "./veoaifree-web.ts"; -import { DuckDuckGoWebExecutor } from "./duckduckgo-web.ts"; -import { FeloWebExecutor } from "./felo-web.ts"; -import { T3ChatWebExecutor } from "./t3-chat-web.ts"; -import { ClaudeWebExecutor } from "./claude-web.ts"; -import { InnerAiExecutor } from "./inner-ai.ts"; -import { HuggingChatExecutor } from "./huggingchat.ts"; -import { YuanbaoWebExecutor } from "./yuanbao-web.ts"; -import { PoeWebExecutor } from "./poe-web.ts"; -import { VeniceWebExecutor } from "./venice-web.ts"; -import { NotionWebExecutor } from "./notion-web.ts"; -import { V0VercelWebExecutor } from "./v0-vercel-web.ts"; -import { CheaperInferenceExecutor } from "./cheaperinference.ts"; -import { KimiWebExecutor } from "./kimi-web.ts"; -import { DoubaoWebExecutor } from "./doubao-web.ts"; -import { QwenWebExecutor } from "./qwen-web.ts"; -import { RaycastExecutor } from "./raycast.ts"; -import { HailuoWebExecutor } from "./hailuo-web.ts"; -import { ZaiWebExecutor } from "./zai-web.ts"; -import { KimiExecutor } from "./kimi.ts"; -import { MoonshotExecutor } from "./moonshot.ts"; -import { TheOldLlmExecutor } from "./theoldllm.ts"; -import { ChipotleExecutor } from "./chipotle.ts"; -import { LMArenaExecutor } from "./lmarena.ts"; -import { GrokCliExecutor } from "./grok-cli.ts"; -import { CodeBuddyCnExecutor } from "./codebuddy-cn.ts"; -import { ZenmuxFreeExecutor } from "./zenmux-free.ts"; -import { CloudflarePlaygroundExecutor } from "./cloudflare-playground.ts"; -import { TinyCmsExecutor } from "./tinycms.ts"; -import { HyperAgentExecutor } from "./hyperagent.ts"; -import { XaiExecutor } from "./xai.ts"; -import { PromptQlExecutor } from "./promptql.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 = { - antigravity: new AntigravityExecutor(), - agy: new AntigravityExecutor(), - github: new GithubExecutor(), - "ghe-copilot": new GheCopilotExecutor(), - qoder: new QoderExecutor(), - kiro: new KiroExecutor(), - "amazon-q": new KiroExecutor("amazon-q"), - bedrock: new BedrockExecutor(), - codex: new CodexExecutor(), - "codex-app-server": new CodexAppServerExecutor({}, "codex-app-server"), - "chatgpt-web-codex": new ChatGptWebCodexExecutor(), - "cgpt-codex": new ChatGptWebCodexExecutor(), - cursor: new CursorExecutor(), - trae: new TraeExecutor(), - glm: new GlmExecutor("glm"), - "glm-cn": new GlmExecutor("glm-cn"), - glmt: new GlmExecutor("glmt"), - cu: new CursorExecutor(), // Alias for cursor - "cursor-api": new CursorExecutor("cursor-api"), - cua: new CursorExecutor("cursor-api"), - "azure-openai": new AzureOpenAIExecutor(), - "azure-ai": new AzureAiExecutor(), - "command-code": new CommandCodeExecutor(), - cmd: new CommandCodeExecutor(), // Alias - gitlab: new GitlabExecutor(), - "gitlab-duo": new GitlabExecutor("gitlab-duo"), - nlpcloud: new NlpCloudExecutor(), - pollinations: new PollinationsExecutor(), - pol: new PollinationsExecutor(), // Alias - "cloudflare-ai": new CloudflareAIExecutor(), - cf: new CloudflareAIExecutor(), // Alias - freebuff: new FreebuffExecutor(), - fb: new FreebuffExecutor(), // Alias - "opencode-zen": new OpencodeExecutor("opencode-zen"), - "opencode-go": new OpencodeExecutor("opencode-go"), - opencode: new OpencodeExecutor("opencode-zen"), // Alias for opencode-zen - vertex: new VertexExecutor(), - "vertex-partner": new VertexExecutor(), - cliproxyapi: new CliproxyapiExecutor(), - cpa: new CliproxyapiExecutor(), // Alias - dario: new DarioExecutor(), - dr: new DarioExecutor(), // Alias - "9router": new NineRouterExecutor(), - nr: new NineRouterExecutor(), // Alias - "perplexity-web": new PerplexityWebExecutor(), - "pplx-web": new PerplexityWebExecutor(), // Alias - "grok-web": new GrokWebExecutor(), - "claude-web": new ClaudeWebExecutor(), - "cw-web": new ClaudeWebExecutor(), // Alias - "gemini-web": new GeminiWebExecutor(), - gweb: new GeminiWebExecutor(), // Alias - "gemini-business": new GeminiBusinessExecutor(), - gembiz: new GeminiBusinessExecutor(), // Alias - "chatgpt-web": new ChatGptWebExecutor(), - "cgpt-web": new ChatGptWebExecutor(), // Alias - "blackbox-web": new BlackboxWebExecutor(), - "bb-web": new BlackboxWebExecutor(), // Alias - "muse-spark-web": new MuseSparkWebExecutor(), - "ms-web": new MuseSparkWebExecutor(), // Alias - "devin-desktop": new DevinDesktopExecutor(), - "zed-hosted": new ZedHostedExecutor(), - "devin-cli": new DevinCliExecutor(), - zcode: new ZcodeExecutor(), - zc: new ZcodeExecutor(), // Alias - "devin-cli-agentic": new DevinCliAgenticExecutor(), - devin: new DevinCliExecutor(), // Alias - "deepseek-web": new DeepSeekWebWithAutoRefreshExecutor(), - "ds-web": new DeepSeekWebWithAutoRefreshExecutor(), // Alias - "adapta-web": new AdaptaWebExecutor(), - "adp-web": new AdaptaWebExecutor(), // Alias - "copilot-web": new CopilotWebExecutor(), - "copilot-m365-web": new CopilotM365WebExecutor(), - copilot: new CopilotWebExecutor(), // Alias - "microsoft-designer-web": new MicrosoftDesignerWebExecutor(), - msdesigner: new MicrosoftDesignerWebExecutor(), // Alias - "adobe-firefly": new AdobeFireflyExecutor(), - firefly: new AdobeFireflyExecutor(), // Alias - "veoaifree-web": new VeoAIFreeWebExecutor(), - "veo-free": new VeoAIFreeWebExecutor(), // Alias - "duckduckgo-web": new DuckDuckGoWebExecutor(), - ddgw: new DuckDuckGoWebExecutor(), // Alias - "felo-web": new FeloWebExecutor(), - felo: new FeloWebExecutor(), // Alias - "t3-web": new T3ChatWebExecutor(), - t3chat: new T3ChatWebExecutor(), // Alias - "inner-ai": new InnerAiExecutor(), - "in-ai": new InnerAiExecutor(), // Alias - huggingchat: new HuggingChatExecutor(), - hc: new HuggingChatExecutor(), // Alias - "yuanbao-web": new YuanbaoWebExecutor(), - "tencent-aistudio-web": new TencentAIStudioWebExecutor(), - tasw: new TencentAIStudioWebExecutor(), - ybw: new YuanbaoWebExecutor(), // Alias - "poe-web": new PoeWebExecutor(), +// R0.3 — declarative built-in table, made LAZY by #11220. +// +// The object literal below 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 its values are now deferred loaders instead of +// eagerly-constructed instances. At module load every ALIAS is registered in +// declaration order; the class import + construction happen on first use via +// loadRegisteredExecutor() and are cached in the same registry a static +// registration would have populated. +// +// Why: importing this barrel previously pulled all ~100 executor modules and +// constructed every instance at boot — ~0.7–1.2s and ~35MB of heap measured on +// top of the minimal set — even for deployments that use a handful of +// providers. Bundlers split the dynamic imports into on-demand chunks. +// +// Contract preserved (pinned by tests/unit/executor-map-golden.test.ts): +// - keys and their ORDER are byte-stable +// - each alias still gets its OWN instance (aliases never share) +// - ctor arguments are unchanged +const lazyExecutors: Record Promise> = { + antigravity: () => import("./antigravity.ts").then((m) => new m.AntigravityExecutor()), + agy: () => import("./antigravity.ts").then((m) => new m.AntigravityExecutor()), + github: () => import("./github.ts").then((m) => new m.GithubExecutor()), + "ghe-copilot": () => import("./ghe-copilot.ts").then((m) => new m.GheCopilotExecutor()), + qoder: () => import("./qoder.ts").then((m) => new m.QoderExecutor()), + kiro: () => import("./kiro.ts").then((m) => new m.KiroExecutor()), + "amazon-q": () => import("./kiro.ts").then((m) => new m.KiroExecutor("amazon-q")), + bedrock: () => import("./bedrock.ts").then((m) => new m.BedrockExecutor()), + codex: () => import("./codex.ts").then((m) => new m.CodexExecutor()), + "codex-app-server": () => + import("./codex-app-server.ts").then( + (m) => new m.CodexAppServerExecutor({}, "codex-app-server") + ), + "chatgpt-web-codex": () => + import("./chatgpt-web-codex.ts").then((m) => new m.ChatGptWebCodexExecutor()), + "cgpt-codex": () => + import("./chatgpt-web-codex.ts").then((m) => new m.ChatGptWebCodexExecutor()), + cursor: () => import("./cursor.ts").then((m) => new m.CursorExecutor()), + trae: () => import("./trae.ts").then((m) => new m.TraeExecutor()), + glm: () => import("./glm.ts").then((m) => new m.GlmExecutor("glm")), + "glm-cn": () => import("./glm.ts").then((m) => new m.GlmExecutor("glm-cn")), + glmt: () => import("./glm.ts").then((m) => new m.GlmExecutor("glmt")), + cu: () => import("./cursor.ts").then((m) => new m.CursorExecutor()), // Alias for cursor + "cursor-api": () => import("./cursor.ts").then((m) => new m.CursorExecutor("cursor-api")), + cua: () => import("./cursor.ts").then((m) => new m.CursorExecutor("cursor-api")), + "azure-openai": () => import("./azure-openai.ts").then((m) => new m.AzureOpenAIExecutor()), + "azure-ai": () => import("./azure-ai.ts").then((m) => new m.AzureAiExecutor()), + "command-code": () => import("./commandCode.ts").then((m) => new m.CommandCodeExecutor()), + cmd: () => import("./commandCode.ts").then((m) => new m.CommandCodeExecutor()), // Alias + gitlab: () => import("./gitlab.ts").then((m) => new m.GitlabExecutor()), + "gitlab-duo": () => import("./gitlab.ts").then((m) => new m.GitlabExecutor("gitlab-duo")), + nlpcloud: () => import("./nlpcloud.ts").then((m) => new m.NlpCloudExecutor()), + pollinations: () => import("./pollinations.ts").then((m) => new m.PollinationsExecutor()), + pol: () => import("./pollinations.ts").then((m) => new m.PollinationsExecutor()), // Alias + "cloudflare-ai": () => import("./cloudflare-ai.ts").then((m) => new m.CloudflareAIExecutor()), + cf: () => import("./cloudflare-ai.ts").then((m) => new m.CloudflareAIExecutor()), // Alias + freebuff: () => import("./freebuff.ts").then((m) => new m.FreebuffExecutor()), + fb: () => import("./freebuff.ts").then((m) => new m.FreebuffExecutor()), // Alias + "opencode-zen": () => + import("./opencode.ts").then((m) => new m.OpencodeExecutor("opencode-zen")), + "opencode-go": () => + import("./opencode.ts").then((m) => new m.OpencodeExecutor("opencode-go")), + opencode: () => + import("./opencode.ts").then((m) => new m.OpencodeExecutor("opencode-zen")), // Alias for opencode-zen + vertex: () => import("./vertex.ts").then((m) => new m.VertexExecutor()), + "vertex-partner": () => import("./vertex.ts").then((m) => new m.VertexExecutor()), + cliproxyapi: () => import("./cliproxyapi.ts").then((m) => new m.CliproxyapiExecutor()), + cpa: () => import("./cliproxyapi.ts").then((m) => new m.CliproxyapiExecutor()), // Alias + dario: () => import("./dario.ts").then((m) => new m.DarioExecutor()), + dr: () => import("./dario.ts").then((m) => new m.DarioExecutor()), // Alias + "9router": () => import("./ninerouter.ts").then((m) => new m.NineRouterExecutor()), + nr: () => import("./ninerouter.ts").then((m) => new m.NineRouterExecutor()), // Alias + "perplexity-web": () => + import("./perplexity-web.ts").then((m) => new m.PerplexityWebExecutor()), + "pplx-web": () => + import("./perplexity-web.ts").then((m) => new m.PerplexityWebExecutor()), // Alias + "grok-web": () => import("./grok-web.ts").then((m) => new m.GrokWebExecutor()), + "claude-web": () => import("./claude-web.ts").then((m) => new m.ClaudeWebExecutor()), + "cw-web": () => import("./claude-web.ts").then((m) => new m.ClaudeWebExecutor()), // Alias + "gemini-web": () => import("./gemini-web.ts").then((m) => new m.GeminiWebExecutor()), + gweb: () => import("./gemini-web.ts").then((m) => new m.GeminiWebExecutor()), // Alias + "gemini-business": () => + import("./gemini-business.ts").then((m) => new m.GeminiBusinessExecutor()), + gembiz: () => + import("./gemini-business.ts").then((m) => new m.GeminiBusinessExecutor()), // Alias + "chatgpt-web": () => import("./chatgpt-web.ts").then((m) => new m.ChatGptWebExecutor()), + "cgpt-web": () => import("./chatgpt-web.ts").then((m) => new m.ChatGptWebExecutor()), // Alias + "blackbox-web": () => import("./blackbox-web.ts").then((m) => new m.BlackboxWebExecutor()), + "bb-web": () => import("./blackbox-web.ts").then((m) => new m.BlackboxWebExecutor()), // Alias + "muse-spark-web": () => + import("./muse-spark-web.ts").then((m) => new m.MuseSparkWebExecutor()), + "ms-web": () => import("./muse-spark-web.ts").then((m) => new m.MuseSparkWebExecutor()), // Alias + "devin-desktop": () => import("./devin-desktop.ts").then((m) => new m.DevinDesktopExecutor()), + "zed-hosted": () => import("./zed-hosted.ts").then((m) => new m.ZedHostedExecutor()), + "devin-cli": () => import("./devin-cli.ts").then((m) => new m.DevinCliExecutor()), + zcode: () => import("./zcode.ts").then((m) => new m.ZcodeExecutor()), + zc: () => import("./zcode.ts").then((m) => new m.ZcodeExecutor()), // Alias + "devin-cli-agentic": () => + import("./devin-cli-agentic.ts").then((m) => new m.DevinCliAgenticExecutor()), + devin: () => import("./devin-cli.ts").then((m) => new m.DevinCliExecutor()), // Alias + "deepseek-web": () => + import("./deepseek-web-with-auto-refresh.ts").then( + (m) => new m.DeepSeekWebWithAutoRefreshExecutor() + ), + "ds-web": () => + import("./deepseek-web-with-auto-refresh.ts").then( + (m) => new m.DeepSeekWebWithAutoRefreshExecutor() + ), // Alias + "adapta-web": () => import("./adapta-web.ts").then((m) => new m.AdaptaWebExecutor()), + "adp-web": () => import("./adapta-web.ts").then((m) => new m.AdaptaWebExecutor()), // Alias + "copilot-web": () => import("./copilot-web.ts").then((m) => new m.CopilotWebExecutor()), + "copilot-m365-web": () => + import("./copilot-m365-web.ts").then((m) => new m.CopilotM365WebExecutor()), + copilot: () => import("./copilot-web.ts").then((m) => new m.CopilotWebExecutor()), // Alias + "microsoft-designer-web": () => + import("./microsoft-designer-web.ts").then((m) => new m.MicrosoftDesignerWebExecutor()), + msdesigner: () => + import("./microsoft-designer-web.ts").then((m) => new m.MicrosoftDesignerWebExecutor()), // Alias + "adobe-firefly": () => import("./adobe-firefly.ts").then((m) => new m.AdobeFireflyExecutor()), + firefly: () => import("./adobe-firefly.ts").then((m) => new m.AdobeFireflyExecutor()), // Alias + "veoaifree-web": () => import("./veoaifree-web.ts").then((m) => new m.VeoAIFreeWebExecutor()), + "veo-free": () => import("./veoaifree-web.ts").then((m) => new m.VeoAIFreeWebExecutor()), // Alias + "duckduckgo-web": () => + import("./duckduckgo-web.ts").then((m) => new m.DuckDuckGoWebExecutor()), + ddgw: () => import("./duckduckgo-web.ts").then((m) => new m.DuckDuckGoWebExecutor()), // Alias + "felo-web": () => import("./felo-web.ts").then((m) => new m.FeloWebExecutor()), + felo: () => import("./felo-web.ts").then((m) => new m.FeloWebExecutor()), // Alias + "t3-web": () => import("./t3-chat-web.ts").then((m) => new m.T3ChatWebExecutor()), + t3chat: () => import("./t3-chat-web.ts").then((m) => new m.T3ChatWebExecutor()), // Alias + "inner-ai": () => import("./inner-ai.ts").then((m) => new m.InnerAiExecutor()), + "in-ai": () => import("./inner-ai.ts").then((m) => new m.InnerAiExecutor()), // Alias + huggingchat: () => import("./huggingchat.ts").then((m) => new m.HuggingChatExecutor()), + hc: () => import("./huggingchat.ts").then((m) => new m.HuggingChatExecutor()), // Alias + "yuanbao-web": () => import("./yuanbao-web.ts").then((m) => new m.YuanbaoWebExecutor()), + "tencent-aistudio-web": () => + import("./tencent-aistudio-web.ts").then((m) => new m.TencentAIStudioWebExecutor()), + tasw: () => + import("./tencent-aistudio-web.ts").then((m) => new m.TencentAIStudioWebExecutor()), // Alias + ybw: () => import("./yuanbao-web.ts").then((m) => new m.YuanbaoWebExecutor()), // Alias + "poe-web": () => import("./poe-web.ts").then((m) => new m.PoeWebExecutor()), // #8969: do NOT alias canonical `poe` (API-key / api.poe.com) to PoeWebExecutor. // Registry declares executor:"default"; the hard-coded map previously won and // routed API-key traffic to GraphQL /api/gql_POST → HTTP 405. - "venice-web": new VeniceWebExecutor(), - ven: new VeniceWebExecutor(), // Alias - "notion-web": new NotionWebExecutor(), - nw: new NotionWebExecutor(), // Alias - promptql: new PromptQlExecutor(), - pql: new PromptQlExecutor(), // Alias - "v0-vercel-web": new V0VercelWebExecutor(), - v0: new V0VercelWebExecutor(), // Alias - "kimi-web": new KimiWebExecutor(), - "kimi-coding-apikey": new KimiExecutor("kimi-coding-apikey"), // Legacy alias - "kimi-coding": new KimiExecutor(), // Alias - moonshot: new MoonshotExecutor(), - kimi: new MoonshotExecutor("kimi"), // Hidden legacy Moonshot provider id - cheaperinference: new CheaperInferenceExecutor(), - cinf: new CheaperInferenceExecutor("cheaperinference"), // Alias - "doubao-web": new DoubaoWebExecutor(), - db: new DoubaoWebExecutor(), // Alias - "qwen-web": new QwenWebExecutor(), - raycast: new RaycastExecutor(), - rc: new RaycastExecutor(), // Alias - "hailuo-web": new HailuoWebExecutor(), - "zai-web": new ZaiWebExecutor(), - zw: new ZaiWebExecutor(), // Alias - theoldllm: new TheOldLlmExecutor(), - tllm: new TheOldLlmExecutor(), // Alias - chipotle: new ChipotleExecutor(), - pepper: new ChipotleExecutor(), // Alias - lmarena: new LMArenaExecutor(), - lma: new LMArenaExecutor(), // Alias - "grok-cli": new GrokCliExecutor(), - gc: new GrokCliExecutor(), // Alias - "codebuddy-cn": new CodeBuddyCnExecutor(), - cbcn: new CodeBuddyCnExecutor(), // Alias for codebuddy-cn - "zenmux-free": new ZenmuxFreeExecutor(), - "cloudflare-playground": new CloudflarePlaygroundExecutor(), - cfp: new CloudflarePlaygroundExecutor(), // Alias for cloudflare-playground - "tinycms-web": new TinyCmsExecutor(), - tcw: new TinyCmsExecutor(), // Alias - hyperagent: new HyperAgentExecutor(), - ha: new HyperAgentExecutor(), // Alias - zmf: new ZenmuxFreeExecutor(), // Alias for zenmux-free - auggie: new AuggieExecutor(), - xai: new XaiExecutor(), - "xai-oauth": new XaiExecutor("xai-oauth"), - xao: new XaiExecutor("xai-oauth"), - qw: new QwenWebExecutor(), // Alias - "conol-web": new ConolWebExecutor(), - cnl: new ConolWebExecutor(), // Alias + "venice-web": () => import("./venice-web.ts").then((m) => new m.VeniceWebExecutor()), + ven: () => import("./venice-web.ts").then((m) => new m.VeniceWebExecutor()), // Alias + "notion-web": () => import("./notion-web.ts").then((m) => new m.NotionWebExecutor()), + nw: () => import("./notion-web.ts").then((m) => new m.NotionWebExecutor()), // Alias + promptql: () => import("./promptql.ts").then((m) => new m.PromptQlExecutor()), + pql: () => import("./promptql.ts").then((m) => new m.PromptQlExecutor()), // Alias + "v0-vercel-web": () => import("./v0-vercel-web.ts").then((m) => new m.V0VercelWebExecutor()), + v0: () => import("./v0-vercel-web.ts").then((m) => new m.V0VercelWebExecutor()), // Alias + "kimi-web": () => import("./kimi-web.ts").then((m) => new m.KimiWebExecutor()), + "kimi-coding-apikey": () => + import("./kimi.ts").then((m) => new m.KimiExecutor("kimi-coding-apikey")), // Legacy alias + "kimi-coding": () => import("./kimi.ts").then((m) => new m.KimiExecutor()), // Alias + moonshot: () => import("./moonshot.ts").then((m) => new m.MoonshotExecutor()), + kimi: () => import("./moonshot.ts").then((m) => new m.MoonshotExecutor("kimi")), // Hidden legacy Moonshot provider id + cheaperinference: () => + import("./cheaperinference.ts").then((m) => new m.CheaperInferenceExecutor()), + cinf: () => + import("./cheaperinference.ts").then( + (m) => new m.CheaperInferenceExecutor("cheaperinference") + ), // Alias + "doubao-web": () => import("./doubao-web.ts").then((m) => new m.DoubaoWebExecutor()), + db: () => import("./doubao-web.ts").then((m) => new m.DoubaoWebExecutor()), // Alias + "qwen-web": () => import("./qwen-web.ts").then((m) => new m.QwenWebExecutor()), + raycast: () => import("./raycast.ts").then((m) => new m.RaycastExecutor()), + rc: () => import("./raycast.ts").then((m) => new m.RaycastExecutor()), // Alias + "hailuo-web": () => import("./hailuo-web.ts").then((m) => new m.HailuoWebExecutor()), + "zai-web": () => import("./zai-web.ts").then((m) => new m.ZaiWebExecutor()), + zw: () => import("./zai-web.ts").then((m) => new m.ZaiWebExecutor()), // Alias + theoldllm: () => import("./theoldllm.ts").then((m) => new m.TheOldLlmExecutor()), + tllm: () => import("./theoldllm.ts").then((m) => new m.TheOldLlmExecutor()), // Alias + chipotle: () => import("./chipotle.ts").then((m) => new m.ChipotleExecutor()), + pepper: () => import("./chipotle.ts").then((m) => new m.ChipotleExecutor()), // Alias + lmarena: () => import("./lmarena.ts").then((m) => new m.LMArenaExecutor()), + lma: () => import("./lmarena.ts").then((m) => new m.LMArenaExecutor()), // Alias + "grok-cli": () => import("./grok-cli.ts").then((m) => new m.GrokCliExecutor()), + gc: () => import("./grok-cli.ts").then((m) => new m.GrokCliExecutor()), // Alias + "codebuddy-cn": () => import("./codebuddy-cn.ts").then((m) => new m.CodeBuddyCnExecutor()), + cbcn: () => import("./codebuddy-cn.ts").then((m) => new m.CodeBuddyCnExecutor()), // Alias for codebuddy-cn + "zenmux-free": () => import("./zenmux-free.ts").then((m) => new m.ZenmuxFreeExecutor()), + "cloudflare-playground": () => + import("./cloudflare-playground.ts").then((m) => new m.CloudflarePlaygroundExecutor()), + cfp: () => + import("./cloudflare-playground.ts").then((m) => new m.CloudflarePlaygroundExecutor()), // Alias for cloudflare-playground + "tinycms-web": () => import("./tinycms.ts").then((m) => new m.TinyCmsExecutor()), + tcw: () => import("./tinycms.ts").then((m) => new m.TinyCmsExecutor()), // Alias + hyperagent: () => import("./hyperagent.ts").then((m) => new m.HyperAgentExecutor()), + ha: () => import("./hyperagent.ts").then((m) => new m.HyperAgentExecutor()), // Alias + zmf: () => import("./zenmux-free.ts").then((m) => new m.ZenmuxFreeExecutor()), // Alias for zenmux-free + auggie: () => import("./auggie.ts").then((m) => new m.AuggieExecutor()), + xai: () => import("./xai.ts").then((m) => new m.XaiExecutor()), + "xai-oauth": () => import("./xai.ts").then((m) => new m.XaiExecutor("xai-oauth")), + xao: () => import("./xai.ts").then((m) => new m.XaiExecutor("xai-oauth")), + qw: () => import("./qwen-web.ts").then((m) => new m.QwenWebExecutor()), // Alias + "conol-web": () => import("./conol-web.ts").then((m) => new m.ConolWebExecutor()), + cnl: () => import("./conol-web.ts").then((m) => new m.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) as [string, BaseExecutor][]) { - registerExecutor(alias, executor); +// Bootstrap: declare every built-in alias in the ExecutorRegistry. Duplicate +// aliases fail at module load, exactly as loudly as a duplicate object key +// would have failed at lint time. Instances materialize on first use (#11220). +for (const [alias, load] of Object.entries(lazyExecutors)) { + registerLazyExecutor(alias, load); } const defaultCache = new Map(); @@ -267,9 +239,9 @@ const CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS = new Set(["jules"]); // providers must be executed through /v1/search, never the chat-completions path. const CHAT_UNSUPPORTED_SEARCH_PROVIDERS = new Set(Object.keys(SEARCH_PROVIDERS)); -export function getExecutor(provider) { - const registered = getRegisteredExecutor(provider); - if (registered) return registered; +export async function getExecutor(provider: string): Promise { + const loaded = await loadRegisteredExecutor(provider); + if (loaded) return loaded; if (CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS.has(provider)) { 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.` @@ -285,77 +257,19 @@ export function getExecutor(provider) { throw err; } if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider)); - return defaultCache.get(provider); + return defaultCache.get(provider)!; } -export function hasSpecializedExecutor(provider) { +export function hasSpecializedExecutor(provider: string): boolean { return hasRegisteredExecutor(provider); } -export { registerExecutor, listExecutorAliases } from "./registry.ts"; - +export { + registerExecutor, + registerLazyExecutor, + listExecutorAliases, +} from "./registry.ts"; +// Value re-export: base.ts is already eager (DefaultExecutor extends it), and +// scripts/check/check-known-symbols.ts reads this export from the module. export { BaseExecutor } from "./base.ts"; -export { AntigravityExecutor } from "./antigravity.ts"; -export { GithubExecutor } from "./github.ts"; -export { QoderExecutor } from "./qoder.ts"; -export { KiroExecutor } from "./kiro.ts"; -export { CodexExecutor } from "./codex.ts"; -export { CursorExecutor } from "./cursor.ts"; -export { TraeExecutor } from "./trae.ts"; export { DefaultExecutor } from "./default.ts"; -export { BedrockExecutor } from "./bedrock.ts"; -export { GlmExecutor } from "./glm.ts"; -export { PollinationsExecutor } from "./pollinations.ts"; -export { CloudflareAIExecutor } from "./cloudflare-ai.ts"; -export { OpencodeExecutor } from "./opencode.ts"; -export { CliproxyapiExecutor } from "./cliproxyapi.ts"; -export { DarioExecutor } from "./dario.ts"; -export { NineRouterExecutor } from "./ninerouter.ts"; -export { VertexExecutor } from "./vertex.ts"; -export { PerplexityWebExecutor } from "./perplexity-web.ts"; -export { GrokWebExecutor } from "./grok-web.ts"; -export { GeminiWebExecutor } from "./gemini-web.ts"; -export { KieExecutor } from "./kie.ts"; -export { ChatGptWebExecutor } from "./chatgpt-web.ts"; -export { BlackboxWebExecutor } from "./blackbox-web.ts"; -export { MuseSparkWebExecutor } from "./muse-spark-web.ts"; -export { AzureOpenAIExecutor } from "./azure-openai.ts"; -export { AzureAiExecutor } from "./azure-ai.ts"; -export { CommandCodeExecutor } from "./commandCode.ts"; -export { GitlabExecutor } from "./gitlab.ts"; -export { NlpCloudExecutor } from "./nlpcloud.ts"; -export { DevinDesktopExecutor } from "./devin-desktop.ts"; -export { ZedHostedExecutor } from "./zed-hosted.ts"; -export { DevinCliExecutor } from "./devin-cli.ts"; -export { DevinCliAgenticExecutor } from "./devin-cli-agentic.ts"; -export { AuggieExecutor } from "./auggie.ts"; -export { CopilotWebExecutor } from "./copilot-web.ts"; -export { CopilotM365WebExecutor } from "./copilot-m365-web.ts"; -export { MicrosoftDesignerWebExecutor } from "./microsoft-designer-web.ts"; -export { AdobeFireflyExecutor } from "./adobe-firefly.ts"; -export { VeoAIFreeWebExecutor } from "./veoaifree-web.ts"; -export { DuckDuckGoWebExecutor } from "./duckduckgo-web.ts"; -export { FeloWebExecutor } from "./felo-web.ts"; -export { ClaudeWebExecutor } from "./claude-web.ts"; -export { DeepSeekWebExecutor } from "./deepseek-web.ts"; -export { DeepSeekWebWithAutoRefreshExecutor } from "./deepseek-web-with-auto-refresh.ts"; -export { AdaptaWebExecutor } from "./adapta-web.ts"; -export { YuanbaoWebExecutor } from "./yuanbao-web.ts"; -export { T3ChatWebExecutor } from "./t3-chat-web.ts"; -export { InnerAiExecutor } from "./inner-ai.ts"; -export { QwenWebExecutor } from "./qwen-web.ts"; -export { HailuoWebExecutor } from "./hailuo-web.ts"; -export { TheOldLlmExecutor } from "./theoldllm.ts"; -export { ChipotleExecutor } from "./chipotle.ts"; -export { LMArenaExecutor } from "./lmarena.ts"; -export { GrokCliExecutor } from "./grok-cli.ts"; -export { CodeBuddyCnExecutor } from "./codebuddy-cn.ts"; -export { ZenmuxFreeExecutor } from "./zenmux-free.ts"; -export { CloudflarePlaygroundExecutor } from "./cloudflare-playground.ts"; -export { TinyCmsExecutor } from "./tinycms.ts"; -export { HyperAgentExecutor } from "./hyperagent.ts"; -export { XaiExecutor } from "./xai.ts"; -export { MoonshotExecutor } from "./moonshot.ts"; -export { CheaperInferenceExecutor } from "./cheaperinference.ts"; -export { PromptQlExecutor } from "./promptql.ts"; -export { ConolWebExecutor } from "./conol-web.ts"; diff --git a/open-sse/executors/registry.ts b/open-sse/executors/registry.ts index 4d6a002ecd..ae61fd85ed 100644 --- a/open-sse/executors/registry.ts +++ b/open-sse/executors/registry.ts @@ -28,11 +28,46 @@ export function getRegisteredExecutor(alias: string): BaseExecutor | undefined { return registry.get(alias); } -export function hasRegisteredExecutor(alias: string): boolean { - return registry.has(alias); +// ── #11220: lazy registration ─────────────────────────────────────────────── +// Aliases may register a deferred loader instead of an instance. The alias and +// its registration ORDER are declared eagerly — hasRegisteredExecutor() and +// listExecutorAliases() stay synchronous and the golden snapshot keeps its +// shape — while the class import + construction happen on first use. A +// completed load caches into `registry`, so later resolution is identical to a +// static registration. +const lazyLoaders = new Map Promise>(); +const lazyInFlight = new Map>(); + +export function registerLazyExecutor(alias: string, load: () => Promise): void { + if (registry.has(alias) || lazyLoaders.has(alias)) { + throw new Error(`executor alias already registered: "${alias}"`); + } + lazyLoaders.set(alias, load); } -/** All registered aliases, in registration order. */ +export function loadRegisteredExecutor(alias: string): Promise | undefined { + const cached = registry.get(alias); + if (cached) return Promise.resolve(cached); + const load = lazyLoaders.get(alias); + if (!load) return undefined; + let inFlight = lazyInFlight.get(alias); + if (!inFlight) { + inFlight = load().then((executor) => { + registerExecutor(alias, executor); + lazyLoaders.delete(alias); + lazyInFlight.delete(alias); + return executor; + }); + lazyInFlight.set(alias, inFlight); + } + return inFlight; +} + +export function hasRegisteredExecutor(alias: string): boolean { + return registry.has(alias) || lazyLoaders.has(alias); +} + +/** All registered aliases — static and lazy — in registration order. */ export function listExecutorAliases(): string[] { - return [...registry.keys()]; + return [...registry.keys(), ...lazyLoaders.keys()]; } diff --git a/open-sse/handlers/chatCore/cliproxyModelMapping.ts b/open-sse/handlers/chatCore/cliproxyModelMapping.ts index 99914e2f01..af8e00112b 100644 --- a/open-sse/handlers/chatCore/cliproxyModelMapping.ts +++ b/open-sse/handlers/chatCore/cliproxyModelMapping.ts @@ -14,12 +14,12 @@ type ExecutorInput = { model: string; body: unknown; - [key: string]: unknown; }; +// No index signature: executors (BaseExecutor subclasses) must satisfy this +// structurally, and class instances don't carry index signatures. type ExecutorLike = { execute: (input: ExecutorInput) => Promise; - [key: string]: unknown; }; export type CliproxyapiModelMapping = Record | null | undefined; diff --git a/open-sse/handlers/chatCore/cliproxyapiCredentials.ts b/open-sse/handlers/chatCore/cliproxyapiCredentials.ts index 4789afb62c..e9fc5fbe7b 100644 --- a/open-sse/handlers/chatCore/cliproxyapiCredentials.ts +++ b/open-sse/handlers/chatCore/cliproxyapiCredentials.ts @@ -19,12 +19,12 @@ import type { ProviderCredentials } from "../../executors/base.ts"; type ExecutorInput = { credentials: ProviderCredentials; - [key: string]: unknown; }; +// No index signature: executors (BaseExecutor subclasses) must satisfy this +// structurally, and class instances don't carry index signatures. type ExecutorLike = { execute: (input: ExecutorInput) => Promise; - [key: string]: unknown; }; /** diff --git a/open-sse/handlers/chatCore/executorProxy.ts b/open-sse/handlers/chatCore/executorProxy.ts index 19dc55f4d0..bb0a3b0753 100644 --- a/open-sse/handlers/chatCore/executorProxy.ts +++ b/open-sse/handlers/chatCore/executorProxy.ts @@ -76,12 +76,15 @@ async function loadCliproxyapiSettings(): Promise<{ * dedicated-credential wrappers applied. Used by the direct `cliproxyapi` leg * and the CLIProxyAPI branch of `fallback`. */ -function resolveCliproxyapiExecutor( +async function resolveCliproxyapiExecutor( cliproxyapiModelMapping: Record | null, dedicatedApiKey: string | null ) { return wrapExecutorWithCliproxyapiCredentials( - wrapExecutorWithCliproxyapiModelMapping(getExecutor("cliproxyapi"), cliproxyapiModelMapping), + wrapExecutorWithCliproxyapiModelMapping( + await getExecutor("cliproxyapi"), + cliproxyapiModelMapping + ), dedicatedApiKey ); } @@ -138,7 +141,7 @@ export async function resolveExecutorWithProxy( // backend on specific failures. The backend defaults to CLIProxyAPI so every // pre-existing fallback config behaves exactly as before; fallbackBackend // === "dario" opts the retry leg over to Dario instead. - const nativeExec = getExecutor(prov); + const nativeExec = await getExecutor(prov); const fallbackBackend: FallbackBackend = cfg.fallbackBackend; const { fallbackCodes, dedicatedApiKey } = await loadCliproxyapiSettings(); @@ -146,8 +149,8 @@ export async function resolveExecutorWithProxy( // the native leg must keep seeing the original, unmapped model. const proxyExec = fallbackBackend === "dario" - ? getExecutor("dario") - : resolveCliproxyapiExecutor(cfg.cliproxyapiModelMapping, dedicatedApiKey); + ? await getExecutor("dario") + : await resolveCliproxyapiExecutor(cfg.cliproxyapiModelMapping, dedicatedApiKey); const backendLabel = fallbackBackend === "dario" ? "Dario" : "CLIProxyAPI"; const isRetryableStatus = (s: number) => fallbackCodes.includes(s) || s === 0; diff --git a/open-sse/handlers/videoGeneration.ts b/open-sse/handlers/videoGeneration.ts index 17cb1db0dc..e97b95995c 100644 --- a/open-sse/handlers/videoGeneration.ts +++ b/open-sse/handlers/videoGeneration.ts @@ -374,7 +374,7 @@ async function handleVertexVeoGeneration({ model, body, credentials, log }) { * Submits an AnimateDiff or SVD workflow, polls for completion, fetches output video */ async function handleVeoAiFreeVideoGeneration({ model, provider, body, credentials, log }) { - const executor = getExecutor(provider); + const executor = await getExecutor(provider); if (!executor) { return { success: false, status: 400, error: `Unknown video provider: ${provider}` }; } diff --git a/open-sse/services/compression/eval/executorModelClient.ts b/open-sse/services/compression/eval/executorModelClient.ts index dba6a65592..7847d99057 100644 --- a/open-sse/services/compression/eval/executorModelClient.ts +++ b/open-sse/services/compression/eval/executorModelClient.ts @@ -16,9 +16,10 @@ export function createExecutorModelClient( credentials: ProviderCredentials, costPerKTokenOut?: number ): ModelClient { - const executor = getExecutor(provider); return { async complete(model: string, messages: ChatTurn[]): Promise { + // #11220: getExecutor is async (lazy registry) — resolve per call. + const executor = await getExecutor(provider); const body = { model, messages, stream: false }; const input: ExecuteInput = { model, diff --git a/scripts/check/check-known-symbols.ts b/scripts/check/check-known-symbols.ts index 6537db388c..d3beb0ba88 100644 --- a/scripts/check/check-known-symbols.ts +++ b/scripts/check/check-known-symbols.ts @@ -142,8 +142,8 @@ export function diffComboStrategies( * getExecutor() na função main(). */ export function extractExecutorAliases(indexSource: string): string[] { - const start = indexSource.indexOf("const executors = {"); - if (start < 0) throw new Error("could not find `const executors = {` in executors/index.ts"); + const start = indexSource.indexOf("const lazyExecutors"); + if (start < 0) throw new Error("could not find 'const lazyExecutors' in executors/index.ts"); const end = indexSource.indexOf("\n};", start); if (end < 0) throw new Error("could not find end of executors map (`\\n};`)"); const block = indexSource.slice(start, end); diff --git a/src/lib/compression/judgeModelClient.ts b/src/lib/compression/judgeModelClient.ts index b1f2f1d61e..022b761a0e 100644 --- a/src/lib/compression/judgeModelClient.ts +++ b/src/lib/compression/judgeModelClient.ts @@ -18,9 +18,10 @@ export function createPricedJudgeClient( provider: string, credentials: ProviderCredentials ): ModelClient { - const executor = getExecutor(provider); return { async complete(model: string, messages: ChatTurn[]): Promise { + // #11220: getExecutor is async (lazy registry) — resolve per call. + const executor = await getExecutor(provider); const input: ExecuteInput = { model, body: { model, messages, stream: false }, diff --git a/src/lib/providers/validation/anthropicFormat.ts b/src/lib/providers/validation/anthropicFormat.ts index 8ed7f84515..da2d567694 100644 --- a/src/lib/providers/validation/anthropicFormat.ts +++ b/src/lib/providers/validation/anthropicFormat.ts @@ -132,12 +132,13 @@ export async function validateClaudeOAuthInline({ modelId: string | null | undefined; providerSpecificData?: Record; }) { - const testModelId = - providerSpecificData?.validationModelId || modelId || "claude-haiku-4-5-20251001"; + const override = providerSpecificData?.validationModelId; + const testModelId: string = + typeof override === "string" && override ? override : modelId || "claude-haiku-4-5-20251001"; try { const { getExecutor } = await import("@omniroute/open-sse/executors/index.ts"); - const { response } = await getExecutor("claude").execute({ + const executed = await (await getExecutor("claude")).execute({ model: testModelId, body: { model: testModelId, @@ -148,6 +149,7 @@ export async function validateClaudeOAuthInline({ credentials: { accessToken: apiKey, providerSpecificData }, }); + const response = executed instanceof Response ? executed : executed.response; if (response.status === 401 || response.status === 403) { return { valid: false, error: "Invalid OAuth token" }; } diff --git a/src/lib/services/quotaAutoPing.ts b/src/lib/services/quotaAutoPing.ts index 94e5ea8247..4a9ce8fe3e 100644 --- a/src/lib/services/quotaAutoPing.ts +++ b/src/lib/services/quotaAutoPing.ts @@ -23,6 +23,7 @@ import { logger } from "@omniroute/open-sse/utils/logger.ts"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts"; import { getExecutor } from "@omniroute/open-sse/executors/index.ts"; +import type { BaseExecutor } from "@omniroute/open-sse/executors/base"; import { getCodexUsage } from "@omniroute/open-sse/services/usage/codex.ts"; import { getSettings, getProviderConnections, updateProviderConnection } from "@/lib/localDb"; import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLeaseIsolation"; @@ -67,7 +68,7 @@ export interface QuotaAutoPingDeps { accessToken?: string, providerSpecificData?: JsonRecord ) => Promise; - getExecutor: (provider: string) => { execute: (input: JsonRecord) => Promise }; + getExecutor: (provider: string) => Promise; canExecuteProvider: (provider: string) => boolean; isConnectionUnavailableToAuxiliaryActivity: (connectionId: string) => Promise; } @@ -208,7 +209,7 @@ async function sendCodexPing( providerConfig: QuotaAutoPingProviderConfig, deps: QuotaAutoPingDeps ): Promise { - const executor = deps.getExecutor("codex"); + const executor = await deps.getExecutor("codex"); const result = await executor.execute({ model: providerConfig.pingModel, stream: true, diff --git a/tests/unit/adobe-firefly.test.ts b/tests/unit/adobe-firefly.test.ts index 200907ed3c..0f87a1817c 100644 --- a/tests/unit/adobe-firefly.test.ts +++ b/tests/unit/adobe-firefly.test.ts @@ -84,7 +84,7 @@ test("adobe-firefly is registered in VIDEO_PROVIDERS with adobe-firefly-video fo }); test("getExecutor(adobe-firefly) rejects chat completions", async () => { - const executor = getExecutor("adobe-firefly"); + const executor = await getExecutor("adobe-firefly"); assert.ok(executor); const result = await executor.execute({ model: "adobe-firefly/nano-banana-pro", @@ -95,9 +95,10 @@ test("getExecutor(adobe-firefly) rejects chat completions", async () => { stream: false, credentials: { apiKey: "tok" }, }); - assert.ok(result.response, "executor must return a Response wrapper"); - assert.equal(result.response.status, 400); - const bodyText = await result.response.text(); + const response = result instanceof Response ? result : result.response; + assert.ok(response, "executor must return a Response wrapper"); + assert.equal(response.status, 400); + const bodyText = await response.text(); assert.match(bodyText, /images\/generations|videos\/generations|media-generation/i); }); diff --git a/tests/unit/azure-param-rules.test.ts b/tests/unit/azure-param-rules.test.ts index 4e46b0788f..78292835f2 100644 --- a/tests/unit/azure-param-rules.test.ts +++ b/tests/unit/azure-param-rules.test.ts @@ -5,7 +5,8 @@ import { applyAzureParamRules, AZURE_COMPLETION_TOKEN_DEPLOYMENT, } from "../../open-sse/executors/azureParamRules.ts"; -import { getExecutor, AzureAiExecutor } from "../../open-sse/executors/index.ts"; +import { getExecutor } from "../../open-sse/executors/index.ts"; +import { AzureAiExecutor } from "../../open-sse/executors/azure-ai.ts"; /** * Regression guards for two Azure 400s observed against a live Azure AI Foundry @@ -87,8 +88,8 @@ test("the regex does not match unrelated names by accident", () => { assert.equal(AZURE_COMPLETION_TOKEN_DEPLOYMENT.test("Kimi-K2.7-Code"), false); }); -test("azure-ai resolves to AzureAiExecutor, not the bare DefaultExecutor", () => { - const executor = getExecutor("azure-ai"); +test("azure-ai resolves to AzureAiExecutor, not the bare DefaultExecutor", async () => { + const executor = await getExecutor("azure-ai"); assert.ok( executor instanceof AzureAiExecutor, "azure-ai must have its own executor so it inherits the Azure param rules" diff --git a/tests/unit/blackbox-web.test.ts b/tests/unit/blackbox-web.test.ts index cd7f88704c..049a6f7530 100644 --- a/tests/unit/blackbox-web.test.ts +++ b/tests/unit/blackbox-web.test.ts @@ -59,11 +59,11 @@ function mockFetchCapture(status = 200, text = "Hello from Blackbox") { }; } -test("BlackboxWebExecutor is registered in executor index", () => { +test("BlackboxWebExecutor is registered in executor index", async () => { assert.ok(hasSpecializedExecutor("blackbox-web")); assert.ok(hasSpecializedExecutor("bb-web")); - const executor = getExecutor("blackbox-web"); - const alias = getExecutor("bb-web"); + const executor = await getExecutor("blackbox-web"); + const alias = await getExecutor("bb-web"); assert.ok(executor instanceof BlackboxWebExecutor); assert.ok(alias instanceof BlackboxWebExecutor); }); diff --git a/tests/unit/chatcore-executor-proxy.test.ts b/tests/unit/chatcore-executor-proxy.test.ts index a67e23a4f1..dd4cc6e2fa 100644 --- a/tests/unit/chatcore-executor-proxy.test.ts +++ b/tests/unit/chatcore-executor-proxy.test.ts @@ -41,7 +41,7 @@ after(() => { test("no config (disabled by default) returns the provider's own executor", async () => { clearUpstreamProxyConfigCache("openai"); const exec = await resolveExecutorWithProxy("openai"); - assert.equal(exec, getExecutor("openai")); + assert.equal(exec, await getExecutor("openai")); }); test("mode 'native' returns the provider's own executor", async () => { @@ -52,7 +52,7 @@ test("mode 'native' returns the provider's own executor", async () => { }); clearUpstreamProxyConfigCache("openai"); const exec = await resolveExecutorWithProxy("openai"); - assert.equal(exec, getExecutor("openai")); + assert.equal(exec, await getExecutor("openai")); }); test("mode 'cliproxyapi' returns the CLIProxyAPI passthrough executor", async () => { @@ -63,7 +63,7 @@ test("mode 'cliproxyapi' returns the CLIProxyAPI passthrough executor", async () }); clearUpstreamProxyConfigCache("anthropic"); const exec = await resolveExecutorWithProxy("anthropic"); - assert.equal(exec, getExecutor("cliproxyapi")); + assert.equal(exec, await getExecutor("cliproxyapi")); }); test("mode 'fallback' returns a distinct wrapper owning its own execute()", async () => { @@ -74,8 +74,8 @@ test("mode 'fallback' returns a distinct wrapper owning its own execute()", asyn }); clearUpstreamProxyConfigCache("openai"); const exec = await resolveExecutorWithProxy("openai"); - assert.notEqual(exec, getExecutor("openai")); - assert.notEqual(exec, getExecutor("cliproxyapi")); + assert.notEqual(exec, await getExecutor("openai")); + assert.notEqual(exec, await getExecutor("cliproxyapi")); assert.equal(typeof exec.execute, "function"); }); @@ -94,7 +94,7 @@ test("connection override 'claude-native' selects CLIProxyAPI even when provider const exec = await resolveExecutorWithProxy("openai", undefined, { cliproxyapiMode: "claude-native", }); - assert.equal(exec, getExecutor("cliproxyapi")); + assert.equal(exec, await getExecutor("cliproxyapi")); }); test("connection override 'claude-native' selects CLIProxyAPI even with no provider config (default)", async () => { @@ -102,7 +102,7 @@ test("connection override 'claude-native' selects CLIProxyAPI even with no provi const exec = await resolveExecutorWithProxy("anthropic", undefined, { cliproxyapiMode: "claude-native", }); - assert.equal(exec, getExecutor("cliproxyapi")); + assert.equal(exec, await getExecutor("cliproxyapi")); }); test("no connection override + provider mode native → native executor (unchanged)", async () => { @@ -115,13 +115,13 @@ test("no connection override + provider mode native → native executor (unchang const exec = await resolveExecutorWithProxy("openai", undefined, { someOtherField: "x", }); - assert.equal(exec, getExecutor("openai")); + assert.equal(exec, await getExecutor("openai")); }); test("connection override absent (undefined providerSpecificData) preserves default behaviour", async () => { clearUpstreamProxyConfigCache("openai"); const exec = await resolveExecutorWithProxy("openai"); - assert.equal(exec, getExecutor("openai")); + assert.equal(exec, await getExecutor("openai")); }); test("connection override wins over provider mode 'fallback'", async () => { @@ -135,5 +135,5 @@ test("connection override wins over provider mode 'fallback'", async () => { cliproxyapiMode: "claude-native", }); // Connection override short-circuits to the passthrough executor, not the fallback wrapper. - assert.equal(exec, getExecutor("cliproxyapi")); + assert.equal(exec, await getExecutor("cliproxyapi")); }); diff --git a/tests/unit/chatcore-translation-paths.test.ts b/tests/unit/chatcore-translation-paths.test.ts index 6ecb048788..aa4fce1922 100644 --- a/tests/unit/chatcore-translation-paths.test.ts +++ b/tests/unit/chatcore-translation-paths.test.ts @@ -456,7 +456,7 @@ test("chatCore times out upstream execution before provider response headers", a // (fresh-DB default leaves it off → the waitFor below would never resolve; // failed deterministically on CI and on an isolated run, incl. at v3.8.18). await settingsDb.updateSettings({ call_log_pipeline_enabled: true }); - const executor = getExecutor("openai"); + const executor = await getExecutor("openai"); const originalGetTimeoutMs = executor.getTimeoutMs?.bind(executor); executor.getTimeoutMs = () => 200; diff --git a/tests/unit/chatgpt-web.test.ts b/tests/unit/chatgpt-web.test.ts index 267c0eb6dc..13814f1353 100644 --- a/tests/unit/chatgpt-web.test.ts +++ b/tests/unit/chatgpt-web.test.ts @@ -370,16 +370,16 @@ function reset() { // ─── Registration ─────────────────────────────────────────────────────────── -test("ChatGptWebExecutor is registered in executor index", () => { +test("ChatGptWebExecutor is registered in executor index", async () => { assert.ok(hasSpecializedExecutor("chatgpt-web")); assert.ok(hasSpecializedExecutor("cgpt-web")); - const executor = getExecutor("chatgpt-web"); + const executor = await getExecutor("chatgpt-web"); assert.ok(executor instanceof ChatGptWebExecutor); }); -test("ChatGptWebExecutor alias resolves to same type", () => { - const a = getExecutor("chatgpt-web"); - const b = getExecutor("cgpt-web"); +test("ChatGptWebExecutor alias resolves to same type", async () => { + const a = await getExecutor("chatgpt-web"); + const b = await getExecutor("cgpt-web"); assert.ok(a instanceof ChatGptWebExecutor); assert.ok(b instanceof ChatGptWebExecutor); }); diff --git a/tests/unit/chipotle-executor.test.ts b/tests/unit/chipotle-executor.test.ts index 07a093cd01..ff0f1308b9 100644 --- a/tests/unit/chipotle-executor.test.ts +++ b/tests/unit/chipotle-executor.test.ts @@ -63,14 +63,14 @@ describe("ChipotleExecutor", () => { it("is registered in executor index", async () => { const { getExecutor } = await import("../../open-sse/executors/index.ts"); - const exec = getExecutor("chipotle"); + const exec = await getExecutor("chipotle"); assert.ok(exec, "chipotle executor should be registered"); assert.ok(exec instanceof ChipotleExecutor); }); it("pepper alias works", async () => { const { getExecutor } = await import("../../open-sse/executors/index.ts"); - const exec = getExecutor("pepper"); + const exec = await getExecutor("pepper"); assert.ok(exec, "pepper alias should be registered"); assert.ok(exec instanceof ChipotleExecutor); }); diff --git a/tests/unit/claude-web.test.ts b/tests/unit/claude-web.test.ts index 043ff129e6..1ec40feb13 100644 --- a/tests/unit/claude-web.test.ts +++ b/tests/unit/claude-web.test.ts @@ -22,14 +22,14 @@ test("B: ClaudeWebExecutor alias cw-web is registered", () => { assert.ok(hasSpecializedExecutor("cw-web")); }); -test("C: ClaudeWebExecutor can be retrieved from executor registry", () => { - const executor = getExecutor("claude-web"); +test("C: ClaudeWebExecutor can be retrieved from executor registry", async () => { + const executor = await getExecutor("claude-web"); assert.ok(executor instanceof ClaudeWebExecutor); }); -test("D: ClaudeWebExecutor cw-web alias resolves to same type", () => { - const a = getExecutor("claude-web"); - const b = getExecutor("cw-web"); +test("D: ClaudeWebExecutor cw-web alias resolves to same type", async () => { + const a = await getExecutor("claude-web"); + const b = await getExecutor("cw-web"); assert.ok(a instanceof ClaudeWebExecutor); assert.ok(b instanceof ClaudeWebExecutor); }); diff --git a/tests/unit/cloudflare-playground-provider.test.ts b/tests/unit/cloudflare-playground-provider.test.ts index f1e07a6b61..092389eb1f 100644 --- a/tests/unit/cloudflare-playground-provider.test.ts +++ b/tests/unit/cloudflare-playground-provider.test.ts @@ -167,9 +167,9 @@ test("cloudflare-playground registry entry has no-auth shape and curated models" assert.equal(llama?.supportsReasoning, undefined); }); -test("executor resolves for both the id and the cfp alias", () => { - const byId = getExecutor("cloudflare-playground"); - const byAlias = getExecutor("cfp"); +test("executor resolves for both the id and the cfp alias", async () => { + const byId = await getExecutor("cloudflare-playground"); + const byAlias = await getExecutor("cfp"); assert.ok(byId instanceof CloudflarePlaygroundExecutor); assert.ok(byAlias instanceof CloudflarePlaygroundExecutor); }); diff --git a/tests/unit/codebuddy-cn-provider.test.ts b/tests/unit/codebuddy-cn-provider.test.ts index 129910a73b..0d08c293cb 100644 --- a/tests/unit/codebuddy-cn-provider.test.ts +++ b/tests/unit/codebuddy-cn-provider.test.ts @@ -185,10 +185,10 @@ test("codebuddy-cn vision flag is set on the visual models", () => { } }); -test("getExecutor returns the CodeBuddyCnExecutor for 'codebuddy-cn' and the 'cbcn' alias", () => { - const e = getExecutor("codebuddy-cn"); +test("getExecutor returns the CodeBuddyCnExecutor for 'codebuddy-cn' and the 'cbcn' alias", async () => { + const e = await getExecutor("codebuddy-cn"); assert.ok(e instanceof CodeBuddyCnExecutor, "executor must be CodeBuddyCnExecutor"); - const aliasExec = getExecutor("cbcn"); + const aliasExec = await getExecutor("cbcn"); assert.ok(aliasExec instanceof CodeBuddyCnExecutor, "alias 'cbcn' must resolve to same executor"); }); diff --git a/tests/unit/codex-quota-sync-no-proactive-refresh.test.ts b/tests/unit/codex-quota-sync-no-proactive-refresh.test.ts index e82f9de0dc..9a2e7496a5 100644 --- a/tests/unit/codex-quota-sync-no-proactive-refresh.test.ts +++ b/tests/unit/codex-quota-sync-no-proactive-refresh.test.ts @@ -31,7 +31,7 @@ const { getExecutor } = await import("../../open-sse/executors/index.ts"); const { refreshAndUpdateCredentials } = await import("../../src/lib/usage/providerLimits.ts"); test("codex: quota-sync must NOT proactively rotate the refresh_token (Auth0 family-revocation cascade guard)", async () => { - const exec = getExecutor("codex"); + const exec = await getExecutor("codex"); const origNeeds = exec.needsRefresh; const origRefresh = exec.refreshCredentials; let refreshCalls = 0; @@ -68,7 +68,7 @@ test("codex: quota-sync must NOT proactively rotate the refresh_token (Auth0 fam }); test("non-rotating OAuth provider is still refreshed proactively from quota-sync (gate is not over-broad)", async () => { - const exec = getExecutor("cursor"); + const exec = await getExecutor("cursor"); const origNeeds = exec.needsRefresh; const origRefresh = exec.refreshCredentials; let refreshCalls = 0; diff --git a/tests/unit/command-code-executor.test.ts b/tests/unit/command-code-executor.test.ts index e9b305468d..14cb0ae5a6 100644 --- a/tests/unit/command-code-executor.test.ts +++ b/tests/unit/command-code-executor.test.ts @@ -91,15 +91,15 @@ test("Command Code provider catalog has pinned models and alias lookup", () => { assert.equal(getRegistryEntry("cmd"), entry); }); -test("getExecutor returns the specialized Command Code executor", () => { +test("getExecutor returns the specialized Command Code executor", async () => { assert.equal(hasSpecializedExecutor("command-code"), true); - assert.ok(getExecutor("command-code") instanceof CommandCodeExecutor); - assert.ok(getExecutor("cmd") instanceof CommandCodeExecutor); + assert.ok((await getExecutor("command-code")) instanceof CommandCodeExecutor); + assert.ok((await getExecutor("cmd")) instanceof CommandCodeExecutor); }); test("Command Code executor posts a flat OpenAI body + standard headers to /provider/v1/chat/completions (#10265)", async () => { const calls = captureFetch({}); - const executor = getExecutor("command-code"); + const executor = await getExecutor("command-code"); const { response, url, headers } = await executor.execute({ model: "gpt-5.4-mini", stream: false, @@ -143,7 +143,7 @@ test("Command Code executor posts a flat OpenAI body + standard headers to /prov test("Command Code executor passes reasoning/thinking fields through at the top level of the OpenAI body", async () => { const calls = captureFetch({}); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "deepseek/deepseek-v4-pro", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -166,7 +166,7 @@ test("Command Code executor passes reasoning/thinking fields through at the top test("Command Code executor honors body.model rewrite from payload rules", async () => { const calls = captureFetch({}); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "deepseek-v4-pro-max", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -187,7 +187,7 @@ test("Command Code executor maps unsupported minimal reasoning_effort to low (up const calls = captureFetch({}); // `minimal` (a Muse Spark catalog tier) must be downgraded to `low` before // the wire body is built, on BOTH the combo and single-model paths. - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "poolside/laguna-s-2.1-free", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -232,7 +232,7 @@ test("Command Code executor passes the upstream OpenAI SSE stream through untouc }); }; - const { response } = await getExecutor("command-code").execute({ + const { response } = (await getExecutor("command-code")).execute({ model: "gpt-5.4", stream: true, credentials: { apiKey: "cc_test_key" }, @@ -266,7 +266,7 @@ test("Command Code executor passes the upstream OpenAI JSON through untouched (n }); }; - const { response } = await getExecutor("command-code").execute({ + const { response } = (await getExecutor("command-code")).execute({ model: "gpt-5.4-mini", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -279,7 +279,7 @@ test("Command Code executor passes the upstream OpenAI JSON through untouched (n test("Command Code executor surfaces upstream errors", async () => { globalThis.fetch = async () => new Response("bad key", { status: 401, statusText: "Unauthorized" }); - const upstreamFailure = await getExecutor("command-code").execute({ + const upstreamFailure = (await getExecutor("command-code")).execute({ model: "gpt-5.4-mini", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -291,7 +291,7 @@ test("Command Code executor surfaces upstream errors", async () => { test("Command Code executor omits max_tokens when the client does not supply one", async () => { const calls = captureFetch({}); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "zai-org/GLM-5.1", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -305,7 +305,7 @@ test("Command Code executor omits max_tokens when the client does not supply one test("Command Code executor clamps an oversized client-supplied max_tokens to the endpoint ceiling", async () => { const calls = captureFetch({}); // A client asking for more than the 200000 endpoint ceiling is clamped down. - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "deepseek/deepseek-v4-pro", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -316,7 +316,7 @@ test("Command Code executor clamps an oversized client-supplied max_tokens to th test("Command Code executor honors a smaller client-provided max_tokens", async () => { const calls = captureFetch({}); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "zai-org/GLM-5.1", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -350,7 +350,7 @@ test("Command Code stream preserves the upstream OpenAI usage chunk (passthrough globalThis.fetch = async () => new Response(sse, { status: 200, headers: { "Content-Type": "text/event-stream" } }); - const { response } = await getExecutor("command-code").execute({ + const { response } = (await getExecutor("command-code")).execute({ model: "gpt-5.4-mini", stream: true, credentials: { apiKey: "cc_test_key" }, diff --git a/tests/unit/command-code-maxtokens-negative-5166.test.ts b/tests/unit/command-code-maxtokens-negative-5166.test.ts index 2e30975b99..7b31f8d9a3 100644 --- a/tests/unit/command-code-maxtokens-negative-5166.test.ts +++ b/tests/unit/command-code-maxtokens-negative-5166.test.ts @@ -40,7 +40,7 @@ async function captureBody(body: Record): Promise { calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) }); return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]); }; - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "deepseek/deepseek-v4-pro", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -73,4 +73,4 @@ test("Command Code omits max_tokens when the client sends 0 (#5166)", async () = test("Command Code still honors a positive client max_tokens after the #5166 fix", async () => { const call = await captureBody({ max_tokens: 2048 }); assert.equal(call.body.max_tokens, 2048); -}); \ No newline at end of file +}); diff --git a/tests/unit/command-code-user-array-5166.test.ts b/tests/unit/command-code-user-array-5166.test.ts index a692012ed0..cb4cd5f682 100644 --- a/tests/unit/command-code-user-array-5166.test.ts +++ b/tests/unit/command-code-user-array-5166.test.ts @@ -15,9 +15,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -const TEST_DATA_DIR = fs.mkdtempSync( - path.join(os.tmpdir(), "omniroute-cmd-code-user-array-5166-") -); +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-cmd-code-user-array-5166-")); process.env.DATA_DIR = TEST_DATA_DIR; const { getExecutor } = await import("../../open-sse/executors/index.ts"); @@ -58,7 +56,7 @@ function captureFetch(response: Response) { test("#5166 user message with multi-part array content passes through as an OpenAI array", async () => { const calls = captureFetch(okResponse()); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "deepseek/deepseek-v4-pro", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -86,7 +84,7 @@ test("#5166 user message with multi-part array content passes through as an Open test("#5166 user message with single text-part array passes through", async () => { const calls = captureFetch(okResponse()); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "deepseek/deepseek-v4-pro", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -102,7 +100,7 @@ test("#5166 user message with single text-part array passes through", async () = test("#5166 user message with plain string content passes through unchanged", async () => { const calls = captureFetch(okResponse()); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "deepseek/deepseek-v4-pro", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -114,7 +112,7 @@ test("#5166 user message with plain string content passes through unchanged", as test("#5166 user message with mixed parts (text + image_url) keeps all parts", async () => { const calls = captureFetch(okResponse()); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "deepseek/deepseek-v4-pro", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -135,4 +133,4 @@ test("#5166 user message with mixed parts (text + image_url) keeps all parts", a assert.equal(parts.length, 2, "text + image both preserved"); assert.equal(parts[0].text, "Describe this:"); assert.equal(parts[1].type, "image_url"); -}); \ No newline at end of file +}); diff --git a/tests/unit/command-code-vision.test.ts b/tests/unit/command-code-vision.test.ts index 3640139755..5af6176a49 100644 --- a/tests/unit/command-code-vision.test.ts +++ b/tests/unit/command-code-vision.test.ts @@ -66,7 +66,7 @@ function wireModel(calls: FetchCall[]): string { test("#10809: command-code/mimo-v2.5 wire model is normalized to xiaomi/mimo-v2.5", async () => { const calls = captureFetch(okResponse()); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "command-code/mimo-v2.5", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -77,7 +77,7 @@ test("#10809: command-code/mimo-v2.5 wire model is normalized to xiaomi/mimo-v2. test("#10809: cmd/mimo-v2.5 (alias prefix) is also normalized", async () => { const calls = captureFetch(okResponse()); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "cmd/mimo-v2.5", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -88,7 +88,7 @@ test("#10809: cmd/mimo-v2.5 (alias prefix) is also normalized", async () => { test("#10809: already vendor-prefixed wire ids pass through unchanged", async () => { const calls = captureFetch(okResponse()); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "command-code/deepseek/deepseek-v4-pro", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -104,7 +104,7 @@ test("#10809: already vendor-prefixed wire ids pass through unchanged", async () test("image_url parts pass through unchanged (text + image preserved)", async () => { const calls = captureFetch(okResponse()); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "MiniMaxAI/MiniMax-M3", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -125,15 +125,12 @@ test("image_url parts pass through unchanged (text + image preserved)", async () assert.equal(content.length, 2); assert.equal(content[0].type, "text"); assert.equal(content[1].type, "image_url", "image_url part preserved as-is"); - assert.equal( - (content[1].image_url as { url: string }).url, - "data:image/png;base64,iVBORw0KGgo=" - ); + assert.equal((content[1].image_url as { url: string }).url, "data:image/png;base64,iVBORw0KGgo="); }); test("Anthropic Messages-style source image blocks pass through unchanged", async () => { const calls = captureFetch(okResponse()); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "xiaomi/mimo-v2.5", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -161,12 +158,15 @@ test("Anthropic Messages-style source image blocks pass through unchanged", asyn assert.equal(content.length, 2, "text + image parts preserved"); assert.equal(content[1].type, "image"); assert.equal((content[1].source as { type: string }).type, "base64"); - assert.equal((content[1].source as { data: string }).data, "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="); + assert.equal( + (content[1].source as { data: string }).data, + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" + ); }); test("Anthropic source.url image block passes through unchanged", async () => { const calls = captureFetch(okResponse()); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "xiaomi/mimo-v2.5", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -191,7 +191,7 @@ test("Anthropic source.url image block passes through unchanged", async () => { test("multiple image parts are all preserved", async () => { const calls = captureFetch(okResponse()); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "minimax-m3", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -217,7 +217,7 @@ test("multiple image parts are all preserved", async () => { test("plain string content passes through unchanged", async () => { const calls = captureFetch(okResponse()); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "minimax-m3", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -233,7 +233,7 @@ test("text-only model still forwards image parts (passthrough, no CLI stripping) // The /provider/v1 OpenAI surface accepts image content for any model id; the // executor forwards content untouched, so there is no text-only stripping. const calls = captureFetch(okResponse()); - await getExecutor("command-code").execute({ + (await getExecutor("command-code")).execute({ model: "deepseek/deepseek-v4-pro", stream: false, credentials: { apiKey: "cc_test_key" }, @@ -253,4 +253,4 @@ test("text-only model still forwards image parts (passthrough, no CLI stripping) const content = userContent(calls) as Record[]; assert.equal(content.length, 2, "content array forwarded unchanged"); assert.equal(content[1].type, "image_url"); -}); \ No newline at end of file +}); diff --git a/tests/unit/cursor-apikey-provider.test.ts b/tests/unit/cursor-apikey-provider.test.ts index da803448e3..890d044f2d 100644 --- a/tests/unit/cursor-apikey-provider.test.ts +++ b/tests/unit/cursor-apikey-provider.test.ts @@ -15,8 +15,9 @@ const { cursorProvider, cursor_apiProvider } = await import("../../open-sse/config/providers/registry/cursor/index.ts"); const { REGISTRY, generateAliasMap, getProviderCategory } = await import("../../open-sse/config/providerRegistry.ts"); -const { CursorExecutor, getExecutor, hasSpecializedExecutor } = +const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts"); +const { CursorExecutor } = await import("../../open-sse/executors/cursor.ts"); const { __resetCursorApiKeyAuthForTest } = await import("../../open-sse/services/cursorApiKeyAuth.ts"); const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts"); @@ -54,14 +55,14 @@ describe("cursor-api provider wiring", () => { assert.equal(isManagedProviderConnectionId("cursor-api"), true); }); - it("routes cursor-api and its alias to a CursorExecutor bound to the cursor-api id", () => { + it("routes cursor-api and its alias to a CursorExecutor bound to the cursor-api id", async () => { for (const key of ["cursor-api", "cua"]) { assert.equal(hasSpecializedExecutor(key), true, key); - const executor = getExecutor(key); + const executor = await getExecutor(key); assert.ok(executor instanceof CursorExecutor, key); assert.equal(executor.getProvider(), "cursor-api"); } - assert.equal(getExecutor("cursor").getProvider(), "cursor"); + assert.equal((await getExecutor("cursor")).getProvider(), "cursor"); }); }); @@ -154,6 +155,9 @@ describe("CursorExecutor credential resolution", () => { body: { messages: [] }, stream: false, credentials: { apiKey: API_KEY, connectionId: "cursor-api-test" }, + signal: null, + log: null, + upstreamExtraHeaders: null, }); assert.equal(result.response.status, 500); diff --git a/tests/unit/deepseek-web.test.ts b/tests/unit/deepseek-web.test.ts index af1fc82c49..17266edf00 100644 --- a/tests/unit/deepseek-web.test.ts +++ b/tests/unit/deepseek-web.test.ts @@ -18,13 +18,13 @@ test("DeepSeekWebExecutor registered as deepseek-web and ds-web", () => { assert.ok(hasSpecializedExecutor("ds-web")); }); -test("getExecutor returns DeepSeekWebWithAutoRefreshExecutor", () => { - const exec = getExecutor("deepseek-web"); +test("getExecutor returns DeepSeekWebWithAutoRefreshExecutor", async () => { + const exec = await getExecutor("deepseek-web"); assert.ok(exec instanceof DeepSeekWebWithAutoRefreshExecutor); }); -test("alias ds-web resolves same executor", () => { - assert.ok(getExecutor("ds-web") instanceof DeepSeekWebWithAutoRefreshExecutor); +test("alias ds-web resolves same executor", async () => { + assert.ok(await getExecutor("ds-web") instanceof DeepSeekWebWithAutoRefreshExecutor); }); test("provider name is deepseek-web", () => { diff --git a/tests/unit/devin-providers.test.ts b/tests/unit/devin-providers.test.ts index 0009246c50..b0cd44b24a 100644 --- a/tests/unit/devin-providers.test.ts +++ b/tests/unit/devin-providers.test.ts @@ -4,6 +4,7 @@ import { readFile } from "node:fs/promises"; import { REGISTRY } from "../../open-sse/config/providers/index.ts"; import { getExecutor, hasSpecializedExecutor } from "../../open-sse/executors/index.ts"; +import { DevinDesktopExecutor } from "../../open-sse/executors/devin-desktop.ts"; import { OAUTH_PROVIDERS } from "../../src/shared/constants/providers/oauth.ts"; test("Devin Desktop exposes the supported BYOK-free catalog", () => { @@ -20,26 +21,29 @@ test("public registries do not expose windsurf or ws aliases", () => { assert.ok(Object.values(REGISTRY).every((entry) => entry.alias !== "ws")); }); -test("executor factory exposes only the dedicated Devin Desktop executor", () => { +test("executor factory exposes only the dedicated Devin Desktop executor", async () => { assert.equal(hasSpecializedExecutor("devin-desktop"), true); assert.equal(hasSpecializedExecutor("windsurf"), false); assert.equal(hasSpecializedExecutor("ws"), false); - assert.equal(getExecutor("devin-desktop").constructor.name, "DevinDesktopExecutor"); + assert.equal((await getExecutor("devin-desktop")).constructor.name, "DevinDesktopExecutor"); }); -test("Devin Desktop executor uses the live endpoint and verified default identity", () => { - const executor = getExecutor("devin-desktop"); +test("Devin Desktop executor uses the live endpoint and verified default identity", async () => { + const executor = await getExecutor("devin-desktop"); delete process.env.DEVIN_DESKTOP_VERSION; + // getExecutor() widens to BaseExecutor whose buildUrl requires args; the concrete + // DevinDesktopExecutor override takes none. + const desktop = executor as DevinDesktopExecutor; assert.equal( - executor.buildUrl(), + desktop.buildUrl(), "https://server.codeium.com/exa.api_server_pb.ApiServerService/GetChatMessage" ); assert.equal(executor.buildHeaders({ accessToken: "token" })["User-Agent"], "windsurf/3.6.27"); }); -test("Devin Desktop executor applies only valid version overrides to its user agent", () => { - const executor = getExecutor("devin-desktop"); +test("Devin Desktop executor applies only valid version overrides to its user agent", async () => { + const executor = await getExecutor("devin-desktop"); process.env.DEVIN_DESKTOP_VERSION = "3.5.1"; try { assert.equal(executor.buildHeaders({ accessToken: "token" })["User-Agent"], "windsurf/3.5.1"); @@ -51,7 +55,7 @@ test("Devin Desktop executor applies only valid version overrides to its user ag }); test("Devin Desktop executor returns 401 before the upstream call without a token", async () => { - const executor = getExecutor("devin-desktop"); + const executor = await getExecutor("devin-desktop"); const originalFetch = globalThis.fetch; let fetchCalled = false; globalThis.fetch = async () => { @@ -67,16 +71,17 @@ test("Devin Desktop executor returns 401 before the upstream call without a toke credentials: {}, }); + const response = result instanceof Response ? result : result.response; assert.equal(fetchCalled, false); - assert.equal(result.response.status, 401); - assert.match(await result.response.text(), /Devin Desktop API key is required/); + assert.equal(response.status, 401); + assert.match(await response.text(), /Devin Desktop API key is required/); } finally { globalThis.fetch = originalFetch; } }); test("Devin Desktop stream errors do not expose local paths or stack traces", async () => { - const executor = getExecutor("devin-desktop"); + const executor = await getExecutor("devin-desktop"); const originalFetch = globalThis.fetch; globalThis.fetch = async () => new Response( @@ -95,7 +100,7 @@ test("Devin Desktop stream errors do not expose local paths or stack traces", as stream: true, credentials: { accessToken: "test-token" }, }); - const text = await result.response.text(); + const text = await (result instanceof Response ? result : result.response).text(); assert.match(text, /stream failed/); assert.doesNotMatch(text, /private\.ts|\/Users\/example|\bat\s+\//); diff --git a/tests/unit/duckduckgo-web-executor.test.ts b/tests/unit/duckduckgo-web-executor.test.ts index 5469185cc7..62438cae1e 100644 --- a/tests/unit/duckduckgo-web-executor.test.ts +++ b/tests/unit/duckduckgo-web-executor.test.ts @@ -241,7 +241,7 @@ describe("DuckDuckGoWebExecutor", () => { it("should be registered in executor index", async () => { const { getExecutor } = await import("../../open-sse/executors/index.ts"); - const executor = getExecutor("duckduckgo-web"); + const executor = await getExecutor("duckduckgo-web"); assert.ok(executor, "executor should be registered in index"); assert.equal( typeof executor.execute, diff --git a/tests/unit/executor-agy.test.ts b/tests/unit/executor-agy.test.ts index 008b6694fa..5dc983b527 100644 --- a/tests/unit/executor-agy.test.ts +++ b/tests/unit/executor-agy.test.ts @@ -1,8 +1,11 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { getExecutor, AntigravityExecutor } from "../../open-sse/executors/index.ts"; -import { processAntigravitySSEPayload } from "../../open-sse/executors/antigravity.ts"; +import { getExecutor } from "../../open-sse/executors/index.ts"; +import { + AntigravityExecutor, + processAntigravitySSEPayload, +} from "../../open-sse/executors/antigravity.ts"; function emptyCollected(): any { return { @@ -14,21 +17,21 @@ function emptyCollected(): any { }; } -test("getExecutor('agy') returns AntigravityExecutor (not DefaultExecutor)", () => { - const executor = getExecutor("agy"); +test("getExecutor('agy') returns AntigravityExecutor (not DefaultExecutor)", async () => { + const executor = await getExecutor("agy"); assert.ok(executor instanceof AntigravityExecutor, "agy provider should use AntigravityExecutor"); }); -test("getExecutor('antigravity') returns AntigravityExecutor", () => { - const executor = getExecutor("antigravity"); +test("getExecutor('antigravity') returns AntigravityExecutor", async () => { + const executor = await getExecutor("antigravity"); assert.ok( executor instanceof AntigravityExecutor, "antigravity provider should use AntigravityExecutor" ); }); -test("getExecutor('agy') builds valid streaming URL", () => { - const executor = getExecutor("agy"); +test("getExecutor('agy') builds valid streaming URL", async () => { + const executor = await getExecutor("agy"); const url = executor.buildUrl("gemini-3.7-flash-high", true); assert.ok( url.includes("streamGenerateContent?alt=sse"), @@ -36,8 +39,8 @@ test("getExecutor('agy') builds valid streaming URL", () => { ); }); -test("getExecutor('agy') builds valid non-streaming URL", () => { - const executor = getExecutor("agy"); +test("getExecutor('agy') builds valid non-streaming URL", async () => { + const executor = await getExecutor("agy"); const url = executor.buildUrl("gemini-3.7-flash-high", false); // Antigravity executor always uses streaming endpoint (buildUrl ignores stream flag) assert.ok( @@ -46,8 +49,8 @@ test("getExecutor('agy') builds valid non-streaming URL", () => { ); }); -test("getExecutor('agy') buildHeaders returns Bearer auth", () => { - const executor = getExecutor("agy"); +test("getExecutor('agy') buildHeaders returns Bearer auth", async () => { + const executor = await getExecutor("agy"); const headers = executor.buildHeaders({ accessToken: "test-token" }); assert.equal(headers.Authorization, "Bearer test-token"); }); diff --git a/tests/unit/executor-gitlab.test.ts b/tests/unit/executor-gitlab.test.ts index f222eeb681..8bc41efb1f 100644 --- a/tests/unit/executor-gitlab.test.ts +++ b/tests/unit/executor-gitlab.test.ts @@ -19,11 +19,11 @@ function jsonResponse(body: unknown, status = 200) { }); } -test("GitlabExecutor is registered in the executor index", () => { +test("GitlabExecutor is registered in the executor index", async () => { assert.equal(hasSpecializedExecutor("gitlab"), true); - assert.ok(getExecutor("gitlab") instanceof GitlabExecutor); + assert.ok((await getExecutor("gitlab")) instanceof GitlabExecutor); assert.equal(hasSpecializedExecutor("gitlab-duo"), true); - assert.ok(getExecutor("gitlab-duo") instanceof GitlabExecutor); + assert.ok((await getExecutor("gitlab-duo")) instanceof GitlabExecutor); }); test("GitlabExecutor posts PAT-backed code suggestion requests to the configured instance", async () => { @@ -147,7 +147,7 @@ test("GitlabExecutor maps upstream auth failures to OpenAI-style errors", async }); test("GitlabExecutor uses GitLab direct_access for gitlab-duo and persists the cache", async () => { - const executor = getExecutor("gitlab-duo") as GitlabExecutor; + const executor = (await getExecutor("gitlab-duo")) as GitlabExecutor; const originalFetch = globalThis.fetch; const calls: Array<{ url: string; headers: Record }> = []; const refreshedPatches: Array> = []; @@ -223,7 +223,7 @@ test("GitlabExecutor uses GitLab direct_access for gitlab-duo and persists the c }); test("GitlabExecutor falls back to the public Code Suggestions endpoint when direct_access is disabled", async () => { - const executor = getExecutor("gitlab-duo") as GitlabExecutor; + const executor = (await getExecutor("gitlab-duo")) as GitlabExecutor; const originalFetch = globalThis.fetch; const calls: string[] = []; @@ -274,7 +274,7 @@ test("GitlabExecutor falls back to the public Code Suggestions endpoint when dir // Code Suggestions completions endpoint (same resilience as the 403-disabled case // above), instead of surfacing an opaque 401 token error with no fallback. test("GitlabExecutor falls back to the public Code Suggestions endpoint when direct_access returns 401", async () => { - const executor = getExecutor("gitlab-duo") as GitlabExecutor; + const executor = (await getExecutor("gitlab-duo")) as GitlabExecutor; const originalFetch = globalThis.fetch; const calls: string[] = []; diff --git a/tests/unit/executor-kimi.test.ts b/tests/unit/executor-kimi.test.ts index 848d181865..00a95b1b0d 100644 --- a/tests/unit/executor-kimi.test.ts +++ b/tests/unit/executor-kimi.test.ts @@ -47,10 +47,10 @@ function credentials( } describe("KimiExecutor", () => { - it("forces the primary Kimi upstream to stream while preserving JSON client semantics", () => { + it("forces the primary Kimi upstream to stream while preserving JSON client semantics", async () => { assert.equal(REGISTRY.kimi?.forceStream, true); - const executor = getExecutor("kimi"); + const executor = await getExecutor("kimi"); assert.ok(executor instanceof MoonshotExecutor); assert.equal( executor.buildUrl("kimi-k2.5", true, 0, credentials(FORMATS.OPENAI)), diff --git a/tests/unit/executor-map-golden.test.ts b/tests/unit/executor-map-golden.test.ts index ea83e56756..e589755afc 100644 --- a/tests/unit/executor-map-golden.test.ts +++ b/tests/unit/executor-map-golden.test.ts @@ -37,11 +37,11 @@ function readSpecializedKeys(): string[] { 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 mapMatch = src.match(/const lazyExecutors[^\n]*= \{([\s\S]*?)\n\};/); + assert.ok(mapMatch, "lazyExecutors 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 /); + const m = line.match(/^\s*(?:"([^"]+)"|([A-Za-z0-9_$-]+)):\s*(?:async )?\(\)\s*=>/); if (m) keys.push(m[1] ?? m[2]); } return keys; @@ -74,7 +74,7 @@ function describeExecutor(instance: unknown): { const specializedKeys = readSpecializedKeys(); -test("golden: specialized executor map — key → class + provider identity + config source", () => { +test("golden: specialized executor map — key → class + provider identity + config source", async () => { assert.ok(specializedKeys.length >= 100, `suspiciously few keys: ${specializedKeys.length}`); const entries: Record< @@ -85,7 +85,7 @@ test("golden: specialized executor map — key → class + provider identity + c for (const key of [...specializedKeys].sort()) { assert.equal(hasSpecializedExecutor(key), true, `hasSpecializedExecutor(${key})`); - const instance = getExecutor(key); + const instance = await getExecutor(key); entries[key] = describeExecutor(instance); const group = byInstance.get(instance) ?? []; group.push(key); @@ -106,18 +106,18 @@ test("golden: specialized executor map — key → class + provider identity + c }); }); -test("golden: getExecutor dispatch rules — fallback, cache and 400-guards", () => { +test("golden: getExecutor dispatch rules — fallback, cache and 400-guards", async () => { // 1. Unknown provider → DefaultExecutor for that provider, memoized. const unknown = "golden-test-unknown-provider"; assert.equal(hasSpecializedExecutor(unknown), false); - const fallback = getExecutor(unknown); + const fallback = await getExecutor(unknown); assert.ok(fallback instanceof DefaultExecutor, "fallback must be DefaultExecutor"); - assert.equal(getExecutor(unknown), fallback, "DefaultExecutor fallback must be cached"); + assert.equal(await getExecutor(unknown), fallback, "DefaultExecutor fallback must be cached"); // 2. Cloud-agent guard (#6699) and search guard (#10274) → status-400 throw. - const guardOutcome = (provider: string) => { + const guardOutcome = async (provider: string) => { try { - getExecutor(provider); + await getExecutor(provider); return { throws: false as const }; } catch (err) { const e = err as Error & { status?: number }; @@ -128,7 +128,9 @@ test("golden: getExecutor dispatch rules — fallback, cache and 400-guards", () 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)])), + cloudAgentGuard: { jules: await guardOutcome("jules") }, + searchGuard: Object.fromEntries( + await Promise.all(searchProviders.map(async (p) => [p, await guardOutcome(p)])) + ), }); }); diff --git a/tests/unit/executor-nlpcloud.test.ts b/tests/unit/executor-nlpcloud.test.ts index 5dc6791483..c131bcdd8e 100644 --- a/tests/unit/executor-nlpcloud.test.ts +++ b/tests/unit/executor-nlpcloud.test.ts @@ -30,9 +30,9 @@ function sseResponse(events: string[]) { ); } -test("NlpCloudExecutor is registered in the executor index", () => { +test("NlpCloudExecutor is registered in the executor index", async () => { assert.equal(hasSpecializedExecutor("nlpcloud"), true); - assert.ok(getExecutor("nlpcloud") instanceof NlpCloudExecutor); + assert.ok((await getExecutor("nlpcloud")) instanceof NlpCloudExecutor); }); test.skip("NlpCloudExecutor converts OpenAI messages into chatbot input/context/history and wraps JSON responses", async () => { diff --git a/tests/unit/executor-registry.test.ts b/tests/unit/executor-registry.test.ts index c05128681b..b1c82c8099 100644 --- a/tests/unit/executor-registry.test.ts +++ b/tests/unit/executor-registry.test.ts @@ -21,13 +21,13 @@ test.after(() => { fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); -test("built-ins are registered at module load and resolve through the registry", () => { +test("built-ins are registered at module load and resolve through the registry", async () => { 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); + assert.equal(await getExecutor(alias), getRegisteredExecutor(alias)); + assert.ok((await getExecutor(alias)) instanceof BaseExecutor); } }); @@ -37,21 +37,21 @@ test("registerExecutor throws on duplicate alias", () => { }); }); -test("registering a new alias makes it resolvable via getExecutor and hasSpecializedExecutor", () => { +test("registering a new alias makes it resolvable via getExecutor and hasSpecializedExecutor", async () => { 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); + assert.equal(await getExecutor(alias), instance); }); -test("registry lookup is exact — Object.prototype names are not executors", () => { +test("registry lookup is exact — Object.prototype names are not executors", async () => { // 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); + assert.ok((await getExecutor(name)) instanceof DefaultExecutor, name); } }); diff --git a/tests/unit/executor-web-cookie-sweep.test.ts b/tests/unit/executor-web-cookie-sweep.test.ts index 9392247ab8..1f67c92fca 100644 --- a/tests/unit/executor-web-cookie-sweep.test.ts +++ b/tests/unit/executor-web-cookie-sweep.test.ts @@ -119,7 +119,7 @@ describe("web-cookie + noauth executor wrapper contract sweep", () => { describe("WEB_COOKIE_PROVIDERS (26)", () => { for (const providerId of WEB_COOKIE_IDS) { it(`${providerId} executor returns wrapper shape`, async () => { - const executor = getExecutor(providerId); + const executor = await getExecutor(providerId); assert.ok(executor, `[${providerId}] getExecutor must return an executor`); const result = await executor.execute({ @@ -160,7 +160,7 @@ describe("web-cookie + noauth executor wrapper contract sweep", () => { for (const providerId of TARGETS) { it(`${providerId} noauth executor returns wrapper shape`, async () => { - const executor = getExecutor(providerId); + const executor = await getExecutor(providerId); assert.ok(executor, `[${providerId}] getExecutor must return an executor`); // Use a pre-aborted signal so the executor short-circuits via diff --git a/tests/unit/executor-xai.test.ts b/tests/unit/executor-xai.test.ts index 4668efd22e..991d606454 100644 --- a/tests/unit/executor-xai.test.ts +++ b/tests/unit/executor-xai.test.ts @@ -15,9 +15,9 @@ import { xaiProvider } from "../../open-sse/config/providers/registry/xai/index. const credentials = { apiKey: "test-key" }; -test("XaiExecutor is registered under the 'xai' key and set as the registry executor", () => { +test("XaiExecutor is registered under the 'xai' key and set as the registry executor", async () => { assert.equal(hasSpecializedExecutor("xai"), true); - assert.ok(getExecutor("xai") instanceof XaiExecutor); + assert.ok((await getExecutor("xai")) instanceof XaiExecutor); assert.equal(xaiProvider.executor, "xai"); }); diff --git a/tests/unit/free-tier-providers-phase3-integration.test.ts b/tests/unit/free-tier-providers-phase3-integration.test.ts index 89c3141d8b..6bbf9d5862 100644 --- a/tests/unit/free-tier-providers-phase3-integration.test.ts +++ b/tests/unit/free-tier-providers-phase3-integration.test.ts @@ -20,7 +20,7 @@ const providers = [ ] as const; for (const [id, alias, endpoint] of providers) { - test(`${id} is wired through registry, metadata, endpoint and default executor`, () => { + test(`${id} is wired through registry, metadata, endpoint and default executor`, async () => { const registry = REGISTRY[id]; const metadata = APIKEY_PROVIDERS[id]; @@ -35,7 +35,7 @@ for (const [id, alias, endpoint] of providers) { assert.equal(metadata.hasFree, true); assert.equal(metadata.passthroughModels, true); assert.ok(typeof metadata.freeNote === "string" && metadata.freeNote.length > 0); - assert.ok(getExecutor(id) instanceof DefaultExecutor); + assert.ok((await getExecutor(id)) instanceof DefaultExecutor); assert.equal(isValidModel(id, "future/live-catalog-model"), true); assert.equal(isValidModel(alias, "future/live-catalog-model"), true); }); diff --git a/tests/unit/free-tier-providers-wave2-integration.test.ts b/tests/unit/free-tier-providers-wave2-integration.test.ts index 83128bd3c0..aeca3ab67b 100644 --- a/tests/unit/free-tier-providers-wave2-integration.test.ts +++ b/tests/unit/free-tier-providers-wave2-integration.test.ts @@ -25,7 +25,7 @@ const providers = [ ] as const; for (const [id, endpoint, modelIds] of providers) { - test(`${id} is wired through registry, metadata, endpoint and default executor`, () => { + test(`${id} is wired through registry, metadata, endpoint and default executor`, async () => { const registry = REGISTRY[id]; const metadata = APIKEY_PROVIDERS[id]; @@ -41,7 +41,7 @@ for (const [id, endpoint, modelIds] of providers) { assert.equal(metadata.passthroughModels, true); assert.ok(typeof metadata.freeNote === "string" && metadata.freeNote.length > 0); assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true); - assert.ok(getExecutor(id) instanceof DefaultExecutor); + assert.ok((await getExecutor(id)) instanceof DefaultExecutor); assert.equal(isValidModel(id, "future/live-catalog-model"), true); assert.deepEqual( registry.models.map((model) => model.id), diff --git a/tests/unit/free-tier-providers-wave3-integration.test.ts b/tests/unit/free-tier-providers-wave3-integration.test.ts index ec8ae79cf3..41a84082df 100644 --- a/tests/unit/free-tier-providers-wave3-integration.test.ts +++ b/tests/unit/free-tier-providers-wave3-integration.test.ts @@ -20,7 +20,7 @@ const providers = [ ] as const; for (const [id, endpoint] of providers) { - test(`${id} is wired through registry, metadata, endpoint and default executor`, () => { + test(`${id} is wired through registry, metadata, endpoint and default executor`, async () => { const registry = REGISTRY[id]; const metadata = APIKEY_PROVIDERS[id]; @@ -36,7 +36,8 @@ for (const [id, endpoint] of providers) { assert.equal(metadata.passthroughModels, true); assert.ok(typeof metadata.freeNote === "string" && metadata.freeNote.length > 0); assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true); - assert.ok(getExecutor(id) instanceof DefaultExecutor); + const executor = await getExecutor(id); + assert.ok(executor instanceof DefaultExecutor); assert.equal(isValidModel(id, "future/live-catalog-model"), true); assert.deepEqual(registry.models, []); }); diff --git a/tests/unit/free-tier-providers-wave4-a.test.ts b/tests/unit/free-tier-providers-wave4-a.test.ts index 277b9b2778..cf89f1cfc9 100644 --- a/tests/unit/free-tier-providers-wave4-a.test.ts +++ b/tests/unit/free-tier-providers-wave4-a.test.ts @@ -21,7 +21,7 @@ test("FreeInference exposes an OpenAI-compatible Bearer registry", () => { assert.equal(freeinferenceProvider.passthroughModels, true); }); -test("FreeInference uses DefaultExecutor without specialized behavior", () => { +test("FreeInference uses DefaultExecutor without specialized behavior", async () => { assert.equal(hasSpecializedExecutor("freeinference"), false); - assert.ok(getExecutor("freeinference") instanceof DefaultExecutor); + assert.ok(await getExecutor("freeinference") instanceof DefaultExecutor); }); diff --git a/tests/unit/free-tier-providers-wave4-b.test.ts b/tests/unit/free-tier-providers-wave4-b.test.ts index a547e150a0..63e72a35c5 100644 --- a/tests/unit/free-tier-providers-wave4-b.test.ts +++ b/tests/unit/free-tier-providers-wave4-b.test.ts @@ -21,7 +21,7 @@ test("Free.ai exposes its exact OpenAI-compatible endpoint and live catalog", () assert.equal(freeAiProvider.passthroughModels, true); }); -test("Free.ai uses DefaultExecutor without a specialized executor", () => { - assert.ok(getExecutor("free-ai") instanceof DefaultExecutor); +test("Free.ai uses DefaultExecutor without a specialized executor", async () => { + assert.ok(await getExecutor("free-ai") instanceof DefaultExecutor); assert.equal(hasSpecializedExecutor("free-ai"), false); }); diff --git a/tests/unit/free-tier-providers-wave4-integration.test.ts b/tests/unit/free-tier-providers-wave4-integration.test.ts index 743e188271..dd3a96a7b4 100644 --- a/tests/unit/free-tier-providers-wave4-integration.test.ts +++ b/tests/unit/free-tier-providers-wave4-integration.test.ts @@ -17,7 +17,7 @@ const providers = [ ] as const; for (const [id, endpoint] of providers) { - test(`${id} is fully wired without a specialized executor`, () => { + test(`${id} is fully wired without a specialized executor`, async () => { const registry = REGISTRY[id]; const metadata = APIKEY_PROVIDERS[id]; @@ -33,7 +33,7 @@ for (const [id, endpoint] of providers) { assert.equal(metadata.passthroughModels, true); assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true); assert.equal(hasSpecializedExecutor(id), false); - const executor = getExecutor(id); + const executor = await getExecutor(id); assert.ok(executor instanceof DefaultExecutor); assert.equal(executor.buildUrl("live-model", false), endpoint); assert.equal(isValidModel(id, "future/live-catalog-model"), true); diff --git a/tests/unit/free-tier-providers-wave5-integration.test.ts b/tests/unit/free-tier-providers-wave5-integration.test.ts index 48d24c2081..02a2bbc4b1 100644 --- a/tests/unit/free-tier-providers-wave5-integration.test.ts +++ b/tests/unit/free-tier-providers-wave5-integration.test.ts @@ -27,7 +27,7 @@ const providers = [ ] as const; for (const { id, endpoint, modelsUrl, hasFree } of providers) { - test(`${id} is fully wired through the public provider interfaces`, () => { + test(`${id} is fully wired through the public provider interfaces`, async () => { const registry = REGISTRY[id]; const metadata = APIKEY_PROVIDERS[id]; @@ -52,7 +52,7 @@ for (const { id, endpoint, modelsUrl, hasFree } of providers) { assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true); assert.equal(hasSpecializedExecutor(id), false); - const executor = getExecutor(id); + const executor = await getExecutor(id); assert.ok(executor instanceof DefaultExecutor); assert.equal(executor.buildUrl("live-model", false), endpoint); assert.equal(isValidModel(id, "future/live-catalog-model"), true); diff --git a/tests/unit/g4f-space-gateway-6650.test.ts b/tests/unit/g4f-space-gateway-6650.test.ts index bf946886c6..728b436699 100644 --- a/tests/unit/g4f-space-gateway-6650.test.ts +++ b/tests/unit/g4f-space-gateway-6650.test.ts @@ -73,8 +73,8 @@ for (const [id, subPath] of Object.entries(SUB_PATHS)) { ); }); - test(`#6650 ${id} resolves through getExecutor() as a DefaultExecutor instance`, () => { - const executor = getExecutor(id); + test(`#6650 ${id} resolves through getExecutor() as a DefaultExecutor instance`, async () => { + const executor = await getExecutor(id); assert.ok( executor instanceof DefaultExecutor, `${id} has no custom executor — must fall through to DefaultExecutor` diff --git a/tests/unit/gemini-web.test.ts b/tests/unit/gemini-web.test.ts index b8f31f4faa..bab367a1c1 100644 --- a/tests/unit/gemini-web.test.ts +++ b/tests/unit/gemini-web.test.ts @@ -7,9 +7,9 @@ const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/exe // ─── Registration ─────────────────────────────────────────────────────────── -test("GeminiWebExecutor is registered in executor index", () => { +test("GeminiWebExecutor is registered in executor index", async () => { assert.ok(hasSpecializedExecutor("gemini-web")); - const executor = getExecutor("gemini-web"); + const executor = await getExecutor("gemini-web"); assert.ok(executor instanceof GeminiWebExecutor); }); diff --git a/tests/unit/glm-executor.test.ts b/tests/unit/glm-executor.test.ts index 3d47b375c3..4cb31e90f6 100644 --- a/tests/unit/glm-executor.test.ts +++ b/tests/unit/glm-executor.test.ts @@ -129,10 +129,10 @@ test("GlmExecutor normalizes GLM coding and Anthropic URLs without duplicating e ); }); -test("GlmExecutor separates OpenAI-compatible coding headers from Anthropic headers", () => { - assert.equal(getExecutor("glm") instanceof GlmExecutor, true); - assert.equal(getExecutor("glm-cn") instanceof GlmExecutor, true); - assert.equal(getExecutor("glmt") instanceof GlmExecutor, true); +test("GlmExecutor separates OpenAI-compatible coding headers from Anthropic headers", async () => { + assert.equal(await getExecutor("glm") instanceof GlmExecutor, true); + assert.equal(await getExecutor("glm-cn") instanceof GlmExecutor, true); + assert.equal(await getExecutor("glmt") instanceof GlmExecutor, true); const executor = new GlmExecutor("glm"); const codingHeaders = executor.buildHeaders( diff --git a/tests/unit/grok-web.test.ts b/tests/unit/grok-web.test.ts index e87d9c74a5..273e728179 100644 --- a/tests/unit/grok-web.test.ts +++ b/tests/unit/grok-web.test.ts @@ -83,9 +83,9 @@ test.afterEach(() => { // ─── Registration ─────────────────────────────────────────────────────────── -test("GrokWebExecutor is registered in executor index", () => { +test("GrokWebExecutor is registered in executor index", async () => { assert.ok(hasSpecializedExecutor("grok-web")); - const executor = getExecutor("grok-web"); + const executor = await getExecutor("grok-web"); assert.ok(executor instanceof GrokWebExecutor); }); diff --git a/tests/unit/kie-executor-routing.test.ts b/tests/unit/kie-executor-routing.test.ts index 762b34d9a3..35bc8039d5 100644 --- a/tests/unit/kie-executor-routing.test.ts +++ b/tests/unit/kie-executor-routing.test.ts @@ -5,8 +5,8 @@ import { DefaultExecutor } from "../../open-sse/executors/default.ts"; import { getExecutor, hasSpecializedExecutor } from "../../open-sse/executors/index.ts"; import { KieExecutor } from "../../open-sse/executors/kie.ts"; -test("KIE chat traffic uses the default executor while media keeps its task executor", () => { +test("KIE chat traffic uses the default executor while media keeps its task executor", async () => { assert.equal(hasSpecializedExecutor("kie"), false); - assert.ok(getExecutor("kie") instanceof DefaultExecutor); + assert.ok(await getExecutor("kie") instanceof DefaultExecutor); assert.equal(typeof KieExecutor, "function"); }); diff --git a/tests/unit/moonshot-k3.test.ts b/tests/unit/moonshot-k3.test.ts index ec77244240..ae5b2bb7a3 100644 --- a/tests/unit/moonshot-k3.test.ts +++ b/tests/unit/moonshot-k3.test.ts @@ -4,11 +4,8 @@ import assert from "node:assert/strict"; import { getRegistryEntry } from "../../open-sse/config/providerRegistry.ts"; import { supportsXHighEffort } from "../../open-sse/config/providerModels.ts"; import { sanitizeReasoningEffortForProvider } from "../../open-sse/executors/base.ts"; -import { - getExecutor, - hasSpecializedExecutor, - MoonshotExecutor, -} from "../../open-sse/executors/index.ts"; +import { getExecutor, hasSpecializedExecutor } from "../../open-sse/executors/index.ts"; +import { MoonshotExecutor } from "../../open-sse/executors/moonshot.ts"; import { sanitizeOpenAIResponse, sanitizeResponsesApiResponse, @@ -63,11 +60,11 @@ test("Kimi K3 advertises its 1M context/output and native capabilities", () => { assert.equal(capabilities.interleavedField, "reasoning_content"); }); -test("Moonshot ids use the specialized request normalizer", () => { +test("Moonshot ids use the specialized request normalizer", async () => { assert.equal(hasSpecializedExecutor("moonshot"), true); assert.equal(hasSpecializedExecutor("kimi"), true); - assert.ok(getExecutor("moonshot") instanceof MoonshotExecutor); - assert.ok(getExecutor("kimi") instanceof MoonshotExecutor); + assert.ok((await getExecutor("moonshot")) instanceof MoonshotExecutor); + assert.ok((await getExecutor("kimi")) instanceof MoonshotExecutor); }); test("Kimi K3 uses max reasoning, fixed sampling, and max_completion_tokens", () => { diff --git a/tests/unit/ninerouter-executor.test.ts b/tests/unit/ninerouter-executor.test.ts index 3a9ef88e76..515077d6cc 100644 --- a/tests/unit/ninerouter-executor.test.ts +++ b/tests/unit/ninerouter-executor.test.ts @@ -498,13 +498,13 @@ describe("NineRouterExecutor", () => { describe("getExecutor registration", () => { it("getExecutor('9router') returns a NineRouterExecutor", async () => { const { getExecutor } = await import("../../open-sse/executors/index.ts"); - const exec = getExecutor("9router"); + const exec = await getExecutor("9router"); assert.equal(exec.getProvider(), "9router"); }); it("getExecutor('nr') alias resolves to NineRouterExecutor", async () => { const { getExecutor } = await import("../../open-sse/executors/index.ts"); - const exec = getExecutor("nr"); + const exec = await getExecutor("nr"); assert.equal(exec.getProvider(), "9router"); }); }); diff --git a/tests/unit/openference-oauth-provider.test.ts b/tests/unit/openference-oauth-provider.test.ts index 0e7add513b..983d7e7e61 100644 --- a/tests/unit/openference-oauth-provider.test.ts +++ b/tests/unit/openference-oauth-provider.test.ts @@ -107,8 +107,7 @@ test("Openference OAuth postExchange fetches userinfo when id_token lacks email" assert.equal(mapped.email, "from-userinfo@openference.com"); assert.equal(mapped.name, "Userinfo Name"); }); - -test("Openference is registered as an OAuth gateway with default executor", () => { +test("Openference is registered as an OAuth gateway with default executor", async () => { assert.ok(OAUTH_PROVIDERS.openference); assert.equal(OAUTH_PROVIDERS.openference.alias, "of"); assert.equal(OAUTH_PROVIDERS.openference.color, "#6366F1"); @@ -129,7 +128,7 @@ test("Openference is registered as an OAuth gateway with default executor", () = ); assert.equal(hasSpecializedExecutor("openference"), false); - const headers = getExecutor("openference").buildHeaders({ accessToken: "oauth-access" }, false); + const headers = (await getExecutor("openference")).buildHeaders({ accessToken: "oauth-access" }, false); assert.equal(headers.Authorization, "Bearer oauth-access"); }); diff --git a/tests/unit/perplexity-web.test.ts b/tests/unit/perplexity-web.test.ts index f6f4b7f7a7..1ab1ab3d10 100644 --- a/tests/unit/perplexity-web.test.ts +++ b/tests/unit/perplexity-web.test.ts @@ -91,16 +91,16 @@ function mockFetchError(error) { // ─── Test: Executor registration ──────────────────────────────────────────── -test("PerplexityWebExecutor is registered in executor index", () => { +test("PerplexityWebExecutor is registered in executor index", async () => { assert.ok(hasSpecializedExecutor("perplexity-web")); assert.ok(hasSpecializedExecutor("pplx-web")); - const executor = getExecutor("perplexity-web"); + const executor = await getExecutor("perplexity-web"); assert.ok(executor instanceof PerplexityWebExecutor); }); -test("PerplexityWebExecutor alias resolves to same type", () => { - const a = getExecutor("perplexity-web"); - const b = getExecutor("pplx-web"); +test("PerplexityWebExecutor alias resolves to same type", async () => { + const a = await getExecutor("perplexity-web"); + const b = await getExecutor("pplx-web"); assert.ok(a instanceof PerplexityWebExecutor); assert.ok(b instanceof PerplexityWebExecutor); }); diff --git a/tests/unit/poe-api-executor-regression.test.ts b/tests/unit/poe-api-executor-regression.test.ts index 8ce969b981..85cda39850 100644 --- a/tests/unit/poe-api-executor-regression.test.ts +++ b/tests/unit/poe-api-executor-regression.test.ts @@ -47,17 +47,18 @@ function headerRecord(headers: Record): Record { return out; } -test("#8969: getExecutor(poe) selects DefaultExecutor, not PoeWebExecutor", () => { +test("#8969: getExecutor(poe) selects DefaultExecutor, not PoeWebExecutor", async () => { assert.equal(hasSpecializedExecutor("poe"), false); - const executor = getExecutor("poe"); + const executor = await getExecutor("poe"); assert.ok(executor instanceof DefaultExecutor); assert.equal(executor instanceof PoeWebExecutor, false); assert.equal(executor.provider, "poe"); }); -test("#8969: getExecutor(poe-web) still selects PoeWebExecutor", () => { +test("#8969: getExecutor(poe-web) still selects PoeWebExecutor", async () => { assert.equal(hasSpecializedExecutor("poe-web"), true); - assert.ok(getExecutor("poe-web") instanceof PoeWebExecutor); + const executor = await getExecutor("poe-web"); + assert.ok(executor instanceof PoeWebExecutor); }); test("#8969: registry declares API-key executor + all three Poe protocol URLs", () => { @@ -80,8 +81,8 @@ test("#8969: registry declares API-key executor + all three Poe protocol URLs", assert.notEqual(gpt.targetFormat, "claude"); }); -test("#8969: buildUrl routes chat / responses / messages correctly", () => { - const executor = getExecutor("poe") as DefaultExecutor; +test("#8969: buildUrl routes chat / responses / messages correctly", async () => { + const executor = (await getExecutor("poe")) as DefaultExecutor; const creds = { apiKey: "poe-test-key", providerSpecificData: {} }; assert.equal(executor.buildUrl("gemma-4-31b", false, 0, creds), CHAT_URL); @@ -175,8 +176,8 @@ test("#8969: resolvePoeUpstreamUrl normalizes registry-default / bare-host /v1/ } }); -test("#8969: buildHeaders uses Bearer auth and never sends Cookie", () => { - const executor = getExecutor("poe") as DefaultExecutor; +test("#8969: buildHeaders uses Bearer auth and never sends Cookie", async () => { + const executor = (await getExecutor("poe")) as DefaultExecutor; for (const stream of [false, true]) { const headers = headerRecord( executor.buildHeaders({ apiKey: "poe-test-key", providerSpecificData: {} }, stream) @@ -200,7 +201,7 @@ test("#8969: resolveExecutionCredentials forces responses upstream for poe", () }); test("#8969: mocked execute posts Chat Completions with Bearer, no Cookie, stripped model", async () => { - const executor = getExecutor("poe") as DefaultExecutor; + const executor = (await getExecutor("poe")) as DefaultExecutor; const originalFetch = globalThis.fetch; const seen: Array<{ url: string; @@ -259,7 +260,7 @@ test("#8969: mocked execute posts Chat Completions with Bearer, no Cookie, strip }); test("#8969: mocked execute routes Responses + Messages fixtures to the right URLs", async () => { - const executor = getExecutor("poe") as DefaultExecutor; + const executor = (await getExecutor("poe")) as DefaultExecutor; const originalFetch = globalThis.fetch; let lastUrl = ""; @@ -326,7 +327,7 @@ test("#8969: mocked execute routes Responses + Messages fixtures to the right UR }); test("#8969: mocked upstream 405 is preserved (not swallowed)", async () => { - const executor = getExecutor("poe") as DefaultExecutor; + const executor = (await getExecutor("poe")) as DefaultExecutor; const originalFetch = globalThis.fetch; globalThis.fetch = (async () => { diff --git a/tests/unit/probe-6699-jules-executor-misroute.test.ts b/tests/unit/probe-6699-jules-executor-misroute.test.ts index a2072814f4..290a8301b2 100644 --- a/tests/unit/probe-6699-jules-executor-misroute.test.ts +++ b/tests/unit/probe-6699-jules-executor-misroute.test.ts @@ -16,7 +16,7 @@ test("#6699: jules has no specialized executor (falls through to DefaultExecutor assert.equal(hasSpecializedExecutor("jules"), false); }); -test("#6699: a chat-completion request routed to provider 'jules' must not silently hit OpenAI's endpoint", () => { +test("#6699: a chat-completion request routed to provider 'jules' must not silently hit OpenAI's endpoint", async () => { // Desired behavior: the Jules provider (a cloud-agent, registered only in // CLOUD_AGENT_PROVIDERS/staticModels, never in the chat REGISTRY) must not silently // resolve to OpenAI's chat/completions endpoint when routed through the normal @@ -27,9 +27,9 @@ test("#6699: a chat-completion request routed to provider 'jules' must not silen // genuine Jules key). Before the fix, getExecutor("jules") returned a working // executor whose buildUrl() resolved to OpenAI's endpoint -- this assertion FAILS on // unfixed release/v3.8.49 code because no error is thrown at all. - assert.throws( - () => getExecutor("jules"), - (err) => { + await assert.rejects( + getExecutor("jules"), + (err: Error & { status?: number }) => { assert.match(err.message, /cloud-agent provider/i); assert.match(err.message, /does not support direct chat completions/i); assert.equal(err.status, 400); diff --git a/tests/unit/provider-limits-accesstoken-fallback-on-refresh-failure.test.ts b/tests/unit/provider-limits-accesstoken-fallback-on-refresh-failure.test.ts index df8fd531c8..89f23b7983 100644 --- a/tests/unit/provider-limits-accesstoken-fallback-on-refresh-failure.test.ts +++ b/tests/unit/provider-limits-accesstoken-fallback-on-refresh-failure.test.ts @@ -39,7 +39,7 @@ function geminiConnection() { } test("falls back to the existing accessToken for a non-github provider when refreshCredentials returns null", async () => { - const exec = getExecutor("gemini"); + const exec = await getExecutor("gemini"); const origNeeds = exec.needsRefresh; const origRefresh = exec.refreshCredentials; exec.needsRefresh = () => true; // force the refresh attempt @@ -65,7 +65,7 @@ test("falls back to the existing accessToken for a non-github provider when refr }); test("still throws when refresh fails AND there is no accessToken to fall back on", async () => { - const exec = getExecutor("gemini"); + const exec = await getExecutor("gemini"); const origNeeds = exec.needsRefresh; const origRefresh = exec.refreshCredentials; exec.needsRefresh = () => true; diff --git a/tests/unit/provider-limits-force-refresh-imported.test.ts b/tests/unit/provider-limits-force-refresh-imported.test.ts index 7e3717503f..041b01064f 100644 --- a/tests/unit/provider-limits-force-refresh-imported.test.ts +++ b/tests/unit/provider-limits-force-refresh-imported.test.ts @@ -33,7 +33,7 @@ function importedCodexConnection() { } test("force re-mints an imported rotating account that needsRefresh would skip (#3019 reactive)", async () => { - const exec = getExecutor("codex"); + const exec = await getExecutor("codex"); const origNeeds = exec.needsRefresh; const origRefresh = exec.refreshCredentials; let refreshCalls = 0; @@ -65,7 +65,7 @@ test("force re-mints an imported rotating account that needsRefresh would skip ( }); test("force does NOT override the bulk #3019 guard (no allowRotatingRefresh → no mint)", async () => { - const exec = getExecutor("codex"); + const exec = await getExecutor("codex"); const origNeeds = exec.needsRefresh; const origRefresh = exec.refreshCredentials; let refreshCalls = 0; diff --git a/tests/unit/provider-registry-freetheai.test.ts b/tests/unit/provider-registry-freetheai.test.ts index 2df8061a4f..0a18e0cdc3 100644 --- a/tests/unit/provider-registry-freetheai.test.ts +++ b/tests/unit/provider-registry-freetheai.test.ts @@ -33,8 +33,8 @@ test("#6670 freetheai is registered in the executor registry with an OpenAI-comp assert.ok(Array.isArray(entry.models) && entry.models.length > 0, "must seed a fallback model list"); }); -test("#6670 freetheai resolves through getExecutor() as a DefaultExecutor instance", () => { - const executor = getExecutor("freetheai"); +test("#6670 freetheai resolves through getExecutor() as a DefaultExecutor instance", async () => { + const executor = await getExecutor("freetheai"); assert.ok(executor instanceof DefaultExecutor, "freetheai has no custom executor — must fall through to DefaultExecutor"); }); diff --git a/tests/unit/provider-request-failure-pipeline.test.ts b/tests/unit/provider-request-failure-pipeline.test.ts index 34bac642f4..2b6684f452 100644 --- a/tests/unit/provider-request-failure-pipeline.test.ts +++ b/tests/unit/provider-request-failure-pipeline.test.ts @@ -153,7 +153,7 @@ test("network failure persisted call log includes providerRequest in pipeline pa test("network timeout persisted call log includes providerRequest in pipeline payloads", async () => { const { getExecutor } = await import("../../open-sse/executors/index.ts"); - const executor = getExecutor("openai"); + const executor = await getExecutor("openai"); const originalGetTimeoutMs = executor.getTimeoutMs?.bind(executor); executor.getTimeoutMs = () => 200; diff --git a/tests/unit/provider-tinycms-web.test.ts b/tests/unit/provider-tinycms-web.test.ts index 7fea970dee..e986a25c71 100644 --- a/tests/unit/provider-tinycms-web.test.ts +++ b/tests/unit/provider-tinycms-web.test.ts @@ -16,7 +16,8 @@ import assert from "node:assert/strict"; import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers/web-cookie.ts"; import { REGISTRY } from "../../open-sse/config/providers/index.ts"; -import { getExecutor, TinyCmsExecutor } from "../../open-sse/executors/index.ts"; +import { getExecutor } from "../../open-sse/executors/index.ts"; +import { TinyCmsExecutor } from "../../open-sse/executors/tinycms.ts"; import { setupDomMocks, type DomMockRestore } from "../../open-sse/executors/tinycmsSigner.ts"; // tinycmsSigner.ts intentionally does NOT install its window/document/canvas @@ -123,13 +124,13 @@ test("supportsReasoning is set on gpt-5.3-thinking-free", () => { // ── Executor ────────────────────────────────────────────────────────────────── -test("getExecutor returns TinyCmsExecutor for 'tinycms-web'", () => { - const e = getExecutor("tinycms-web"); +test("getExecutor returns TinyCmsExecutor for 'tinycms-web'", async () => { + const e = await getExecutor("tinycms-web"); assert.ok(e instanceof TinyCmsExecutor, "executor must be TinyCmsExecutor"); }); -test("getExecutor returns TinyCmsExecutor for 'tcw' alias", () => { - const e = getExecutor("tcw"); +test("getExecutor returns TinyCmsExecutor for 'tcw' alias", async () => { + const e = await getExecutor("tcw"); assert.ok(e instanceof TinyCmsExecutor, "alias 'tcw' must resolve to TinyCmsExecutor"); }); @@ -150,7 +151,7 @@ test("TinyCmsExecutor returns 401 when UUID is missing", async () => { credentials: {}, signal: AbortSignal.timeout(5000), }); - assert.ok(result.response, "response must be present"); + assert.ok("response" in result, "response must be present"); assert.equal(result.response.status, 401); const body = await result.response.json(); const errMsg = body?.error?.message || ""; @@ -168,7 +169,7 @@ test("TinyCmsExecutor returns 401 when UUID does not start with 'R'", async () = credentials: { apiKey: "abc123" }, // does not start with 'R' signal: AbortSignal.timeout(5000), }); - assert.ok(result.response, "response must be present"); + assert.ok("response" in result, "response must be present"); assert.equal(result.response.status, 401); const body = await result.response.json(); const errMsg = body?.error?.message || ""; @@ -256,7 +257,7 @@ test("TinyCmsExecutor sanitizes errors (no stack traces in error response)", asy signal: AbortSignal.timeout(5000), }); - assert.ok(result.response, "response must be present"); + assert.ok("response" in result, "response must be present"); const body = await result.response.json(); const errMsg = body?.error?.message || ""; assert.ok(errMsg.includes("Invalid or missing device UUID"), "error must mention missing UUID"); diff --git a/tests/unit/providers-g4f-batch3.test.ts b/tests/unit/providers-g4f-batch3.test.ts index a64860bd70..40969c2180 100644 --- a/tests/unit/providers-g4f-batch3.test.ts +++ b/tests/unit/providers-g4f-batch3.test.ts @@ -68,8 +68,8 @@ for (const [id, info] of Object.entries(PROVIDERS)) { assert.equal(entry.passthroughModels, true); }); - test(`#6674 ${id} resolves through getExecutor() as a DefaultExecutor instance`, () => { - const executor = getExecutor(id); + test(`#6674 ${id} resolves through getExecutor() as a DefaultExecutor instance`, async () => { + const executor = await getExecutor(id); assert.ok( executor instanceof DefaultExecutor, `${id} has no custom executor — must fall through to DefaultExecutor` diff --git a/tests/unit/providers-yuanbao-web.test.ts b/tests/unit/providers-yuanbao-web.test.ts index 03fae5084e..a8a00411f0 100644 --- a/tests/unit/providers-yuanbao-web.test.ts +++ b/tests/unit/providers-yuanbao-web.test.ts @@ -39,11 +39,11 @@ test("yuanbao-web appears in the web-cookie catalog with a cookie authHint", () assert.match(String(entry.website), /yuanbao\.tencent\.com/); }); -test("YuanbaoWebExecutor is wired under id and alias", () => { +test("YuanbaoWebExecutor is wired under id and alias", async () => { assert.ok(hasSpecializedExecutor("yuanbao-web")); assert.ok(hasSpecializedExecutor("ybw")); - assert.ok(getExecutor("yuanbao-web") instanceof YuanbaoWebExecutor); - assert.ok(getExecutor("ybw") instanceof YuanbaoWebExecutor); + assert.ok(await getExecutor("yuanbao-web") instanceof YuanbaoWebExecutor); + assert.ok(await getExecutor("ybw") instanceof YuanbaoWebExecutor); }); // ── Behavioral: SSE → OpenAI translation (mocked upstream) ───────────────────── diff --git a/tests/unit/repro-9550-amazon-q-alias-resolution.test.ts b/tests/unit/repro-9550-amazon-q-alias-resolution.test.ts index 7f31ef4b83..26a8779ae9 100644 --- a/tests/unit/repro-9550-amazon-q-alias-resolution.test.ts +++ b/tests/unit/repro-9550-amazon-q-alias-resolution.test.ts @@ -27,8 +27,8 @@ describe("Issue #9550 - amazon-q alias resolution", () => { assert.equal(parsed.model, "amazon-q"); }); - it('getExecutor("amazon-q") should exist and be a KiroExecutor', () => { - const executor = getExecutor("amazon-q"); + it('getExecutor("amazon-q") should exist and be a KiroExecutor', async () => { + const executor = await getExecutor("amazon-q"); assert.ok(executor, "getExecutor('amazon-q') should return an executor"); assert.equal( executor.constructor.name, diff --git a/tests/unit/search-providers-chat-guard.test.ts b/tests/unit/search-providers-chat-guard.test.ts index ac04fd5650..f0785355b9 100644 --- a/tests/unit/search-providers-chat-guard.test.ts +++ b/tests/unit/search-providers-chat-guard.test.ts @@ -29,7 +29,7 @@ test("#10274: no search provider has a specialized chat executor", () => { } }); -test("#10274: a chat-completion request routed to a search provider must not silently hit OpenAI's endpoint", () => { +test("#10274: a chat-completion request routed to a search provider must not silently hit OpenAI's endpoint", async () => { // Desired behavior: search providers (registered only in SEARCH_PROVIDERS, never in the // chat REGISTRY) must not silently resolve to OpenAI's chat/completions endpoint when // routed through the normal chat-completions executor path. getExecutor() must throw a @@ -40,9 +40,9 @@ test("#10274: a chat-completion request routed to a search provider must not sil // OpenAI's endpoint -- this assertion FAILS on unfixed release/v3.8.50 code because no // error is thrown at all. for (const id of SEARCH_PROVIDER_IDS) { - assert.throws( - () => getExecutor(id), - (err) => { + await assert.rejects( + getExecutor(id), + (err: Error & { status?: number }) => { assert.match(err.message, /search provider/i); assert.match(err.message, /does not support chat completions/i); assert.match(err.message, /\/v1\/search/i); diff --git a/tests/unit/t3-chat-web.test.ts b/tests/unit/t3-chat-web.test.ts index c0d28ac49f..1de24fc454 100644 --- a/tests/unit/t3-chat-web.test.ts +++ b/tests/unit/t3-chat-web.test.ts @@ -20,13 +20,13 @@ test("hasSpecializedExecutor returns true for t3chat alias", () => { assert.ok(hasSpecializedExecutor("t3chat")); }); -test("getExecutor returns T3ChatWebExecutor for t3-web", () => { - const exec = getExecutor("t3-web"); +test("getExecutor returns T3ChatWebExecutor for t3-web", async () => { + const exec = await getExecutor("t3-web"); assert.ok(exec instanceof T3ChatWebExecutor); }); -test("getExecutor returns T3ChatWebExecutor for t3chat alias", () => { - const exec = getExecutor("t3chat"); +test("getExecutor returns T3ChatWebExecutor for t3chat alias", async () => { + const exec = await getExecutor("t3chat"); assert.ok(exec instanceof T3ChatWebExecutor); }); diff --git a/tests/unit/tinycms-secure-nonce-randomness.test.ts b/tests/unit/tinycms-secure-nonce-randomness.test.ts index ebc30cb13a..b731a2e676 100644 --- a/tests/unit/tinycms-secure-nonce-randomness.test.ts +++ b/tests/unit/tinycms-secure-nonce-randomness.test.ts @@ -16,7 +16,7 @@ import test, { before, after } from "node:test"; import assert from "node:assert/strict"; -import { TinyCmsExecutor } from "../../open-sse/executors/index.ts"; +import { TinyCmsExecutor } from "../../open-sse/executors/tinycms.ts"; import { setupDomMocks, type DomMockRestore } from "../../open-sse/executors/tinycmsSigner.ts"; let restoreDomMocks: DomMockRestore; diff --git a/tests/unit/web-cookie-providers-new.test.ts b/tests/unit/web-cookie-providers-new.test.ts index 1c199a5dab..14e3a2860a 100644 --- a/tests/unit/web-cookie-providers-new.test.ts +++ b/tests/unit/web-cookie-providers-new.test.ts @@ -110,49 +110,51 @@ const noopExecuteInput = { // ── Registration Tests ─────────────────────────────────────────────────────── -test("HuggingChat executor is registered", () => { +test("HuggingChat executor is registered", async () => { assert.ok(hasSpecializedExecutor("huggingchat")); assert.ok(hasSpecializedExecutor("hc")); - const executor = getExecutor("huggingchat"); + const executor = await getExecutor("huggingchat"); assert.ok(executor instanceof HuggingChatExecutor); }); -test("Poe Web executor is registered", () => { +test("Poe Web executor is registered", async () => { assert.ok(hasSpecializedExecutor("poe-web")); - const executor = getExecutor("poe-web"); + const executor = await getExecutor("poe-web"); assert.ok(executor instanceof PoeWebExecutor); // #8969: canonical API-key `poe` must not route through PoeWebExecutor. assert.equal(hasSpecializedExecutor("poe"), false); - assert.ok(!(getExecutor("poe") instanceof PoeWebExecutor)); + const poeApiExecutor = await getExecutor("poe"); + assert.ok(!(poeApiExecutor instanceof PoeWebExecutor)); }); -test("Venice Web executor is registered", () => { +test("Venice Web executor is registered", async () => { assert.ok(hasSpecializedExecutor("venice-web")); assert.ok(hasSpecializedExecutor("ven")); - const executor = getExecutor("venice-web"); + const executor = await getExecutor("venice-web"); assert.ok(executor instanceof VeniceWebExecutor); }); -test("v0 Vercel Web executor is registered", () => { +test("v0 Vercel Web executor is registered", async () => { assert.ok(hasSpecializedExecutor("v0-vercel-web")); assert.ok(hasSpecializedExecutor("v0")); - const executor = getExecutor("v0-vercel-web"); + const executor = await getExecutor("v0-vercel-web"); assert.ok(executor instanceof V0VercelWebExecutor); }); -test("Kimi Web executor is registered", () => { - assert.ok(getExecutor("kimi-web") instanceof KimiWebExecutor); +test("Kimi Web executor is registered", async () => { + const kimiWebExecutor = await getExecutor("kimi-web"); + assert.ok(kimiWebExecutor instanceof KimiWebExecutor); // #4699: the legacy `kimi` API-key id must never route through Kimi Web. assert.ok(hasSpecializedExecutor("kimi")); - const legacyExecutor = getExecutor("kimi"); + const legacyExecutor = await getExecutor("kimi"); assert.ok(legacyExecutor instanceof MoonshotExecutor); assert.ok(!(legacyExecutor instanceof KimiWebExecutor)); }); -test("Doubao Web executor is registered", () => { +test("Doubao Web executor is registered", async () => { assert.ok(hasSpecializedExecutor("doubao-web")); assert.ok(hasSpecializedExecutor("db")); - const executor = getExecutor("doubao-web"); + const executor = await getExecutor("doubao-web"); assert.ok(executor instanceof DoubaoWebExecutor); }); @@ -190,9 +192,9 @@ test("Doubao Web sets correct provider", () => { // ── Registration Tests (Qwen Web) ──────────────────────────────────────────── -test("Qwen Web executor is registered", () => { +test("Qwen Web executor is registered", async () => { assert.ok(hasSpecializedExecutor("qwen-web")); - const executor = getExecutor("qwen-web"); + const executor = await getExecutor("qwen-web"); assert.ok(executor instanceof QwenWebExecutor); }); diff --git a/tests/unit/windsurf-devin-executors.test.ts b/tests/unit/windsurf-devin-executors.test.ts index fd27062aa8..123d446112 100644 --- a/tests/unit/windsurf-devin-executors.test.ts +++ b/tests/unit/windsurf-devin-executors.test.ts @@ -16,7 +16,7 @@ function containsBytes(haystack: Uint8Array, needle: Uint8Array): boolean { } test("Devin Desktop sends the curated raw model id without alias rewriting", async () => { - const executor = getExecutor("devin-desktop"); + const executor = await getExecutor("devin-desktop"); const originalFetch = globalThis.fetch; let requestBody: Uint8Array | null = null; globalThis.fetch = async (url, init) => { @@ -37,7 +37,7 @@ test("Devin Desktop sends the curated raw model id without alias rewriting", asy credentials: { accessToken: "test-devin-desktop-token" }, }); - assert.equal(result.response.status, 418); + assert.equal((result instanceof Response ? result : result.response).status, 418); assert.ok(requestBody); assert.equal(containsBytes(requestBody, new TextEncoder().encode(model)), true); } finally { diff --git a/tests/unit/xai-oauth-provider.test.ts b/tests/unit/xai-oauth-provider.test.ts index 6d58db0bc6..7bec1fbbc9 100644 --- a/tests/unit/xai-oauth-provider.test.ts +++ b/tests/unit/xai-oauth-provider.test.ts @@ -81,14 +81,14 @@ test("xAI OAuth maps refreshable tokens and safe id_token display metadata", () assert.equal(mapped.name, "Grok User"); }); -test("xAI OAuth is a distinct OAuth registry entry backed by the xAI executor", () => { +test("xAI OAuth is a distinct OAuth registry entry backed by the xAI executor", async () => { assert.equal(xai_oauthProvider.authType, "oauth"); assert.equal(xai_oauthProvider.baseUrl, "https://api.x.ai/v1/chat/completions"); assert.ok(xai_oauthProvider.models?.some((model) => model.id === "grok-4.5")); assert.equal(hasSpecializedExecutor("xai-oauth"), true); - assert.ok(getExecutor("xai-oauth") instanceof XaiExecutor); + assert.ok(await getExecutor("xai-oauth") instanceof XaiExecutor); - const headers = getExecutor("xai-oauth").buildHeaders({ accessToken: "oauth-access" }, false); + const headers = (await getExecutor("xai-oauth")).buildHeaders({ accessToken: "oauth-access" }, false); assert.equal(headers.Authorization, "Bearer oauth-access"); }); diff --git a/tests/unit/zed-provider.test.ts b/tests/unit/zed-provider.test.ts index e9f0cfdfab..4f48edc198 100644 --- a/tests/unit/zed-provider.test.ts +++ b/tests/unit/zed-provider.test.ts @@ -45,9 +45,9 @@ describe("zed-hosted registry entry", () => { assert.equal(entry.oauth, undefined); }); - test("executor is wired in the executors map", () => { + test("executor is wired in the executors map", async () => { assert.ok(hasSpecializedExecutor("zed-hosted")); - assert.ok(getExecutor("zed-hosted") instanceof ZedHostedExecutor); + assert.ok(await getExecutor("zed-hosted") instanceof ZedHostedExecutor); }); }); diff --git a/tests/unit/zenmux-free-provider.test.ts b/tests/unit/zenmux-free-provider.test.ts index ff677b4e9d..2728aa8076 100644 --- a/tests/unit/zenmux-free-provider.test.ts +++ b/tests/unit/zenmux-free-provider.test.ts @@ -86,13 +86,13 @@ test("zenmux-free model names are human-readable strings", () => { // ── Executor ────────────────────────────────────────────────────────────────── -test("getExecutor returns ZenmuxFreeExecutor for 'zenmux-free'", () => { - const e = getExecutor("zenmux-free"); +test("getExecutor returns ZenmuxFreeExecutor for 'zenmux-free'", async () => { + const e = await getExecutor("zenmux-free"); assert.ok(e instanceof ZenmuxFreeExecutor, "executor must be ZenmuxFreeExecutor"); }); -test("getExecutor returns ZenmuxFreeExecutor for 'zmf' alias", () => { - const e = getExecutor("zmf"); +test("getExecutor returns ZenmuxFreeExecutor for 'zmf' alias", async () => { + const e = await getExecutor("zmf"); assert.ok(e instanceof ZenmuxFreeExecutor, "alias 'zmf' must resolve to ZenmuxFreeExecutor"); });