Skip to content

Commit 90f56d2

Browse files
Merge upstream main 53cb5b5 into fws/u3-observation-completeness (1912)
Conflicts were add/add on src/core/task/observationRegistry.ts and its spec: neither side had the file at the merge base 73931fe. Module resolved to main's version verbatim, 3,010 bytes, sha256 head 6d057ff9c1b150e3. That is this branch's 60-line module plus the retired flag, the guard at the head of observe(), close() and get closed, and main's Task.ts line 3784 calls observationRegistry.close() while main's Task.dispose.test.ts line 166 asserts closed, so trunk's consumer requires those two members and taking main's shape is following trunk rather than inventing disposal here. No isClosed accessor exists on this registry on either side: every isClosed in this repository at both revisions is the terminal interface under src/integrations/terminal, so there was no second name for one terminal state to reconcile. Spec resolved row by row against the merged product code rather than by taking either file as a set. main's two closure cases are kept because the merged module has close() and closed. main's weaker forms of two timing cases are replaced by this branch's stronger forms: main asserts only that observedAt is a number where this branch pins the clock and asserts the recorded instant, and main leaves vi.useRealTimers() inline where this branch puts it in a finally so a failed assertion cannot leak fake timers into the next test. The 16 case titles are unchanged from main's file, so nothing was dropped from trunk's coverage. src/core/task/Task.ts merged without conflict and now calls observationRegistry.clear() at line 3727, added by this branch, and observationRegistry.close() at line 3792, from main, both inside disposeOnce(). Both are kept. close() also empties the entries, so the earlier clear() is redundant on the final state, and it is kept rather than removed because it is this unit's own authored cleanup and removing it would be a behavioural edit this merge was not asked to make. Stated here rather than left in the diff. Trunk's docstring still describes the completeness check as gating a full-file replacement, lines 9 and 19 of main's module, and nothing in the shipped code enforces that gate; the registry records completeness and the guarded write does not consult it as a gate. That gap is recorded and unenforced, not papered over.
2 parents 9d420ad + 53cb5b5 commit 90f56d2

107 files changed

Lines changed: 4353 additions & 618 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/code-qa.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ jobs:
247247
- name: Verify coverage cache inputs
248248
if: matrix.collect-coverage
249249
run: pnpm --dir src run verify:coverage-cache-inputs
250+
# Validate build/static cache inputs (issue #2290 contract) so a
251+
# stale webview or extension bundle is never restored as a hit.
252+
- name: Verify build cache inputs
253+
if: matrix.collect-coverage
254+
run: pnpm --dir src run verify:build-cache-inputs
250255
- name: Save Turbo cache
251256
if: steps.turbo-cache.outputs.cache-hit != 'true'
252257
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0

‎apps/vscode-e2e/AGENTS.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,13 @@ The `model` field can be added to either match when a test targets a specific mo
140140
Use `streamingProfile: { ttft: <ms> }` on a fixture, not a flat `latency: <ms>`, when a test needs
141141
to simulate a slow or hung provider (e.g. to cancel an in-flight request mid-stream). `ttft` delays
142142
only the first SSE chunk, so the pending window is exactly the configured value. Flat `latency`
143-
delays _every_ chunk, and aimock never observes client disconnects — after a test cancels the
144-
request, a flat-latency stream keeps flushing chunks server-side for `chunks × latency` before
145-
reaching the dead socket, which can interleave with the next test's request against the same mock
146-
server. See `SUBTASK_API_HANG_RESPONSE_LATENCY_MS` in `fixtures/subtasks.ts` for an example,
147-
including the bounded post-test drain the calling suite uses to wait out that window.
143+
delays _every_ chunk. Before aimock 1.47.0, the mock did not observe client disconnects — after a
144+
test cancelled the request, a flat-latency stream kept flushing chunks server-side for
145+
`chunks × latency` before reaching the dead socket, which could interleave with the next test's
146+
request against the same mock server. Version 1.47.0 stops the reply on disconnect, but the
147+
suite keeps its drain wait until an e2e run shows it is not needed. See
148+
`SUBTASK_API_HANG_RESPONSE_LATENCY_MS` in `fixtures/subtasks.ts` for an example, including the
149+
bounded post-test drain the calling suite uses to wait out that window.
148150

