Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d298d4a
feat(api): abort signal support for requesty (completePrompt + shared…
easonliang28 Sep 5, 2026
ebd5404
test(api): strengthen requesty abort assertions and share settle guar…
easonliang28 Sep 5, 2026
5e0d29b
Merge branch main into feat/abort-r1-requesty-completeprompt
easonliang28 Sep 6, 2026
2d915cb
test(api): assert pre-aborted Requesty completePrompt skips model loo…
easonliang28 Sep 6, 2026
58b4ef1
chore: re-trigger CodeRabbit review
easonliang28 Sep 11, 2026
bdbfff3
Merge remote-tracking branch 'upstream/main' into feat/abort-r1-reque…
easonliang28 Sep 14, 2026
547f6a3
Merge branch 'main' into feat/abort-r1-requesty-completeprompt
easonLiangWorldedtech Sep 28, 2026
cecbdbd
Merge remote-tracking branch 'upstream/main' into feat/abort-r1-reque…
Oct 2, 2026
84baa46
chore: re-trigger CodeRabbit re-review and label reconcile
Oct 2, 2026
137e071
chore: trigger CodeRabbit re-review (findings already addressed)
Oct 2, 2026
5382ee4
Merge commit 'refs/shapes/upstream-main' into feat/abort-r1-requesty-…
Oct 2, 2026
1ad08bc
chore: trigger CodeRabbit re-review / label reconcile
Oct 2, 2026
2d12c01
chore: trigger CodeRabbit re-review at current head (all findings alr…
Oct 3, 2026
a0173d8
fix(abort): consume the pending rejection when the signal is already …
Oct 3, 2026
bf3afa5
Merge upstream main (3859e5dd8) into feat/abort-r1-requesty
Oct 4, 2026
145a93d
Re-trigger review at the current head
Oct 4, 2026
2ebd748
Merge branch 'main' into feat/abort-r1-requesty-completeprompt
easonLiangWorldedtech Oct 5, 2026
c9552fa
Merge upstream/main (4f1dee18e) into feat/abort-r1-requesty-completep…
easonliang28 Oct 16, 2026
2c25eae
test(requesty): pin the provider abort message in the timeout test
Oct 21, 2026
efb9ac8
test(requesty): pin ordinary SDK failures with a live signal/timeout
easonLiangWorldedtech Oct 27, 2026
16f92db
fix(requesty): forward a validated SDK timeout via the shared helpers
easonLiangWorldedtech Oct 27, 2026
7683c73
Merge remote-tracking branch 'upstream/main' into feat/abort-r1-reque…
easonLiangWorldedtech Oct 27, 2026
7f61c02
chore: drop the duplicated timeout helpers after the upstream merge
easonLiangWorldedtech Oct 27, 2026
4573cdb
Merge upstream/main into feat/abort-r1-requesty-completeprompt
Nov 1, 2026
444c26c
fix(merge): restore the upstream hunks dropped by the main merge
easonLiangWorldedtech Nov 1, 2026
311f6b7
fix(requesty): detach the SDK abort signal and race the completion
easonLiangWorldedtech Nov 4, 2026
530033c
test(requesty): pin the late-completion discard and the empty-content…
easonLiangWorldedtech Nov 4, 2026
1975c27
test(requesty): pin timeout normalisation, timeout-covered lookup and…
easonLiangWorldedtech Nov 7, 2026
bd60384
test(lint): flag bare abort substring in toThrow assertions
edelauna Nov 9, 2026
deee63a
fix(eslint): tighten abort assertion guards and test lane coverage
edelauna Nov 9, 2026
d780061
fix(requesty): pass the request signal to model lookup, widen abort lint
edelauna Nov 9, 2026
801a672
merge(main): bring main e487cd7b2 into feat/abort-r1-requesty-complet…
Nov 9, 2026
a326612
test(opencode-go,requesty): pin the abort assertions the new lint rul…
Nov 9, 2026
177fb95
fix(config-eslint): flag an escaped dot or anchor in a bare abort ass…
Nov 9, 2026
df3e889
config-eslint,api: close the escaped-group hole and reach the ignored…
Nov 9, 2026
729fad6
test(api): prove the Requesty lookup timeout aborts with a live calle…
easonLiangWorldedtech Nov 9, 2026
622bb32
fix: address CodeRabbit abort assertion findings
edelauna Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/config-eslint/api-provider-hygiene.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,24 @@ export const apiProviderHygieneConfig = {
},
}

// Shared by every spec lane, not only src/api: an abort assertion must not pass on a raw SDK error.
export const abortSubstringSelector = {
selector:
// Strings contain literal punctuation; only regex source can contain anchors, wildcards,
// optional suffix groups, or word boundaries around the generic abort wording.
"CallExpression[callee.property.name=/^toThrow(Error)?$/]:not([callee.object.property.name='not']):matches([arguments.0.value=/^(the )?(request )?(was )?abort(ed)?[.]*$/i], [arguments.0.regex.pattern=/^[$^]?(?:\\.\\*)?(the )?(request )?(was )?abort(ed)?(?:\\(\\?:(?:ed|ion|al)\\)\\??)?(?:\\\\b)?(?:\\.\\*)?[\\\\.$]*$/i])",
message:
"A bare 'abort' substring also matches a raw SDK error. Assert the AbortError name and the full provider message.",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

// Spec files under src/api. A loop of expects in one test stops at the first failing case and hides the rest.
export const apiProviderTestSelectors = [
{
selector: "ForOfStatement[right.type='ArrayExpression']:has(CallExpression[callee.name='expect'])",
message:
"Use it.each for repeated cases. A loop of expects in one test stops at the first failing case and does not name it.",
},
abortSubstringSelector,
]

// A new @ts-ignore in a provider spec hides a private-member access. Use bracket notation or a typed double.
Expand Down
99 changes: 98 additions & 1 deletion packages/config-eslint/api-provider-hygiene.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"
import { fileURLToPath, URL } from "node:url"

import { Linter } from "eslint"
import { ESLint, Linter } from "eslint"
import typescriptEslint from "typescript-eslint"

import { apiProviderHygieneConfig, apiProviderTestSelectors, apiSpecTsIgnoreConfig } from "./api-provider-hygiene.js"
Expand Down Expand Up @@ -173,6 +174,37 @@ describe("api provider hygiene: spec loops", () => {
assert.deepEqual(lintSpec(code), [])
})
}

for (const code of [
'await expect(p).rejects.toThrow("aborted")',
"expect(() => f()).toThrow('abort')",
'await expect(p).rejects.toThrowError("aborted")',
"expect(() => f()).toThrow(/abort/i)",
"await expect(p).rejects.toThrowError(/aborted/i)",
'await expect(p).rejects.toThrow("was aborted")',
'await expect(p).rejects.toThrow("Request was aborted.")',
'await expect(p).rejects.toThrow("The request was aborted")',
"await expect(p).rejects.toThrow(/was aborted/)",
]) {
it(`flags a bare abort substring: ${code}`, () => {
assert.equal(lintSpec(code).length, 1)
})
}

for (const code of [
'await expect(p).rejects.toThrow("The Requesty request was aborted")',
'await expect(p).rejects.toMatchObject({ name: "AbortError" })',
'await expect(p).rejects.toThrow("API Error")',
'expect(() => f()).not.toThrow("aborted")',
'await expect(p).rejects.not.toThrow("aborted")',
"expect(() => f()).not.toThrowError(/abort/i)",
"await expect(p).rejects.not.toThrowError(/aborted/i)",
"await expect(p).rejects.toThrow(/^The Requesty request was aborted$/)",
]) {
it(`allows: ${code}`, () => {
assert.deepEqual(lintSpec(code), [])
})
}
})

