refactor(sse): declare the executor execute() result contract - #8489
diegosouzapw merged 1 commit into
Conversation
`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.
|
Thanks for another clean slice of the TS7 migration — this is exactly the right fix. Declaring Verified locally: the new One small optional idea for a later slice: Merging this as-is — nice work. |
|
Merged into |
`BaseExecutor.buildHeaders(credentials, stream?, clientHeaders?, model?, health?)` was shadowed in three executors by same-named helpers with unrelated signatures: hailuo-web private buildHeaders(token: string, yy: string) lmarena protected buildHeaders(_model: string, credentials: unknown, _body: unknown) qwen-web private buildHeaders(token: string, cookieHeader: string, chatId?: string) Name collisions, not overrides — each reported TS2416. They are renamed to `buildStreamHeaders` / `buildRequestHeaders` / `buildApiHeaders`; the two lmarena test files that called the helper directly are updated with them. Worth stating precisely, because the shadow sat on a live dispatch path without being a live bug: `BaseExecutor.countTokens()` calls `this.buildHeaders(credentials, false)`, and all three inherit `countTokens()`. It is unreachable today only because `buildCountTokensUrl()` returns null unless `config.format === "claude"` and the URL carries `/messages` — hailuo-web and qwen-web set no format, lmarena sets `"openai"` — so `countTokens()` returns at the guard above. Latent, not live; one `format` change away from passing a credentials object where a token string is expected. Two more, surfaced by clearing the above: * `lmarena` declared `buildUrl` and `transformRequest` `protected` while both are public on BaseExecutor (TS2415 — a subclass may widen visibility, never narrow it). Both were masked behind the buildHeaders TS2416 and appeared one at a time as it cleared. Runtime is unaffected; JavaScript has no member visibility. * `GithubExecutor.refreshCredentials` had no declared return type, so TypeScript inferred the union of its four literal returns. `GheCopilotExecutor` legitimately overrides it with a wider `providerSpecificData` (it also records the enterprise proxy URL) and no `expiresIn`, which is not assignable to that inferred union. Declared as `RefreshedCopilotCredentials | null` — same shape of fix as diegosouzapw#8489, on a different method. Validation: full tsc error-set diff against the base config — 335 -> 331, zero new errors (line-number-agnostic). `typecheck:core` clean; the 15 existing test files importing a touched executor pass, including lmarena's 44 across the two updated files. `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 that the inherited method is no longer shadowed — verified to fail on the base, where all three prototypes still carry their own `buildHeaders` — and that the `countTokens()` early return which kept it harmless still holds.
`BaseExecutor.buildHeaders(credentials, stream?, clientHeaders?, model?, health?)` was shadowed in three executors by same-named helpers with unrelated signatures: hailuo-web private buildHeaders(token: string, yy: string) lmarena protected buildHeaders(_model: string, credentials: unknown, _body: unknown) qwen-web private buildHeaders(token: string, cookieHeader: string, chatId?: string) Name collisions, not overrides — each reported TS2416. They are renamed to `buildStreamHeaders` / `buildRequestHeaders` / `buildApiHeaders`; the two lmarena test files that called the helper directly are updated with them. Worth stating precisely, because the shadow sat on a live dispatch path without being a live bug: `BaseExecutor.countTokens()` calls `this.buildHeaders(credentials, false)`, and all three inherit `countTokens()`. It is unreachable today only because `buildCountTokensUrl()` returns null unless `config.format === "claude"` and the URL carries `/messages` — hailuo-web and qwen-web set no format, lmarena sets `"openai"` — so `countTokens()` returns at the guard above. Latent, not live; one `format` change away from passing a credentials object where a token string is expected. Two more, surfaced by clearing the above: * `lmarena` declared `buildUrl` and `transformRequest` `protected` while both are public on BaseExecutor (TS2415 — a subclass may widen visibility, never narrow it). Both were masked behind the buildHeaders TS2416 and appeared one at a time as it cleared. Runtime is unaffected; JavaScript has no member visibility. * `GithubExecutor.refreshCredentials` had no declared return type, so TypeScript inferred the union of its four literal returns. `GheCopilotExecutor` legitimately overrides it with a wider `providerSpecificData` (it also records the enterprise proxy URL) and no `expiresIn`, which is not assignable to that inferred union. Declared as `RefreshedCopilotCredentials | null` — same shape of fix as diegosouzapw#8489, on a different method. Validation: full tsc error-set diff against the base config — 335 -> 331, zero new errors (line-number-agnostic). `typecheck:core` clean; the 15 existing test files importing a touched executor pass, including lmarena's 44 across the two updated files. `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 that the inherited method is no longer shadowed — verified to fail on the base, where all three prototypes still carry their own `buildHeaders` — and that the `countTokens()` early return which kept it harmless still holds.
`BaseExecutor.buildHeaders(credentials, stream?, clientHeaders?, model?, health?)` was shadowed in three executors by same-named helpers with unrelated signatures: hailuo-web private buildHeaders(token: string, yy: string) lmarena protected buildHeaders(_model: string, credentials: unknown, _body: unknown) qwen-web private buildHeaders(token: string, cookieHeader: string, chatId?: string) Name collisions, not overrides — each reported TS2416. They are renamed to `buildStreamHeaders` / `buildRequestHeaders` / `buildApiHeaders`; the two lmarena test files that called the helper directly are updated with them. Worth stating precisely, because the shadow sat on a live dispatch path without being a live bug: `BaseExecutor.countTokens()` calls `this.buildHeaders(credentials, false)`, and all three inherit `countTokens()`. It is unreachable today only because `buildCountTokensUrl()` returns null unless `config.format === "claude"` and the URL carries `/messages` — hailuo-web and qwen-web set no format, lmarena sets `"openai"` — so `countTokens()` returns at the guard above. Latent, not live; one `format` change away from passing a credentials object where a token string is expected. Two more, surfaced by clearing the above: * `lmarena` declared `buildUrl` and `transformRequest` `protected` while both are public on BaseExecutor (TS2415 — a subclass may widen visibility, never narrow it). Both were masked behind the buildHeaders TS2416 and appeared one at a time as it cleared. Runtime is unaffected; JavaScript has no member visibility. * `GithubExecutor.refreshCredentials` had no declared return type, so TypeScript inferred the union of its four literal returns. `GheCopilotExecutor` legitimately overrides it with a wider `providerSpecificData` (it also records the enterprise proxy URL) and no `expiresIn`, which is not assignable to that inferred union. Declared as `RefreshedCopilotCredentials | null` — same shape of fix as diegosouzapw#8489, on a different method. Validation: full tsc error-set diff against the base config — 335 -> 331, zero new errors (line-number-agnostic). `typecheck:core` clean; the 15 existing test files importing a touched executor pass, including lmarena's 44 across the two updated files. `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 that the inherited method is no longer shadowed — verified to fail on the base, where all three prototypes still carry their own `buildHeaders` — and that the `countTokens()` early return which kept it harmless still holds.
…rs (#8498) `BaseExecutor.buildHeaders(credentials, stream?, clientHeaders?, model?, health?)` was shadowed in three executors by same-named helpers with unrelated signatures: hailuo-web private buildHeaders(token: string, yy: string) lmarena protected buildHeaders(_model: string, credentials: unknown, _body: unknown) qwen-web private buildHeaders(token: string, cookieHeader: string, chatId?: string) Name collisions, not overrides — each reported TS2416. They are renamed to `buildStreamHeaders` / `buildRequestHeaders` / `buildApiHeaders`; the two lmarena test files that called the helper directly are updated with them. Worth stating precisely, because the shadow sat on a live dispatch path without being a live bug: `BaseExecutor.countTokens()` calls `this.buildHeaders(credentials, false)`, and all three inherit `countTokens()`. It is unreachable today only because `buildCountTokensUrl()` returns null unless `config.format === "claude"` and the URL carries `/messages` — hailuo-web and qwen-web set no format, lmarena sets `"openai"` — so `countTokens()` returns at the guard above. Latent, not live; one `format` change away from passing a credentials object where a token string is expected. Two more, surfaced by clearing the above: * `lmarena` declared `buildUrl` and `transformRequest` `protected` while both are public on BaseExecutor (TS2415 — a subclass may widen visibility, never narrow it). Both were masked behind the buildHeaders TS2416 and appeared one at a time as it cleared. Runtime is unaffected; JavaScript has no member visibility. * `GithubExecutor.refreshCredentials` had no declared return type, so TypeScript inferred the union of its four literal returns. `GheCopilotExecutor` legitimately overrides it with a wider `providerSpecificData` (it also records the enterprise proxy URL) and no `expiresIn`, which is not assignable to that inferred union. Declared as `RefreshedCopilotCredentials | null` — same shape of fix as #8489, on a different method. Validation: full tsc error-set diff against the base config — 335 -> 331, zero new errors (line-number-agnostic). `typecheck:core` clean; the 15 existing test files importing a touched executor pass, including lmarena's 44 across the two updated files. `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 that the inherited method is no longer shadowed — verified to fail on the base, where all three prototypes still carry their own `buildHeaders` — and that the `countTokens()` early return which kept it harmless still holds.
…ouzapw#8489) `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.
…rs (diegosouzapw#8498) `BaseExecutor.buildHeaders(credentials, stream?, clientHeaders?, model?, health?)` was shadowed in three executors by same-named helpers with unrelated signatures: hailuo-web private buildHeaders(token: string, yy: string) lmarena protected buildHeaders(_model: string, credentials: unknown, _body: unknown) qwen-web private buildHeaders(token: string, cookieHeader: string, chatId?: string) Name collisions, not overrides — each reported TS2416. They are renamed to `buildStreamHeaders` / `buildRequestHeaders` / `buildApiHeaders`; the two lmarena test files that called the helper directly are updated with them. Worth stating precisely, because the shadow sat on a live dispatch path without being a live bug: `BaseExecutor.countTokens()` calls `this.buildHeaders(credentials, false)`, and all three inherit `countTokens()`. It is unreachable today only because `buildCountTokensUrl()` returns null unless `config.format === "claude"` and the URL carries `/messages` — hailuo-web and qwen-web set no format, lmarena sets `"openai"` — so `countTokens()` returns at the guard above. Latent, not live; one `format` change away from passing a credentials object where a token string is expected. Two more, surfaced by clearing the above: * `lmarena` declared `buildUrl` and `transformRequest` `protected` while both are public on BaseExecutor (TS2415 — a subclass may widen visibility, never narrow it). Both were masked behind the buildHeaders TS2416 and appeared one at a time as it cleared. Runtime is unaffected; JavaScript has no member visibility. * `GithubExecutor.refreshCredentials` had no declared return type, so TypeScript inferred the union of its four literal returns. `GheCopilotExecutor` legitimately overrides it with a wider `providerSpecificData` (it also records the enterprise proxy URL) and no `expiresIn`, which is not assignable to that inferred union. Declared as `RefreshedCopilotCredentials | null` — same shape of fix as diegosouzapw#8489, on a different method. Validation: full tsc error-set diff against the base config — 335 -> 331, zero new errors (line-number-agnostic). `typecheck:core` clean; the 15 existing test files importing a touched executor pass, including lmarena's 44 across the two updated files. `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 that the inherited method is no longer shadowed — verified to fail on the base, where all three prototypes still carry their own `buildHeaders` — and that the `countTokens()` early return which kept it harmless still holds.
…ouzapw#8489) `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.
…rs (diegosouzapw#8498) `BaseExecutor.buildHeaders(credentials, stream?, clientHeaders?, model?, health?)` was shadowed in three executors by same-named helpers with unrelated signatures: hailuo-web private buildHeaders(token: string, yy: string) lmarena protected buildHeaders(_model: string, credentials: unknown, _body: unknown) qwen-web private buildHeaders(token: string, cookieHeader: string, chatId?: string) Name collisions, not overrides — each reported TS2416. They are renamed to `buildStreamHeaders` / `buildRequestHeaders` / `buildApiHeaders`; the two lmarena test files that called the helper directly are updated with them. Worth stating precisely, because the shadow sat on a live dispatch path without being a live bug: `BaseExecutor.countTokens()` calls `this.buildHeaders(credentials, false)`, and all three inherit `countTokens()`. It is unreachable today only because `buildCountTokensUrl()` returns null unless `config.format === "claude"` and the URL carries `/messages` — hailuo-web and qwen-web set no format, lmarena sets `"openai"` — so `countTokens()` returns at the guard above. Latent, not live; one `format` change away from passing a credentials object where a token string is expected. Two more, surfaced by clearing the above: * `lmarena` declared `buildUrl` and `transformRequest` `protected` while both are public on BaseExecutor (TS2415 — a subclass may widen visibility, never narrow it). Both were masked behind the buildHeaders TS2416 and appeared one at a time as it cleared. Runtime is unaffected; JavaScript has no member visibility. * `GithubExecutor.refreshCredentials` had no declared return type, so TypeScript inferred the union of its four literal returns. `GheCopilotExecutor` legitimately overrides it with a wider `providerSpecificData` (it also records the enterprise proxy URL) and no `expiresIn`, which is not assignable to that inferred union. Declared as `RefreshedCopilotCredentials | null` — same shape of fix as diegosouzapw#8489, on a different method. Validation: full tsc error-set diff against the base config — 335 -> 331, zero new errors (line-number-agnostic). `typecheck:core` clean; the 15 existing test files importing a touched executor pass, including lmarena's 44 across the two updated files. `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 that the inherited method is no longer shadowed — verified to fail on the base, where all three prototypes still carry their own `buildHeaders` — and that the `countTokens()` early return which kept it harmless still holds.
Part of #8484. Type-only; no toolchain change.
What was wrong
normalizeExecutorResult()acceptsResponse | { response, url, headers, transformedBody }and wraps the bare arm — that union is the real contract, it is written into the normalizer's parameter type, andchatcore-upstream-timeouts.test.tsalready asserts both shapes work.BaseExecutor.executehas no explicit return type, so TypeScript inferred it from the method's singlereturnstatement — the object shape alone. Every override returning a bareResponsewas therefore rejected:duckduckgo-web.ts— 14 valid returns oferrorResponse()/processResponse()TS2739felo-web.ts,gitlab.ts— declarePromise<Response>TS2416duckduckgo-web.tscompounded it by pinning its ownexecute()signature to just the object shape while returningResponsefrom those 14 paths.What this does
Fixes the declaration, not the call sites.
base.tsexportsExecutorExecuteResult— the same union the normalizer already accepts — and annotatesBaseExecutor.executewith it.duckduckgo-web.tsthen drops its over-narrow annotation, matchingBaseExecutorand the ~38 other executors that leave the return type inferred (only ~6 declare one, and two of those are theTS2416s above).Two subclasses read
.responsestraight offsuper.execute()and now narrow first:github.ts— its existing!result.responseguard already meant "bare Response, nothing to materialize". That intent is now expressed asresult instanceof Response. Same branch taken for every input shape (bare / object / nullish).pollinations.ts— reads the status through both arms for its session-pool bookkeeping.Wrapping DuckDuckGo's 14 returns would have been the wrong fix. Those values are already correct, and
normalizeExecutorResult()produces exactly{ response, url: "", headers: {}, transformedBody: null }for them — wrapping by hand would duplicate the normalizer and churn 14 call sites to satisfy a declaration that was itself wrong.Validation
Full tsc error-set diff against the base config (base run with
ignoreDeprecations: "6.0"so compilation gets pastTS5101):Two
duckduckgo-web.tsTS2345s appear in a naive line-based diff — they are the same two pre-existing errors renumbered by the comments this PR adds. A line-number-agnostic diff of the two error sets is empty. They are unrelated root causes (an argument type and aBuffer/BodyInitmismatch) and are left for a later slice rather than padding this one.npm run typecheck:corenpm run check:type-coveragetests/unit/ts7-executor-result-contract.test.tsTwo pre-existing conditions on the base branch, both verified on a worktree carrying none of these changes, neither touched here:
tests/unit/plan3-p0.test.ts— 37 pass / 1 fail. Reads the developer's real~/.omnirouteDB instead of a test-scopedDATA_DIR, so it resolves against whatever providers happen to be configured locally.npm run lint— 4 errors intests/unit/claude-to-openai-think-close-5123.test.ts. Itseslint-suppressions.jsonentry allowscount: 2but the file now holds moreanys. Baseline drift, worth its own tiny PR. Lint is clean for every file this PR touches (base.tscarries no suppression entry and reports 0).Tests
The normalizer half is already covered by
chatcore-upstream-timeouts.test.ts. The new file pins the half this PR changed — thatGithubExecutor.executepasses a bareResponsethrough untouched, still materializes the capture-object arm, and tolerates a nullish result — so a later "simplification" can't quietly drop the bare-Response arm now that the type permits it.