149151
## 404 errors in logs are expected
150152

‎apps/vscode-e2e/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"@roo-code/config-eslint": "workspace:^",
2626
"@roo-code/config-typescript": "workspace:^",
2727
"@roo-code/types": "workspace:^",
28-
"@copilotkit/aimock": "1.35.0",
28+
"@copilotkit/aimock": "1.47.0",
2929
"@types/mocha": "10.0.10",
3030
"@types/node": "catalog:",
3131
"@types/vscode": "1.100.0",

‎apps/vscode-e2e/src/suite/subtasks.test.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,11 @@ const waitForAimockRequestContaining = async (
9393
// suite becomes flaky again on slow CI runners, widen this value first.
9494
const SUBTASK_API_HANG_DRAIN_GRACE_MS = 500
9595

96-
// aimock does not observe client disconnects: after the API-hang child request is cancelled,
97-
// the mock keeps the delayed stream pending server-side until the fixture's ttft has fully
98-
// elapsed, then flushes the remaining chunks to the dead socket. A streamed request opened by
99-
// the next test can interleave with that late flush, so wait out the remainder of the delayed
96+
// aimock 1.47.0 stops and journals a reply when the client disconnects. Before that, the mock
97+
// kept the delayed stream pending server-side until the fixture's ttft had fully elapsed, then
98+
// flushed the remaining chunks to the dead socket. A streamed request opened by the next test
99+
// could interleave with that late flush. This wait is a conservative guard kept until an e2e
100+
// run shows it is not needed: wait out the remainder of the delayed
100101
// window before the next test runs. The deadline is anchored to the journal timestamp of the
101102
// request this test created (never earlier traffic), and bounded by one latency window plus
102103
// grace, so it cannot hide a genuine hang.

‎packages/config-eslint/api-provider-hygiene.js‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,24 @@ export const apiProviderHygieneConfig = {
6363
},
6464
}
6565

66+
// Shared by every spec lane, not only src/api: an abort assertion must not pass on a raw SDK error.
67+
export const abortSubstringSelector = {
68+
selector:
69+
// Strings contain literal punctuation; only regex source can contain anchors, wildcards,
70+
// optional suffix groups, or word boundaries around the generic abort wording.
71+
"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])",
72+
message:
73+
"A bare 'abort' substring also matches a raw SDK error. Assert the AbortError name and the full provider message.",
74+
}
75+
6676
// Spec files under src/api. A loop of expects in one test stops at the first failing case and hides the rest.
6777
export const apiProviderTestSelectors = [
6878
{
6979
selector: "ForOfStatement[right.type='ArrayExpression']:has(CallExpression[callee.name='expect'])",
7080
message:
7181
"Use it.each for repeated cases. A loop of expects in one test stops at the first failing case and does not name it.",
7282
},
83+
abortSubstringSelector,
7384
]
7485

7586
// A new @ts-ignore in a provider spec hides a private-member access. Use bracket notation or a typed double.

‎packages/config-eslint/api-provider-hygiene.test.js‎

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import assert from "node:assert/strict"
22
import { describe, it } from "node:test"
3+
import { fileURLToPath, URL } from "node:url"
34

4-
import { Linter } from "eslint"
5+
import { ESLint, Linter } from "eslint"
56
import typescriptEslint from "typescript-eslint"
67

