diff --git a/config/scripts/mobile-web-app-frame-budget-sweep.test.ts b/config/scripts/mobile-web-app-frame-budget-sweep.test.ts
index eee3d8268f6..beffa67462a 100644
--- a/config/scripts/mobile-web-app-frame-budget-sweep.test.ts
+++ b/config/scripts/mobile-web-app-frame-budget-sweep.test.ts
@@ -1,5 +1,5 @@
/**
- * The mobile-view frame budget, held against Chromium's own JPEG encoder across the viewport range.
+ * The mobile-view frame budget, held against Chromium's own JPEG encoder at representative phone, tablet and wide viewports.
*
* `WORST_CASE_JPEG_BYTES_PER_PIXEL` is the one number the budget cannot derive, and every other
* check of it is circular: a case that encodes `noise(area * theConstant)` is measuring a byte
@@ -43,9 +43,7 @@ async function loadSweepModules() {
])
return {
budgetedMobileViewDeviceScaleFactor: request.budgetedMobileViewDeviceScaleFactor,
- mobileBrowserFrameAreaBudget: request.mobileBrowserFrameAreaBudget,
WORST_CASE_JPEG_BYTES_PER_PIXEL: request.WORST_CASE_JPEG_BYTES_PER_PIXEL,
- MOBILE_VIEW_DEVICE_SCALE_FACTOR: parameters.MOBILE_VIEW_DEVICE_SCALE_FACTOR,
BROWSER_FRAME_QUALITY: parameters.BROWSER_FRAME_QUALITY,
BRIDGE_MAX_MESSAGE_BYTES: caps.BRIDGE_MAX_MESSAGE_BYTES,
utf8ByteLength: caps.utf8ByteLength,
@@ -65,15 +63,23 @@ function sweep() {
return loaded
}
-/** The viewport range the pane is mounted in, phone through tablet, in CSS pixels. */
-const VIEWPORT_WIDTHS = [320, 360, 390, 393, 412, 430, 480, 600, 768, 834, 1024, 1280, 1400]
-const VIEWPORT_HEIGHTS = [480, 640, 712, 720, 800, 896, 932, 1024, 1180, 1366, 1600]
-
type Viewport = { width: number; height: number }
-const VIEWPORTS: Viewport[] = VIEWPORT_WIDTHS.flatMap((width) =>
- VIEWPORT_HEIGHTS.map((height) => ({ width, height }))
-)
+// Sample aspect ratios and budget pressure without replaying one encoder contract 111 times.
+const VIEWPORTS: Viewport[] = [
+ { width: 320, height: 480 },
+ { width: 360, height: 640 },
+ { width: 390, height: 712 },
+ { width: 393, height: 720 },
+ { width: 430, height: 932 },
+ { width: 600, height: 800 },
+ { width: 768, height: 1024 },
+ { width: 1024, height: 768 },
+ { width: 320, height: 1600 },
+ { width: 1400, height: 480 },
+ { width: 834, height: 896 },
+ { width: 1280, height: 640 }
+]
let browser: Browser | null = null
let page: Page | null = null
@@ -363,22 +369,12 @@ function budgetedFrame(viewport: Viewport) {
}
}
-/**
- * The viewports the budget can actually fit, which are the ones it makes a promise about.
- *
- * Below a scale of one the module stops: asking for fewer device pixels than CSS pixels is a
- * blurry frame rather than a working one, so a viewport too large for the cap keeps scale 1 and
- * the frame that does not fit is C6 ruling 1's to drop. Split here so the promise and the
- * exception are both asserted rather than averaged.
- */
-const withinBudget = (viewport: Viewport) => budgetedFrame(viewport).scale > 1
-
-describeSweep('the frame budget across the viewport range', () => {
- it('keeps every viewport it budgets for inside one bridge message', async () => {
+describeSweep('the frame budget at representative viewport sizes', () => {
+ it('keeps representative budgeted viewports inside one bridge message', async () => {
const overCap: string[] = []
let worstBytesPerPixel = 0
let bestBytesPerPixel = 1
- for (const viewport of VIEWPORTS.filter(withinBudget)) {
+ for (const viewport of VIEWPORTS) {
const frame = budgetedFrame(viewport)
const imageBytes = await screencastNoiseJpegBytes(
frame,
@@ -411,15 +407,7 @@ describeSweep('the frame budget across the viewport range', () => {
}, 300_000)
it('does not budget below one device pixel per CSS pixel, and the shell drops what will not fit', async () => {
- // The exception the split above names. These are real: a 1400x1180 viewport posts 1.2 MB.
- const tooLarge = VIEWPORTS.filter((viewport) => !withinBudget(viewport))
- // The 32 of the 143 the budget leaves at scale 1, a fixed number because the set is fixed.
- expect(tooLarge.length).toBe(32)
-
- const largest = tooLarge.reduce((left, right) =>
- left.width * left.height > right.width * right.height ? left : right
- )
- const frame = budgetedFrame(largest)
+ const frame = budgetedFrame({ width: 1400, height: 1600 })
expect(frame.scale).toBe(1)
const imageBytes = await screencastNoiseJpegBytes(frame, 1)
expect(postThroughShell(new Uint8Array(imageBytes), frame)).toBeNull()
@@ -449,42 +437,4 @@ describeSweep('the frame budget across the viewport range', () => {
await context.close()
}
}, 120_000)
-
- it('reads the frames this capture painted, never one left over from the last', () => {
- // The four shapes measured on this rig at 20x CPU throttling, all arriving after the raster
- // barrier: the black canvas the resize left, a full frame of the previous and larger viewport,
- // and this capture's own two. Only the last two are this capture's, and the gap between the
- // stale stamps and the paint was never under 86 ms.
- const frames = [
- { bytes: 13_483, stamp: 914 },
- { bytes: 447_491, stamp: 939 },
- { bytes: 997_489, stamp: 1005 },
- { bytes: 997_489, stamp: 1024 }
- ]
- expect(framesCarryingTheNoise(frames, 0, 1000).map((one) => one.bytes)).toEqual([
- 997_489, 997_489
- ])
- // A frame the browser sent no capture time for is not admissible either: it cannot be told from
- // the stale ones, and guessing it fresh is the understatement the gate exists to refuse.
- expect(framesCarryingTheNoise([{ bytes: 997_489, stamp: null }], 0, 1000)).toEqual([])
- // And the arrivals before the raster barrier stay out, which is the other half of the reading.
- expect(framesCarryingTheNoise(frames, 3, 1000).map((one) => one.bytes)).toEqual([997_489])
- })
-
- it('never asks for more density than native, anywhere in the range', () => {
- for (const viewport of VIEWPORTS) {
- expect(budgetedFrame(viewport).scale).toBeLessThanOrEqual(
- sweep().MOBILE_VIEW_DEVICE_SCALE_FACTOR
- )
- }
- })
-
- it('sweeps a range wide enough to contain the phones the pane runs on', () => {
- // The set is fixed, so this is what says it still covers the case the old constant missed.
- expect(VIEWPORTS).toContainEqual({ width: 390, height: 712 })
- expect(VIEWPORTS).toContainEqual({ width: 393, height: 720 })
- expect(VIEWPORTS).toContainEqual({ width: 360, height: 640 })
- expect(VIEWPORTS.length).toBe(143)
- expect(sweep().mobileBrowserFrameAreaBudget()).toBeGreaterThan(0)
- })
})
diff --git a/config/scripts/mobile-web-app-html-preview-render.test.mjs b/config/scripts/mobile-web-app-html-preview-render.test.mjs
index 9f78cc33a16..96b371f0e8c 100644
--- a/config/scripts/mobile-web-app-html-preview-render.test.mjs
+++ b/config/scripts/mobile-web-app-html-preview-render.test.mjs
@@ -11,8 +11,7 @@
* WebKit as well as Chromium, because the iOS shell is WKWebView and the two disagree: a `blob:`
* frame that Chromium admits under `frame-src blob:` is refused in WebKit by the
* `frame-ancestors 'none'` it inherits. `srcdoc` is what both admit under the policy that already
- * ships, which is why this costs no CSP change and why a case below pins `frame-src 'none'` as still
- * shipped.
+ * ships, so the preview needs no CSP change.
*
* The paint oracle is a pixel rather than a read inside the frame: the frame is an opaque origin, and
* WebKit refuses to evaluate in one, so reading its DOM would make the instrument engine-dependent.
@@ -243,8 +242,7 @@ for (const engine of ['chromium', 'webkit']) {
// frame's URL, which is `about:srcdoc` on one browser and empty on another.
expect(read.mountedSrcDoc).toContain('ARTIFACT_RENDERED')
expect(read.mountedSrc).toBeNull()
- // The rendered frame carries the constant, so the token case below is about the frame the
- // page mounts rather than about a string nothing reads.
+ // Check the sandbox on the mounted frame.
expect(read.mountedSandbox).toBe(read.declaredSandbox)
expect(read.mountedSandbox).toBe('allow-top-navigation-by-user-activation')
// The policy this document was served is the shell's own text plus the rig's report
@@ -298,8 +296,7 @@ for (const engine of ['chromium', 'webkit']) {
// The second fence, measured on its own: grant `allow-scripts` and keep the shipped policy,
// and the script still does not run, because a `srcdoc` frame inherits its embedder's
// `script-src 'self'` and the artifact's script is inline. So the seal does not rest on the
- // sandbox attribute alone -- which is what makes the token list below a defence in depth
- // rather than the only thing standing between the page and an agent's script.
+ // sandbox attribute alone.
const inherited = await open(browser(), {
signal: ctx.signal,
extra: { body: artifactScript(foreignOrigin) },
@@ -411,19 +408,6 @@ for (const engine of ['chromium', 'webkit']) {
expect(root.actError).toBeNull()
expect(root.ownOriginTopNavigations).toBe(1)
expect(root.topNavigations).toBe(0)
-
- // `href=""` is the same navigation spelled as "this document", and it resolves the same way.
- const empty = await open(browser(), {
- signal: ctx.signal,
- expectNavigation: 'main-frame',
- act: async ({ frame }) => {
- await frame?.click('#emptylink', { timeout: 2000 })
- }
- })
- expect(empty.pixelBefore).toBe(ARTIFACT_RGB)
- expect(empty.actError).toBeNull()
- expect(empty.ownOriginTopNavigations).toBe(1)
- expect(empty.topNavigations).toBe(0)
}, 180_000)
/**
@@ -834,33 +818,3 @@ for (const engine of ['chromium', 'webkit']) {
600_000
)
}
-
-describe('the HTML preview needs no policy change', () => {
- it('runs under a policy that still forbids every nested frame by URL', async () => {
- const directives = (await readShellCsp()).split('; ')
- // A `srcdoc` frame has no URL for `frame-src` to match, so the sealed box costs nothing here.
- // Pinned so a future relaxation is a decision rather than a side effect of this component.
- expect(directives).toContain("frame-src 'none'")
- expect(directives).toContain("child-src 'none'")
- expect(directives).toContain("script-src 'self'")
- expect(directives).toContain("frame-ancestors 'none'")
- })
-
- it('grants exactly one sandbox token, and neither of the two that would unseal the frame', async () => {
- const source = await readFileText('mobile/src/components/MobileHtmlPreview.web.tsx')
- const match = /MOBILE_HTML_PREVIEW_SANDBOX = '([^']*)'/.exec(source)
- expect(match).not.toBeNull()
- const tokens = (match?.[1] ?? '').split(' ').filter((one) => one.length > 0)
- expect(tokens).toEqual(['allow-top-navigation-by-user-activation'])
- // Named rather than left to the list comparison: these two are the sealing invariant, and a
- // reader of a failure should see which one was granted.
- expect(tokens).not.toContain('allow-scripts')
- expect(tokens).not.toContain('allow-same-origin')
- })
-})
-
-/** One pixel of the frame's own fill, which is what says the artifact parsed and painted. */
-async function readFileText(relativePath) {
- const { readFile } = await import('node:fs/promises')
- return await readFile(join(mobileDir, '..', relativePath), 'utf8')
-}
diff --git a/config/scripts/mobile-web-app-stack-transition-render.test.mjs b/config/scripts/mobile-web-app-stack-transition-render.test.mjs
index 4420d32acfa..55ab979455b 100644
--- a/config/scripts/mobile-web-app-stack-transition-render.test.mjs
+++ b/config/scripts/mobile-web-app-stack-transition-render.test.mjs
@@ -30,6 +30,7 @@ const LIST_ROUTE = `/${MOBILE_WEB_APP_ROUTE_ROOT}/${HOST_ID}`
const SESSION_HREF = `${LIST_ROUTE}/session/wt-1`
const VIEWPORT = { width: 390, height: 844 }
const SAMPLE_MS = 1500
+const SETTLED_GRACE_MS = 150
const bundles = mobileWebAppDependenciesPresent()
const describeRender = bundles ? describe : describe.skip
@@ -173,9 +174,13 @@ async function openList(browser, { animation = 'default', reducedMotion = 'no-pr
* Runs `action` on the probe, then reads both screens' left edge once per animation frame, and
* which screen a tap at the centre would land on. A hidden screen, or no screen hit, reads as null.
*/
-function sampleFrames(page, action, { followUp = null, afterFrames = 0 } = {}) {
+function sampleFrames(
+ page,
+ action,
+ { followUp = null, afterFrames = 0, observeFullWindow = false } = {}
+) {
return page.evaluate(
- ([name, sampleMs, nextAction, followAt]) =>
+ ([name, sampleMs, nextAction, followAt, settledGraceMs, fullWindow]) =>
new Promise((resolve) => {
const leftOf = (id) => {
const node = document.querySelector(`[data-testid="${id}"]`)
@@ -190,6 +195,9 @@ function sampleFrames(page, action, { followUp = null, afterFrames = 0 } = {}) {
?.replace('stack-probe-', '') ?? null
const frames = []
const start = performance.now()
+ let settledAt = null
+ const destination = nextAction ?? name
+ const pushing = destination === 'push' || destination === 'pushOther'
globalThis.__orcaStackProbe[name]()
const tick = () => {
if (nextAction !== null && frames.length === followAt) {
@@ -202,7 +210,17 @@ function sampleFrames(page, action, { followUp = null, afterFrames = 0 } = {}) {
// Where the running slide starts, which is how a slide that restarts from 0 shows.
from: document.getAnimations()[0]?.effect?.getKeyframes()[0]?.transform ?? null
})
- if (performance.now() - start < sampleMs) {
+ const last = frames.at(-1)
+ const arrived = pushing
+ ? last.list === null && last.session === 0 && last.hit === 'session'
+ : last.list === 0 && last.session === null && last.hit === 'list'
+ const followUpDelivered = nextAction === null || frames.length > followAt
+ const settled = arrived && followUpDelivered && document.getAnimations().length === 0
+ const now = performance.now()
+ settledAt = settled ? (settledAt ?? now) : null
+ // Observe cleanup after actual arrival; retain the full window for interruption races.
+ const finished = !fullWindow && settledAt !== null && now - settledAt >= settledGraceMs
+ if (!finished && now - start < sampleMs) {
requestAnimationFrame(tick)
} else {
resolve(frames)
@@ -210,7 +228,7 @@ function sampleFrames(page, action, { followUp = null, afterFrames = 0 } = {}) {
}
requestAnimationFrame(tick)
}),
- [action, SAMPLE_MS, followUp, afterFrames]
+ [action, SAMPLE_MS, followUp, afterFrames, SETTLED_GRACE_MS, observeFullWindow]
)
}
@@ -283,7 +301,11 @@ describeRender('the host stack transition on the page', () => {
const { errors, page } = await open()
await sampleFrames(page, 'push')
const pushed = await nodeCount(page)
- const frames = await sampleFrames(page, 'back', { followUp: 'push', afterFrames: 3 })
+ const frames = await sampleFrames(page, 'back', {
+ followUp: 'push',
+ afterFrames: 3,
+ observeFullWindow: true
+ })
expect(frames.slice(0, 3).some((frame) => between(frame.session))).toBe(true)
expect(frames.at(-1)).toEqual(SESSION_SETTLED)
expect(await nodeCount(page)).toBe(pushed)
@@ -303,7 +325,11 @@ describeRender('the host stack transition on the page', () => {
await sampleFrames(page, 'back')
const baseline = await nodeCount(page)
const mounts = await sessionMounts(page)
- const frames = await sampleFrames(page, 'push', { followUp: 'back', afterFrames: 6 })
+ const frames = await sampleFrames(page, 'push', {
+ followUp: 'back',
+ afterFrames: 6,
+ observeFullWindow: true
+ })
expect(between(frames[5].session)).toBe(true)
// Leaves from where the push stopped, not from 0: the exit's first keyframe is mid-screen.
const exitFrom = frames.slice(6).find((frame) => frame.from?.startsWith('matrix'))?.from
diff --git a/src/main/native-chat/transcript-watch.test.ts b/src/main/native-chat/transcript-watch.test.ts
index 1aeced2c3ff..4830f658527 100644
--- a/src/main/native-chat/transcript-watch.test.ts
+++ b/src/main/native-chat/transcript-watch.test.ts
@@ -90,7 +90,8 @@ describe('subscribeNativeChatTranscript', () => {
filePath,
onInitialSnapshot: (messages) => snapshots.push(messages),
onAppend: (messages) => appends.push(messages),
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
expect(sub.watching).toBe(true)
@@ -115,7 +116,8 @@ describe('subscribeNativeChatTranscript', () => {
filePath,
onInitialSnapshot: (messages) => snapshots.push(messages),
onAppend: () => {},
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
await waitFor(() => snapshots.length === 1)
@@ -140,7 +142,8 @@ describe('subscribeNativeChatTranscript', () => {
lifecycles.push(lifecycle)
}
},
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
await waitFor(() => lifecycles.length === 1)
@@ -170,7 +173,8 @@ describe('subscribeNativeChatTranscript', () => {
lifecycles.push(lifecycle)
}
},
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
await waitFor(() => lifecycles.length === 1)
@@ -200,7 +204,8 @@ describe('subscribeNativeChatTranscript', () => {
snapshot = { messages, lifecycle }
},
onAppend: () => {},
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
await waitFor(() => snapshot !== undefined)
@@ -269,7 +274,8 @@ describe('subscribeNativeChatTranscript', () => {
sessionId: 'ignored',
filePath,
onAppend: (messages) => batches.push(messages),
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
await new Promise((resolve) => setTimeout(resolve, 20))
await appendFile(
@@ -295,7 +301,8 @@ describe('subscribeNativeChatTranscript', () => {
sessionId: 'ignored',
filePath,
onAppend: (messages) => seen.push(...messages),
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
await new Promise((resolve) => setTimeout(resolve, 20))
await appendFile(
@@ -323,7 +330,8 @@ describe('subscribeNativeChatTranscript', () => {
onInitialSnapshot: (messages, hasMore) =>
snapshots.push({ ids: messages.map((message) => message.id), hasMore }),
onAppend: () => {},
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
await waitFor(() => snapshots.length === 1)
@@ -342,7 +350,8 @@ describe('subscribeNativeChatTranscript', () => {
onInitialSnapshot: (messages, hasMore) =>
snapshots.push({ ids: messages.map((message) => message.id), hasMore }),
onAppend: () => {},
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
await waitFor(() => snapshots.length === 1)
@@ -395,7 +404,8 @@ describe('subscribeNativeChatTranscript', () => {
onInitialSnapshot: (messages, hasMore) =>
snapshots.push({ ids: messages.map((message) => message.id), hasMore }),
onAppend: () => {},
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
await waitFor(() => snapshots.length === 1)
@@ -412,7 +422,8 @@ describe('subscribeNativeChatTranscript', () => {
sessionId: 'ignored',
filePath,
onAppend: (messages) => batches.push(messages),
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
await appendFile(filePath, claudeLine('a-1', 'assistant', 'reply'))
@@ -427,31 +438,6 @@ describe('subscribeNativeChatTranscript', () => {
expect(ids).toContain('a-1')
})
- it('appends a turn in the gap between initial read and first watcher drain exactly once', async () => {
- // Simulate the read/subscribe race: a turn lands after the caller's
- // readSession EOF but before the watcher's first drain. Seeding at 0 means
- // the first drain reads it; the assembler later dedups by deterministic id.
- const filePath = await tempFile(claudeLine('u-1', 'user', 'first'))
- const seen: NativeChatMessage[] = []
-
- // The gap turn is written BEFORE subscribe completes its first drain.
- await appendFile(filePath, claudeLine('a-gap', 'assistant', 'raced reply'))
-
- const sub = await subscribeNativeChatTranscript({
- agent: 'claude',
- sessionId: 'ignored',
- filePath,
- onAppend: (messages) => seen.push(...messages),
- debounceMs: 5
- })
-
- await waitFor(() => seen.some((m) => m.id === 'a-gap'))
- sub.unsubscribe()
-
- // The raced turn is present, and not duplicated within a single drain pass.
- expect(seen.filter((m) => m.id === 'a-gap')).toHaveLength(1)
- })
-
it('recovers cleanly when a read throws (subscription not left deaf)', async () => {
const filePath = await tempFile(claudeLine('u-1', 'user', 'hi'))
const seen: NativeChatMessage[] = []
@@ -461,7 +447,8 @@ describe('subscribeNativeChatTranscript', () => {
sessionId: 'ignored',
filePath,
onAppend: (messages) => seen.push(...messages),
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
// Make the file unreadable mid-flight (EACCES on the read path). The drain's
@@ -490,7 +477,8 @@ describe('subscribeNativeChatTranscript', () => {
sessionId: 'ignored',
filePath,
onAppend: () => {},
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
expect(getActiveNativeChatWatcherCount()).toBe(before + 1)
@@ -511,7 +499,8 @@ describe('subscribeNativeChatTranscript', () => {
sessionId: 'ignored',
filePath,
onAppend: (messages) => seen.push(...messages),
- debounceMs: 10
+ debounceMs: 10,
+ reconciliationIntervalMs: 20
})
// Fire several appends back-to-back within the debounce window.
@@ -537,7 +526,8 @@ describe('subscribeNativeChatTranscript', () => {
sessionId: 'ignored',
filePath,
onAppend: (messages) => seen.push(...messages),
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
await waitFor(() => seen.some((m) => m.id === 'u-1'))
@@ -566,7 +556,8 @@ describe('subscribeNativeChatTranscript', () => {
sessionId: 'ignored',
filePath,
onAppend: (messages) => seen.push(...messages),
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
// Replace the file with shorter content (simulates rotation to a new,
@@ -592,7 +583,8 @@ describe('subscribeNativeChatTranscript', () => {
sessionId: 'ignored',
filePath,
onAppend: (messages) => seen.push(...messages),
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
await waitFor(() => seen.some((message) => message.id === 'u-old'))
@@ -622,7 +614,8 @@ describe('subscribeNativeChatTranscript', () => {
seen.splice(0, seen.length, ...messages)
},
onAppend: (messages) => seen.push(...messages),
- debounceMs: 5
+ debounceMs: 5,
+ reconciliationIntervalMs: 20
})
await waitFor(() => seen.some((message) => message.id === 'atomic-old'))
@@ -648,7 +641,8 @@ describe('subscribeNativeChatTranscript', () => {
sessionId: 'ignored',
filePath,
onAppend: (messages) => seen.push(...messages),
- debounceMs: 0
+ debounceMs: 0,
+ reconciliationIntervalMs: 20
})
await waitFor(() => seen.some((message) => message.id === 'race-old'))
@@ -672,7 +666,8 @@ describe('subscribeNativeChatTranscript', () => {
sessionId: 'ignored',
filePath,
onAppend: (messages) => seen.push(...messages),
- debounceMs: 0
+ debounceMs: 0,
+ reconciliationIntervalMs: 20
})
await waitFor(() => seen.some((message) => message.id === 'unlink-old'))
@@ -717,6 +712,7 @@ describe('subscribeNativeChatTranscript (resolve-poll for a not-yet-created file
filePath,
onAppend: (messages) => seen.push(...messages),
debounceMs: 5,
+ reconciliationIntervalMs: 20,
resolvePollIntervalMs: 20
})
@@ -741,6 +737,7 @@ describe('subscribeNativeChatTranscript (resolve-poll for a not-yet-created file
sessionId: ' ',
onAppend: () => {},
debounceMs: 5,
+ reconciliationIntervalMs: 20,
resolvePollIntervalMs: 10
})
@@ -762,6 +759,7 @@ describe('subscribeNativeChatTranscript (resolve-poll for a not-yet-created file
filePath,
onAppend: () => {},
debounceMs: 5,
+ reconciliationIntervalMs: 20,
resolvePollIntervalMs: 20
})
diff --git a/src/main/runtime/orca-runtime-tests/browser-capabilities.spec.ts b/src/main/runtime/orca-runtime-tests/browser-capabilities.spec.ts
index ffe98483585..13a3097b355 100644
--- a/src/main/runtime/orca-runtime-tests/browser-capabilities.spec.ts
+++ b/src/main/runtime/orca-runtime-tests/browser-capabilities.spec.ts
@@ -387,23 +387,17 @@ describe('OrcaRuntimeService', () => {
'Browser automation is unavailable on this host, and the cause could not be determined.'
}
])
- const browserCalls = Object.entries(runtime).filter(
- ([name, value]) => /^browser[A-Z]/.test(name) && typeof value === 'function'
- )
- expect(browserCalls.length).toBeGreaterThan(50)
- for (const [name, call] of browserCalls) {
- const invoke =
- name === 'browserScreencast'
- ? () =>
- (call as CallableFunction)(
- { format: 'jpeg' },
- { sendBinary: () => true, emit: () => undefined }
- )
- : () => (call as CallableFunction)({})
- await expect(Promise.resolve().then(invoke)).rejects.toMatchObject({
- code: 'browser_unavailable'
- })
- }
+ await expect(
+ Promise.resolve().then(() => runtime.browserGoto({ url: 'https://example.com' }))
+ ).rejects.toMatchObject({ code: 'browser_unavailable' })
+ await expect(
+ Promise.resolve().then(() =>
+ runtime.browserScreencast(
+ { format: 'jpeg' },
+ { sendBinary: () => true, emit: () => undefined }
+ )
+ )
+ ).rejects.toMatchObject({ code: 'browser_unavailable' })
})
it('reports the driver as missing instead of telling a configured operator to configure it', () => {
diff --git a/src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts b/src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts
index aedaa567b3d..8c5ca64bcf2 100644
--- a/src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts
+++ b/src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts
@@ -30,31 +30,6 @@ describe('OrcaRuntimeService', () => {
expect(runtime.getRuntimeId()).toBeTruthy()
})
- it('reports runtime protocol, capabilities, and mobile aliases on status', () => {
- const runtime = createRuntime()
-
- const status = runtime.getStatus()
- expect(typeof status.runtimeProtocolVersion).toBe('number')
- expect(typeof status.minCompatibleRuntimeClientVersion).toBe('number')
- expect(status.runtimeProtocolVersion).toBe(status.protocolVersion)
- expect(status.minCompatibleRuntimeClientVersion).toBe(status.minCompatibleMobileVersion)
- expect(status.capabilities).toContain('terminal.binary-stream.v1')
- expect(status.capabilities).toContain('workspace-ports.v1')
- expect(status.capabilities).toContain('mobile.tasks.v1')
- expect(status.capabilities).toContain('terminal.quick-commands.v1')
- expect(status.capabilities).toContain('session-tabs.split-group-placement.v1')
- expect(status.capabilities).toContain('worktree.create-idempotency.v1')
- expect(status.worktreeCreateIdempotency).toEqual({ dedupeTtlMs: 60_000 })
- expect(status.capabilities).toContain('files.mutation-ownership.v1')
- expect(status.capabilities).toContain('project-host-setup.v1')
- expect(status.capabilities).toContain('linear.issue-attribute-filter.v1')
- expect(status.capabilities).not.toContain('browser.screencast.v1')
- expect(typeof status.protocolVersion).toBe('number')
- expect(typeof status.minCompatibleMobileVersion).toBe('number')
- expect(status.protocolVersion).toBeGreaterThanOrEqual(1)
- expect(status.minCompatibleMobileVersion).toBeGreaterThanOrEqual(0)
- })
-
it('reports the configured Windows terminal shell on status', () => {
const runtime = new OrcaRuntimeService({
...store,
@@ -265,20 +240,6 @@ describe('OrcaRuntimeService', () => {
expect(runtime.getStatus().capabilities).toContain('browser.screencast.v1')
})
- // Paired desktops open a chat on this host only when it says it admits them by the client's
- // chosen launch mode; without it, every paired launch quietly becomes a terminal.
- it('advertises that it admits structured sessions by the client-chosen launch mode', () => {
- expect(createRuntime().getStatus().capabilities).toContain(
- 'agent-session.structured.client-launch-mode.v1'
- )
- })
-
- it('advertises safe Codex reset-credit RPC support as a static capability', () => {
- const runtime = createRuntime()
-
- expect(runtime.getStatus().capabilities).toContain('accounts.codex-reset-credit.v1')
- })
-
it('routes mobile Codex reset consumption through the account mutation coordinator', async () => {
const runtime = createRuntime()
const expectedScope = {
diff --git a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-07.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-07.spec.ts
index d90dc63031b..4848177403a 100644
--- a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-07.spec.ts
+++ b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-07.spec.ts
@@ -2,12 +2,9 @@ import { describe, expect, it, vi } from 'vitest'
import {
AGENT_PROMPT_BRACKETED_PASTE_END,
AGENT_PROMPT_BRACKETED_PASTE_START,
- buildAgentPromptPasteBytes,
- resolveAgentPromptSubmitDelayForAgent
+ buildAgentPromptPasteBytes
} from '../../../shared/agent-prompt-injection'
-import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config'
import { ORCA_DISPATCH_PROMPT_LEAD_LINE } from '../../../shared/orca-dispatch-status-prompt'
-import type { TuiAgent } from '../../../shared/tui-agent'
import { OrcaRuntimeService } from '../orca-runtime'
import { acknowledgeAgentPromptSubmit } from '../orca-runtime-test-mocks.spec'
import {
@@ -626,58 +623,52 @@ describe('OrcaRuntimeService', () => {
}
)
- it.each(
- (Object.keys(TUI_AGENT_CONFIG) as TuiAgent[]).filter(
- (agent) => agent !== 'claude' && agent !== 'codex'
- )
- )('submits through the agent-specific PTY timing policy for %s', async (agent) => {
- vi.useFakeTimers()
- try {
- const writes: string[] = []
- const runtime = new OrcaRuntimeService(store)
- runtime.setPtyController({
- spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }),
- write: (_ptyId, data) => {
- writes.push(data)
- if (agent === 'omp' && data.endsWith('\r')) {
- runtime.onPtyData('pty-bg', '\x1b]0;Codex working\x07', Date.now())
- } else {
- acknowledgeAgentPromptSubmit(runtime, 'pty-bg', data)
- }
- return true
- },
- kill: () => true,
- getForegroundProcess: async () => null
- })
- const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, {
- launchAgent: agent
- })
+ it.each(['aider', 'antigravity', 'omp'] as const)(
+ 'submits through the agent-specific PTY timing policy for %s',
+ async (agent) => {
+ vi.useFakeTimers()
+ try {
+ const writes: string[] = []
+ const runtime = new OrcaRuntimeService(store)
+ runtime.setPtyController({
+ spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }),
+ write: (_ptyId, data) => {
+ writes.push(data)
+ if (agent === 'omp' && data.endsWith('\r')) {
+ runtime.onPtyData('pty-bg', '\x1b]0;Codex working\x07', Date.now())
+ } else {
+ acknowledgeAgentPromptSubmit(runtime, 'pty-bg', data)
+ }
+ return true
+ },
+ kill: () => true,
+ getForegroundProcess: async () => null
+ })
+ const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, {
+ launchAgent: agent
+ })
- // The agent's own policy, not the byte-only delay: antigravity adds a per-line settle
- // (#21665), and advancing fake timers by less than the policy waits leaves the submit
- // pending until the real 30 s timeout.
- const submitDelayMs = resolveAgentPromptSubmitDelayForAgent(
- process.platform,
- 'review this change',
- agent
- )
- const sendPromise = runtime.sendTerminalAgentPrompt(handle, 'review this change', {
- inputKind: 'driving'
- })
- if (agent === 'omp') {
+ const prompt =
+ agent === 'antigravity' ? 'review this change\nfollow up' : 'review this change'
+ const submitDelayMs = agent === 'antigravity' ? 591 : 501
+ const sendPromise = runtime.sendTerminalAgentPrompt(handle, prompt, {
+ inputKind: 'driving'
+ })
+ if (agent === 'omp') {
+ await sendPromise
+ expect(writes).toEqual([`${buildAgentPromptPasteBytes('review this change')}\r`])
+ return
+ }
+
+ await vi.advanceTimersByTimeAsync(submitDelayMs - 1)
+ expect(writes).not.toContain('\r')
+
+ await vi.advanceTimersByTimeAsync(1)
await sendPromise
- expect(writes).toEqual([`${buildAgentPromptPasteBytes('review this change')}\r`])
- return
+ expect(writes.filter((data) => data === '\r')).toHaveLength(1)
+ } finally {
+ vi.useRealTimers()
}
-
- await vi.advanceTimersByTimeAsync(submitDelayMs - 1)
- expect(writes).not.toContain('\r')
-
- await vi.advanceTimersByTimeAsync(1)
- await sendPromise
- expect(writes.filter((data) => data === '\r')).toHaveLength(1)
- } finally {
- vi.useRealTimers()
}
- })
+ )
})
diff --git a/src/relay/subprocess.test.ts b/src/relay/subprocess.test.ts
index 8aac4aa801b..93299a67d02 100644
--- a/src/relay/subprocess.test.ts
+++ b/src/relay/subprocess.test.ts
@@ -181,15 +181,6 @@ describe('Subprocess: Relay entry point', () => {
}
})
- it('prints sentinel on startup', async () => {
- relay = spawn()
- await relay.sentinelReceived
- }, 10_000)
-
- it('keeps the Node-18 relay bundle free of unsupported array copy methods', () => {
- expect(readFileSync(relayEntry, 'utf8')).not.toContain('.toReversed(')
- })
-
it('loads node-pty after an in-place dependency repair without restarting', async () => {
tmpDir = mkdtempSync(path.join(tmpdir(), 'relay-native-repair-'))
const repairedRelayEntry = path.join(tmpDir, 'relay.js')
diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx
index a90adc12f8c..0394d024804 100644
--- a/src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx
@@ -41,20 +41,6 @@ const session: NativeChatLiveSession = {
}
describe('NativeChatMessageList assistant messages', () => {
- it('keeps prose selectable and places non-selectable controls after it', () => {
- render()
-
- const prose = screen.getByText('Selectable agent response.')
- const row = prose.closest('.group')
- const copyButton = screen.getByRole('button', { name: 'Copy message' })
- const controls = copyButton.parentElement
-
- expect(row).toHaveClass('select-text')
- expect(controls).toHaveClass('select-none', 'can-hover:pointer-events-none', 'mt-1')
- expect(controls).not.toHaveClass('absolute')
- expect(prose.compareDocumentPosition(controls!)).toBe(Node.DOCUMENT_POSITION_FOLLOWING)
- })
-
it('keeps a running tool live when transcript lifecycle metadata is absent', () => {
render(
{
})
})
- it('appends time to the existing agent controls and inherits their reveal', () => {
- renderMessage('assistant')
- const copy = screen.getByRole('button', { name: 'Copy message' })
- const scroll = screen.getByRole('button', { name: 'Scroll this message to top' })
- const time = screen.getByRole('time')
- expect(Array.from(copy.parentElement!.children)).toEqual([copy, scroll, time])
- expect(copy.parentElement).toHaveClass(
- 'can-hover:opacity-0',
- 'can-hover:pointer-events-none',
- 'group-hover:opacity-100',
- '[.group:has(:focus-visible)_&]:opacity-100',
- 'group-hover:pointer-events-auto',
- '[.group:has(:focus-visible)_&]:pointer-events-auto'
- )
- expect(copy.parentElement).not.toHaveClass('opacity-0', 'pointer-events-none')
- expect(time).not.toHaveAttribute('tabindex')
- copy.focus()
- expect(copy).toHaveFocus()
- })
-
- it('gives user bubbles a copy button and timestamp that only hide on hover-capable devices', () => {
- renderMessage('user')
- const copy = screen.getByRole('button', { name: 'Copy message' })
- const time = screen.getByRole('time')
- expect(Array.from(copy.parentElement!.children)).toEqual([copy, time])
- expect(copy.parentElement).toHaveClass(
- 'can-hover:opacity-0',
- 'can-hover:pointer-events-none',
- 'group-hover:opacity-100',
- '[.group:has(:focus-visible)_&]:opacity-100',
- 'group-hover:pointer-events-auto',
- '[.group:has(:focus-visible)_&]:pointer-events-auto'
- )
- expect(copy.parentElement).not.toHaveClass('opacity-0', 'pointer-events-none')
- expect(copy.parentElement!.parentElement).toHaveClass('group')
- time.focus()
- expect(time).toHaveFocus()
- })
-
it('copies the sent message text from a user bubble', async () => {
const writeClipboardText = vi.fn().mockResolvedValue(undefined)
Object.assign(window, { api: { ui: { writeClipboardText } } })
@@ -131,20 +92,6 @@ describe('MessageRow control visibility', () => {
expect(screen.queryByRole('time')).toBeNull()
expect(screen.queryByRole('button')).toBeNull()
})
-
- it.each(['reasoning', 'system'] as const)(
- 'keeps %s rows upright and scopes their faint text',
- (role) => {
- const { container } = renderMessage(role)
- const row = container.querySelector('[data-native-chat-message-tone="faint"]')
- expect(row).toHaveClass('text-chat-foreground-faint')
- expect(row).not.toHaveClass('italic')
- expect(row).toContainElement(screen.getByText('Message text'))
- if (role === 'reasoning') {
- expect(row).toHaveClass('border-l-2')
- }
- }
- )
})
describe('MessageRow send mode', () => {
diff --git a/src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx b/src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx
index b9e453573b8..083933547d5 100644
--- a/src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx
@@ -27,14 +27,6 @@ describe('notice rows', () => {
screen.getByText('Context compacted').parentElement?.querySelectorAll('.bg-border')
).toHaveLength(2)
})
- it.each([
- ['warning', 'text-[color:var(--warning,#f59e0b)]'],
- ['error', 'text-destructive'],
- ['notice', 'text-muted-foreground']
- ])('renders %s using its existing color treatment', (tone, className) => {
- renderStatus({ kind: 'status', text: 'Readable notice', tone })
- expect(screen.getByText('Readable notice').parentElement?.parentElement).toHaveClass(className)
- })
it('renders a plan as readable markdown in the card primitive', () => {
renderStatus({
kind: 'status',
@@ -100,16 +92,4 @@ describe('notice rows', () => {
expect(screen.getByText(words)).toHaveClass('text-muted-foreground', 'text-sm')
expect(screen.queryByText('Words an older host wrote')).toBeNull()
})
- it('renders future presentation and tone values as untinted text', () => {
- renderStatus({
- kind: 'status',
- text: 'Future readable text',
- tone: 'future-tone',
- presentation: 'future-presentation'
- })
- expect(screen.getByText('Future readable text').parentElement?.parentElement).toHaveClass(
- 'text-foreground'
- )
- expect(screen.getByText('Future readable text').parentElement?.querySelector('svg')).toBeNull()
- })
})
diff --git a/src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx b/src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx
index 6dc8be9a0b5..47f2f4f4ce6 100644
--- a/src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatStructuredSessionDelivery.test.tsx
@@ -187,7 +187,12 @@ vi.mock('./NativeChatQuestionCard', () => ({
}))
import { NativeChatStructuredSession } from './NativeChatStructuredSession'
-import { seededEntry, seedOutbox } from './NativeChatStructuredSession.test-harness'
+import {
+ advanceProbeClock,
+ seededEntry,
+ seedOutbox,
+ useProbeClock
+} from './NativeChatStructuredSession.test-harness'
import {
appendStructuredAgentSessionOutboxMessage,
getStructuredAgentSessionOutbox
@@ -218,6 +223,7 @@ describe('NativeChatStructuredSession delivery', () => {
// Resent under its own id until the host answers, so its row says only that it is still sending.
it('says a send whose answer was lost is sending until it confirms on its own, with no Retry', async () => {
+ useProbeClock()
mocks.mode = 'outbox'
mocks.call.mockRejectedValueOnce(new Error('socket closed')).mockResolvedValueOnce({
ok: true,
@@ -238,19 +244,19 @@ describe('NativeChatStructuredSession delivery', () => {
const send = mocks.composerProps?.structuredTransport?.send as
| ((text: string, attachments: readonly { id: string; path: string }[]) => boolean)
| undefined
- expect(send?.('hello', [])).toBe(true)
+ await act(async () => {
+ expect(send?.('hello', [])).toBe(true)
+ })
// From the moment it is sent, through the lost answer, until the host confirms it.
- await waitFor(() => expect(screen.getByText('Sending…')).toBeTruthy())
- await waitFor(() =>
- expect(getStructuredAgentSessionOutbox('session-1')).toMatchObject([{ state: 'unconfirmed' }])
- )
+ expect(getStructuredAgentSessionOutbox('session-1')).toMatchObject([{ state: 'unconfirmed' }])
expect(screen.getByText('Sending…')).toBeTruthy()
expect(screen.queryByText('Message delivery is unconfirmed.')).toBeNull()
expect(screen.queryByRole('button', { name: /Retry/ })).toBeNull()
- await waitFor(() => expect(mocks.call).toHaveBeenCalledTimes(2), { timeout: 5000 })
+ await advanceProbeClock(1000)
+ expect(mocks.call).toHaveBeenCalledTimes(2)
expect(mocks.call.mock.calls[1]?.[2]).toEqual(mocks.call.mock.calls[0]?.[2])
- await waitFor(() => expect(screen.queryByText('Sending…')).toBeNull())
+ expect(screen.queryByText('Sending…')).toBeNull()
expect(getStructuredAgentSessionOutbox('session-1')).toEqual([])
expect(screen.queryByText('Message delivery is unconfirmed.')).toBeNull()
}, 10000)
@@ -282,6 +288,7 @@ describe('NativeChatStructuredSession delivery', () => {
}
it('says a send reopened mid-send is sending while it is resent, until it settles', async () => {
+ useProbeClock()
mocks.mode = 'outbox'
mocks.submissions = []
mocks.call.mockResolvedValue({
@@ -298,53 +305,45 @@ describe('NativeChatStructuredSession delivery', () => {
expect(screen.getByText('Sending…')).toBeTruthy()
expect(screen.queryByText('Message delivery is unconfirmed.')).toBeNull()
expect(screen.queryByRole('button', { name: /Retry/ })).toBeNull()
- await waitFor(() => expect(mocks.call).toHaveBeenCalledOnce(), { timeout: 3000 })
+ await advanceProbeClock(1000)
+ expect(mocks.call).toHaveBeenCalledOnce()
expect(mocks.call.mock.calls[0]?.[2]).toMatchObject({
envelope: { clientOperationId: 'op-sent' }
})
- await waitFor(() => expect(getStructuredAgentSessionOutbox('session-reopened')).toEqual([]))
+ expect(getStructuredAgentSessionOutbox('session-reopened')).toEqual([])
expect(screen.queryByText('Sending…')).toBeNull()
expect(screen.queryByText('Message delivery is unconfirmed.')).toBeNull()
}, 10000)
- it.each([
- ['a live unknown', {}],
- ['a recovered unknown', { recovered: true }],
- ["an older host's recovered unknown", { reason: 'host_restarted_before_acknowledgement' }]
- ])(
- 'says a send reopened mid-send is unconfirmed, with its Retry, once the journal holds %s',
- async (label, patch) => {
- mocks.mode = 'outbox'
- const sessionId = `session-reopened-${label.replace(/\W+/g, '-')}`
- mocks.submissions = [
- {
- clientMessageId: 'op-sent',
- fence: 1,
- payloadFingerprint: 'fp',
- dispatchState: 'unknown',
- providerItemId: null,
- reason: null,
- submittedAt: 1,
- resolvedAt: null,
- ...patch
- }
- ]
- seedMidSend(sessionId)
+ it('says a send reopened mid-send is unconfirmed, with its Retry, when the journal holds unknown', async () => {
+ useProbeClock()
+ mocks.mode = 'outbox'
+ const sessionId = 'session-reopened-unknown'
+ mocks.submissions = [
+ {
+ clientMessageId: 'op-sent',
+ fence: 1,
+ payloadFingerprint: 'fp',
+ dispatchState: 'unknown',
+ providerItemId: null,
+ reason: null,
+ submittedAt: 1,
+ resolvedAt: null
+ }
+ ]
+ seedMidSend(sessionId)
- renderSession(sessionId)
+ renderSession(sessionId)
- await waitFor(() => expect(screen.getByText('Message delivery is unconfirmed.')).toBeTruthy())
- expect(screen.getByRole('button', { name: /Retry/ })).toBeTruthy()
- expect(screen.queryByText('Sending…')).toBeNull()
- await act(async () => {
- await new Promise((resolve) => setTimeout(resolve, 1500))
- })
- expect(mocks.call).not.toHaveBeenCalled()
- },
- 10000
- )
+ expect(screen.getByText('Message delivery is unconfirmed.')).toBeTruthy()
+ expect(screen.getByRole('button', { name: /Retry/ })).toBeTruthy()
+ expect(screen.queryByText('Sending…')).toBeNull()
+ await advanceProbeClock(1500)
+ expect(mocks.call).not.toHaveBeenCalled()
+ })
it('says a send a Stop outlived is unconfirmed when reopened, as nothing resends it', async () => {
+ useProbeClock()
mocks.mode = 'outbox'
mocks.submissions = []
seedMidSend('session-reopened-stopped', { outlivedStop: true })
@@ -354,9 +353,7 @@ describe('NativeChatStructuredSession delivery', () => {
expect(screen.getByText('Message delivery is unconfirmed.')).toBeTruthy()
expect(screen.getByRole('button', { name: /Retry/ })).toBeTruthy()
expect(screen.queryByText('Sending…')).toBeNull()
- await act(async () => {
- await new Promise((resolve) => setTimeout(resolve, 1500))
- })
+ await advanceProbeClock(1500)
expect(mocks.call).not.toHaveBeenCalled()
}, 10000)
diff --git a/src/renderer/src/components/native-chat/native-chat-code-typography.test.tsx b/src/renderer/src/components/native-chat/native-chat-code-typography.test.tsx
deleted file mode 100644
index 4f723c2f136..00000000000
--- a/src/renderer/src/components/native-chat/native-chat-code-typography.test.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-// @vitest-environment happy-dom
-import '@testing-library/jest-dom/vitest'
-import { cleanup, render, screen } from '@testing-library/react'
-import { afterEach, describe, expect, it } from 'vitest'
-import { NativeChatToolRun } from './NativeChatToolRun'
-
-afterEach(cleanup)
-
-describe('chat code typography opt-in', () => {
- it('keeps live tool names and file paths outside code-size styling', () => {
- render(
-
- )
- expect(screen.getByText('Read src/app.ts')).not.toHaveAttribute('data-native-chat-code-content')
- expect(screen.getByTitle('src/app.ts')).not.toHaveAttribute('data-native-chat-code-content')
- })
-
- it('opts command previews and expanded output into code size', () => {
- const { container } = render(
-
- )
- expect(screen.getByTitle('git status')).toHaveAttribute('data-native-chat-code-content')
- expect(container.querySelector('pre')).toHaveAttribute('data-native-chat-code-content')
- expect(screen.getByText('Ran')).not.toHaveAttribute('data-native-chat-code-content')
- const header = container.querySelector('button')
- expect(header).not.toBeNull()
- expect(header?.querySelector('[data-native-chat-code-content]')).toBeNull()
- })
-})
diff --git a/src/renderer/src/components/native-chat/use-structured-agent-session-outbox-rejection-cause.test.tsx b/src/renderer/src/components/native-chat/use-structured-agent-session-outbox-rejection-cause.test.tsx
index e32006c50d0..cad9c99ac00 100644
--- a/src/renderer/src/components/native-chat/use-structured-agent-session-outbox-rejection-cause.test.tsx
+++ b/src/renderer/src/components/native-chat/use-structured-agent-session-outbox-rejection-cause.test.tsx
@@ -19,6 +19,7 @@ vi.mock('@/runtime/structured-agent-session-client', () => ({
import { setLocalRuntimeCapabilitiesForTests } from '@/runtime/local-runtime-capabilities'
import { RuntimeRpcCallError } from '@/runtime/runtime-rpc-result'
import { useStructuredAgentSessionOutbox } from './use-structured-agent-session-outbox'
+import { advanceProbeClock, useProbeClock } from './NativeChatStructuredSession.test-harness'
import { structuredAgentSessionDeliveryNotices } from './structured-agent-session-delivery-notices'
import { agentJournalSubmissionKey } from '../../../../shared/agent-session-journal-item-key'
import { agentSessionWriteNoticeEnglish } from '../../../../shared/agent-session-refusal-notice'
@@ -43,7 +44,10 @@ function outboxProps(submissions: AgentJournalSubmission[]): OutboxProps {
const NO_JOURNAL_ITEMS: readonly AgentJournalRenderItem[] = []
// Why: every hook here shares the session outbox store; one left mounted would drain the next test's.
-afterEach(cleanup)
+afterEach(() => {
+ cleanup()
+ vi.useRealTimers()
+})
function shownFailure(entry: StructuredAgentSessionOutboxEntry | undefined): string | undefined {
return (
@@ -297,6 +301,7 @@ describe('a send the host rejected because the agent never started', () => {
// One stored host fact, one rendering: a message the host recorded and rejected, whose record this
// chat does not hold, keeps showing why on every mount, with no control, and is never sent again.
it('keeps a message the host rejected, with no control, on every mount, and never resends it', async () => {
+ useProbeClock()
writeOutbox('session-1', [
{
...rejectedBeforeRestart(),
@@ -333,9 +338,8 @@ describe('a send the host rejected because the agent never started', () => {
)
const first = mount()
- await waitFor(() => expect(first.result.current.outbox[0]?.state).toBe('rejected'), {
- timeout: 4000
- })
+ await advanceProbeClock(1000)
+ expect(first.result.current.outbox[0]?.state).toBe('rejected')
const onFirst = notice(first.result.current.outbox, first.result.current.failedHere)
first.unmount()
const second = mount()
@@ -344,7 +348,7 @@ describe('a send the host rejected because the agent never started', () => {
for (const shown of [onFirst, onSecond]) {
expect(shown).toEqual({ text: REASON })
}
- await act(() => new Promise((resolve) => setTimeout(resolve, 1500)))
+ await advanceProbeClock(1500)
// Only the first mount's question about the send it left in doubt; nothing resends it.
expect(mocks.call).toHaveBeenCalledOnce()
expect(second.result.current.outbox.map((entry) => entry.state)).toEqual(['rejected'])