diff --git a/changelog.d/fixes/11582-known-symbols-async-executor.md b/changelog.d/fixes/11582-known-symbols-async-executor.md new file mode 100644 index 0000000000..11931c0d30 --- /dev/null +++ b/changelog.d/fixes/11582-known-symbols-async-executor.md @@ -0,0 +1 @@ +- **fix(check):** the `known-symbols` executor conformance gate awaits the now-async `getExecutor()`, so it stops reporting all 142 registered aliases as dead symbols and can detect a lazy import that fails ([#11582](https://github.com/diegosouzapw/OmniRoute/pull/11582)) diff --git a/scripts/check/check-known-symbols.ts b/scripts/check/check-known-symbols.ts index d3beb0ba88..e655c169bb 100644 --- a/scripts/check/check-known-symbols.ts +++ b/scripts/check/check-known-symbols.ts @@ -166,17 +166,34 @@ export type ExecutorLike = { * Dada a lista de aliases e um resolvedor (getExecutor), retorna os aliases que NÃO * resolvem para um BaseExecutor válido (não é instância, ou falta execute/getProvider). * isInstance é injetado para manter a função pura/testável com inputs sintéticos. + * + * O resolvedor é aguardado: desde #11421 o registro é lazy e `getExecutor()` devolve + * uma Promise (a classe só é importada e construída no primeiro uso). Sem o await, + * TODO alias reprova — uma Promise nunca é `instanceof BaseExecutor` — e a checagem + * deixa de proteger qualquer coisa. Uma rejeição também conta como não-conforme: com + * carregamento lazy o import de um alias pode falhar em runtime, e esse é exatamente + * o símbolo morto que esta sub-checagem existe para achar. */ -export function findNonConformingExecutors( +export async function findNonConformingExecutors( aliases: string[], - resolve: (alias: string) => ExecutorLike | null | undefined, + resolve: ( + alias: string + ) => PromiseLike | ExecutorLike | null | undefined, isInstance: (value: unknown) => boolean -): string[] { - return aliases.filter((alias) => { - const ex = resolve(alias); - if (!ex || !isInstance(ex)) return true; - return typeof ex.execute !== "function" || typeof ex.getProvider !== "function"; - }); +): Promise { + const verdicts = await Promise.all( + aliases.map(async (alias) => { + let ex: ExecutorLike | null | undefined; + try { + ex = await resolve(alias); + } catch { + return true; // o alias não carrega — símbolo morto + } + if (!ex || !isInstance(ex)) return true; + return typeof ex.execute !== "function" || typeof ex.getProvider !== "function"; + }) + ); + return aliases.filter((_alias, index) => verdicts[index]); } // ─────────────────────────────────────────────────────────────────────────── @@ -460,7 +477,7 @@ async function main(): Promise { // ── (1) Executor conformance ────────────────────────────────────────────── const executorsMod = await import("@omniroute/open-sse/executors/index.ts"); - const getExecutor = executorsMod.getExecutor as (alias: string) => ExecutorLike; + const getExecutor = executorsMod.getExecutor as (alias: string) => Promise; const BaseExecutor = executorsMod.BaseExecutor as new (...args: never[]) => unknown; const indexSource = readFileSync(resolvePath(REPO_ROOT, "open-sse/executors/index.ts"), "utf8"); const aliases = extractExecutorAliases(indexSource); @@ -470,7 +487,7 @@ async function main(): Promise { ); } const isExecutorInstance = (value: unknown) => value instanceof BaseExecutor; - const badExecutors = findNonConformingExecutors(aliases, getExecutor, isExecutorInstance); + const badExecutors = await findNonConformingExecutors(aliases, getExecutor, isExecutorInstance); if (badExecutors.length) { failures.push( `[executor] ${badExecutors.length} alias(es) registrado(s) não resolvem para um BaseExecutor válido (instância + execute() + getProvider()):\n` + diff --git a/tests/unit/check-known-symbols.test.ts b/tests/unit/check-known-symbols.test.ts index 2dbf4fe837..8230f753ec 100644 --- a/tests/unit/check-known-symbols.test.ts +++ b/tests/unit/check-known-symbols.test.ts @@ -127,15 +127,16 @@ test("combo dispatch registry (runtime import) covers the canonical strategy set // (1) EXECUTOR CONFORMANCE — extractExecutorAliases + findNonConformingExecutors // ─────────────────────────────────────────────────────────────────────────── -test("extractExecutorAliases parses quoted and bare keys from the executors literal", () => { +// The literal is `const lazyExecutors` since #11421 — every value is a thunk that +// imports and constructs on first use, so the fixture has to carry that shape. +test("extractExecutorAliases parses quoted and bare keys from the lazy executors literal", () => { const src = [ - 'import { Foo } from "./foo.ts";', - "const executors = {", - " antigravity: new Foo(),", - " agy: new Foo(), // Alias", - ' "amazon-q": new Foo("amazon-q"),', + "const lazyExecutors: Record Promise> = {", + ' antigravity: () => import("./antigravity.ts").then((m) => new m.AntigravityExecutor()),', + ' agy: () => import("./antigravity.ts").then((m) => new m.AntigravityExecutor()), // Alias', + ' "amazon-q": () => import("./amazon-q.ts").then((m) => new m.AmazonQExecutor()),', "};", - "export function getExecutor() {}", + "export async function getExecutor() {}", ].join("\n"); assert.deepEqual(extractExecutorAliases(src), ["antigravity", "agy", "amazon-q"]); }); @@ -144,36 +145,60 @@ test("extractExecutorAliases throws when the executors map cannot be located", ( assert.throws(() => extractExecutorAliases("const other = { a: 1 };"), /could not find/); }); -test("findNonConformingExecutors returns [] when every alias resolves to a valid executor", () => { +test("findNonConformingExecutors returns [] when every alias resolves to a valid executor", async () => { const good = { execute: () => {}, getProvider: () => "x" } as ExecutorLike; - const resolve = (_alias: string) => good; + const resolve = async (_alias: string) => good; const isInstance = (_value: unknown) => true; - assert.deepEqual(findNonConformingExecutors(["a", "b"], resolve, isInstance), []); + assert.deepEqual(await findNonConformingExecutors(["a", "b"], resolve, isInstance), []); }); -test("findNonConformingExecutors flags an alias that does not resolve at all", () => { +// The real resolver is async (#11421). Awaiting is the whole point: a Promise is +// never `instanceof BaseExecutor`, so a sync call reports every alias as broken and +// the sub-check silently stops guarding anything. +test("findNonConformingExecutors awaits an async resolver instead of judging the Promise", async () => { const good = { execute: () => {}, getProvider: () => "x" } as ExecutorLike; - const resolve = (alias: string) => (alias === "ghost" ? null : good); - const isInstance = (_value: unknown) => true; - assert.deepEqual(findNonConformingExecutors(["a", "ghost", "b"], resolve, isInstance), ["ghost"]); + const resolve = (_alias: string) => Promise.resolve(good); + const isInstance = (value: unknown) => value === good; + assert.deepEqual(await findNonConformingExecutors(["a", "b"], resolve, isInstance), []); }); -test("findNonConformingExecutors flags an alias resolving to a non-BaseExecutor instance", () => { +test("findNonConformingExecutors flags an alias whose lazy load rejects", async () => { + const good = { execute: () => {}, getProvider: () => "x" } as ExecutorLike; + const resolve = async (alias: string) => { + if (alias === "boom") throw new Error("module not found"); + return good; + }; + const isInstance = (_value: unknown) => true; + assert.deepEqual(await findNonConformingExecutors(["a", "boom", "b"], resolve, isInstance), [ + "boom", + ]); +}); + +test("findNonConformingExecutors flags an alias that does not resolve at all", async () => { + const good = { execute: () => {}, getProvider: () => "x" } as ExecutorLike; + const resolve = async (alias: string) => (alias === "ghost" ? null : good); + const isInstance = (_value: unknown) => true; + assert.deepEqual(await findNonConformingExecutors(["a", "ghost", "b"], resolve, isInstance), [ + "ghost", + ]); +}); + +test("findNonConformingExecutors flags an alias resolving to a non-BaseExecutor instance", async () => { const stray = { execute: () => {}, getProvider: () => "x" } as ExecutorLike; - const resolve = (_alias: string) => stray; + const resolve = async (_alias: string) => stray; // Simulate `instanceof BaseExecutor` returning false for the stray object. const isInstance = (_value: unknown) => false; - assert.deepEqual(findNonConformingExecutors(["stray"], resolve, isInstance), ["stray"]); + assert.deepEqual(await findNonConformingExecutors(["stray"], resolve, isInstance), ["stray"]); }); -test("findNonConformingExecutors flags an executor missing execute() or getProvider()", () => { +test("findNonConformingExecutors flags an executor missing execute() or getProvider()", async () => { const noExecute = { getProvider: () => "x" } as ExecutorLike; const noProvider = { execute: () => {} } as ExecutorLike; const valid = { execute: () => {}, getProvider: () => "x" } as ExecutorLike; const map: Record = { ne: noExecute, np: noProvider, ok: valid }; - const resolve = (alias: string) => map[alias]; + const resolve = async (alias: string) => map[alias]; const isInstance = (_value: unknown) => true; - assert.deepEqual(findNonConformingExecutors(["ne", "np", "ok"], resolve, isInstance), [ + assert.deepEqual(await findNonConformingExecutors(["ne", "np", "ok"], resolve, isInstance), [ "ne", "np", ]);