78
import { apiProviderHygieneConfig, apiProviderTestSelectors, apiSpecTsIgnoreConfig } from "./api-provider-hygiene.js"
@@ -173,6 +174,37 @@ describe("api provider hygiene: spec loops", () => {
173174
assert.deepEqual(lintSpec(code), [])
174175
})
175176
}
177+
178+
for (const code of [
179+
'await expect(p).rejects.toThrow("aborted")',
180+
"expect(() => f()).toThrow('abort')",
181+
'await expect(p).rejects.toThrowError("aborted")',
182+
"expect(() => f()).toThrow(/abort/i)",
183+
"await expect(p).rejects.toThrowError(/aborted/i)",
184+
'await expect(p).rejects.toThrow("was aborted")',
185+
'await expect(p).rejects.toThrow("Request was aborted.")',
186+
'await expect(p).rejects.toThrow("The request was aborted")',
187+
"await expect(p).rejects.toThrow(/was aborted/)",
188+
]) {
189+
it(`flags a bare abort substring: ${code}`, () => {
190+
assert.equal(lintSpec(code).length, 1)
191+
})
192+
}
193+
194+
for (const code of [
195+
'await expect(p).rejects.toThrow("The Requesty request was aborted")',
196+
'await expect(p).rejects.toMatchObject({ name: "AbortError" })',
197+
'await expect(p).rejects.toThrow("API Error")',
198+
'expect(() => f()).not.toThrow("aborted")',
199+
'await expect(p).rejects.not.toThrow("aborted")',
200+
"expect(() => f()).not.toThrowError(/abort/i)",
201+
"await expect(p).rejects.not.toThrowError(/aborted/i)",
202+
"await expect(p).rejects.toThrow(/^The Requesty request was aborted$/)",
203+
]) {
204+
it(`allows: ${code}`, () => {
205+
assert.deepEqual(lintSpec(code), [])
206+
})
207+
}
176208
})
177209

