mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 15:22:12 +03:00
`normalizeExecutorResult()` has always accepted `Response | { response, url, headers,
transformedBody }` — the bare arm is what the web/scraping executors return from their
error and passthrough paths, and `chatcore-upstream-timeouts.test.ts` already covers
that both shapes are handled. But `BaseExecutor.execute` has no explicit return type,
so TypeScript inferred it from the method's single `return` — the object shape alone.
Every override returning a bare `Response` was therefore reported as incompatible:
* 14 × TS2739 in `duckduckgo-web.ts`, whose `execute()` additionally pinned its own
signature to just the object shape while returning `errorResponse()` /
`processResponse()` (both `Response`) from 14 valid paths
* TS2416 in `felo-web.ts` and `gitlab.ts`, which declare `Promise<Response>`
Fix the declaration rather than the call sites: export `ExecutorExecuteResult` from
`base.ts` — the same union `normalizeExecutorResult()` accepts — and annotate
`BaseExecutor.execute` with it. `duckduckgo-web.ts` then drops its over-narrow
annotation, matching BaseExecutor and the ~38 other executors that let the return type
be inferred.
Two subclasses read `.response` straight off `super.execute()` and now narrow first:
* `github.ts` — the existing `!result.response` guard already meant "bare Response,
nothing to materialize"; it is now expressed as `result instanceof Response`, which
is the same branch for every input (bare / object / nullish)
* `pollinations.ts` — reads the status through both arms for its pool bookkeeping
Wrapping DuckDuckGo's 14 returns would have been the wrong fix: the values are already
correct, and `normalizeExecutorResult()` produces exactly `{ response, url: "",
headers: {}, transformedBody: null }` for them.
Validation: full tsc error-set diff against the base config — 335 -> 319, **zero new
errors** (line-number-agnostic diff is empty; the two `duckduckgo-web.ts` TS2345s that
appear to move are the same two pre-existing errors renumbered by added comments, and
are left for a later slice). `typecheck:core` clean, `check:type-coverage` 92.17% ->
94.17%, and 49 of the 50 existing test files importing a touched executor pass —
`plan3-p0.test.ts` fails identically with and without this change (it reads the
developer's real ~/.omniroute DB rather than a test-scoped DATA_DIR).
The new test pins the runtime behavior of the narrowing so a later simplification
cannot quietly drop the bare-Response arm.
90 lines
3.3 KiB
TypeScript
90 lines
3.3 KiB
TypeScript
/**
|
|
* Guards the executor `execute()` result contract.
|
|
*
|
|
* `normalizeExecutorResult()` has always accepted `Response | { response, ... }` — the
|
|
* bare arm is what the web/scraping executors return from their error and passthrough
|
|
* paths (see `chatcore-upstream-timeouts.test.ts`, which covers the normalizer itself).
|
|
* `BaseExecutor.execute` nonetheless *inferred* only the object shape from its single
|
|
* return statement, so every override returning a bare `Response` was reported as
|
|
* incompatible (TS2416) and DuckDuckGo's 14 valid `return`s as TS2739.
|
|
*
|
|
* Declaring `ExecutorExecuteResult` on the base fixed that, and required the two
|
|
* subclasses that consume `super.execute()` to narrow before reading `.response`.
|
|
* These tests pin the runtime behavior of that narrowing so a future "simplification"
|
|
* cannot quietly drop the bare-Response arm.
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { BaseExecutor } from "../../open-sse/executors/base.ts";
|
|
import { GithubExecutor } from "../../open-sse/executors/github.ts";
|
|
|
|
type ExecuteFn = typeof BaseExecutor.prototype.execute;
|
|
|
|
/** Swap BaseExecutor.execute for the duration of one call, then restore it. */
|
|
async function withBaseExecuteStub<T>(stub: ExecuteFn, run: () => Promise<T>): Promise<T> {
|
|
const original = BaseExecutor.prototype.execute;
|
|
BaseExecutor.prototype.execute = stub;
|
|
try {
|
|
return await run();
|
|
} finally {
|
|
BaseExecutor.prototype.execute = original;
|
|
}
|
|
}
|
|
|
|
const INPUT = {
|
|
model: "gpt-4o",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: false,
|
|
credentials: {},
|
|
signal: new AbortController().signal,
|
|
};
|
|
|
|
test("GithubExecutor.execute passes a bare Response through untouched", async () => {
|
|
const bare = new Response("upstream body", { status: 503 });
|
|
|
|
const result = await withBaseExecuteStub(
|
|
(async () => bare) as ExecuteFn,
|
|
() => new GithubExecutor().execute(INPUT)
|
|
);
|
|
|
|
assert.equal(
|
|
result,
|
|
bare,
|
|
"the bare-Response arm has no capture object to materialize and must be returned as-is"
|
|
);
|
|
});
|
|
|
|
test("GithubExecutor.execute still materializes the capture-object arm", async () => {
|
|
const captured = {
|
|
response: new Response("hello", { status: 200, statusText: "OK" }),
|
|
url: "https://api.githubcopilot.com/chat/completions",
|
|
headers: { "x-req": "1" },
|
|
transformedBody: { a: 1 },
|
|
};
|
|
|
|
const result = await withBaseExecuteStub(
|
|
(async () => captured) as ExecuteFn,
|
|
() => new GithubExecutor().execute(INPUT)
|
|
);
|
|
|
|
assert.ok(!(result instanceof Response), "the object arm must stay an object");
|
|
const obj = result as typeof captured;
|
|
|
|
// The body is re-wrapped into a native Response so downstream reads work after
|
|
// wreq-js clone/text semantics have consumed the original.
|
|
assert.equal(obj.response.status, 200);
|
|
assert.equal(await obj.response.text(), "hello");
|
|
assert.equal(obj.url, captured.url, "the capture fields must survive materialization");
|
|
assert.deepEqual(obj.transformedBody, { a: 1 });
|
|
});
|
|
|
|
test("GithubExecutor.execute tolerates a nullish result without throwing", async () => {
|
|
const result = await withBaseExecuteStub(
|
|
(async () => undefined) as unknown as ExecuteFn,
|
|
() => new GithubExecutor().execute(INPUT)
|
|
);
|
|
|
|
assert.equal(result, undefined, "a nullish base result must short-circuit, not throw");
|
|
});
|