describe("api provider hygiene: hand-built AbortError", () => {
Expand Down Expand Up @@ -245,3 +277,68 @@ describe("api provider hygiene: @ts-ignore in specs", () => {
assert.deepEqual(lintSpecFile("// @ts-ignore x\nhandler.client", "services/__tests__/example.spec.ts"), [])
})
})

// Resolve each lane through the extension's real config before exercising its syntax rules.
// Isolating syntax rules avoids requiring fixture files in the TypeScript project.
describe("abort assertions in the extension ESLint config", () => {
const srcRoot = fileURLToPath(new URL("../../src/", import.meta.url))
const eslint = new ESLint({ cwd: srcRoot })

for (const filename of [
"core/task/__tests__/example.spec.ts",
"core/config/__tests__/example.spec.ts",
// The workflow tests are exempt from the path-traversal selectors, not from the abort
// rule; listing one proves the lane that re-applies it actually resolves for them.
"services/__tests__/pr-review-state-workflow.test.ts",
]) {
it(`checks rejected and accepted assertions in ${filename}`, async () => {
const resolved = await eslint.calculateConfigForFile(filename)
const syntaxConfig = [
{
files: ["**/*.ts"],
languageOptions: { parser: typescriptEslint.parser },
rules: { "no-restricted-syntax": resolved.rules["no-restricted-syntax"] },
},
]
for (const [code, expectedCount] of [
['await expect(p).rejects.toThrow("aborted")', 1],
["await expect(p).rejects.toThrowError(/abort/i)", 1],
// An escaped dot in the source text is still a bare abort substring: the pattern
// keeps a backslash, so the matcher must allow it before the trailing dot or anchor.
["await expect(p).rejects.toThrow(/^aborted$/)", 1],
["await expect(p).rejects.toThrow(/.*aborted.*/i)", 1],
["await expect(p).rejects.toThrowError(/abort(?:ed)/i)", 1],
["await expect(p).rejects.toThrow(/aborted\\b/i)", 1],
['await expect(p).rejects.toThrow("abort$")', 0],
['await expect(p).rejects.toThrow("^abort")', 0],
["await expect(p).rejects.toThrow(/.*The Requesty request was aborted.*/i)", 0],
["await expect(p).rejects.toThrow(/The Requesty request was aborted\\b/i)", 0],
["await expect(p).rejects.toThrow(/^Request was aborted\\.$/)", 1],
["await expect(p).rejects.toThrowError(/request aborted\\.$/i)", 1],
["await expect(p).rejects.toThrow(/^request aborted\\$/)", 1],
// An inline non-capturing group is regex source, not part of the asserted
// message: an anchored match that stops short of it lets a bare abort through.
["await expect(p).rejects.toThrow(/abort(?:ed)?/i)", 1],
["await expect(p).rejects.toThrowError(/^request aborted(?:ed)?$/i)", 1],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
["await expect(p).rejects.toThrow(/cancel(?:ed)?/i)", 0],
[
"await expect(p).rejects.toThrow(/^The Requesty request was aborted(?:ed)? because the peer closed the stream\\.$/)",
0,
],
['await expect(p).rejects.not.toThrow("aborted")', 0],
["await expect(p).rejects.toThrow(/^Request was aborted because the peer closed the stream\\.$/)", 0],
[
'await expect(p).rejects.toMatchObject({ name: "AbortError", message: "The Requesty request was aborted" })',
0,
],
]) {
const messages = linter.verify(code, syntaxConfig, { filename })
assert.equal(messages.length, expectedCount, code)
if (expectedCount) {
assert.equal(messages[0].ruleId, "no-restricted-syntax")
assert.match(messages[0].message, /bare 'abort' substring/)
}
}
})
}
})
25 changes: 20 additions & 5 deletions src/api/providers/__tests__/opencode-go.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1100,13 +1100,19 @@ describe("OpencodeGoHandler", () => {

it("stops an in-flight Responses iterator when its abort signal rejects the read", async () => {
const controller = new AbortController()
// The iterator's own listener and the request-level listener race to reject the same
// promise, so the double is one object and the assertion below pins which rejection
// propagated. opencode-go hands the caller's signal straight to the SDK and never
// normalises it into a provider AbortError, so there is no provider message to assert
// on this path: identity is the strongest statement that is true here.
const abortDouble = new Error("request aborted")
let rejectNext: ((reason?: unknown) => void) | undefined
const iterator = {
next: vitest.fn().mockImplementation(
() =>
new Promise((_resolve, reject) => {
rejectNext = reject
controller.signal.addEventListener("abort", () => reject(new Error("request aborted")), {
controller.signal.addEventListener("abort", () => reject(abortDouble), {
once: true,
})
}),
Expand All @@ -1117,7 +1123,7 @@ describe("OpencodeGoHandler", () => {
},
}
mockResponsesCreate.mockImplementation(async (_body: unknown, options: { signal?: AbortSignal }) => {
options.signal?.addEventListener("abort", () => rejectNext?.(new Error("request aborted")), {
options.signal?.addEventListener("abort", () => rejectNext?.(abortDouble), {
once: true,
})
return iterator
Expand All @@ -1132,7 +1138,7 @@ describe("OpencodeGoHandler", () => {
await vitest.waitFor(() => expect(mockResponsesCreate).toHaveBeenCalled())
controller.abort()

await expect(nextPromise).rejects.toThrow("request aborted")
await expect(nextPromise).rejects.toBe(abortDouble)
expect(iterator.return).toHaveBeenCalled()
})

Expand Down Expand Up @@ -1473,8 +1479,14 @@ describe("OpencodeGoHandler", () => {

it("rejects non-streaming Responses completion when the abort signal fires", async () => {
const controller = new AbortController()
// One double object here too. The Responses completion path wraps every failure
// (`opencode-go.ts:439` throws `Opencode Go completion error: <message>`), so the
// abort surfaces as a new Error carrying the double's text behind the provider prefix,
// not as the double and not as an AbortError. The assertion below pins that whole
// message; a bare "abort" substring would also match a raw SDK error.
const abortDouble = new Error("request aborted")
const request = new Promise<never>((_resolve, reject) => {
controller.signal.addEventListener("abort", () => reject(new Error("request aborted")), { once: true })
controller.signal.addEventListener("abort", () => reject(abortDouble), { once: true })
})
mockResponsesCreate.mockReturnValue(request)
const handler = new OpencodeGoHandler(lunaOptions)
Expand All @@ -1483,7 +1495,10 @@ describe("OpencodeGoHandler", () => {
await vitest.waitFor(() => expect(mockResponsesCreate).toHaveBeenCalled())
controller.abort()

await expect(completion).rejects.toThrow("request aborted")
await expect(completion).rejects.toMatchObject({
name: "Error",
message: `Opencode Go completion error: ${abortDouble.message}`,
})
})

it("completePrompt calls responses.create and returns output_text", async () => {
Expand Down
Loading
Loading