178210
describe("api provider hygiene: hand-built AbortError", () => {
@@ -245,3 +277,68 @@ describe("api provider hygiene: @ts-ignore in specs", () => {
245277
assert.deepEqual(lintSpecFile("// @ts-ignore x\nhandler.client", "services/__tests__/example.spec.ts"), [])
246278
})
247279
})
280+
281+
// Resolve each lane through the extension's real config before exercising its syntax rules.
282+
// Isolating syntax rules avoids requiring fixture files in the TypeScript project.
283+
describe("abort assertions in the extension ESLint config", () => {
284+
const srcRoot = fileURLToPath(new URL("../../src/", import.meta.url))
285+
const eslint = new ESLint({ cwd: srcRoot })
286+
287+
for (const filename of [
288+
"core/task/__tests__/example.spec.ts",
289+
"core/config/__tests__/example.spec.ts",
290+
// The workflow tests are exempt from the path-traversal selectors, not from the abort
291+
// rule; listing one proves the lane that re-applies it actually resolves for them.
292+
"services/__tests__/pr-review-state-workflow.test.ts",
293+
]) {
294+
it(`checks rejected and accepted assertions in ${filename}`, async () => {
295+
const resolved = await eslint.calculateConfigForFile(filename)
296+
const syntaxConfig = [
297+
{
298+
files: ["**/*.ts"],
299+
languageOptions: { parser: typescriptEslint.parser },
300+
rules: { "no-restricted-syntax": resolved.rules["no-restricted-syntax"] },
301+
},
302+
]
303+
for (const [code, expectedCount] of [
304+
['await expect(p).rejects.toThrow("aborted")', 1],
305+
["await expect(p).rejects.toThrowError(/abort/i)", 1],
306+
// An escaped dot in the source text is still a bare abort substring: the pattern
307+
// keeps a backslash, so the matcher must allow it before the trailing dot or anchor.
308+
["await expect(p).rejects.toThrow(/^aborted$/)", 1],
309+
["await expect(p).rejects.toThrow(/.*aborted.*/i)", 1],
310+
["await expect(p).rejects.toThrowError(/abort(?:ed)/i)", 1],
311+
["await expect(p).rejects.toThrow(/aborted\\b/i)", 1],
312+
['await expect(p).rejects.toThrow("abort$")', 0],
313+
['await expect(p).rejects.toThrow("^abort")', 0],
314+
["await expect(p).rejects.toThrow(/.*The Requesty request was aborted.*/i)", 0],
315+
["await expect(p).rejects.toThrow(/The Requesty request was aborted\\b/i)", 0],
316+
["await expect(p).rejects.toThrow(/^Request was aborted\\.$/)", 1],
317+
["await expect(p).rejects.toThrowError(/request aborted\\.$/i)", 1],
318+
["await expect(p).rejects.toThrow(/^request aborted\\$/)", 1],
319+
// An inline non-capturing group is regex source, not part of the asserted
320+
// message: an anchored match that stops short of it lets a bare abort through.
321+
["await expect(p).rejects.toThrow(/abort(?:ed)?/i)", 1],
322+
["await expect(p).rejects.toThrowError(/^request aborted(?:ed)?$/i)", 1],
323+
["await expect(p).rejects.toThrow(/cancel(?:ed)?/i)", 0],
324+
[
325+
"await expect(p).rejects.toThrow(/^The Requesty request was aborted(?:ed)? because the peer closed the stream\\.$/)",
326+
0,
327+
],
328+
['await expect(p).rejects.not.toThrow("aborted")', 0],
329+
["await expect(p).rejects.toThrow(/^Request was aborted because the peer closed the stream\\.$/)", 0],
330+
[
331+
'await expect(p).rejects.toMatchObject({ name: "AbortError", message: "The Requesty request was aborted" })',
332+
0,
333+
],
334+
]) {
335+
const messages = linter.verify(code, syntaxConfig, { filename })
336+
assert.equal(messages.length, expectedCount, code)
337+
if (expectedCount) {
338+
assert.equal(messages[0].ruleId, "no-restricted-syntax")
339+
assert.match(messages[0].message, /bare 'abort' substring/)
340+
}
341+
}
342+
})
343+
}
344+
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { experimentIds, experimentIdsSchema, experimentsSchema } from "../experiment.js"
2+
3+
describe("dynamicThinkingEffort experiment", () => {
4+
it("is part of the experiment id enum", () => {
5+
expect(experimentIds).toContain("dynamicThinkingEffort")
6+
expect(experimentIdsSchema.safeParse("dynamicThinkingEffort").success).toBe(true)
7+
})
8+
9+
it("parses enabled and disabled states", () => {
10+
expect(experimentsSchema.parse({ dynamicThinkingEffort: true })).toEqual({ dynamicThinkingEffort: true })
11+
expect(experimentsSchema.parse({ dynamicThinkingEffort: false })).toEqual({ dynamicThinkingEffort: false })
12+
expect(experimentsSchema.parse({})).toEqual({})
13+
})
14+
15+
it("rejects non-boolean values", () => {
16+
expect(experimentsSchema.safeParse({ dynamicThinkingEffort: "yes" }).success).toBe(false)
17+
expect(experimentIdsSchema.safeParse("dynamic-thinking-effort").success).toBe(false)
18+
})
19+
})

‎packages/types/src/__tests__/openai-models.test.ts‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,71 @@ import { openAiCodexDefaultModelId, openAiCodexModels } from "../providers/opena
22
import { openAiNativeDefaultModelId, openAiNativeModels } from "../providers/openai.js"
33

44
describe("OpenAI native models", () => {
5+
it("describes GPT-6.1 Terra pricing and optional native reasoning without changing defaults", () => {
6+
expect(openAiNativeDefaultModelId).toBe("gpt-5.6-sol")
7+
expect(openAiCodexDefaultModelId).toBe("gpt-5.6-sol")
8+
const model = openAiNativeModels["gpt-6.1-terra"]
9+
expect(model).toMatchObject({
10+
maxTokens: 128_000,
11+
contextWindow: 1_050_000,
12+
includedTools: ["apply_patch"],
13+
excludedTools: ["apply_diff", "write_to_file"],
14+
supportsImages: true,
15+
supportsPromptCache: true,
16+
supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"],
17+
reasoningEffort: "medium",
18+
supportsTemperature: false,
19+
supportsVerbosity: true,
20+
inputPrice: 1,
21+
outputPrice: 5,
22+
cacheWritesPrice: 1.25,
23+
cacheWritesPrice12h: 2.5,
24+
cacheReadsPrice: 0.05,
25+
longContextPricing: {
26+
thresholdTokens: 272_000,
27+
inputPriceMultiplier: 2,
28+
outputPriceMultiplier: 1.5,
29+
cacheWritesPriceMultiplier: 2,
30+
cacheReadsPriceMultiplier: 2,
31+
appliesToServiceTiers: ["default", "flex", "priority"],
32+
},
33+
})
34+
expect(model).not.toHaveProperty("requiredReasoningEffort", true)
35+
expect(model.tiers).toEqual([
36+
{
37+
name: "flex",
38+
contextWindow: 1_050_000,
39+
inputPrice: 0.5,
40+
outputPrice: 2.5,
41+
cacheWritesPrice: 0.625,
42+
cacheReadsPrice: 0.025,
43+
},
44+
{
45+
name: "priority",
46+
contextWindow: 1_050_000,
47+
inputPrice: 2,
48+
outputPrice: 10,
49+
cacheWritesPrice: 2.5,
50+
cacheReadsPrice: 0.1,
51+
},
52+
])
53+
expect(openAiCodexModels["gpt-6.1-terra"]).toMatchObject({
54+
maxTokens: 128_000,
55+
contextWindow: 872_000,
56+
includedTools: ["apply_patch"],
57+
excludedTools: ["apply_diff", "write_to_file"],
58+
supportsImages: true,
59+
supportsPromptCache: true,
60+
supportsReasoningEffort: ["low", "medium", "high", "xhigh", "max"],
61+
requiredReasoningEffort: true,
62+
reasoningEffort: "low",
63+
supportsTemperature: false,
64+
supportsVerbosity: true,
65+
inputPrice: 0,
66+
outputPrice: 0,
67+
})
68+
})
69+
570
it("describes GPT-6.1 Sol pricing and required reasoning without changing defaults", () => {
671
expect(openAiNativeDefaultModelId).toBe("gpt-5.6-sol")
772
expect(openAiCodexDefaultModelId).toBe("gpt-5.6-sol")

‎packages/types/src/__tests__/provider-identifiers.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ describe("provider identifiers", () => {
114114
providerIdentifiers.nanogpt,
115115
providerIdentifiers.ioIntelligence,
116116
providerIdentifiers.kimiCode,
117+
providerIdentifiers.friendli,
117118
])
118119
expect(localProviders).toEqual([providerIdentifiers.ollama, providerIdentifiers.lmstudio])
119120
expect(internalProviders).toEqual([providerIdentifiers.vscodeLm, providerIdentifiers.githubCopilot])

‎packages/types/src/experiment.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const experimentIds = [
1212
"runSlashCommand",
1313
"customTools",
1414
"parallelToolExecution",
15+
"dynamicThinkingEffort",
1516
] as const
1617

1718
export const experimentIdsSchema = z.enum(experimentIds)
@@ -28,6 +29,7 @@ export const experimentsSchema = z.object({
2829
runSlashCommand: z.boolean().optional(),
2930
customTools: z.boolean().optional(),
3031
parallelToolExecution: z.boolean().optional(),
32+
dynamicThinkingEffort: z.boolean().optional(),
3133
})
3234

3335
export type Experiments = z.infer<typeof experimentsSchema>

0 commit comments

Comments
 (0)