From 2de73812043e877f9e31e8772b7949972008c91e Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:06:33 -0400 Subject: [PATCH 01/53] fix(relay): supersede a reserve seat from the feed once a later, newer join shows (#28716) * test(relay): check invariant 4 (one demotion winner) in the step-5 simulation Every demotion now names its winner (the newer booking, or the row). The model flags a director that names a different winner for a divergence another already named, and a host whose two seats are each demoted for the other. A fault where directors break level ties by their own cell order shows the check fires. Violations are capped per invariant so one flood cannot hide another class. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * test(relay): bound unseen duplicates by the silence close and assert all invariants on contested ties A reserve seat the booking director's map had not seen (joined, then half-open within a poll) is never superseded and has no lease to re-check, so it lasts until the cell's control silence close (75 s + one 15 s ping). Invariant 3 now uses that bound for such seats, and the level-divergence config asserts every invariant. Stale maps now judge a divergence another director already demoted, so the config's "contested" counter (divergences judged by two or more directors) is the case invariant 4 guards. The silence-close constant now matches production (90 s, not 100 s). Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * test(relay): raise the floor on map-judged demotes and count only level contests A reconcile whose map has not seen another director's demotion still demotes in production, which raises its epoch floor; the model now does too. The contested counter counts only same-epoch pairs, the only ones invariant 4 can split. The keep-this-seat rule is one helper. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * refactor(relay): name the simulation's fault-kept seat check for the fault Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): supersede a reserve seat from the feed once a later, newer join shows A half-open seat the booking director's map had not yet seen was never demoted and held its cell until the 75 s control silence close. Directors now demote it once both joins are in the map; the simulation holds invariant 3 at the supersede bound for it. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): outrank only past cell clock skew, reuse demoteSeat, keep invariant 3's loose clock running Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * refactor(relay): share the outrank rule with the simulation, pin the skew boundary Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): count only confirmed level demotions and every second judgment, and bound duplicates by placement records alone Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): a drained seat keeps its drain grace under a demote, and the feed reports each seat once Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * refactor(relay): name the outranked-report bound and the reconcile backstop Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f --- .../cell-reserve-admission.blackbox.test.ts | 13 ++++ cloud/apps/relay/src/host-session-registry.ts | 4 +- cloud/apps/relay/src/relay-server.ts | 1 + .../apps/relay/src/reserve-assignment.test.ts | 11 +++ cloud/apps/relay/src/reserve-assignment.ts | 52 +++++++------ .../src/reserve-placement-simulation.test.ts | 2 + .../relay/src/reserve-placement-simulation.ts | 55 +++++++++++--- .../relay/src/shadow-seat-directory.test.ts | 74 +++++++++++++++++++ cloud/apps/relay/src/shadow-seat-directory.ts | 67 ++++++++++++++++- 9 files changed, 242 insertions(+), 37 deletions(-) diff --git a/cloud/apps/relay/src/cell-reserve-admission.blackbox.test.ts b/cloud/apps/relay/src/cell-reserve-admission.blackbox.test.ts index a952679ff53..543d40040f5 100644 --- a/cloud/apps/relay/src/cell-reserve-admission.blackbox.test.ts +++ b/cloud/apps/relay/src/cell-reserve-admission.blackbox.test.ts @@ -330,6 +330,19 @@ describe('a reserve-mode cell admits from memory with its database wedged', () = expect(cell.book(identity, 9)).toEqual([{ outcome: 'ok' }]) }) + it('leaves a drained seat to its drain grace instead of the shorter demotion close', async () => { + const cell = await startCell({ admitMode: 'reserve' }) + const identity = await cell.host() + cell.book(identity, 9) + await cell.connect(identity, 9) + const page = cell.relay.sessions.seatFeed(null) + const joinedAt = 'full' in page ? page.full[0]!.joinedAt : -1 + cell.relay.sessions.drain(120_000) + const seat = { v: 1 as const, userId: 'user-1', relayHostId: identity.hostId, epoch: 9, joinedAt } + expect(cell.relay.sessions.demote(seat)).toBe('draining') + expect(cell.relay.sessions.demote(seat)).toBe('draining') + }) + it('gives every booking unit back when the cell starts to drain', async () => { const cell = await startCell({ admitMode: 'reserve' }) const identity = await cell.host() diff --git a/cloud/apps/relay/src/host-session-registry.ts b/cloud/apps/relay/src/host-session-registry.ts index 1df66ae5edb..4bbbf896830 100644 --- a/cloud/apps/relay/src/host-session-registry.ts +++ b/cloud/apps/relay/src/host-session-registry.ts @@ -719,7 +719,7 @@ export class HostSessionRegistry { // A director names this seat the loser of a duplicate: stop taking phones for it now, and // close it once its splices have had time to finish. Repeats are one demotion. - demote(request: DemoteRequest): 'demoted' | 'already-demoted' | 'not-seated' | 'off' { + demote(request: DemoteRequest): 'demoted' | 'already-demoted' | 'draining' | 'not-seated' | 'off' { // Only a reserve-mode cell takes demotions, so flipping admitMode back reverts the route. if (!this.reserveMode()) return 'off' const session = this.get(request) @@ -735,6 +735,8 @@ export class HostSessionRegistry { ) { return 'not-seated' } + // A drain already told this host to go: its own grace stands, never cut to the demotion's. + if (this.drainSentHosts.has(session.relayHostId) || session.regionalDrainAttemptId) return 'draining' const socket = session.socket if (this.demotedSeats.get(key) === socket) return 'already-demoted' this.demotedSeats.set(key, socket) diff --git a/cloud/apps/relay/src/relay-server.ts b/cloud/apps/relay/src/relay-server.ts index 078a799ab62..5d69a392772 100644 --- a/cloud/apps/relay/src/relay-server.ts +++ b/cloud/apps/relay/src/relay-server.ts @@ -255,6 +255,7 @@ export function createRelayServer( epoch: join.epoch }) } + directory.onOutranked = (seat) => reservePlacement?.supersedeOutranked(seat) reserveLedger.start() const reconcile = (): void => { const now = (options.now ?? Date.now)() diff --git a/cloud/apps/relay/src/reserve-assignment.test.ts b/cloud/apps/relay/src/reserve-assignment.test.ts index 56c465fd0d5..b385c3ab1a7 100644 --- a/cloud/apps/relay/src/reserve-assignment.test.ts +++ b/cloud/apps/relay/src/reserve-assignment.test.ts @@ -350,6 +350,17 @@ describe('reserve assignment on a director', () => { expect(demoted).toEqual([{ cellId: 'c1', epoch: 5, joinedAt: 1_000 }]) }) + // The booking map had not seen the c1 seat (half-open, joined within a poll lag): the feed + // demotes it once both joins are in the map, instead of the 75 s silence close. + it('tells an old reserve seat the booking missed to go once a newer join shows in the feed', () => { + const { assignment, directory, demoted, now } = setup() + directory.onOutranked = (seat) => assignment.supersedeOutranked(seat) + directory.apply('c2', feed('c2', { full: [seated(6, 3_000)] }), now.value, now.value) + expect(demoted).toEqual([]) + directory.apply('c1', feed('c1', { full: [seated(5, 1_000)] }), now.value, now.value) + expect(demoted).toEqual([{ cellId: 'c1', epoch: 5, joinedAt: 1_000 }]) + }) + it('answers a reconnect only at the newest epoch, newest join first', async () => { const { assignment, reserved } = setup({ feeds: [feed('c1', { full: [seated(6, 1_000)] }), feed('c2', { full: [seated(5, 3_000)] })] diff --git a/cloud/apps/relay/src/reserve-assignment.ts b/cloud/apps/relay/src/reserve-assignment.ts index 47fe8d66883..ac5da96678f 100644 --- a/cloud/apps/relay/src/reserve-assignment.ts +++ b/cloud/apps/relay/src/reserve-assignment.ts @@ -13,7 +13,7 @@ import { type PlacementCell, type ReservePlacer } from './reserve-placement.js' -import type { SeatFeedCell, ShadowSeat, ShadowSeatDirectory } from './shadow-seat-directory.js' +import type { OutrankedSeat, SeatFeedCell, ShadowSeat, ShadowSeatDirectory } from './shadow-seat-directory.js' // Step 5 on a director: answer a desktop from the in-memory map for cells that report // admitMode=reserve, and fall back to today's database path for everything else. The cell @@ -377,10 +377,10 @@ export class ReserveAssignment { return undefined } - // Fire-and-forget; the cell checks the seat's epoch and join itself. - demoteSeat(seat: { userId: string; relayHostId: string; cellId: string; epoch: number; joinedAt: number }): void { + // Fire-and-forget; the cell checks the seat's epoch and join itself. False: no such cell listed. + demoteSeat(seat: { userId: string; relayHostId: string; cellId: string; epoch: number; joinedAt: number }): boolean { const cell = this.input.cells().find((entry) => entry.cellId === seat.cellId) - if (!cell) return + if (!cell) return false void this.input.client.demote(cell, { v: 1, userId: seat.userId, @@ -388,6 +388,7 @@ export class ReserveAssignment { epoch: seat.epoch, joinedAt: seat.joinedAt }) + return true } // Supersede at placement: the old seats this director's map holds on reserve cells are told @@ -396,27 +397,34 @@ export class ReserveAssignment { for (const seat of seats) { if (seat.cellId === cellId || seat.epoch >= epoch) continue if (this.input.directory.admitModeOf(seat.cellId) !== 'reserve') continue - const cell = this.input.cells().find((entry) => entry.cellId === seat.cellId) - if (!cell) continue - this.summary.supersedes += 1 - void this.input.client.demote(cell, { - v: 1, - userId: identity.userId, - relayHostId: identity.relayHostId, - epoch: seat.epoch, - joinedAt: seat.joinedAt - }) - this.input.log?.( - JSON.stringify({ - event: 'orca_relay_reserve_supersede', - relayHostIdDigest: relayHostLogDigest(identity.relayHostId), - fromCellId: seat.cellId, - toCellId: cellId - }) - ) + this.demoteSuperseded({ ...identity, ...seat }, cellId, 'placement') } } + // The feed's half of supersede: a seat the booking map did not hold, found once both joins + // are in this map. Without it that seat holds its cell until the 75 s silence close. + supersedeOutranked(seat: OutrankedSeat): void { + this.demoteSuperseded(seat, seat.byCellId, 'feed') + } + + private demoteSuperseded( + seat: { userId: string; relayHostId: string; cellId: string; epoch: number; joinedAt: number }, + toCellId: string, + via: 'placement' | 'feed' + ): void { + if (!this.demoteSeat(seat)) return + this.summary.supersedes += 1 + this.input.log?.( + JSON.stringify({ + event: 'orca_relay_reserve_supersede', + relayHostIdDigest: relayHostLogDigest(seat.relayHostId), + fromCellId: seat.cellId, + toCellId, + via + }) + ) + } + private flushSummary(): void { const now = this.now() if (now - this.summaryAt < SUMMARY_INTERVAL_MS) return diff --git a/cloud/apps/relay/src/reserve-placement-simulation.test.ts b/cloud/apps/relay/src/reserve-placement-simulation.test.ts index 6dae3349684..44f2cafa24e 100644 --- a/cloud/apps/relay/src/reserve-placement-simulation.test.ts +++ b/cloud/apps/relay/src/reserve-placement-simulation.test.ts @@ -294,6 +294,8 @@ describe('step 5 deterministic simulation', () => { [1, nearFull({ bookPastCap: true })], [2, drain({ intakeBurst: 200, directorShare: 0.05 })], [3, drainedWithoutSupersede()], + // Half-open seats the booking map had not seen hold their cell until the silence close. + [3, levelSeats(11, { skipFeedSupersede: true })], [4, levelSeats(11, { localTieBreak: true })], [5, mixed({ unguardedLedger: true })], // A director restart mid-queue loses its booked joins' rows until its boot reconcile. diff --git a/cloud/apps/relay/src/reserve-placement-simulation.ts b/cloud/apps/relay/src/reserve-placement-simulation.ts index 8eaa673d189..c6b9483ab61 100644 --- a/cloud/apps/relay/src/reserve-placement-simulation.ts +++ b/cloud/apps/relay/src/reserve-placement-simulation.ts @@ -9,6 +9,7 @@ import { ReservePlacer, type PlacementCell } from './reserve-placement.js' +import { seatOutranks } from './shadow-seat-directory.js' // A seeded discrete-event model of step 5 that drives the real CellReserveBook and // ReservePlacer. Everything else (sockets, the database, desktops) is modelled. The checks @@ -111,8 +112,10 @@ export type SimulationFaults = { intakeBurst?: number // Each director assumes this share of every cell's intake, instead of a fifth. directorShare?: number - // Placement does not supersede the old seat: a duplicate then outlives its bound. + // Nothing supersedes the old seat, at placement or from the feed: a duplicate outlives its bound. skipSupersede?: boolean + // Only placement supersedes: a half-open seat the booking map had not seen lasts to its silence close. + skipFeedSupersede?: boolean // A restarted director forgets its ledger queue and never reconciles. noBootReconcile?: boolean // The reconcile leaves a seat whose row is ahead of it alone. @@ -666,10 +669,15 @@ export async function runReservePlacementSimulation( placement: placementCell(cell, snapshot, requestedAt) } director.views.set(cell.cellId, current) + supersedeOutranked(director, seats!.keys()) } else if (current && current.incarnation === snapshot.incarnation) { for (const change of changes) applyChange(director, cell, current, change) current.cursor = snapshot.cursor current.placement = placementCell(cell, snapshot, requestedAt) + supersedeOutranked( + director, + changes.filter((change) => change.kind === 'join').map((change) => change.host) + ) } else { // A restarted director's (or a stale) delta: the next poll asks for a full snapshot. return @@ -758,6 +766,26 @@ export async function runReservePlacementSimulation( } } + // As shadow-seat-directory.ts onOutranked: once both joins are in this map, a reserve seat behind + // a later join at a newer epoch is told to go. A lower epoch that joined later is the row's. + function supersedeOutranked(director: SimDirector, hosts: Iterable): void { + if (faults.skipSupersede || faults.skipFeedSupersede) return + for (const host of new Set(hosts)) { + const seats = [...director.views].flatMap(([cellId, view]) => { + const seat = view.seats.get(host) + return seat ? [{ cellId, view, seat }] : [] + }) + if (seats.length < 2) continue + for (const { cellId, view, seat } of seats) { + if (seat.demoted || !view.placement.reserve) continue + const by = seats.find((other) => seatOutranks(other.seat, seat)) + if (!by) continue + report.supersedes += 1 + demoteFor(director, host, cellId, seat, { cellId: by.cellId, epoch: by.seat.epoch }) + } + } + } + // The seats the booking director's map held when it placed each booking: only those it supersedes. const heldAtBooking = new Map>() @@ -1258,9 +1286,9 @@ export async function runReservePlacementSimulation( report.maxOccupancyShare[cell.cellId] = Math.max(report.maxOccupancyShare[cell.cellId] ?? 0, share) } } - // A booking supersedes the old seats its director's map held within a round trip. Any other - // duplicate (a half-open seat today's path placed past, or one the booking map had not yet - // seen) has no lease to re-check and lasts until that seat's silence close. + // A booking supersedes the old seats its director's map held within a round trip, and the feed + // any seat a later join outranks. Any other duplicate (a half-open seat today's path placed past + // at a lower epoch than it) has no lease to re-check and lasts until that seat's silence close. const supersedeBound = Math.max(...directors.map((director) => config.pollLagMs(director.index))) + 2 * Math.max(...cells.map((cell) => config.rttMs(cell.region))) + @@ -1293,13 +1321,20 @@ export async function runReservePlacementSimulation( for (const { cell, seat } of seats) { if (cell.mode !== 'reserve' || seat.epoch === newest) continue // A leased seat goes by its renewal, as today. - const bound = !seat.leased && supersededBy(cell.cellId, seat.joinedAt) ? supersedeBound : duplicateBound + const tight = + !seat.leased && + (supersededBy(cell.cellId, seat.joinedAt) || seats.some((other) => seatOutranks(other.seat, seat))) + // The loose clock runs from the first outranking; the tight one from when supersede applies. const key = `${host}\u0000${cell.cellId}\u0000${seat.joinedAt}` - live.add(key) - const since = outrankedSince.get(key) ?? clock.now - outrankedSince.set(key, since) - if (clock.now - since > bound) { - violate(3, `${host}@${seat.epoch} on ${cell.cellId} outranked for ${clock.now - since} ms`) + const clocks: Array<[string, number]> = [[key, duplicateBound]] + if (tight) clocks.push([`${key}\u0000tight`, supersedeBound]) + for (const [clockKey, bound] of clocks) { + live.add(clockKey) + const since = outrankedSince.get(clockKey) ?? clock.now + outrankedSince.set(clockKey, since) + if (clock.now - since > bound) { + violate(3, `${host}@${seat.epoch} on ${cell.cellId} outranked for ${clock.now - since} ms`) + } } } } diff --git a/cloud/apps/relay/src/shadow-seat-directory.test.ts b/cloud/apps/relay/src/shadow-seat-directory.test.ts index 09cc2a352dd..fa0d4635d24 100644 --- a/cloud/apps/relay/src/shadow-seat-directory.test.ts +++ b/cloud/apps/relay/src/shadow-seat-directory.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { RelayAssignmentStore } from './assignment-store.js' import { openInMemoryRelayDatabase } from './database.js' import { + SHADOW_SEAT_OUTRANK_SKEW_MS, SHADOW_SEAT_RECENTLY_LEFT_MAX_HOSTS, ShadowSeatDirectory, startShadowSeatPoller, @@ -577,3 +578,76 @@ describe('ShadowSeatDirectory.placementModeOf', () => { expect(directory.placementModeOf('cell-a')).toBe('reserve') }) }) + +describe('ShadowSeatDirectory.onOutranked', () => { + const reserve = { generation: 3, flags: { admitMode: 'reserve' } } + const db = { generation: 3, flags: { admitMode: 'db' } } + const full = (cellId: string, epoch: number, joinedAt: number, flags = reserve, state = 'active') => + feed({ + cellId, + seq: 1, + flagsApplied: flags, + full: [{ userId: 'user-a', relayHostId: 'host-1', epoch, generation: 1, state, joinedAt }] + }) + const directory = () => { + const map = new ShadowSeatDirectory() + map.setCells(['cell-a', 'cell-b']) + const outranked: unknown[] = [] + map.onOutranked = (seat) => outranked.push(seat) + return { map, outranked } + } + const halfOpen = { + cellId: 'cell-a', + userId: 'user-a', + relayHostId: 'host-1', + epoch: 3, + joinedAt: 10, + byCellId: 'cell-b' + } + + it('reports a reserve seat a later, newer join elsewhere outranks, in either feed order', () => { + const first = directory() + first.map.apply('cell-a', full('cell-a', 3, 10), 100) + first.map.apply('cell-b', full('cell-b', 4, 2_000), 101) + expect(first.outranked).toEqual([halfOpen]) + const reversed = directory() + reversed.map.apply('cell-b', full('cell-b', 4, 2_000), 100) + reversed.map.apply('cell-a', full('cell-a', 3, 10), 101) + expect(reversed.outranked).toEqual([halfOpen]) + const justPastSkew = directory() + justPastSkew.map.apply('cell-a', full('cell-a', 3, 10), 100) + justPastSkew.map.apply('cell-b', full('cell-b', 4, 11 + SHADOW_SEAT_OUTRANK_SKEW_MS), 101) + expect(justPastSkew.outranked).toEqual([halfOpen]) + }) + + it('reports a seat once, even when a full resync re-judges its host', () => { + const { map, outranked } = directory() + map.apply('cell-a', full('cell-a', 3, 10), 100) + map.apply('cell-b', full('cell-b', 4, 2_000), 101) + map.apply('cell-b', { ...full('cell-b', 4, 2_000), incarnation: '22222222-2222-4222-8222-222222222222' }, 102) + map.apply('cell-a', { ...full('cell-a', 3, 10), incarnation: '22222222-2222-4222-8222-222222222222' }, 103) + expect(outranked).toEqual([halfOpen]) + }) + + it('leaves a database-cell seat, a drain-only seat, a later lower epoch, and a join within the skew', () => { + const onDatabaseCell = directory() + onDatabaseCell.map.apply('cell-a', full('cell-a', 3, 10, db), 100) + onDatabaseCell.map.apply('cell-b', full('cell-b', 4, 2_000), 101) + const drainOnly = directory() + drainOnly.map.apply('cell-a', full('cell-a', 3, 10, reserve, 'drain-only'), 100) + drainOnly.map.apply('cell-b', full('cell-b', 4, 2_000), 101) + // Answered the old seat from a stale map after the newer join: the row decides that one. + const staleSticky = directory() + staleSticky.map.apply('cell-a', full('cell-a', 3, 3_000), 100) + staleSticky.map.apply('cell-b', full('cell-b', 4, 2_000), 101) + const withinSkew = directory() + withinSkew.map.apply('cell-a', full('cell-a', 3, 10), 100) + withinSkew.map.apply('cell-b', full('cell-b', 4, 10 + SHADOW_SEAT_OUTRANK_SKEW_MS), 101) + expect([onDatabaseCell, drainOnly, staleSticky, withinSkew].map(({ outranked }) => outranked)).toEqual([ + [], + [], + [], + [] + ]) + }) +}) diff --git a/cloud/apps/relay/src/shadow-seat-directory.ts b/cloud/apps/relay/src/shadow-seat-directory.ts index 144270bd8f3..7dacf6a21df 100644 --- a/cloud/apps/relay/src/shadow-seat-directory.ts +++ b/cloud/apps/relay/src/shadow-seat-directory.ts @@ -16,6 +16,16 @@ export const SHADOW_SEAT_POLL_TIMEOUT_MS = 2_000 export const SHADOW_SEAT_CELL_LIST_REFRESH_MS = 30_000 export const SHADOW_SEAT_RECENTLY_LEFT_TTL_MS = 24 * 60 * 60 * 1_000 export const SHADOW_SEAT_RECENTLY_LEFT_MAX_HOSTS = 100_000 +// Cells stamp joinedAt by their own clocks: a later join must clear this to outrank across cells. +export const SHADOW_SEAT_OUTRANK_SKEW_MS = 1_000 +const SHADOW_SEAT_OUTRANKED_REPORTS_MAX = 100_000 + +export function seatOutranks( + newer: { epoch: number; joinedAt: number }, + seat: { epoch: number; joinedAt: number } +): boolean { + return newer.epoch > seat.epoch && newer.joinedAt > seat.joinedAt + SHADOW_SEAT_OUTRANK_SKEW_MS +} const RECENTLY_LEFT_PER_HOST = 4 // Google identity tokens live an hour; one per poll would be 26 metadata reads a second. const IDENTITY_TOKEN_REUSE_MS = 10 * 60_000 @@ -172,6 +182,16 @@ export type BookedJoin = { reservedBy: string } +// A reserve seat behind a later, newer-epoch join of its host elsewhere that placement supersede missed. +export type OutrankedSeat = { + cellId: string + userId: string + relayHostId: string + epoch: number + joinedAt: number + byCellId: string +} + type CellCursor = SeatFeedCellState & { seats: Map } export type HeartbeatSnapshot = { readAt: number; expiresAt: ReadonlyMap } @@ -180,6 +200,11 @@ function hostKey(userId: string, relayHostId: string): string { return `${userId}\u0000${relayHostId}` } +function parseHostKey(key: string): { userId: string; relayHostId: string } { + const separator = key.indexOf('\u0000') + return { userId: key.slice(0, separator), relayHostId: key.slice(separator + 1) } +} + export class ShadowSeatDirectory { private readonly cells = new Map() private readonly hosts = new Map>() @@ -193,6 +218,10 @@ export class ShadowSeatDirectory { // Per host, the newest booking any cell reported (step 5 epoch mint). Age-ordered. private readonly bookings = new Map() onBookedJoin?: (join: BookedJoin) => void + onOutranked?: (seat: OutrankedSeat) => void + private readonly joinedSinceApply = new Set() + // Seats already reported, insertion-ordered: a full resync re-judges a host without a second demote. + private readonly reportedOutranked = new Set() // Cells no longer listed are forgotten with their seats; new ones start pending. // `required` defaults to every listed cell. @@ -225,6 +254,11 @@ export class ShadowSeatDirectory { // `polledAt` is when the request left; the reply holds every change before it. apply(cellId: string, response: SeatFeedResponse, now: number, polledAt = now): void { + this.applyResponse(cellId, response, now, polledAt) + this.reportOutranked() + } + + private applyResponse(cellId: string, response: SeatFeedResponse, now: number, polledAt: number): void { const cursor = this.cells.get(cellId) if (!cursor) return if (response.cellId !== cellId) { @@ -307,10 +341,7 @@ export class ShadowSeatDirectory { seatsOnCell(cellId: string): Array<{ userId: string; relayHostId: string; seat: ShadowSeat }> { const cursor = this.cells.get(cellId) if (!cursor) return [] - return [...cursor.seats].map(([key, seat]) => { - const separator = key.indexOf('\u0000') - return { userId: key.slice(0, separator), relayHostId: key.slice(separator + 1), seat } - }) + return [...cursor.seats].map(([key, seat]) => ({ ...parseHostKey(key), seat })) } // An old cell (404) has no feed; its last known seats stay, marked by status. @@ -558,6 +589,34 @@ export class ShadowSeatDirectory { this.hosts.set(key, seats) } seats.set(cursor.cellId, seat) + if (this.onOutranked && seats.size > 1) this.joinedSinceApply.add(key) + } + + // After the whole response, so a cell's first snapshot is judged by the mode it carries. Only a + // join triggers it, once per seat: a cell flipped to reserve later, or a lost demote, falls to + // the silence close or the ledger reconcile, as does a lower epoch that joined later. + private reportOutranked(): void { + const report = this.onOutranked + const keys = [...this.joinedSinceApply] + this.joinedSinceApply.clear() + if (!report) return + for (const key of keys) { + const seats = [...(this.hosts.get(key)?.values() ?? [])] + for (const seat of seats) { + if (seat.state !== 'active' || this.admitModeOf(seat.cellId) !== 'reserve') continue + const by = seats.find((other) => seatOutranks(other, seat)) + if (!by) continue + const { cellId, epoch, joinedAt } = seat + const reported = `${key}\u0000${cellId}\u0000${epoch}\u0000${joinedAt}` + if (this.reportedOutranked.has(reported)) continue + this.reportedOutranked.add(reported) + if (this.reportedOutranked.size > SHADOW_SEAT_OUTRANKED_REPORTS_MAX) { + const oldest = this.reportedOutranked.values().next().value + if (oldest !== undefined) this.reportedOutranked.delete(oldest) + } + report({ ...parseHostKey(key), cellId, epoch, joinedAt, byCellId: by.cellId }) + } + } } private unseat(cursor: CellCursor, key: string): void { From b024043feb58c97ec99c7f1817fdc6b3f20ccf83 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:07:20 -0400 Subject: [PATCH 02/53] refactor(tabs): delete unused copy-tab and merge-group store actions (#28836) * refactor(tabs): delete unused copy-tab and merge-group store actions copyUnifiedTabToGroup and mergeGroupIntoSibling had no callers outside the store and its tests. Tests that used the copy action as a setup step now create the duplicate tab directly, the same call the action made. Co-Authored-By: Claude Opus 5.5 * chore(tabs): follow the split-ratio file rename in the layout-writer baseline Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- config/renderer-layout-writer-baseline.txt | 2 +- .../ipc-events/runtime-file-position.test.ts | 6 +- ...b-session-focus-intent-visible-tab.test.ts | 2 +- .../tabs-pane-layout-operations.test.ts | 58 ----------- .../store/slices/tabs/create-tabs-slice.ts | 4 +- .../slices/tabs/tabs-secondary-actions.ts | 97 ------------------- .../store/slices/tabs/tabs-slice-contract.ts | 18 ---- .../slices/tabs/tabs-split-ratio-actions.ts | 43 ++++++++ tests/e2e/cli-file-open-position.spec.ts | 10 +- 9 files changed, 60 insertions(+), 180 deletions(-) delete mode 100644 src/renderer/src/store/slices/tabs/tabs-secondary-actions.ts create mode 100644 src/renderer/src/store/slices/tabs/tabs-split-ratio-actions.ts diff --git a/config/renderer-layout-writer-baseline.txt b/config/renderer-layout-writer-baseline.txt index 225a2c2f0ab..50775b2cb2b 100644 --- a/config/renderer-layout-writer-baseline.txt +++ b/config/renderer-layout-writer-baseline.txt @@ -95,8 +95,8 @@ 8 src/renderer/src/store/slices/tabs/tabs-label-actions.ts 10 src/renderer/src/store/slices/tabs/tabs-move-actions.ts 9 src/renderer/src/store/slices/tabs/tabs-reconciliation.ts -1 src/renderer/src/store/slices/tabs/tabs-secondary-actions.ts 4 src/renderer/src/store/slices/tabs/tabs-session-actions.ts +1 src/renderer/src/store/slices/tabs/tabs-split-ratio-actions.ts 5 src/renderer/src/store/slices/tabs/tabs-surface.ts 6 src/renderer/src/store/slices/terminal-orphan-helpers.ts 4 src/renderer/src/store/slices/terminal-tab-title-batch.ts diff --git a/src/renderer/src/hooks/ipc-events/runtime-file-position.test.ts b/src/renderer/src/hooks/ipc-events/runtime-file-position.test.ts index 2a61e0179ca..6aea9f8bfb0 100644 --- a/src/renderer/src/hooks/ipc-events/runtime-file-position.test.ts +++ b/src/renderer/src/hooks/ipc-events/runtime-file-position.test.ts @@ -173,7 +173,11 @@ it('scopes the floating reveal to its selected split pane', () => { if (!siblingGroupId) { throw new Error('Split missing') } - store.getState().copyUnifiedTabToGroup(tab.id, siblingGroupId) + store.getState().createUnifiedTab(FLOATING, tab.contentType, { + entityId: tab.entityId, + label: tab.label, + targetGroupId: siblingGroupId + }) store.getState().focusGroup(FLOATING, tab.groupId) position(fileId, { line: 300, column: 7 }, true) flushFrames() diff --git a/src/renderer/src/runtime/web-session-focus-intent-visible-tab.test.ts b/src/renderer/src/runtime/web-session-focus-intent-visible-tab.test.ts index 1434a8ef3d4..e39c43e279e 100644 --- a/src/renderer/src/runtime/web-session-focus-intent-visible-tab.test.ts +++ b/src/renderer/src/runtime/web-session-focus-intent-visible-tab.test.ts @@ -74,7 +74,7 @@ describe('resolveWebSessionVisibleTabId — grouped state is authoritative', () expect(resolveWebSessionVisibleTabId(state, WT)).toBe('tab-shown') }) - // Why: copyUnifiedTabToGroup duplicates contentType + entityId into another group, so a plain + // Why: opening a file into a second split group duplicates contentType + entityId, so a plain // array scan can return the background copy and drag group focus to it. it('prefers the active group copy when one entity exists in two split groups', () => { const background = tab({ diff --git a/src/renderer/src/store/slices/tabs-pane-layout-operations.test.ts b/src/renderer/src/store/slices/tabs-pane-layout-operations.test.ts index 0a05d9780b3..8ef18713edc 100644 --- a/src/renderer/src/store/slices/tabs-pane-layout-operations.test.ts +++ b/src/renderer/src/store/slices/tabs-pane-layout-operations.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import type * as AgentStatusModule from '@/lib/agent-status' -import { getDefaultUIState } from '../../../../shared/constants' import { createTabsSliceMockApi } from './tabs-slice-test-harness' import { createTestStore } from './store-test-helpers' @@ -131,63 +130,6 @@ describe('TabsSlice', () => { ).toEqual([tab.id]) }) - it('copies a unified tab into another group', () => { - const tab = store.getState().createUnifiedTab(WT, 'editor', { - id: 'file-a.ts', - executionHostId: 'runtime:host-b', - label: 'file-a.ts' - }) - const sourceGroupId = store.getState().groupsByWorktree[WT][0].id - const targetGroupId = store.getState().createEmptySplitGroup(WT, sourceGroupId, 'right') - expect(targetGroupId).toBeTruthy() - - const copied = store.getState().copyUnifiedTabToGroup(tab.id, targetGroupId!) - - expect(copied).not.toBeNull() - const state = store.getState() - expect(state.unifiedTabsByWorktree[WT]).toHaveLength(2) - expect( - state.groupsByWorktree[WT].find((group) => group.id === sourceGroupId)?.tabOrder - ).toEqual([tab.id]) - expect( - state.groupsByWorktree[WT].find((group) => group.id === targetGroupId)?.tabOrder - ).toEqual([copied!.id]) - expect(copied?.entityId).toBe(tab.entityId) - expect(copied?.executionHostId).toBe('runtime:host-b') - }) - - it('merges a group into its sibling', () => { - const setMock = vi.mocked(window.api.ui.set) - const t1 = store.getState().createUnifiedTab(WT, 'editor', { - id: 'file-a.ts', - label: 'file-a.ts' - }) - const sourceGroupId = store.getState().groupsByWorktree[WT][0].id - const targetGroupId = store.getState().createEmptySplitGroup(WT, sourceGroupId, 'right') - expect(targetGroupId).toBeTruthy() - store.getState().createUnifiedTab(WT, 'editor', { - id: 'file-b.ts', - label: 'file-b.ts', - targetGroupId: targetGroupId! - }) - store.getState().hydratePersistedUI(getDefaultUIState()) - setMock.mockClear() - - const mergedInto = store.getState().mergeGroupIntoSibling(WT, targetGroupId!) - - expect(mergedInto).toBe(sourceGroupId) - const state = store.getState() - expect(state.groupsByWorktree[WT]).toHaveLength(1) - expect(state.groupsByWorktree[WT][0].tabOrder).toEqual([t1.id, 'file-b.ts']) - expect(state.layoutByWorktree[WT]).toEqual({ type: 'leaf', groupId: sourceGroupId }) - expect(setMock).toHaveBeenCalledTimes(1) - expect(setMock).toHaveBeenCalledWith({ - featureInteractions: { - 'terminal-panes': expect.objectContaining({ interactionCount: 1 }) - } - }) - }) - it('drops a unified tab into another group and collapses an emptied source group', () => { const tab = store.getState().createUnifiedTab(WT, 'editor', { id: 'file-a.ts', diff --git a/src/renderer/src/store/slices/tabs/create-tabs-slice.ts b/src/renderer/src/store/slices/tabs/create-tabs-slice.ts index adbea2f1726..52bf7cff18d 100644 --- a/src/renderer/src/store/slices/tabs/create-tabs-slice.ts +++ b/src/renderer/src/store/slices/tabs/create-tabs-slice.ts @@ -9,7 +9,7 @@ import { createTabsLabelActions } from './tabs-label-actions' import { createTabsGroupActions } from './tabs-group-actions' import { createTabsMoveActions } from './tabs-move-actions' import { createTabsDropActions } from './tabs-drop-actions' -import { createTabsSecondaryActions } from './tabs-secondary-actions' +import { createTabsSplitRatioActions } from './tabs-split-ratio-actions' import { createTabsSessionActions } from './tabs-session-actions' export const createTabsSlice: StateCreator = (set, get) => ({ @@ -25,6 +25,6 @@ export const createTabsSlice: StateCreator = (set, ...createTabsGroupActions(set, get), ...createTabsMoveActions(set, get), ...createTabsDropActions(set, get), - ...createTabsSecondaryActions(set, get), + ...createTabsSplitRatioActions(set), ...createTabsSessionActions(set, get) }) diff --git a/src/renderer/src/store/slices/tabs/tabs-secondary-actions.ts b/src/renderer/src/store/slices/tabs/tabs-secondary-actions.ts deleted file mode 100644 index 3db74535500..00000000000 --- a/src/renderer/src/store/slices/tabs/tabs-secondary-actions.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { TabGroupLayoutNode } from '../../../../../shared/tab-types' -import type { TabsSlice, TabsSliceGet, TabsSliceSet } from './tabs-slice-contract' -import { findGroupAndWorktree, findTabAndWorktree } from '../tab-group-state' -import { - findSiblingGroupId, - updateSplitRatio -} from '../../../../../shared/workspace-layout/tab-group-layout-tree' - -export function createTabsSecondaryActions( - set: TabsSliceSet, - get: TabsSliceGet -): Pick { - return { - copyUnifiedTabToGroup: (tabId, targetGroupId, init) => { - const foundTab = findTabAndWorktree(get().unifiedTabsByWorktree, tabId) - const foundTarget = findGroupAndWorktree(get().groupsByWorktree, targetGroupId) - if (!foundTab || !foundTarget || foundTab.worktreeId !== foundTarget.worktreeId) { - return null - } - const { tab, worktreeId } = foundTab - return get().createUnifiedTab(worktreeId, tab.contentType, { - entityId: init?.entityId ?? tab.entityId, - executionHostId: tab.executionHostId, - label: init?.label ?? tab.label, - generatedLabel: init?.generatedLabel ?? tab.generatedLabel, - quickCommandLabel: init?.quickCommandLabel ?? tab.quickCommandLabel, - customLabel: init?.customLabel ?? tab.customLabel, - color: init?.color ?? tab.color, - isPinned: init?.isPinned ?? tab.isPinned, - id: init?.id, - targetGroupId - }) - }, - - mergeGroupIntoSibling: (worktreeId, groupId) => { - const state = get() - const groups = state.groupsByWorktree[worktreeId] ?? [] - const sourceGroup = groups.find((candidate) => candidate.id === groupId) - const layout = state.layoutByWorktree[worktreeId] - if (!sourceGroup || !layout || groups.length <= 1) { - return null - } - const targetGroupId = findSiblingGroupId(layout, groupId) - if (!targetGroupId) { - return null - } - - const orderedSourceTabs = (state.unifiedTabsByWorktree[worktreeId] ?? []).filter( - (tab) => tab.groupId === groupId - ) - for (const tabId of sourceGroup.tabOrder) { - const item = orderedSourceTabs.find((tab) => tab.id === tabId) - if (!item) { - continue - } - get().moveUnifiedTabToGroup(item.id, targetGroupId, { recordInteraction: false }) - } - get().closeEmptyGroup(worktreeId, groupId) - get().recordFeatureInteraction?.('terminal-panes') - return targetGroupId - }, - - setTabGroupSplitRatio: (worktreeId, nodePath, ratio) => { - set((state) => { - const currentLayout = state.layoutByWorktree[worktreeId] - if (!currentLayout) { - return state - } - // Why: an unchanged ratio must not mint fresh root state — every store - // subscriber wakes on the new reference (STA-3328). - let targetNode: TabGroupLayoutNode | undefined = currentLayout - for (const segment of nodePath.length > 0 ? nodePath.split('.') : []) { - targetNode = - targetNode && - targetNode.type === 'split' && - (segment === 'first' || segment === 'second') - ? targetNode[segment] - : undefined - } - if (!targetNode || targetNode.type !== 'split' || (targetNode.ratio ?? 0.5) === ratio) { - return state - } - return { - layoutByWorktree: { - ...state.layoutByWorktree, - // Why: split ratios belong to the tab-group model (not transient UI), so persist them for restores and multi-step group ops. - [worktreeId]: updateSplitRatio( - currentLayout, - nodePath.length > 0 ? nodePath.split('.') : [], - ratio - ) - } - } - }) - } - } -} diff --git a/src/renderer/src/store/slices/tabs/tabs-slice-contract.ts b/src/renderer/src/store/slices/tabs/tabs-slice-contract.ts index 63dfab374cd..444fe9a5882 100644 --- a/src/renderer/src/store/slices/tabs/tabs-slice-contract.ts +++ b/src/renderer/src/store/slices/tabs/tabs-slice-contract.ts @@ -136,24 +136,6 @@ export type TabsSlice = { splitDirection?: TabSplitDirection } ) => boolean - copyUnifiedTabToGroup: ( - tabId: string, - targetGroupId: string, - init?: Partial< - Pick< - Tab, - | 'id' - | 'entityId' - | 'label' - | 'generatedLabel' - | 'quickCommandLabel' - | 'customLabel' - | 'color' - | 'isPinned' - > - > - ) => Tab | null - mergeGroupIntoSibling: (worktreeId: string, groupId: string) => string | null setTabGroupSplitRatio: (worktreeId: string, nodePath: string, ratio: number) => void reconcileWorktreeTabModel: (worktreeId: string) => { renderableTabCount: number diff --git a/src/renderer/src/store/slices/tabs/tabs-split-ratio-actions.ts b/src/renderer/src/store/slices/tabs/tabs-split-ratio-actions.ts new file mode 100644 index 00000000000..191d105dc61 --- /dev/null +++ b/src/renderer/src/store/slices/tabs/tabs-split-ratio-actions.ts @@ -0,0 +1,43 @@ +import type { TabGroupLayoutNode } from '../../../../../shared/tab-types' +import type { TabsSlice, TabsSliceSet } from './tabs-slice-contract' +import { updateSplitRatio } from '../../../../../shared/workspace-layout/tab-group-layout-tree' + +export function createTabsSplitRatioActions( + set: TabsSliceSet +): Pick { + return { + setTabGroupSplitRatio: (worktreeId, nodePath, ratio) => { + set((state) => { + const currentLayout = state.layoutByWorktree[worktreeId] + if (!currentLayout) { + return state + } + // Why: an unchanged ratio must not mint fresh root state — every store + // subscriber wakes on the new reference (STA-3328). + let targetNode: TabGroupLayoutNode | undefined = currentLayout + for (const segment of nodePath.length > 0 ? nodePath.split('.') : []) { + targetNode = + targetNode && + targetNode.type === 'split' && + (segment === 'first' || segment === 'second') + ? targetNode[segment] + : undefined + } + if (!targetNode || targetNode.type !== 'split' || (targetNode.ratio ?? 0.5) === ratio) { + return state + } + return { + layoutByWorktree: { + ...state.layoutByWorktree, + // Why: split ratios belong to the tab-group model (not transient UI), so persist them for restores and multi-step group ops. + [worktreeId]: updateSplitRatio( + currentLayout, + nodePath.length > 0 ? nodePath.split('.') : [], + ratio + ) + } + } + }) + } + } +} diff --git a/tests/e2e/cli-file-open-position.spec.ts b/tests/e2e/cli-file-open-position.spec.ts index 471ea8300e7..df5ede359e6 100644 --- a/tests/e2e/cli-file-open-position.spec.ts +++ b/tests/e2e/cli-file-open-position.spec.ts @@ -133,9 +133,15 @@ test('CLI file positioning preserves background navigation and reveals the reque await orcaPage.evaluate( ({ id, split }) => { const state = window.__store?.getState() - if (!state?.copyUnifiedTabToGroup(split.tabId, split.siblingGroupId)) { - throw new Error('Editor tab was not copied') + const tab = state?.unifiedTabsByWorktree[id]?.find((item) => item.id === split.tabId) + if (!state || !tab) { + throw new Error('Editor tab missing') } + state.createUnifiedTab(id, tab.contentType, { + entityId: tab.entityId, + label: tab.label, + targetGroupId: split.siblingGroupId + }) state.focusGroup(id, split.groupId) }, { id: worktreeId, split } From 23103e511a379a54957e194072ffc0df4df786c8 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:13:29 -0400 Subject: [PATCH 03/53] perf(layout): re-project only the written partition in the layout stream (#28845) The layout stream (layout.subscribe, still unused) re-loaded every session partition and ran the load's save-and-diff report after each write. A save now names the partition it wrote, the stream re-loads only that partition without the report, and the renderer layout client no longer re-subscribes on a timer. Adds a latency spec variant with a subscriber attached. Co-authored-by: Claude Opus 5.5 --- ...sistence-host-partitioned-sessions.test.ts | 14 ++ .../loading-store/session-host-partitions.ts | 19 ++- .../session-snapshot-operations.ts | 3 +- .../loading-store/store-runtime-state.ts | 3 +- .../workspace-session-snapshot-publication.ts | 5 +- .../workspace-session-write-listeners.ts | 14 +- .../loading-store/write-scheduling.ts | 14 +- .../runtime/workspace-layout-stream.test.ts | 132 +++++++++++++++++- src/main/runtime/workspace-layout-stream.ts | 86 ++++++++---- .../src/runtime/runtime-layout-client.test.ts | 67 +++------ .../src/runtime/runtime-layout-client.ts | 37 ++--- .../workspace-layout/workspace-layout-load.ts | 14 +- .../workspace-layout-oracle-latency.spec.ts | 130 +++++++++++++---- 13 files changed, 383 insertions(+), 155 deletions(-) diff --git a/src/main/persistence-host-partitioned-sessions.test.ts b/src/main/persistence-host-partitioned-sessions.test.ts index 9c3aad9620f..1314d8e2ff5 100644 --- a/src/main/persistence-host-partitioned-sessions.test.ts +++ b/src/main/persistence-host-partitioned-sessions.test.ts @@ -314,6 +314,20 @@ describe('Store host-partitioned workspace sessions', () => { expect(store.getWorkspaceSession('local').activeRepoId).toBe('repo-local') }) + it('names the one partition each session write changed', async () => { + const store = await createStore() + store.setWorkspaceSession(makeHostSession('repo-a'), 'runtime:env-a') + const written: (string | null)[] = [] + store.onWorkspaceSessionWritten((hostId) => written.push(hostId)) + + store.setWorkspaceSession(makeHostSession('repo-local'), 'local') + store.setWorkspaceSession(makeHostSession('repo-a2'), 'runtime:env-a') + store.patchWorkspaceSession({ activeTabId: 'tab-a' }, 'runtime:env-a') + store.removeWorkspaceSessionHost('runtime:env-a') + + expect(written).toEqual(['local', 'runtime:env-a', 'runtime:env-a', 'runtime:env-a']) + }) + it('patches a single host partition without touching the others', async () => { const store = await createStore() store.setWorkspaceSession(makeHostSession('repo-local'), 'local') diff --git a/src/main/persistence/loading-store/session-host-partitions.ts b/src/main/persistence/loading-store/session-host-partitions.ts index aace1ce4b68..12c5b7bc241 100644 --- a/src/main/persistence/loading-store/session-host-partitions.ts +++ b/src/main/persistence/loading-store/session-host-partitions.ts @@ -93,9 +93,11 @@ export class SessionHostPartitionOperations { const { [hostId]: _removed, ...remaining } = partitions runtime.state.workspaceSessionsByHostId = remaining invalidateLocalWorktreeMetadataPruneInputs() - scheduleSave(this[sessionHostPartitionOperationsContext].scheduling, [ - 'workspaceSessionsByHostId' - ]) + scheduleSave( + this[sessionHostPartitionOperationsContext].scheduling, + ['workspaceSessionsByHostId'], + hostId + ) } readTerminalScrollbackSnapshot(ref: string): string | null { @@ -170,7 +172,8 @@ export function removeWorkspaceSessionOwnerInPartition( } scheduleSave( owner[sessionHostPartitionOperationsContext].scheduling, - resolved === LOCAL_EXECUTION_HOST_ID ? ['workspaceSession'] : ['workspaceSessionsByHostId'] + resolved === LOCAL_EXECUTION_HOST_ID ? ['workspaceSession'] : ['workspaceSessionsByHostId'], + resolved ) } @@ -234,9 +237,11 @@ export function setHostWorkspaceSession( ...owner[sessionHostPartitionOperationsContext].runtime.state.workspaceSessionsByHostId, [hostId]: withRequiredWorkspaceSessionMaps(orderTerminalRowsByTabGroups(pruned)) } - scheduleSave(owner[sessionHostPartitionOperationsContext].scheduling, [ - 'workspaceSessionsByHostId' - ]) + scheduleSave( + owner[sessionHostPartitionOperationsContext].scheduling, + ['workspaceSessionsByHostId'], + hostId + ) } /** diff --git a/src/main/persistence/loading-store/session-snapshot-operations.ts b/src/main/persistence/loading-store/session-snapshot-operations.ts index 88c209485b3..c7320cc346e 100644 --- a/src/main/persistence/loading-store/session-snapshot-operations.ts +++ b/src/main/persistence/loading-store/session-snapshot-operations.ts @@ -128,7 +128,8 @@ export class SessionSnapshotOperations { } scheduleSave( scheduling, - hostId === LOCAL_EXECUTION_HOST_ID ? ['workspaceSession'] : ['workspaceSessionsByHostId'] + hostId === LOCAL_EXECUTION_HOST_ID ? ['workspaceSession'] : ['workspaceSessionsByHostId'], + hostId ) } diff --git a/src/main/persistence/loading-store/store-runtime-state.ts b/src/main/persistence/loading-store/store-runtime-state.ts index f72bba42ffe..ce557228c29 100644 --- a/src/main/persistence/loading-store/store-runtime-state.ts +++ b/src/main/persistence/loading-store/store-runtime-state.ts @@ -1,6 +1,7 @@ import { removeStaleDurableWriteTempFiles } from '../../durable-file-write' import type { GlobalSettings } from '../../../shared/global-settings-types' import type { PersistedState } from '../../../shared/persisted-state-types' +import type { WorkspaceSessionWriteListener } from './workspace-session-write-listeners' import type { ActiveViewPreference } from '../../active-view-preference' import { getProfileTerminalScrollbackSnapshotRoot, @@ -84,7 +85,7 @@ export class StoreRuntimeState { >() uiChangeListeners = new Set<(ui: PersistedState['ui']) => void>() /** Observers of persisted workspace-session writes; see notifyWorkspaceSessionWritten. */ - readonly workspaceSessionWriteListeners = new Set<() => void>() + readonly workspaceSessionWriteListeners = new Set() projectHostOperations: ProjectHostPersistenceOperations | null = null projectGroupOperations: ProjectGroupPersistenceOperations | null = null folderWorkspaceOperations: FolderWorkspacePersistenceOperations | null = null diff --git a/src/main/persistence/loading-store/workspace-session-snapshot-publication.ts b/src/main/persistence/loading-store/workspace-session-snapshot-publication.ts index d1446c701c4..f73fa61444c 100644 --- a/src/main/persistence/loading-store/workspace-session-snapshot-publication.ts +++ b/src/main/persistence/loading-store/workspace-session-snapshot-publication.ts @@ -124,11 +124,12 @@ export function setLocalWorkspaceSession( remappedManualUnread.changed ) { // UI remaps include protected fields, so keep the complete serializer boundary. - scheduleSave(context.scheduling) + scheduleSave(context.scheduling, undefined, LOCAL_EXECUTION_HOST_ID) } else { scheduleSave( context.scheduling, - remappedLeases.changed ? ['workspaceSession', 'sshRemotePtyLeases'] : ['workspaceSession'] + remappedLeases.changed ? ['workspaceSession', 'sshRemotePtyLeases'] : ['workspaceSession'], + LOCAL_EXECUTION_HOST_ID ) } } diff --git a/src/main/persistence/loading-store/workspace-session-write-listeners.ts b/src/main/persistence/loading-store/workspace-session-write-listeners.ts index 88407105e7b..9f8d4880012 100644 --- a/src/main/persistence/loading-store/workspace-session-write-listeners.ts +++ b/src/main/persistence/loading-store/workspace-session-write-listeners.ts @@ -1,19 +1,27 @@ +import type { ExecutionHostId } from '../../../shared/execution-host' import type { StoreRuntimeState } from './store-runtime-state' +/** Gets the one partition a write changed, or null when any partition may have changed. */ +export type WorkspaceSessionWriteListener = (hostId: ExecutionHostId | null) => void + const WORKSPACE_SESSION_DOMAINS: readonly string[] = [ 'workspaceSession', 'workspaceSessionsByHostId' ] -/** `dirtyDomains` undefined means a full save, which may carry an in-place session edit. */ +/** + * `dirtyDomains` undefined means a full save, which may carry an in-place session edit. + * `hostId` names the only partition the write changed; without it every partition is suspect. + */ export function notifyWorkspaceSessionWritten( runtime: Pick, - dirtyDomains?: readonly string[] + dirtyDomains?: readonly string[], + hostId?: ExecutionHostId ): void { if (dirtyDomains && !dirtyDomains.some((domain) => WORKSPACE_SESSION_DOMAINS.includes(domain))) { return } for (const listener of runtime.workspaceSessionWriteListeners) { - listener() + listener(hostId ?? null) } } diff --git a/src/main/persistence/loading-store/write-scheduling.ts b/src/main/persistence/loading-store/write-scheduling.ts index edd2754ddd9..9c622b64ee0 100644 --- a/src/main/persistence/loading-store/write-scheduling.ts +++ b/src/main/persistence/loading-store/write-scheduling.ts @@ -1,7 +1,11 @@ import type { StoreRuntimeState } from './store-runtime-state' import type { PrimaryStateWriteOperations } from './primary-state-writes' import { enqueueWrite } from './primary-state-writes' -import { notifyWorkspaceSessionWritten } from './workspace-session-write-listeners' +import type { ExecutionHostId } from '../../../shared/execution-host' +import { + notifyWorkspaceSessionWritten, + type WorkspaceSessionWriteListener +} from './workspace-session-write-listeners' const SAVE_DEBOUNCE_MS = 1_000 const SAVE_MAX_WAIT_MS = 5_000 @@ -40,7 +44,7 @@ export class WriteSchedulingOperations { } /** Called after each in-memory workspace-session write; listeners must not throw or block. */ - onWorkspaceSessionWritten(listener: () => void): () => void { + onWorkspaceSessionWritten(listener: WorkspaceSessionWriteListener): () => void { const { runtime } = this[writeSchedulingOperationsContext] runtime.workspaceSessionWriteListeners.add(listener) return () => { @@ -49,12 +53,14 @@ export class WriteSchedulingOperations { } } +/** `writtenHostId` names the one session partition this save changed, if it changed only one. */ export function scheduleSave( owner: WriteSchedulingOperations, - dirtyDomains?: readonly string[] + dirtyDomains?: readonly string[], + writtenHostId?: ExecutionHostId ): void { const { runtime, writes } = owner[writeSchedulingOperationsContext] - notifyWorkspaceSessionWritten(runtime, dirtyDomains) + notifyWorkspaceSessionWritten(runtime, dirtyDomains, writtenHostId) runtime.automationListProjectionCache = null const trackedDomains = runtime.dirtyProfileStateDomains if (dirtyDomains === undefined) { diff --git a/src/main/runtime/workspace-layout-stream.test.ts b/src/main/runtime/workspace-layout-stream.test.ts index fc9bd1ac572..b85a29efebb 100644 --- a/src/main/runtime/workspace-layout-stream.test.ts +++ b/src/main/runtime/workspace-layout-stream.test.ts @@ -3,39 +3,77 @@ import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../shared/exec import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { localDesktopSession, - relaySshSession + relaySshSession, + serverRuntimeSession } from '../../shared/workspace-layout/workspace-layout-profile.test-fixture' import { emptySession, FOLDER_KEY, GIT_KEY, + SERVER_KEY, SSH_KEY } from '../../shared/workspace-layout/workspace-layout-session.test-fixture' import type { WorkspaceLayoutEvent } from '../../shared/workspace-layout/workspace-layout-stream-frames' +import { loadWorkspaceLayout } from '../../shared/workspace-layout/workspace-layout-load' +import { nameBasedLoadContext } from '../../shared/workspace-layout/workspace-layout-minted-ids' +import { + publishWorkspaceLayout, + type PublishedWorkspaceLayout +} from '../../shared/workspace-layout/workspace-layout-published' import { WorkspaceLayoutStream } from './workspace-layout-stream' const SSH_HOST: ExecutionHostId = 'ssh:target-1' +const SERVER_HOST: ExecutionHostId = 'runtime:env-1' function fakeStore(partitions: Map) { - const listeners = new Set<() => void>() - const onWorkspaceSessionWritten = vi.fn((listener: () => void) => { + const listeners = new Set<(hostId: ExecutionHostId | null) => void>() + const onWorkspaceSessionWritten = vi.fn((listener: (hostId: ExecutionHostId | null) => void) => { listeners.add(listener) return () => listeners.delete(listener) }) + const getWorkspaceSession = vi.fn((hostId: ExecutionHostId) => partitions.get(hostId)!) return { store: { - getWorkspaceSession: (hostId: ExecutionHostId) => partitions.get(hostId)!, + getWorkspaceSession, getWorkspaceSessionHostIds: () => [...partitions.keys()], onWorkspaceSessionWritten }, listeners, + /** A save that names the partition it wrote. */ write(hostId: ExecutionHostId, session: WorkspaceSessionState) { partitions.set(hostId, session) - listeners.forEach((listener) => listener()) + listeners.forEach((listener) => listener(hostId)) + }, + /** A save that names no partition, so every partition may have changed. */ + writeUnnamed(hostId: ExecutionHostId, session: WorkspaceSessionState) { + partitions.set(hostId, session) + listeners.forEach((listener) => listener(null)) + }, + /** A removed host's partition, named by the save that drops it. */ + removePartition(hostId: ExecutionHostId) { + partitions.delete(hostId) + listeners.forEach((listener) => listener(hostId)) } } } +/** Today's projection: every partition loaded with its save-and-diff report, then filtered by home. */ +function fullProjection( + partitions: Map, + homeHostId: (key: string) => ExecutionHostId | null +): [string, PublishedWorkspaceLayout][] { + const projected = new Map() + for (const [hostId, session] of partitions) { + const { layout } = loadWorkspaceLayout(hostId, session, nameBasedLoadContext()) + for (const [key, workspace] of Object.entries(layout.workspaces)) { + if (homeHostId(key) === hostId) { + projected.set(key, publishWorkspaceLayout(workspace, hostId)) + } + } + } + return [...projected] +} + function setup() { const partitions = new Map([ [LOCAL_EXECUTION_HOST_ID, localDesktopSession()], @@ -51,7 +89,7 @@ function setup() { store: () => fake.store, homeHostId: (key) => homes[key] ?? null }) - return { ...fake, stream, homes } + return { ...fake, stream, homes, partitions } } const flush = () => new Promise((resolve) => queueMicrotask(resolve)) @@ -199,4 +237,86 @@ describe('WorkspaceLayoutStream', () => { expect(JSON.stringify(later.tabs)).toBe(JSON.stringify(earlier.tabs.slice(1))) } }) + + it('re-projects only the partition a write names', async () => { + const { stream, store, write, writeUnnamed } = setup() + stream.subscribe(() => {}) + store.getWorkspaceSession.mockClear() + write(SSH_HOST, relaySshSession('target-1')) + await flush() + expect(store.getWorkspaceSession.mock.calls).toEqual([[SSH_HOST]]) + store.getWorkspaceSession.mockClear() + writeUnnamed(SSH_HOST, relaySshSession('target-1')) + await flush() + expect(store.getWorkspaceSession.mock.calls).toEqual([[LOCAL_EXECUTION_HOST_ID], [SSH_HOST]]) + }) + + it('publishes what a full re-projection with the load report would, write after write', async () => { + const { stream, write, writeUnnamed, removePartition, homes, partitions } = setup() + const homeHostId = (key: string) => homes[key] ?? null + let cache = new Map() + const { snapshot } = stream.subscribe((event) => { + if (event.type === 'removed') { + cache.delete(event.key) + } else { + cache.set(event.key, event.layout) + } + }) + cache = new Map(snapshot.map((entry) => [entry.key, entry.layout])) + const expectFullProjection = async (): Promise => { + await flush() + const expected = fullProjection(partitions, homeHostId) + expect(Object.fromEntries(cache)).toEqual(Object.fromEntries(expected)) + // A late subscriber's snapshot keeps today's order too. + const late = stream.subscribe(() => {}) + expect(late.snapshot.map((entry) => [entry.key, entry.layout])).toEqual(expected) + late.unsubscribe() + } + await expectFullProjection() + + const local = localDesktopSession() + local.tabGroups![GIT_KEY]![0]!.tabOrder.reverse() + write(LOCAL_EXECUTION_HOST_ID, local) + await expectFullProjection() + + // A named write and an unnamed one in the same task. + const ssh = relaySshSession('target-1') + delete ssh.tabsByWorktree[SSH_KEY] + delete ssh.unifiedTabs![SSH_KEY] + write(SSH_HOST, ssh) + writeUnnamed(LOCAL_EXECUTION_HOST_ID, localDesktopSession()) + await expectFullProjection() + + // A new partition, a home that moves between partitions, then the partition removed. + homes[SERVER_KEY] = SERVER_HOST + write(SERVER_HOST, serverRuntimeSession('env-1')) + await expectFullProjection() + homes[GIT_KEY] = SERVER_HOST + write(SSH_HOST, relaySshSession('target-1')) + await expectFullProjection() + homes[GIT_KEY] = LOCAL_EXECUTION_HOST_ID + removePartition(SERVER_HOST) + await expectFullProjection() + }) + + it('re-projects every partition after a failed projection', async () => { + const { stream, store, write } = setup() + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const events: WorkspaceLayoutEvent[] = [] + stream.subscribe((event) => events.push(event)) + store.getWorkspaceSession.mockImplementationOnce(() => { + throw new Error('read failed') + }) + const local = localDesktopSession() + local.tabGroups![GIT_KEY]![0]!.tabOrder.reverse() + write(LOCAL_EXECUTION_HOST_ID, local) + await flush() + expect(events).toEqual([]) + // The local edit the failed projection missed is published by a write elsewhere. + write(SSH_HOST, relaySshSession('target-1')) + await flush() + expect(events.map((event) => [event.type, event.key])).toEqual([['workspace', GIT_KEY]]) + expect(warn).toHaveBeenCalledTimes(1) + warn.mockRestore() + }) }) diff --git a/src/main/runtime/workspace-layout-stream.ts b/src/main/runtime/workspace-layout-stream.ts index 8c20e38cd18..fb3b4a0eb72 100644 --- a/src/main/runtime/workspace-layout-stream.ts +++ b/src/main/runtime/workspace-layout-stream.ts @@ -1,9 +1,10 @@ // The read-only layout stream behind `layout.subscribe`: each workspace's published layout, -// re-projected from the Store's sessions after every write while anyone subscribes. Nothing reads -// it until a client opts in; with no subscriber it does no work. +// re-projected from the Store's sessions after every write while anyone subscribes. A write that +// names its partition re-projects only that partition. Nothing reads it until a client opts in; +// with no subscriber it does no work. import type { ExecutionHostId } from '../../shared/execution-host' -import { loadWorkspaceLayout } from '../../shared/workspace-layout/workspace-layout-load' +import { loadWorkspaceLayoutWithoutReport } from '../../shared/workspace-layout/workspace-layout-load' import { stableJson } from '../../shared/workspace-layout/workspace-layout-load-report' import { publishWorkspaceLayout, @@ -26,8 +27,11 @@ export class WorkspaceLayoutStream { private readonly listeners = new Set() /** Workspace key → its published layout, with its JSON for the by-value comparison. */ private published = new Map() + /** Partition → every workspace it holds, its own copy of a workspace it does not own included. */ + private projectedPartitions = new Map>() private stopWatching: (() => void) | null = null - private dirty = false + /** Partitions to re-project at the next flush; 'all' when a write did not name its partition. */ + private dirty: Set | 'all' | null = null private failureLogged = false constructor( @@ -45,9 +49,10 @@ export class WorkspaceLayoutStream { } { if (this.listeners.size === 0) { this.stopWatching = - this.deps.store()?.onWorkspaceSessionWritten?.(() => this.markDirty()) ?? null + this.deps.store()?.onWorkspaceSessionWritten?.((hostId) => this.markDirty(hostId)) ?? null this.published = new Map() - this.dirty = true + this.projectedPartitions = new Map() + this.dirty = 'all' } // A write not yet published reaches existing listeners first, so the snapshot is current. this.flush() @@ -62,28 +67,38 @@ export class WorkspaceLayoutStream { this.stopWatching?.() this.stopWatching = null this.published.clear() + this.projectedPartitions.clear() } } } - private markDirty(): void { - if (this.dirty) { - return + private markDirty(hostId: ExecutionHostId | null): void { + const scheduled = this.dirty !== null + if (hostId === null) { + this.dirty = 'all' + } else if (this.dirty === null) { + this.dirty = new Set([hostId]) + } else if (this.dirty !== 'all') { + this.dirty.add(hostId) + } + if (!scheduled) { + // Coalesces a burst of writes in one task into one projection. + queueMicrotask(() => this.flush()) } - this.dirty = true - // Coalesces a burst of writes in one task into one projection. - queueMicrotask(() => this.flush()) } private flush(): void { - if (!this.dirty) { + const dirty = this.dirty + if (dirty === null) { return } - this.dirty = false + this.dirty = null // A failed projection is logged once; it never reaches the write that notified it. try { - this.reconcile() + this.reconcile(dirty) } catch (error) { + // The cache may be part-updated; the next write re-projects every partition. + this.projectedPartitions.clear() this.logFailure('projection', error) } } @@ -95,28 +110,43 @@ export class WorkspaceLayoutStream { } } - private project(): Map { - const store = this.deps.store() + private projectPartition(hostId: ExecutionHostId): Map { const projected = new Map() - for (const hostId of store?.getWorkspaceSessionHostIds?.() ?? []) { - const session = store?.getWorkspaceSession?.(hostId) - if (!session) { - continue - } - const { layout } = loadWorkspaceLayout(hostId, session, nameBasedLoadContext()) - for (const [key, workspace] of Object.entries(layout.workspaces)) { + const session = this.deps.store()?.getWorkspaceSession?.(hostId) + if (!session) { + return projected + } + const { layout } = loadWorkspaceLayoutWithoutReport(hostId, session, nameBasedLoadContext()) + for (const [key, workspace] of Object.entries(layout.workspaces)) { + const published = publishWorkspaceLayout(workspace, hostId) + projected.set(key, { layout: published, json: stableJson(published) }) + } + return projected + } + + private project(dirty: Set | 'all'): Map { + const hostIds = this.deps.store()?.getWorkspaceSessionHostIds?.() ?? [] + const partitions = new Map>() + for (const hostId of hostIds) { + const cached = + dirty === 'all' || dirty.has(hostId) ? undefined : this.projectedPartitions.get(hostId) + partitions.set(hostId, cached ?? this.projectPartition(hostId)) + } + this.projectedPartitions = partitions + const projected = new Map() + for (const [hostId, workspaces] of partitions) { + for (const [key, workspace] of workspaces) { // Only the owning partition's copy is published; a stray copy elsewhere is not shown. if (this.deps.homeHostId(key) === hostId) { - const layout = publishWorkspaceLayout(workspace, hostId) - projected.set(key, { layout, json: stableJson(layout) }) + projected.set(key, workspace) } } } return projected } - private reconcile(): void { - const next = this.project() + private reconcile(dirty: Set | 'all'): void { + const next = this.project(dirty) const events: WorkspaceLayoutEvent[] = [] for (const key of this.published.keys()) { if (!next.has(key)) { diff --git a/src/renderer/src/runtime/runtime-layout-client.test.ts b/src/renderer/src/runtime/runtime-layout-client.test.ts index b28173419b5..e76b69b1aec 100644 --- a/src/renderer/src/runtime/runtime-layout-client.test.ts +++ b/src/renderer/src/runtime/runtime-layout-client.test.ts @@ -142,15 +142,6 @@ describe.each(targets)('runtime layout client over the %s transport', (_name, ta return host.streams[count - 1]! } - /** Whether a retry opens exactly `delayMs` after the previous loss. */ - async function expectRetryAfter(delayMs: number): Promise { - const count = host.streams.length - await vi.advanceTimersByTimeAsync(delayMs - 1) - expect(host.streams).toHaveLength(count) - await vi.advanceTimersByTimeAsync(1) - return opened(count + 1) - } - function snapshot(workspaces: { key: string; layout: PublishedWorkspaceLayout }[] = []) { return { type: 'snapshot', subscriptionId: 'layout-1', workspaces } } @@ -186,7 +177,7 @@ describe.each(targets)('runtime layout client over the %s transport', (_name, ta expect(stream.unsubscribed).toBe(false) }) - it('after a lost stream, resubscribes and the new snapshot replaces the cache', async () => { + it('after a lost stream, keeps the last state and never reopens it', async () => { subscribe() const first = await opened(1) first.send( @@ -197,36 +188,27 @@ describe.each(targets)('runtime layout client over the %s transport', (_name, ta ) first.drop() expect(first.unsubscribed).toBe(true) - // The last state stays until the runtime sends a new one; nothing is cleared or guessed. - expect(cache).toEqual({ a: layout('a', ['t1', 't2']), b: layout('b') }) - const second = await expectRetryAfter(250) - // A late frame from the dropped stream changes nothing. + expect(status).toEqual({ state: 'lost' }) + await vi.advanceTimersByTimeAsync(60_000) + expect(host.streams).toHaveLength(1) + // The last state stays; a late frame from the dropped stream changes nothing. first.send({ type: 'removed', key: 'a' }) - expect(cache).toHaveProperty('a') - second.send(snapshot([{ key: 'a', layout: layout('a', ['t2']) }])) - expect(cache).toEqual({ a: layout('a', ['t2']) }) + expect(cache).toEqual({ a: layout('a', ['t1', 't2']), b: layout('b') }) }) - it('backs off on failed subscribes and error frames, and a snapshot alone does not reset it', async () => { - host.rejectNextSubscribe() - subscribe() - await vi.advanceTimersByTimeAsync(0) - expect(host.streams).toHaveLength(0) - const first = await expectRetryAfter(250) - first.fail('stream failed') - expect(first.unsubscribed).toBe(true) - // A host that snapshots then drops at once keeps backing off instead of looping fast. - let stream = await expectRetryAfter(500) - for (const delay of [1000, 2000, 4000, 5000, 5000]) { - stream.send(snapshot()) - stream.drop() - stream = await expectRetryAfter(delay) - } - // A change after the snapshot proves the stream stayed up; the next loss starts over. - stream.send(snapshot()) - stream.send({ type: 'workspace', key: 'a', layout: layout('a') }) - stream.drop() - await expectRetryAfter(250) + it.each([ + ['a failed subscribe', () => host.rejectNextSubscribe(), async () => {}], + ['an error frame', () => {}, async () => (await opened(1)).fail('stream failed')] + ])('stops after %s without retrying', async (_case, before, fail) => { + before() + const subscription = subscribe() + await fail() + await vi.advanceTimersByTimeAsync(60_000) + expect(status).toEqual({ state: 'lost' }) + expect(host.streams.length).toBeLessThanOrEqual(1) + expect(host.streams.every((stream) => stream.unsubscribed)).toBe(true) + subscription.close() + expect(status).toEqual({ state: 'lost' }) }) it.each([ @@ -260,7 +242,7 @@ describe.each(targets)('runtime layout client over the %s transport', (_name, ta expect(status).toMatchObject({ state: 'refused', code: 'method_not_found' }) }) - it('close unsubscribes, stops retrying and ignores later frames', async () => { + it('close unsubscribes and ignores later frames', async () => { const subscription = subscribe() const stream = await opened(1) stream.send(snapshot()) @@ -274,15 +256,6 @@ describe.each(targets)('runtime layout client over the %s transport', (_name, ta expect(cache).toEqual({}) }) - it('close while waiting to retry cancels the retry', async () => { - const subscription = subscribe() - ;(await opened(1)).drop() - expect(status).toEqual({ state: 'retrying' }) - subscription.close() - await vi.advanceTimersByTimeAsync(10_000) - expect(host.streams).toHaveLength(1) - }) - it('close before the subscribe settles releases the stream it then gets', async () => { const subscription = subscribe() subscription.close() diff --git a/src/renderer/src/runtime/runtime-layout-client.ts b/src/renderer/src/runtime/runtime-layout-client.ts index 01e6eab01df..fc1609438ba 100644 --- a/src/renderer/src/runtime/runtime-layout-client.ts +++ b/src/renderer/src/runtime/runtime-layout-client.ts @@ -11,20 +11,19 @@ import { import type { RuntimeClientTarget } from '../../../shared/runtime-client-target' import { subscribeRuntimeRpc } from './runtime-rpc-subscribe' -/** `retrying`: waiting to reopen a lost stream. `refused`: the host answered that it will never - * serve this stream, so the client stopped. */ +/** `lost`: the stream ended without a refusal and the client stopped. `refused`: the host answered + * that it will never serve this stream, so the client stopped. */ export type RuntimeLayoutStreamStatus = - | { state: 'connecting' | 'live' | 'retrying' | 'closed' } + | { state: 'connecting' | 'live' | 'lost' | 'closed' } | { state: 'refused'; code: string } -const RETRY_BASE_MS = 250 -const RETRY_MAX_MS = 5000 // An older host without the method, or a caller scope denied it: a retry gets the same answer. const REFUSAL_CODES = ['method_not_found', 'forbidden'] as const /** - * Opens the stream and keeps it open: after a lost stream it resubscribes, and the new snapshot - * replaces the cache. `onFrame` gets a snapshot first, then that stream's changes. + * Opens the stream once; `onFrame` gets a snapshot first, then that stream's changes. A local + * stream ends only on close, document retirement or a refusal, so nothing reopens it. A remote + * stream that drops reports `lost`; reopening it belongs to the connection's reconnect. */ export function subscribeRuntimeLayout( target: RuntimeClientTarget, @@ -35,8 +34,6 @@ export function subscribeRuntimeLayout( // Bumped whenever a stream is given up, so its late callbacks change nothing. let generation = 0 let handle: { unsubscribe: () => void } | null = null - let retryTimer: ReturnType | null = null - let failures = 0 let state: RuntimeLayoutStreamStatus['state'] = 'connecting' const setStatus = (status: RuntimeLayoutStreamStatus): void => { @@ -52,10 +49,6 @@ export function subscribeRuntimeLayout( const stop = (status: RuntimeLayoutStreamStatus): void => { release() - if (retryTimer !== null) { - clearTimeout(retryTimer) - retryTimer = null - } setStatus(status) } @@ -67,18 +60,7 @@ export function subscribeRuntimeLayout( console.warn('[workspace-layout] layout stream lost:', error) } const refusal = REFUSAL_CODES.find((code) => hasRuntimeRpcErrorCode(error, code)) - if (refusal) { - stop({ state: 'refused', code: refusal }) - return - } - release() - setStatus({ state: 'retrying' }) - const delay = Math.min(RETRY_BASE_MS * 2 ** failures, RETRY_MAX_MS) - failures += 1 - retryTimer = setTimeout(() => { - retryTimer = null - void open() - }, delay) + stop(refusal ? { state: 'refused', code: refusal } : { state: 'lost' }) } const receive = (streamGeneration: number, frame: WorkspaceLayoutStreamFrame | null): void => { @@ -95,9 +77,6 @@ export function subscribeRuntimeLayout( } else if (state !== 'live') { // The host sends the snapshot first; a change before it has nothing to replace. return - } else { - // A change after the snapshot proves the stream stays up, unlike a snapshot then a drop. - failures = 0 } onFrame(frame) } @@ -124,7 +103,7 @@ export function subscribeRuntimeLayout( void open() return { close: () => { - if (state !== 'closed' && state !== 'refused') { + if (state === 'connecting' || state === 'live') { stop({ state: 'closed' }) } } diff --git a/src/shared/workspace-layout/workspace-layout-load.ts b/src/shared/workspace-layout/workspace-layout-load.ts index 70844117c1e..2c0931e3937 100644 --- a/src/shared/workspace-layout/workspace-layout-load.ts +++ b/src/shared/workspace-layout/workspace-layout-load.ts @@ -22,6 +22,7 @@ import { } from './workspace-layout-load-bindings' import { diffStoredSession } from './workspace-layout-load-report' import type { + LoadedWorkspaceLayout, WorkspaceLayoutLoadContext, WorkspaceLayoutLoadResult } from './workspace-layout-load-types' @@ -120,6 +121,16 @@ export function loadWorkspaceLayout( stored: WorkspaceSessionState, context: WorkspaceLayoutLoadContext ): WorkspaceLayoutLoadResult { + const loaded = loadWorkspaceLayoutWithoutReport(hostId, stored, context) + return { ...loaded, changes: diffStoredSession(stored, saveWorkspaceLayout(loaded)) } +} + +/** The same load without the save and diff, for a reader that never writes the result back. */ +export function loadWorkspaceLayoutWithoutReport( + hostId: ExecutionHostId, + stored: WorkspaceSessionState, + context: WorkspaceLayoutLoadContext +): LoadedWorkspaceLayout { // The Loader edits its own copy; the Store's object stays untouched. const session = structuredClone(stored) const desktopView: DesktopLayoutView = { @@ -174,6 +185,5 @@ export function loadWorkspaceLayout( loadLeaves(layout, { session, layouts }, { view: desktopView, facts, carried }, context) unbindDuplicateTerminals(layout) layout = applyLegacySurfaceTombstones(layout, session, carried) - const loaded = { layout, desktopView, facts, carried } - return { ...loaded, changes: diffStoredSession(stored, saveWorkspaceLayout(loaded)) } + return { layout, desktopView, facts, carried } } diff --git a/tests/e2e/workspace-layout-oracle-latency.spec.ts b/tests/e2e/workspace-layout-oracle-latency.spec.ts index 18fd201b0e7..3a690a62761 100644 --- a/tests/e2e/workspace-layout-oracle-latency.spec.ts +++ b/tests/e2e/workspace-layout-oracle-latency.spec.ts @@ -2,8 +2,9 @@ * Measures command → drawn update for layout commands, idle and with three panes flooding output * (`yes`), one of them in a hidden worktree, and time to first drawn after relaunching a large * profile. Measuring only: it records each number in the oracle report and never fails on one, - * so a later budget can gate on it. Runs with no layout subscriber, so main is measured as users - * run it. The committed main baseline is workspace-layout-oracle-latency-baseline.json. + * so a later budget can gate on it. The plain run has no layout subscriber, so main is measured as + * users run it; the subscribed run holds one `layout.subscribe` stream open in the window, as the + * layout client will. The committed main baseline is workspace-layout-oracle-latency-baseline.json. * * Each command is timed twice. The in-app clock (helpers/layout-latency-probe.ts) stops in the * window at React's commit and at the next frame. The outer clock stops when the test sees the @@ -12,7 +13,7 @@ import { mkdirSync, writeFileSync } from 'node:fs' import path from 'node:path' -import type { Page, TestInfo } from '@stablyai/playwright-test' +import type { ElectronApplication, Page, TestInfo } from '@stablyai/playwright-test' import { expect, test } from './helpers/orca-app' import { createRestartSession } from './helpers/orca-restart' import { bootstrapFirstLaunch, seededRepoPathOrSkip } from './helpers/terminal-restart-persistence' @@ -57,6 +58,53 @@ const LARGE_PROFILE = { hiddenWorktrees: 12, tabsPerWorktree: 3, activePanes: 4 type Condition = 'idle' | 'flood' type Sample = { outerMs: number; inApp: InAppSample } type Samples = Record +/** Main's memory at a phase's start and end, in MB; no GC is forced, so read it as a trend. */ +type MainMemory = { heapUsedMb: [number, number]; rssMb: [number, number] } +/** What the window's layout stream received during a phase. */ +type StreamTraffic = { frames: number; kb: number } + +declare global { + // oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging requires interface + interface Window { + __layoutStreamTraffic?: { frames: number; bytes: number; errors: number } + } +} + +async function readMainMemory( + app: ElectronApplication +): Promise<{ heapUsed: number; rss: number }> { + return app.evaluate(() => { + const { heapUsed, rss } = process.memoryUsage() + return { heapUsed, rss } + }) +} + +/** Holds one `layout.subscribe` stream open in the window for the rest of the run. */ +async function attachLayoutSubscriber(page: Page): Promise { + await page.evaluate(async () => { + const traffic = { frames: 0, bytes: 0, errors: 0 } + window.__layoutStreamTraffic = traffic + await window.api.runtime.subscribe({ method: 'layout.subscribe', params: {} }, (response) => { + if (!response.ok) { + traffic.errors += 1 + return + } + traffic.frames += 1 + traffic.bytes += JSON.stringify(response.result).length + }) + }) + await expect + .poll(() => page.evaluate(() => window.__layoutStreamTraffic?.frames ?? 0)) + .toBeGreaterThan(0) +} + +async function readStreamTraffic(page: Page): Promise<{ frames: number; bytes: number }> { + const traffic = await page.evaluate(() => window.__layoutStreamTraffic ?? null) + if (traffic && traffic.errors > 0) { + throw new Error(`layout stream reported ${traffic.errors} error frame(s)`) + } + return { frames: traffic?.frames ?? 0, bytes: traffic?.bytes ?? 0 } +} /** Reads can briefly fail while the window re-publishes its terminals; only reads are retried. */ async function read(client: RuntimeClient, method: string, params: unknown): Promise { @@ -243,9 +291,11 @@ type Stats = ReturnType type StartupSample = { firstDrawnMs: number; eventLoopDelayMax: number } type LatencyReport = | { - scenario: 'latency' + scenario: 'latency' | 'latency-subscribed' summary: Record eventLoopDelay: Record + mainMemory: Record + streamTraffic?: Record closeConfirms: Record samples: Samples } @@ -271,9 +321,7 @@ function writeLatencyReport(testInfo: TestInfo, { samples, ...summary }: Latency ) } -// oxlint-disable-next-line no-empty-pattern -- The measurement owns its launch. -test('layout oracle: command to drawn latency', async ({}, testInfo) => { - test.setTimeout(90 * 60_000) +async function measureLatency(testInfo: TestInfo, subscribed: boolean): Promise { const repoPath = seededRepoPathOrSkip() const session = createRestartSession(testInfo) const { app, page } = await session.launch() @@ -284,12 +332,22 @@ test('layout oracle: command to drawn latency', async ({}, testInfo) => { const worktree = `id:${worktreeId}` const client = new RuntimeClient(session.userDataDir, 30_000) await waitForBoundPanes(page, 1) - const phases: Record = { - idle: { from: Date.now(), to: 0, confirms: 0 }, - flood: { from: 0, to: 0, confirms: 0 } + if (subscribed) { + await attachLayoutSubscriber(page) } - phases.idle.confirms = await measureCondition(page, client, worktreeId, 'idle', samples) - phases.idle.to = Date.now() + type Mark = { at: number; heapUsed: number; rss: number; frames: number; bytes: number } + const mark = async (): Promise => ({ + at: Date.now(), + ...(await readMainMemory(app)), + ...(await readStreamTraffic(page)) + }) + type Phase = { from: Mark; to: Mark; confirms: number } + const measurePhase = async (condition: Condition): Promise => { + const from = await mark() + const confirms = await measureCondition(page, client, worktreeId, condition, samples) + return { from, to: await mark(), confirms } + } + const idle = await measurePhase('idle') // Two flooding tabs beside the measured one and one in a worktree that is never shown. const list = await read(client, 'terminal.list', { worktree }) @@ -305,9 +363,7 @@ test('layout oracle: command to drawn latency', async ({}, testInfo) => { } await client.call('terminal.focus', { terminal: measured.handle, navigation: 'host' }) await waitForActiveTerminalManager(page) - phases.flood.from = Date.now() - phases.flood.confirms = await measureCondition(page, client, worktreeId, 'flood', samples) - phases.flood.to = Date.now() + const flood = await measurePhase('flood') const summary = Object.fromEntries( Object.entries(samples).map(([action, values]) => [ @@ -320,18 +376,30 @@ test('layout oracle: command to drawn latency', async ({}, testInfo) => { } ]) ) - const eventLoopDelay = Object.fromEntries( - Object.entries(phases).map(([condition, phase]) => [ - condition, - summarizeEventLoopDelay(session.userDataDir, 'main', phase.from, phase.to) - ]) - ) - const closeConfirms = { idle: phases.idle.confirms, flood: phases.flood.confirms } + const perCondition = (read: (phase: Phase) => T): Record => ({ + idle: read(idle), + flood: read(flood) + }) + const mb = (bytes: number): number => Math.round(bytes / 1024 / 102.4) / 10 writeLatencyReport(testInfo, { - scenario: 'latency', + scenario: subscribed ? 'latency-subscribed' : 'latency', summary, - eventLoopDelay, - closeConfirms, + eventLoopDelay: perCondition(({ from, to }) => + summarizeEventLoopDelay(session.userDataDir, 'main', from.at, to.at) + ), + mainMemory: perCondition(({ from, to }) => ({ + heapUsedMb: [mb(from.heapUsed), mb(to.heapUsed)], + rssMb: [mb(from.rss), mb(to.rss)] + })), + ...(subscribed + ? { + streamTraffic: perCondition(({ from, to }) => ({ + frames: to.frames - from.frames, + kb: Math.round((to.bytes - from.bytes) / 1024) + })) + } + : {}), + closeConfirms: perCondition(({ confirms }) => confirms), samples }) } finally { @@ -341,6 +409,18 @@ test('layout oracle: command to drawn latency', async ({}, testInfo) => { await session.close(app) await session.dispose() } +} + +// oxlint-disable-next-line no-empty-pattern -- The measurement owns its launch. +test('layout oracle: command to drawn latency', async ({}, testInfo) => { + test.setTimeout(90 * 60_000) + await measureLatency(testInfo, false) +}) + +// oxlint-disable-next-line no-empty-pattern -- The measurement owns its launch. +test('layout oracle: command to drawn latency with a layout subscriber', async ({}, testInfo) => { + test.setTimeout(90 * 60_000) + await measureLatency(testInfo, true) }) // oxlint-disable-next-line no-empty-pattern -- The measurement owns its launches. From cc03142b9c08905f3069e6e2f7e70256d03e1443 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:13:51 -0700 Subject: [PATCH 04/53] feat(wsl-mover): background writers step aside for a project a WSL move is taking (D1b-5c-3) (#28835) * feat(wsl-mover): background writers back off a project a WSL move is taking (D1b-5c-3) * fix(wsl-mover): re-check before renames, quiet owned automations, stable folder ids (D1b-5c-3) * test(wsl-mover): seed journals one phase at a time and advance existing moves (D1b-5c-3) * fix(wsl-mover): a branch rename that already ran settles even when the move skips the folder (D1b-5c-3) --- .../scripts/vitest-sqlite-runtime-files.mjs | 3 + .../first-work-branch-rename-wsl-move.test.ts | 228 +++++++++++++++++ .../agent-hooks/first-work-branch-rename.ts | 26 +- .../agent-hooks/first-work-rename-runtime.ts | 7 + .../first-work-workspace-title-rename.ts | 7 + .../automation-dispatch-request.ts | 33 ++- .../automation-wsl-move-refusal.test.ts | 189 ++++++++++++++ .../dispatch-refusal-wsl-move.test.ts | 64 +++++ src/main/automations/dispatch-refusal.ts | 14 ++ .../automations/headless-dispatch-runner.ts | 6 +- src/main/automations/service.ts | 8 +- src/main/ipc/worktree-remote.ts | 5 + ...itative-local-worktree-metadata-pruning.ts | 6 +- .../listing/folder-workspace-catalog.ts | 30 ++- .../wsl-mover/wsl-move-journal-seeding.ts | 20 +- .../wsl-move-background-backoff.test.ts | 91 +++++++ .../wsl-move-background-backoff.ts | 71 ++++++ src/main/repo-backfill-wsl-move.test.ts | 127 ++++++++++ .../repo-git-remote-identity-enrichment.ts | 9 +- .../runtime/orca-runtime-file-commands.ts | 5 + .../runtime/repo-icon-fork-backfill.test.ts | 54 ++-- .../runtime-linear-team-write-commands.ts | 12 +- .../runtime-repository-fork-backfill.ts | 27 +- .../runtime/runtime-worktree-filesystem.ts | 7 +- .../runtime-worktree-lineage-controller.ts | 7 +- src/main/worktree-lineage-pruning.ts | 3 + src/main/wsl-move-incidental-writes.test.ts | 148 +++++++++++ src/main/wsl-move-scan-pruning.test.ts | 237 ++++++++++++++++++ 28 files changed, 1391 insertions(+), 53 deletions(-) create mode 100644 src/main/agent-hooks/first-work-branch-rename-wsl-move.test.ts create mode 100644 src/main/automations/automation-wsl-move-refusal.test.ts create mode 100644 src/main/automations/dispatch-refusal-wsl-move.test.ts create mode 100644 src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.test.ts create mode 100644 src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.ts create mode 100644 src/main/repo-backfill-wsl-move.test.ts create mode 100644 src/main/wsl-move-incidental-writes.test.ts create mode 100644 src/main/wsl-move-scan-pruning.test.ts diff --git a/config/scripts/vitest-sqlite-runtime-files.mjs b/config/scripts/vitest-sqlite-runtime-files.mjs index a96647dfe09..7c74900029d 100644 --- a/config/scripts/vitest-sqlite-runtime-files.mjs +++ b/config/scripts/vitest-sqlite-runtime-files.mjs @@ -7,6 +7,8 @@ export const SQLITE_RUNTIME_INCLUDE = [ 'src/main/ipc/repos/repo-ipc-wsl-move-fence.test.ts', 'src/main/runtime/runtime-managed-worktree-metadata-wsl-move.test.ts', 'src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.test.ts', + 'src/main/wsl-move-scan-pruning.test.ts', + 'src/main/repo-backfill-wsl-move.test.ts', 'src/main/acp/acp-structured-host-live-listing.test.ts', 'src/main/acp/acp-structured-host-configured-default.test.ts', 'src/main/pi/rpc-configured-default.test.ts', @@ -32,6 +34,7 @@ export const SQLITE_RUNTIME_INCLUDE = [ 'src/main/automations/automation-skip-coalescing.test.ts', 'src/main/automations/automation-update-host-retarget.test.ts', 'src/main/automations/automation-workspace-host-attribution.test.ts', + 'src/main/automations/automation-wsl-move-refusal.test.ts', 'src/main/automations/automation-zero-grace-tick-latency.test.ts', 'src/main/automations/refused-manual-run.test.ts', 'src/main/automations/retained-run-reconciliation.test.ts', diff --git a/src/main/agent-hooks/first-work-branch-rename-wsl-move.test.ts b/src/main/agent-hooks/first-work-branch-rename-wsl-move.test.ts new file mode 100644 index 00000000000..36e871f3f3e --- /dev/null +++ b/src/main/agent-hooks/first-work-branch-rename-wsl-move.test.ts @@ -0,0 +1,228 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { Repo } from '../../shared/repo-types' +import { wslMoveCutoverFixture as cutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + loadWslMoveFence, + resetWslMoveFenceForTests +} from '../persistence/moving-wsl-projects/wsl-move-scope-guard' + +const { gitExecFileAsyncMock, generateBranchNameMock, resolveTextGenerationParamsMock } = + vi.hoisted(() => ({ + gitExecFileAsyncMock: vi.fn(), + generateBranchNameMock: vi.fn(), + resolveTextGenerationParamsMock: vi.fn() + })) + +vi.mock('../git/runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock })) +vi.mock('../git/git-username', () => ({ resolveLocalGitUsername: vi.fn(async () => 'you') })) +vi.mock('../text-generation/commit-message-text-generation', () => ({ + generateBranchNameFromContext: generateBranchNameMock, + resolveTextGenerationParams: resolveTextGenerationParamsMock +})) +vi.mock('../text-generation/commit-message-agent-environment', () => ({ + prepareLocalCommitMessageAgentEnv: vi.fn(async () => ({ ok: true })) +})) +vi.mock('../ipc/worktree-logic', () => ({ + computeBranchName: vi.fn((leaf: string) => `you/${leaf}`), + getConfiguredBranchPrefix: vi.fn(() => 'you') +})) + +import { + maybeAutoRenameBranchOnFirstWork, + resetFirstWorkBranchRenameState, + type FirstWorkBranchRenameDeps +} from './first-work-branch-rename' +import { + FOLDER_WORKTREE_ID, + REPO_ID, + gitResponder, + makeBranchRenameDeps, + workingEvent +} from './first-work-branch-rename-test-harness' + +const LOCAL_REPO: Repo = { + id: REPO_ID, + path: '/repo', + displayName: 'repo', + badgeColor: '', + addedAt: 0 +} + +function makeDeps(overrides: Partial = {}) { + return makeBranchRenameDeps(vi.fn, overrides) +} + +let userDataPath: string +beforeEach(() => { + userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-rename-')) + loadWslMoveFence(userDataPath) + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced', {}, { repoIds: [REPO_ID] })) + resetFirstWorkBranchRenameState() + vi.clearAllMocks() + resolveTextGenerationParamsMock.mockReturnValue({ + ok: true, + params: { agentId: 'claude', model: 'm' } + }) + generateBranchNameMock.mockResolvedValue({ success: true, slug: 'fix-auth' }) + gitExecFileAsyncMock.mockImplementation( + gitResponder({ currentBranch: 'you/Nautilus', hasUpstream: false }) + ) +}) +afterEach(() => { + resetWslMoveFenceForTests() + rmSync(userDataPath, { recursive: true, force: true }) +}) + +describe('first-work rename while a WSL move takes the project', () => { + it('backs off before any git command and keeps the rename pending', async () => { + const { deps, setDisplayName, setRenameError } = makeDeps({ + getRepo: () => LOCAL_REPO + }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(gitExecFileAsyncMock).not.toHaveBeenCalled() + expect(setDisplayName).not.toHaveBeenCalled() + expect(setRenameError).not.toHaveBeenCalled() + + // A transient bail, not a settled verdict: once the move settles the next event renames. + resetWslMoveFenceForTests() + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['branch', '-m', 'you/fix-auth'], { + cwd: '/repo/wt' + }) + expect(setDisplayName).toHaveBeenCalled() + }) + + it('backs off when the move starts during generation', async () => { + resetWslMoveFenceForTests() + loadWslMoveFence(join(userDataPath, 'unused')) + generateBranchNameMock.mockImplementation(async () => { + loadWslMoveFence(userDataPath) + return { success: false, error: 'boom' } + }) + const { deps, setDisplayName, setRenameError } = makeDeps({ getRepo: () => LOCAL_REPO }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(generateBranchNameMock).toHaveBeenCalledOnce() + expect(gitExecFileAsyncMock).not.toHaveBeenCalledWith( + expect.arrayContaining(['branch', '-m']), + expect.anything() + ) + expect(setDisplayName).not.toHaveBeenCalled() + expect(setRenameError).not.toHaveBeenCalled() + }) + + it('backs off when the move starts after generation but before git branch -m', async () => { + resetWslMoveFenceForTests() + loadWslMoveFence(join(userDataPath, 'unused')) + let fenced = false + const respond = gitResponder({ + currentBranch: 'you/Nautilus', + hasUpstream: false + }) + gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { + if (args[0] === 'show-ref' && !fenced) { + fenced = true + loadWslMoveFence(userDataPath) + } + return respond(args) + }) + const { deps, setDisplayName, setRenameError, renameWorktreeFolder } = makeDeps({ + getRepo: () => LOCAL_REPO + }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(generateBranchNameMock).toHaveBeenCalledOnce() + expect(gitExecFileAsyncMock).not.toHaveBeenCalledWith( + expect.arrayContaining(['branch', '-m']), + expect.anything() + ) + expect(renameWorktreeFolder).not.toHaveBeenCalled() + expect(setDisplayName).not.toHaveBeenCalled() + expect(setRenameError).not.toHaveBeenCalled() + + // Still pending: once the move settles the next event renames. + resetWslMoveFenceForTests() + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['branch', '-m', 'you/fix-auth'], { + cwd: '/repo/wt' + }) + }) + + it('settles the rename but leaves the folder in place when the move starts during git branch -m', async () => { + resetWslMoveFenceForTests() + loadWslMoveFence(join(userDataPath, 'unused')) + const respond = gitResponder({ + currentBranch: 'you/Nautilus', + hasUpstream: false + }) + gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { + if (args[0] === 'branch' && args[1] === '-m') { + loadWslMoveFence(userDataPath) + } + return respond(args) + }) + const { deps, renameWorktreeFolder } = makeDeps({ + getRepo: () => LOCAL_REPO + }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['branch', '-m', 'you/fix-auth'], { + cwd: '/repo/wt' + }) + expect(renameWorktreeFolder).not.toHaveBeenCalled() + + // The branch already has its new name, so the rename is settled: a later event never renames again. + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + const branchRenames = gitExecFileAsyncMock.mock.calls.filter( + ([args]) => args[0] === 'branch' && args[1] === '-m' + ) + expect(branchRenames).toHaveLength(1) + }) + + it('renames a project the move is not taking', async () => { + const { deps, setDisplayName } = makeDeps({ + getRepo: () => ({ ...LOCAL_REPO, id: 'other-repo' }) + }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(setDisplayName).toHaveBeenCalled() + }) + + it('leaves the same repo id on another host to its own host', async () => { + const info = vi.spyOn(console, 'info').mockImplementation(() => {}) + const { deps } = makeDeps({ + getRepo: () => ({ ...LOCAL_REPO, executionHostId: 'runtime:env-1' }) + }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(info).toHaveBeenCalledWith(expect.stringContaining('branch is owned by runtime:env-1')) + expect(info).not.toHaveBeenCalledWith(expect.stringContaining('WSL move')) + info.mockRestore() + }) + + it('keeps a moving folder workspace title pending', async () => { + const { deps, setDisplayName, setRenameError } = makeDeps({ + resolveWorktreeIdForTab: () => FOLDER_WORKTREE_ID, + getFolderWorkspacePath: () => '/workspace/platform', + isPendingFirstAgentMessageRename: () => true, + isFolderWorkspaceWslMoveBlocked: () => true + }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(generateBranchNameMock).not.toHaveBeenCalled() + expect(setDisplayName).not.toHaveBeenCalled() + expect(setRenameError).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/agent-hooks/first-work-branch-rename.ts b/src/main/agent-hooks/first-work-branch-rename.ts index 72ebc3f9ced..8497b1e41a3 100644 --- a/src/main/agent-hooks/first-work-branch-rename.ts +++ b/src/main/agent-hooks/first-work-branch-rename.ts @@ -30,6 +30,7 @@ import type { CommitMessageAgentEnvironmentResolvers } from '../text-generation/ import type { AgentGenerationFailureOutput } from '../text-generation/agent-failure-output' import { resolveGenerationTarget } from './first-work-generation-target' import { runFolderWorkspaceTitleAutoRename } from './first-work-workspace-title-rename' +import { isWslMoveRepoBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' export type FirstWorkBranchRenameEvent = { paneKey: string @@ -50,6 +51,8 @@ export type FirstWorkBranchRenameDeps = { getCurrentDisplayName: (worktreeId: string) => string | undefined /** Current workspace path for non-git folder workspaces. */ getFolderWorkspacePath?: (worktreeId: string) => string | undefined + /** True while a WSL move is taking the folder workspace, or the WSL host owns it. */ + isFolderWorkspaceWslMoveBlocked?: (worktreeId: string) => boolean /** True while a workspace title is waiting for the first agent message. */ isPendingFirstAgentMessageRename?: (worktreeId: string) => boolean /** True only for Orca-created worktrees whose branch Orca is allowed to rename. */ @@ -176,6 +179,10 @@ async function runAutoRename( return stop('folder project has no branch to rename', true) } const worktreePath = parsed.worktreePath + // Before any git command; retry keeps the rename pending until the move settles. + if (isWslMoveRepoBlocked(repo)) { + return retry('a WSL move is taking the project') + } // Why: a runtime row's connectionId names the server's own SSH target; that host owns the branch, never this client. const route = resolveGitRouteForHost(getRepoExecutionHostId(repo)) @@ -235,6 +242,10 @@ async function runAutoRename( params, target ) + // Generation takes seconds; a move may have started meanwhile. + if (isWslMoveRepoBlocked(repo)) { + return retry('a WSL move started during generation') + } if (!generated.success) { // Transient failure: retry on a later event; skip the badge for a user-canceled generation. if (!generated.canceled) { @@ -281,6 +292,10 @@ async function runAutoRename( return stop(`no distinct unique branch name for slug "${slug}"`, true) } + // The name lookups above await git; a move may have started since the post-generation check. + if (isWslMoveRepoBlocked(repo)) { + return retry('a WSL move started before the branch rename') + } await renameCurrentBranch(exec, newBranch) // resolveUniqueBranchName may append a collision suffix (`-2`, …), so derive names from the resolved leaf, not the slug. @@ -298,7 +313,9 @@ async function runAutoRename( // Best-effort local-only folder align, after setDisplayName so the new name rides into the migrated identity; a failure must never undo the landed rename. let folderRenamed = false - if (deps.renameWorktreeFolder) { + // The branch rename landed, but a move now taking the project must not see its folder moved. + const folderBlocked = isWslMoveRepoBlocked(repo) + if (deps.renameWorktreeFolder && !folderBlocked) { try { folderRenamed = await deps.renameWorktreeFolder(worktreeId, newBranchLeaf) } catch (error) { @@ -313,7 +330,12 @@ async function runAutoRename( const displayLog = updateDisplay ? `display "${currentDisplayName ?? ''}" -> "${newDisplayName}"` : `display kept ("${currentDisplayName}")` - const folderLog = folderRenamed ? '; folder renamed' : '' + // The branch already has its new name, so a move that skipped the folder still settles the rename. + const folderLog = folderRenamed + ? '; folder renamed' + : folderBlocked + ? '; folder kept (a WSL move started)' + : '' console.info( `[auto-branch-rename] renamed ${currentBranch} -> ${newBranch}; ${displayLog}${folderLog}` ) diff --git a/src/main/agent-hooks/first-work-rename-runtime.ts b/src/main/agent-hooks/first-work-rename-runtime.ts index ebc9484071b..2c42a623dd5 100644 --- a/src/main/agent-hooks/first-work-rename-runtime.ts +++ b/src/main/agent-hooks/first-work-rename-runtime.ts @@ -5,6 +5,7 @@ import type { FirstWorkBranchRenameDeps } from './first-work-branch-rename' import { rememberBranchRenameFailureOutput } from './branch-rename-failure-output' import { renameWorktreeFolderOnFirstWork } from './first-work-folder-rename' import { moveWorktree } from '../git/worktree' +import { isWslMoveFolderWorkspaceBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' import type { Store } from '../persistence' import type { OrcaRuntimeService } from '../runtime/orca-runtime' @@ -36,6 +37,12 @@ export function firstWorkRenameDeps( ? store.getFolderWorkspace(scope.folderWorkspaceId)?.folderPath : undefined }, + isFolderWorkspaceWslMoveBlocked: (worktreeId) => { + const scope = parseWorkspaceKey(worktreeId) + const folder = + scope?.type === 'folder' ? store.getFolderWorkspace(scope.folderWorkspaceId) : undefined + return !!folder && isWslMoveFolderWorkspaceBlocked(folder) + }, isPendingFirstAgentMessageRename: (worktreeId) => { const scope = parseWorkspaceKey(worktreeId) return scope?.type === 'folder' diff --git a/src/main/agent-hooks/first-work-workspace-title-rename.ts b/src/main/agent-hooks/first-work-workspace-title-rename.ts index 54962c62afa..da9b4ab3eb5 100644 --- a/src/main/agent-hooks/first-work-workspace-title-rename.ts +++ b/src/main/agent-hooks/first-work-workspace-title-rename.ts @@ -19,6 +19,10 @@ export async function runFolderWorkspaceTitleAutoRename( stop: (reason: string, clearError?: boolean) => true, retry: (reason: string) => false ): Promise { + // Retry keeps the title pending until the move settles. + if (deps.isFolderWorkspaceWslMoveBlocked?.(worktreeId) === true) { + return retry('a WSL move is taking the workspace') + } if (deps.isPendingFirstAgentMessageRename?.(worktreeId) !== true) { return stop('folder workspace is not pending title rename', true) } @@ -52,6 +56,9 @@ export async function runFolderWorkspaceTitleAutoRename( ) { return stop('folder workspace changed during generation', true) } + if (deps.isFolderWorkspaceWslMoveBlocked?.(worktreeId) === true) { + return retry('a WSL move started during generation') + } if (!generated.success) { if (!generated.canceled) { deps.setRenameError(worktreeId, generated.error, generated.failureOutput ?? null) diff --git a/src/main/automations/automation-dispatch-request.ts b/src/main/automations/automation-dispatch-request.ts index 348a1b9ecee..f6f4b0f9854 100644 --- a/src/main/automations/automation-dispatch-request.ts +++ b/src/main/automations/automation-dispatch-request.ts @@ -6,10 +6,17 @@ import type { Store } from '../persistence' import type { AutomationRunWriter } from './automation-run-writer' import type { HeadlessAutomationDispatcher } from './headless-dispatch' import type { HeadlessAutomationDispatchContext } from './headless-dispatch-runner' -import { runHeadlessAutomationDispatch } from './headless-dispatch-runner' +import { + AutomationDispatchRefusedError, + runHeadlessAutomationDispatch +} from './headless-dispatch-runner' import type { AutomationRunTargetResult } from './run-target-resolution' import { createAutomationDispatchToken } from './dispatch-tokens' -import { NO_DISPATCH_HOST, sendRendererDispatch } from './dispatch-refusal' +import { + NO_DISPATCH_HOST, + describeScheduledRefusal, + sendRendererDispatch +} from './dispatch-refusal' export type AutomationRendererChannel = Pick @@ -100,8 +107,12 @@ export async function requestAutomationDispatch( return returnDurable(run) } let target = resolveCurrentTarget() - if (!target.ok || (!ctx.getRenderer() && !ctx.headlessDispatcher)) { - return refuse(target.ok ? NO_DISPATCH_HOST : target.error) + const refusal = describeScheduledRefusal({ + target, + canDispatch: Boolean(ctx.getRenderer() || ctx.headlessDispatcher) + }) + if (refusal || !target.ok) { + return refuse(refusal ?? NO_DISPATCH_HOST) } await ctx.runs.updateRun({ runId: run.id, @@ -115,8 +126,10 @@ export async function requestAutomationDispatch( return returnDurable(run) } target = resolveCurrentTarget() - if (!target.ok) { - return refuse(target.error) + // A WSL move may have started during the claim; refuse before the worktree create would throw. + const lateRefusal = describeScheduledRefusal({ target, canDispatch: true }) + if (lateRefusal || !target.ok) { + return refuse(lateRefusal ?? NO_DISPATCH_HOST) } const renderer = ctx.getRenderer() if (renderer) { @@ -148,6 +161,14 @@ export async function requestAutomationDispatch( if (!latestTarget.ok) { throw new AutomationDispatchCancelledError(latestTarget.error) } + // The precheck can run for minutes; a WSL move starting meanwhile is a skip, not a failure. + const latestRefusal = describeScheduledRefusal({ + target: latestTarget, + canDispatch: true + }) + if (latestRefusal) { + throw new AutomationDispatchRefusedError(latestRefusal) + } return dispatcher({ ...request, target: latestTarget }) } }) diff --git a/src/main/automations/automation-wsl-move-refusal.test.ts b/src/main/automations/automation-wsl-move-refusal.test.ts new file mode 100644 index 00000000000..c659311a832 --- /dev/null +++ b/src/main/automations/automation-wsl-move-refusal.test.ts @@ -0,0 +1,189 @@ +/** + * A move in flight skips each run with a reason that promises a resume; once the WSL host owns the + * project, the retained local copy goes quiet instead of writing a skip every tick. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { mkdtempSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import type { Repo } from '../../shared/repo-types' +import type { Automation } from '../../shared/automations-types' +import { removeTreeSync } from '../../shared/windows-transient-lock-removal' +import type { Store } from '../persistence' +import { closeTestStores, createStore, testState } from '../persistence-test-harness' +import { wslMoveCutoverFixture as cutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + loadWslMoveFence, + resetWslMoveFenceForTests +} from '../persistence/moving-wsl-projects/wsl-move-scope-guard' +import { AutomationService } from './service' +import { WSL_MOVE_IN_PROGRESS, WSL_MOVE_OWNED_BY_HOST } from './dispatch-refusal' + +const runAutomationPrecheckMock = vi.hoisted(() => vi.fn()) + +vi.mock('electron', () => ({ + app: { + getPath: () => testState.dir + }, + safeStorage: { + isEncryptionAvailable: () => true, + encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'), + decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length) + } +})) + +vi.mock('./precheck-runner', () => ({ + runAutomationPrecheck: runAutomationPrecheckMock +})) + +const makeRepo = (): Repo => ({ + id: 'r1', + path: '/repo', + displayName: 'test', + badgeColor: '#fff', + addedAt: 1 +}) + +function seedAutomation(store: Store, precheck = false): Automation { + store.addRepo(makeRepo()) + return store.createAutomation({ + name: 'Nightly check', + prompt: 'Check the repo', + ...(precheck ? { precheck: { command: 'true', timeoutSeconds: 30 } } : {}), + agentId: 'claude', + projectId: 'r1', + workspaceMode: 'new_per_run', + timezone: 'UTC', + rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0', + dtstart: new Date('2026-05-12T00:00:00').getTime() + }) +} + +function attachedService(store: Store, publish = vi.fn()): AutomationService { + const service = new AutomationService(store, { + tickMs: 60_000, + onAutomationsChanged: publish + }) + service.setWebContents({ isDestroyed: () => false, send: vi.fn() }) + return service +} + +/** One scheduler pass. `nextRunAt` advancing is the signal that it finished. */ +async function evaluateAt( + store: Store, + service: AutomationService, + automationId: string, + when: string +): Promise { + const nextRunAt = (): number | undefined => + store.listAutomations().find((entry) => entry.id === automationId)?.nextRunAt + const before = nextRunAt() + vi.setSystemTime(new Date(when)) + // Re-poked each try: the previous pass may still hold the re-entrancy guard while it flushes. + await vi.waitFor(() => { + service.setRendererReady() + expect(nextRunAt()).toBeGreaterThan(before ?? 0) + }) +} + +let fenceDir: string +beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'orca-automation-wsl-move-')) + fenceDir = mkdtempSync(join(tmpdir(), 'orca-automation-wsl-fence-')) + loadWslMoveFence(fenceDir) + runAutomationPrecheckMock.mockReset() + vi.useFakeTimers() +}) +afterEach(async () => { + await closeTestStores() + vi.useRealTimers() + resetWslMoveFenceForTests() + removeTreeSync(testState.dir) + removeTreeSync(fenceDir) +}) + +describe('scheduled automations under a WSL move', () => { + it('skips a run while the move is in flight, promising a resume', async () => { + vi.setSystemTime(new Date('2026-05-13T08:00:00')) + const store = await createStore() + const automation = seedAutomation(store) + seedWslMoveCutover(fenceDir, cutover('move-1', 'source-fenced', {}, { repoIds: ['r1'] })) + const service = attachedService(store) + + await evaluateAt(store, service, automation.id, '2026-05-13T09:01:00') + + const runs = store.listAutomationRuns(automation.id) + expect(runs).toHaveLength(1) + expect(runs[0]).toMatchObject({ + status: 'skipped_unavailable', + error: WSL_MOVE_IN_PROGRESS + }) + expect(WSL_MOVE_IN_PROGRESS).toBe( + "This automation's project is moving to its WSL host, so Orca skipped this run. Runs resume when the move finishes." + ) + }) + + it('writes no skip on any tick once the WSL host owns the project', async () => { + vi.setSystemTime(new Date('2026-05-13T08:00:00')) + const store = await createStore() + const automation = seedAutomation(store) + seedWslMoveCutover( + fenceDir, + cutover('move-1', 'destination-committed', {}, { repoIds: ['r1'] }) + ) + const publish = vi.fn() + const service = attachedService(store, publish) + + await evaluateAt(store, service, automation.id, '2026-05-13T09:01:00') + await evaluateAt(store, service, automation.id, '2026-05-14T09:01:00') + await evaluateAt(store, service, automation.id, '2026-05-15T09:01:00') + + expect(store.listAutomationRuns(automation.id)).toEqual([]) + expect(publish).not.toHaveBeenCalled() + expect(store.listAutomations().find((entry) => entry.id === automation.id)?.nextRunAt).toBe( + new Date('2026-05-16T09:00:00').getTime() + ) + expect(WSL_MOVE_OWNED_BY_HOST).toBe( + "This automation's project now runs on its WSL host, which runs this automation there. Orca no longer runs this local copy." + ) + }) + + it('skips, not fails, a headless run whose move starts during the precheck', async () => { + vi.setSystemTime(new Date('2026-05-13T08:00:00')) + const store = await createStore() + const automation = seedAutomation(store, true) + runAutomationPrecheckMock.mockImplementation(async () => { + seedWslMoveCutover(fenceDir, cutover('move-1', 'source-fenced', {}, { repoIds: ['r1'] })) + return { + command: 'true', + exitCode: 0, + timedOut: false, + durationMs: 1, + stdout: '', + stderr: '', + stdoutTruncated: false, + stderrTruncated: false, + error: null, + startedAt: Date.now(), + completedAt: Date.now() + } + }) + const dispatcher = vi.fn() + const service = new AutomationService(store, { + tickMs: 60_000, + headlessDispatcher: dispatcher + }) + + vi.setSystemTime(new Date('2026-05-13T09:01:00')) + service.start() + await vi.waitFor(() => + expect(store.listAutomationRuns(automation.id)[0]?.status).toBe('skipped_unavailable') + ) + service.stop() + + expect(runAutomationPrecheckMock).toHaveBeenCalledOnce() + expect(dispatcher).not.toHaveBeenCalled() + expect(store.listAutomationRuns(automation.id)[0]?.error).toBe(WSL_MOVE_IN_PROGRESS) + }) +}) diff --git a/src/main/automations/dispatch-refusal-wsl-move.test.ts b/src/main/automations/dispatch-refusal-wsl-move.test.ts new file mode 100644 index 00000000000..eda84d15bfa --- /dev/null +++ b/src/main/automations/dispatch-refusal-wsl-move.test.ts @@ -0,0 +1,64 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import type { Repo } from '../../shared/repo-types' +import { wslMoveCutoverFixture as cutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + loadWslMoveFence, + resetWslMoveFenceForTests +} from '../persistence/moving-wsl-projects/wsl-move-scope-guard' +import { + NO_DISPATCH_HOST, + WSL_MOVE_IN_PROGRESS, + WSL_MOVE_OWNED_BY_HOST, + describeScheduledRefusal +} from './dispatch-refusal' + +function repo(id: string, overrides: Partial = {}): Repo { + return { id, path: `/ws/${id}`, displayName: id, badgeColor: '#000', addedAt: 0, ...overrides } +} + +const refusalFor = (target: Repo, canDispatch = true) => + describeScheduledRefusal({ target: { ok: true, cwd: target.path, repo: target }, canDispatch }) + +let userDataPath: string +beforeEach(() => { + userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-automation-')) + loadWslMoveFence(userDataPath) + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) +}) +afterEach(() => { + resetWslMoveFenceForTests() + rmSync(userDataPath, { recursive: true, force: true }) +}) + +describe('describeScheduledRefusal under a WSL move', () => { + it('refuses a run in the moving project with one fixed sentence', () => { + expect(refusalFor(repo('repo-1'))).toBe(WSL_MOVE_IN_PROGRESS) + expect(refusalFor(repo('repo-1'), false)).toBe(WSL_MOVE_IN_PROGRESS) + expect(WSL_MOVE_IN_PROGRESS).toBe( + "This automation's project is moving to its WSL host, so Orca skipped this run. Runs resume when the move finishes." + ) + }) + + it('says the WSL host runs it once the host owns the project', () => { + seedWslMoveCutover(userDataPath, cutover('move-1', 'destination-committed')) + expect(refusalFor(repo('repo-1'))).toBe(WSL_MOVE_OWNED_BY_HOST) + expect(WSL_MOVE_OWNED_BY_HOST).toBe( + "This automation's project now runs on its WSL host, which runs this automation there. Orca no longer runs this local copy." + ) + }) + + it('runs a project the move is not taking', () => { + expect(refusalFor(repo('repo-2'))).toBeNull() + expect(refusalFor(repo('repo-2'), false)).toBe(NO_DISPATCH_HOST) + }) + + it('runs the same repo id on another host', () => { + expect( + refusalFor(repo('repo-1', { connectionId: 'target-1', executionHostId: 'ssh:target-1' })) + ).toBeNull() + }) +}) diff --git a/src/main/automations/dispatch-refusal.ts b/src/main/automations/dispatch-refusal.ts index 286abb2f252..19d1841773a 100644 --- a/src/main/automations/dispatch-refusal.ts +++ b/src/main/automations/dispatch-refusal.ts @@ -14,6 +14,7 @@ import type { } from '../../shared/automations-types' import { resolveAutomationRunTarget, type AutomationRunTargetResult } from './run-target-resolution' import type { AutomationRunWriter } from './automation-run-writer' +import { wslMoveRepoBackoff } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' export const NO_DISPATCH_HOST = 'No Orca window was available to launch the automation.' @@ -24,6 +25,14 @@ export const UNEVALUABLE_SCHEDULE = /** A record the authority refuses to execute at all, with no target diagnosis of its own. */ export const NO_RUNNABLE_HOST = 'This automation has no host to run on.' +/** A move is taking its project; the schedule keeps ticking, so runs resume once it finishes. */ +export const WSL_MOVE_IN_PROGRESS = + "This automation's project is moving to its WSL host, so Orca skipped this run. Runs resume when the move finishes." + +/** The WSL host owns its project now; this retained local copy never runs again. */ +export const WSL_MOVE_OWNED_BY_HOST = + "This automation's project now runs on its WSL host, which runs this automation there. Orca no longer runs this local copy." + /** Every reason this occurrence cannot start, decided before a run row exists so * the scheduler can fold repeats instead of writing one row each. */ export function describeScheduledRefusal(input: { @@ -33,6 +42,11 @@ export function describeScheduledRefusal(input: { if (!input.target.ok) { return input.target.error } + // Refused here, before a run row or a worktree create, so repeats fold into one skip. + const backoff = wslMoveRepoBackoff(input.target.repo) + if (backoff) { + return backoff === 'moving' ? WSL_MOVE_IN_PROGRESS : WSL_MOVE_OWNED_BY_HOST + } return input.canDispatch ? null : NO_DISPATCH_HOST } diff --git a/src/main/automations/headless-dispatch-runner.ts b/src/main/automations/headless-dispatch-runner.ts index 7315e33c390..85e63438bcd 100644 --- a/src/main/automations/headless-dispatch-runner.ts +++ b/src/main/automations/headless-dispatch-runner.ts @@ -26,6 +26,9 @@ export type HeadlessAutomationDispatchContext = { watchRun: (run: AutomationRun) => void } +/** Thrown by a dispatcher that finds the run should not start; recorded as a skip, not a failure. */ +export class AutomationDispatchRefusedError extends Error {} + function describeDispatchError(error: unknown): string { return error instanceof Error ? error.message : String(error) } @@ -51,7 +54,8 @@ export async function runHeadlessAutomationDispatch( } catch (error) { return runs.updateRun({ runId: run.id, - status: 'dispatch_failed', + status: + error instanceof AutomationDispatchRefusedError ? 'skipped_unavailable' : 'dispatch_failed', workspaceId: automation.workspaceId, error: describeDispatchError(error) }) diff --git a/src/main/automations/service.ts b/src/main/automations/service.ts index 107f01ae234..7765a3de42b 100644 --- a/src/main/automations/service.ts +++ b/src/main/automations/service.ts @@ -30,7 +30,8 @@ import { missedBeyondGrace, recordMissedRun, recordRefusedAutomationRun, - recordUnevaluableAutomation + recordUnevaluableAutomation, + WSL_MOVE_OWNED_BY_HOST } from './dispatch-refusal' import type { AutomationsChangedPayload, @@ -283,6 +284,11 @@ export class AutomationService { const target = this.resolveTarget(automation) const canDispatch = this.canDispatchToRenderer() || Boolean(this.headlessDispatcher) const refusal = describeScheduledRefusal({ target, canDispatch }) + // The WSL host runs this one now; a skip per tick would only churn the retained local copy. + if (refusal === WSL_MOVE_OWNED_BY_HOST) { + await this.runs.advanceNextRun(automation.id, now) + return + } if (refusal && (await this.runs.repeatSkip(automation.id, refusal, scheduledFor))) { await this.runs.advanceNextRun(automation.id, now) return diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index a1d42fe22b5..62852004af0 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -139,6 +139,7 @@ import { } from '../worktree-name-retirement' import { createRetiredNameLookup } from '../../shared/worktree/retired-name-registry' import { findPendingWorktreeRemovalConflict } from '../worktree-removal-table' +import { isWslMoveWorktreeBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' // Why: bound the fallback `git fetch origin` so a Windows credential-manager GUI hang (STA-1292) can't wedge worktree creation forever. const CREATE_BASE_FALLBACK_FETCH_TIMEOUT_MS = 60_000 // Why (#17828 CodeRabbit follow-up): the deferred materialize fetch runs off the main @@ -837,6 +838,10 @@ function persistMaterializedPushTargetIfCreated( if (!target.remoteCreated || !worktreeId || !store?.setWorktreeMeta) { return } + // Bookkeeping incidental to the push: a project a WSL move is taking skips it, not the push. + if (isWslMoveWorktreeBlocked(worktreeId, store.getAllWorktreeMeta()[worktreeId]?.hostId)) { + return + } store.setWorktreeMeta(worktreeId, { pushTarget: target }) } diff --git a/src/main/ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning.ts b/src/main/ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning.ts index 3349a986519..5072344bc3d 100644 --- a/src/main/ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning.ts +++ b/src/main/ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning.ts @@ -16,6 +16,7 @@ import { pruneWorkspaceSpaceAnalysisSnapshots } from '../../../workspace-space-a import { isLocalWorktreeScanGenerationCurrent } from '../../../local-worktree-scan-generation' import { localWorktreePathsExistOrAreUnverifiable } from '../../../local-worktree-path-presence' import { worktreeRetentionPathComparisonKey } from '../../../worktree-retention-path-comparison' +import { isWslMoveRepoBlocked } from '../../../persistence/moving-wsl-projects/wsl-move-background-backoff' type AuthoritativeLocalMetadataPruneArgs = { store: Store @@ -65,7 +66,9 @@ export async function pruneMetadataMissingFromAuthoritativeLocalScan({ } const generationCurrent = () => isLocalWorktreeScanGenerationCurrent(repo.id, scanGeneration) const repoOwners = store.getRepos().filter((candidate) => candidate.id === repo.id) + // Mid-move a scan can miss paths that still exist; nothing is pruned until the move settles. if ( + isWslMoveRepoBlocked(repo) || !generationCurrent() || !isCallerCurrent() || gitWorktrees.length === 0 || @@ -124,7 +127,8 @@ export async function pruneMetadataMissingFromAuthoritativeLocalScan({ return result([], generationCurrent()) } - if (!generationCurrent() || !isCallerCurrent()) { + // The path probe awaits, so a move may have started since the entry check. + if (!generationCurrent() || !isCallerCurrent() || isWslMoveRepoBlocked(repo)) { return result([], generationCurrent()) } const removedIds = store.pruneSessionlessMissingLocalWorktreeMetadataForRepo( diff --git a/src/main/ipc/worktrees/listing/folder-workspace-catalog.ts b/src/main/ipc/worktrees/listing/folder-workspace-catalog.ts index abaa594bf6e..c17068b92c0 100644 --- a/src/main/ipc/worktrees/listing/folder-workspace-catalog.ts +++ b/src/main/ipc/worktrees/listing/folder-workspace-catalog.ts @@ -1,6 +1,10 @@ +import { createHash } from 'node:crypto' import type { Repo } from '../../../../shared/repo-types' import type { Worktree, DetectedWorktree } from '../../../../shared/worktree/types' +import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' import type { Store } from '../../../persistence/loading-store/store' +import { mergeWorktreeMetaForWrite } from '../../../persistence/loading-store/worktree-meta-write-normalization' +import { isWslMoveRepoBlocked } from '../../../persistence/moving-wsl-projects/wsl-move-background-backoff' import { createWorktreeVisibilitySourceMatcher, resolveCustomWorktreeVisibilitySources @@ -19,7 +23,28 @@ import { mergeFolderWorkspace } from '../folder-workspace-model' +// A project a WSL move is taking still lists, with its repairs projected in memory only. +function folderMetaWriter( + store: Store, + repo: Repo +): (worktreeId: string, updates: Partial) => WorktreeMeta { + if (!isWslMoveRepoBlocked(repo)) { + return (worktreeId, updates) => store.setWorktreeMeta(worktreeId, updates) + } + return (worktreeId, updates) => { + const existing = store.getWorktreeMeta(worktreeId) + // Nothing persists, so a minted root id would change on every listing; derive it instead. + const instanceId = + existing?.instanceId ?? + (worktreeId === getFolderWorkspaceRootId(repo) + ? createHash('sha256').update(worktreeId).digest('hex').slice(0, 32) + : getFolderWorkspaceInstanceIdentity(repo, worktreeId)) + return mergeWorktreeMetaForWrite(existing, { ...updates, instanceId }) + } +} + export function listFolderWorkspaces(store: Store, repo: Repo): Worktree[] { + const writeMeta = folderMetaWriter(store, repo) const rootId = getFolderWorkspaceRootId(repo) const allMeta = store.getAllWorktreeMeta() const ids = Object.keys(allMeta).filter((worktreeId) => @@ -36,7 +61,7 @@ export function listFolderWorkspaces(store: Store, repo: Repo): Worktree[] { const meta = existing?.instanceId && Object.keys(ownershipUpdates).length === 0 ? existing - : store.setWorktreeMeta(worktreeId, { + : writeMeta(worktreeId, { instanceId: existing?.instanceId ?? getFolderWorkspaceInstanceIdentity(repo, worktreeId), ...ownershipUpdates, @@ -77,6 +102,7 @@ export function buildFolderDetectedWorktrees(store: Store, repo: Repo): Detected } export function listVisibleFolderWorkspaces(store: Store, repo: Repo): Worktree[] { + const writeMeta = folderMetaWriter(store, repo) return buildFolderDetectedWorktrees(store, repo) .filter((worktree) => worktree.visible) .map((worktree) => { @@ -85,7 +111,7 @@ export function listVisibleFolderWorkspaces(store: Store, repo: Repo): Worktree[ const repairedMeta = meta && Object.keys(ownershipUpdates).length === 0 ? meta - : store.setWorktreeMeta(worktree.id, ownershipUpdates) + : writeMeta(worktree.id, ownershipUpdates) return mergeFolderWorkspace(repo, worktree.id, repairedMeta) }) } diff --git a/src/main/persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding.ts b/src/main/persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding.ts index 24dcfd08e64..8e45dcdb77f 100644 --- a/src/main/persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding.ts +++ b/src/main/persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding.ts @@ -1,15 +1,23 @@ -/** Writes a move's record through every phase up to its own, as the journal requires. */ +/** + * Writes a move's record through every phase up to its own, as the journal requires, starting + * after the phase already on disk so a test can also advance an existing move. + */ import { WSL_MOVE_CUTOVER_PHASES, type WslMoveCutover } from '../../../../shared/wsl-move/wsl-move-cutover' -import { writeWslMoveCutover } from '../../moving-wsl-projects/wsl-move-cutover-journal' +import { + listWslMoveCutovers, + writeWslMoveCutover +} from '../../moving-wsl-projects/wsl-move-cutover-journal' export function seedWslMoveCutover(userDataPath: string, cutover: WslMoveCutover): void { - for (const phase of WSL_MOVE_CUTOVER_PHASES) { + const existing = listWslMoveCutovers(userDataPath).find( + (entry) => entry.migrationId === cutover.migrationId + ) + const from = existing ? WSL_MOVE_CUTOVER_PHASES.indexOf(existing.phase) + 1 : 0 + const to = WSL_MOVE_CUTOVER_PHASES.indexOf(cutover.phase) + for (const phase of WSL_MOVE_CUTOVER_PHASES.slice(from, to + 1)) { writeWslMoveCutover(userDataPath, { ...cutover, phase }) - if (phase === cutover.phase) { - return - } } } diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.test.ts b/src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.test.ts new file mode 100644 index 00000000000..9cbeceee116 --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.test.ts @@ -0,0 +1,91 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { wslMoveCutoverFixture as cutover } from '../__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { wslMoveJournalDirectory } from './wsl-move-cutover-journal' +import { seedWslMoveCutover } from '../__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { loadWslMoveFence, resetWslMoveFenceForTests } from './wsl-move-scope-guard' +import { + isWslMoveFolderWorkspaceBlocked, + isWslMoveRepoBlocked, + isWslMoveWorktreeBlocked, + wslMoveRepoBackoff +} from './wsl-move-background-backoff' + +const onSsh = { connectionId: 'target-1', executionHostId: 'ssh:target-1' } +const WSL_PATH = '\\\\wsl.localhost\\Ubuntu\\home\\dev\\app' + +let userDataPath: string +beforeEach(() => { + userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-backoff-')) + loadWslMoveFence(userDataPath) +}) +afterEach(() => { + resetWslMoveFenceForTests() + rmSync(userDataPath, { recursive: true, force: true }) + vi.restoreAllMocks() +}) + +describe('WSL move background backoff', () => { + beforeEach(() => { + seedWslMoveCutover( + userDataPath, + cutover('move-1', 'source-fenced', {}, { repoIds: ['repo-1'], folderWorkspaceIds: ['notes'] }) + ) + }) + + it('backs off only the local row of a moving repo', () => { + expect(isWslMoveRepoBlocked({ id: 'repo-1', path: '/a' })).toBe(true) + expect(isWslMoveRepoBlocked({ id: 'repo-1', path: '/a', ...onSsh })).toBe(false) + expect(isWslMoveRepoBlocked({ id: 'repo-2', path: '/a' })).toBe(false) + }) + + it('backs off only the local row of a moving folder workspace', () => { + expect(isWslMoveFolderWorkspaceBlocked({ id: 'notes', folderPath: '/n' })).toBe(true) + expect(isWslMoveFolderWorkspaceBlocked({ id: 'notes', folderPath: '/n', ...onSsh })).toBe(false) + expect(isWslMoveFolderWorkspaceBlocked({ id: 'other', folderPath: '/n' })).toBe(false) + }) + + it('backs off a worktree by its id and host', () => { + expect(isWslMoveWorktreeBlocked('repo-1::/a/wt', 'local')).toBe(true) + expect(isWslMoveWorktreeBlocked('repo-1::/a/wt', undefined)).toBe(true) + expect(isWslMoveWorktreeBlocked('repo-1::/a/wt', 'ssh:target-1')).toBe(false) + expect(isWslMoveWorktreeBlocked('repo-2::/a/wt', 'local')).toBe(false) + }) +}) + +it('backs off only WSL paths when the journal is unreadable', () => { + vi.spyOn(console, 'warn').mockImplementation(() => undefined) + const directory = wslMoveJournalDirectory(userDataPath) + mkdirSync(directory, { recursive: true }) + writeFileSync(join(directory, 'move-1.json'), '{not json') + loadWslMoveFence(userDataPath) + + expect(isWslMoveRepoBlocked({ id: 'repo-1', path: WSL_PATH })).toBe(true) + expect(isWslMoveRepoBlocked({ id: 'repo-1', path: 'C:\\src\\app' })).toBe(false) + expect(isWslMoveFolderWorkspaceBlocked({ id: 'notes', folderPath: WSL_PATH })).toBe(true) + expect(isWslMoveWorktreeBlocked(`repo-1::${WSL_PATH}\\wt`, 'local')).toBe(true) + expect(isWslMoveWorktreeBlocked('repo-1::C:\\src\\app\\wt', 'local')).toBe(false) +}) + +it('backs off a folder workspace at a WSL path when the journal is unreadable', () => { + vi.spyOn(console, 'warn').mockImplementation(() => undefined) + const directory = wslMoveJournalDirectory(userDataPath) + mkdirSync(directory, { recursive: true }) + writeFileSync(join(directory, 'move-1.json'), '{not json') + loadWslMoveFence(userDataPath) + + expect(isWslMoveWorktreeBlocked('folder:notes', 'local', WSL_PATH)).toBe(true) + expect(isWslMoveWorktreeBlocked('folder:notes', undefined, 'C:\\notes')).toBe(false) + expect(isWslMoveWorktreeBlocked('folder:notes', 'ssh:target-1', WSL_PATH)).toBe(false) +}) + +it('tells a move in flight from a project the WSL host owns', () => { + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) + expect(wslMoveRepoBackoff({ id: 'repo-1', path: '/a' })).toBe('moving') + expect(wslMoveRepoBackoff({ id: 'repo-2', path: '/a' })).toBeNull() + seedWslMoveCutover(userDataPath, cutover('move-1', 'destination-committed')) + expect(wslMoveRepoBackoff({ id: 'repo-1', path: '/a' })).toBe('owned') + expect(wslMoveRepoBackoff({ id: 'repo-1', path: '/a', ...onSsh })).toBeNull() +}) diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.ts b/src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.ts new file mode 100644 index 00000000000..2e0db24c67b --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.ts @@ -0,0 +1,71 @@ +/** + * Background writers (first-work renames, scan pruning, backfills) back off quietly from a project a + * WSL move is taking, or one the WSL host now owns: no throw, no data change. They are all periodic + * or event-driven, so they retry on their own once the move settles. + * + * Each check names the row's host: a move only takes local rows, and the same id on another host is + * a different project. The row's own path stands in for the store when the journal is unreadable. + */ +import { getRepoExecutionHostId } from '../../../shared/execution-host' +import { splitWorktreeIdForFilesystem } from '../../../shared/worktree/id' +import { + isLocalWslMoveRow, + isWslMoveProjectBlocked, + isWslMoveProjectFenced, + isWslMoveSourceHost, + wslMoveProjectRefForWorkspace +} from './wsl-move-scope-guard' + +type HostedRow = { connectionId?: string | null; executionHostId?: string | null } + +export type WslMoveRepoRow = HostedRow & { id: string; path: string } + +export type WslMoveFolderWorkspaceRow = HostedRow & { id: string; folderPath: string } + +function repoStore(repo: WslMoveRepoRow) { + return { getRepo: (id: string) => (id === repo.id ? repo : undefined) } +} + +export function isWslMoveRepoBlocked(repo: WslMoveRepoRow): boolean { + if (!isWslMoveSourceHost(getRepoExecutionHostId(repo))) { + return false + } + return isWslMoveProjectBlocked(repoStore(repo), { repoId: repo.id }) +} + +/** `moving` while a move is taking the repo, `owned` once the WSL host runs it, else null. */ +export function wslMoveRepoBackoff(repo: WslMoveRepoRow): 'moving' | 'owned' | null { + if (!isWslMoveRepoBlocked(repo)) { + return null + } + return isWslMoveProjectFenced(repoStore(repo), { repoId: repo.id }) ? 'moving' : 'owned' +} + +export function isWslMoveFolderWorkspaceBlocked(folder: WslMoveFolderWorkspaceRow): boolean { + if (!isLocalWslMoveRow(folder)) { + return false + } + const store = { + getRepo: () => undefined, + getFolderWorkspace: (id: string) => (id === folder.id ? folder : undefined) + } + return isWslMoveProjectBlocked(store, { folderWorkspaceId: folder.id }) +} + +/** + * For a worktree known only by id and host, such as a resolved worktree or its metadata row. + * A folder workspace key carries no path, so callers holding one pass its folder path. + */ +export function isWslMoveWorktreeBlocked( + worktreeId: string, + hostId: string | null | undefined, + worktreePath?: string +): boolean { + if (!isLocalWslMoveRow({ executionHostId: hostId })) { + return false + } + const path = worktreePath ?? splitWorktreeIdForFilesystem(worktreeId)?.worktreePath + const row = path === undefined ? undefined : { path, folderPath: path } + const store = { getRepo: () => row, getFolderWorkspace: () => row } + return isWslMoveProjectBlocked(store, wslMoveProjectRefForWorkspace(worktreeId)) +} diff --git a/src/main/repo-backfill-wsl-move.test.ts b/src/main/repo-backfill-wsl-move.test.ts new file mode 100644 index 00000000000..4c18b5c3659 --- /dev/null +++ b/src/main/repo-backfill-wsl-move.test.ts @@ -0,0 +1,127 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { getRepoExecutionHostId, type ExecutionHostId } from '../shared/execution-host' +import type { Repo } from '../shared/repo-types' +import { closeTestStores, createStore, testState } from './persistence-test-harness' +import { wslMoveCutoverFixture as cutover } from './persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from './persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + loadWslMoveFence, + resetWslMoveFenceForTests +} from './persistence/moving-wsl-projects/wsl-move-scope-guard' +import { probeGitRemoteIdentity } from './repo-git-remote-identity' +import { + enrichMissingRepoGitRemoteIdentities, + flushRepoGitRemoteIdentityEnrichmentForTests, + resetRepoGitRemoteIdentityEnrichmentForTests +} from './repo-git-remote-identity-enrichment' +import { RuntimeRepositoryForkBackfill } from './runtime/runtime-repository-fork-backfill' + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { isEncryptionAvailable: () => false } +})) +vi.mock('./repo-git-remote-identity', () => ({ probeGitRemoteIdentity: vi.fn() })) +vi.mock('./github/client', () => ({ + getRepoUpstream: vi.fn(async () => ({ owner: 'acme', repo: 'app' })) +})) +vi.mock('./repo-icon-autodetect', () => ({ detectGitHubAvatarIcon: vi.fn(async () => null) })) + +const MOVING = 'repo-1' +const STAYING = 'repo-2' + +function repo(id: string, overrides: Partial = {}): Repo { + return { + id, + path: `/ws/${id}`, + displayName: id, + badgeColor: '#000', + addedAt: 0, + kind: 'git', + ...overrides + } +} + +beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'wsl-move-backfill-')) + loadWslMoveFence(testState.dir) + seedWslMoveCutover(testState.dir, cutover('move-1', 'source-fenced')) + vi.mocked(probeGitRemoteIdentity).mockResolvedValue({ + status: 'resolved', + identity: { canonicalKey: 'github.com/acme/app', remoteName: 'origin', remoteUrl: 'u' } + }) +}) +afterEach(async () => { + resetWslMoveFenceForTests() + resetRepoGitRemoteIdentityEnrichmentForTests() + await closeTestStores() + rmSync(testState.dir, { recursive: true, force: true }) +}) + +describe('git remote identity enrichment under a WSL move', () => { + it('skips only the moving local repo, and retries it once the move settles', async () => { + const rows = [ + repo(MOVING), + repo(STAYING), + repo(MOVING, { connectionId: 'target-1', executionHostId: 'ssh:target-1' }) + ] + const store = { + getRepos: () => rows, + updateRepo: (id: string, updates: Partial, hostId?: ExecutionHostId) => { + const row = rows.find( + (candidate) => candidate.id === id && getRepoExecutionHostId(candidate) === hostId + ) + return row ? Object.assign(row, updates) : null + } + } + + enrichMissingRepoGitRemoteIdentities(store) + await flushRepoGitRemoteIdentityEnrichmentForTests() + + expect(rows.map((row) => row.gitRemoteIdentity?.canonicalKey)).toEqual([ + undefined, + 'github.com/acme/app', + 'github.com/acme/app' + ]) + expect(probeGitRemoteIdentity).not.toHaveBeenCalledWith( + '/ws/repo-1', + 'local', + expect.anything() + ) + + resetWslMoveFenceForTests() + enrichMissingRepoGitRemoteIdentities(store) + await flushRepoGitRemoteIdentityEnrichmentForTests() + expect(rows[0].gitRemoteIdentity?.canonicalKey).toBe('github.com/acme/app') + }) +}) + +describe('fork upstream backfill under a WSL move', () => { + it('leaves the moving repo unset for a later run and fills the rest', async () => { + const store = createStore() + store.addRepo(repo(MOVING)) + store.addRepo(repo(STAYING)) + + await new RuntimeRepositoryForkBackfill(() => store, vi.fn()).run() + + expect(store.getRepo(MOVING)?.upstream).toBeUndefined() + expect(store.getRepo(STAYING)?.upstream).toEqual({ owner: 'acme', repo: 'app' }) + }) + + it('fills the host row, not the hidden local row with the same id', async () => { + seedWslMoveCutover(testState.dir, cutover('move-1', 'destination-committed')) + const store = createStore() + store.addRepo(repo(MOVING)) + store.addRepo(repo(MOVING, { executionHostId: 'runtime:env-1' })) + + await new RuntimeRepositoryForkBackfill(() => store, vi.fn()).run() + + const rows = store.getRepos().filter((row) => row.id === MOVING) + expect(rows.map((row) => [getRepoExecutionHostId(row), row.upstream])).toEqual([ + ['local', undefined], + ['runtime:env-1', { owner: 'acme', repo: 'app' }] + ]) + }) +}) diff --git a/src/main/repo-git-remote-identity-enrichment.ts b/src/main/repo-git-remote-identity-enrichment.ts index f1cea725efc..e222c33d9a9 100644 --- a/src/main/repo-git-remote-identity-enrichment.ts +++ b/src/main/repo-git-remote-identity-enrichment.ts @@ -10,6 +10,7 @@ import { isUnresolvedSshHostAlias } from '../shared/git-remote-host-alias' import { getProjectProviderIdentity } from '../shared/project-host-setup-projection' import { getStoredRepoExecutionHostId } from './repo-execution-host' import { probeGitRemoteIdentity } from './repo-git-remote-identity' +import { isWslMoveRepoBlocked } from './persistence/moving-wsl-projects/wsl-move-background-backoff' const NO_IDENTITY_RETRY_TTL_MS = 5 * 60 * 1000 // Why 6h: a resolved identity only changes when a remote is added or the project is renamed or @@ -158,7 +159,8 @@ function writeIdentity( async function enrichRepoGitRemoteIdentity(store: RepoIdentityStore, repo: Repo): Promise { const hostId = getRepoProbeHostId(repo) - if (!hostId) { + // No probe and no deadline: the next sweep after the move picks it up. + if (!hostId || isWslMoveRepoBlocked(repo)) { return false } const locationKey = getRepoLocationKey(repo) @@ -186,6 +188,11 @@ async function enrichRepoGitRemoteIdentity(store: RepoIdentityStore, repo: Repo) if (controller.signal.aborted) { return false } + // A move started during the probe: drop the answer and its deadline so the next sweep retries. + if (isWslMoveRepoBlocked(repo)) { + probeRetryAfterByLocation.delete(locationKey) + return false + } if (result.status !== 'resolved') { // Why: repos without a parseable remote are common; cache misses briefly so // list calls stay cheap while still allowing recent remote changes to land. diff --git a/src/main/runtime/orca-runtime-file-commands.ts b/src/main/runtime/orca-runtime-file-commands.ts index b40e7f61367..fc9a386b3d1 100644 --- a/src/main/runtime/orca-runtime-file-commands.ts +++ b/src/main/runtime/orca-runtime-file-commands.ts @@ -28,6 +28,7 @@ import { } from './files/chat-scratch-file-root' import type { RuntimeStore } from './runtime-store-contract' +import { isWslMoveWorktreeBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' import type { ResolvedWorktree } from './runtime-worktree-path-identity' import type { Repo } from '../../shared/repo-types' @@ -151,6 +152,10 @@ export abstract class OrcaRuntimeWithFileCommands extends OrcaRuntimeWithPreserv if (!store?.setWorktreeMeta) { return } + // Bookkeeping incidental to the push: a project a WSL move is taking skips it, not the push. + if (isWslMoveWorktreeBlocked(worktreeId, store.getWorktreeMeta?.(worktreeId)?.hostId)) { + return + } store.setWorktreeMeta(worktreeId, { pushTarget }) } }) diff --git a/src/main/runtime/repo-icon-fork-backfill.test.ts b/src/main/runtime/repo-icon-fork-backfill.test.ts index b367fed0bb3..20a4475b9d8 100644 --- a/src/main/runtime/repo-icon-fork-backfill.test.ts +++ b/src/main/runtime/repo-icon-fork-backfill.test.ts @@ -54,15 +54,19 @@ describe('startup fork-upstream backfill', () => { await (runtime as unknown as BackfillInternals).repositoryForkBackfill.run() - expect(updateRepo).toHaveBeenCalledExactlyOnceWith('repo-1', { - upstream: { owner: 'upstream-org', repo: 'rocket' }, - repoIcon: { - type: 'image', - src: 'https://github.com/acme.png?size=64', - source: 'github', - label: 'acme/rocket-pro' - } - }) + expect(updateRepo).toHaveBeenCalledExactlyOnceWith( + 'repo-1', + { + upstream: { owner: 'upstream-org', repo: 'rocket' }, + repoIcon: { + type: 'image', + src: 'https://github.com/acme.png?size=64', + source: 'github', + label: 'acme/rocket-pro' + } + }, + 'local' + ) }) it('migrates a same-name fork to the upstream owner avatar', async () => { @@ -83,15 +87,19 @@ describe('startup fork-upstream backfill', () => { await (runtime as unknown as BackfillInternals).repositoryForkBackfill.run() - expect(updateRepo).toHaveBeenCalledExactlyOnceWith('repo-1', { - upstream: { owner: 'upstream-org', repo: 'rocket' }, - repoIcon: { - type: 'image', - src: 'https://github.com/upstream-org.png?size=64', - source: 'github', - label: 'upstream-org/rocket' - } - }) + expect(updateRepo).toHaveBeenCalledExactlyOnceWith( + 'repo-1', + { + upstream: { owner: 'upstream-org', repo: 'rocket' }, + repoIcon: { + type: 'image', + src: 'https://github.com/upstream-org.png?size=64', + source: 'github', + label: 'upstream-org/rocket' + } + }, + 'local' + ) }) it('skips every row whose files sit on an SSH host', async () => { @@ -137,8 +145,12 @@ describe('startup fork-upstream backfill', () => { resolveSlug({ owner: 'acme', repo: 'rocket-pro' }) await backfill - expect(updateRepo).toHaveBeenCalledExactlyOnceWith('repo-1', { - upstream: { owner: 'upstream-org', repo: 'rocket' } - }) + expect(updateRepo).toHaveBeenCalledExactlyOnceWith( + 'repo-1', + { + upstream: { owner: 'upstream-org', repo: 'rocket' } + }, + 'local' + ) }) }) diff --git a/src/main/runtime/runtime-linear-team-write-commands.ts b/src/main/runtime/runtime-linear-team-write-commands.ts index 97655e68ef2..15926c19ab8 100644 --- a/src/main/runtime/runtime-linear-team-write-commands.ts +++ b/src/main/runtime/runtime-linear-team-write-commands.ts @@ -15,6 +15,7 @@ import type { } from './runtime-linear-command-dependencies' import type { ResolvedWorktree } from './runtime-worktree-path-identity' import { RuntimeLinearDedupeCommands } from './runtime-linear-dedupe-commands' +import { isWslMoveWorktreeBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' export class RuntimeLinearTeamWriteCommands extends RuntimeLinearDedupeCommands { public parseLinearAttachmentUrl(value: string): URL { @@ -268,10 +269,13 @@ export class RuntimeLinearTeamWriteCommands extends RuntimeLinearDedupeCommands worktree.linkedLinearIssueOrganizationUrlKey ) if (backfill?.workspaceId) { - this.setWorktreeMeta(worktree.id, { - linkedLinearIssueWorkspaceId: backfill.workspaceId, - linkedLinearIssueOrganizationUrlKey: backfill.organizationUrlKey ?? null - }) + // The answer still serves this lookup; persisting it waits out a WSL move. + if (!isWslMoveWorktreeBlocked(worktree.id, worktree.hostId, worktree.path)) { + this.setWorktreeMeta(worktree.id, { + linkedLinearIssueWorkspaceId: backfill.workspaceId, + linkedLinearIssueOrganizationUrlKey: backfill.organizationUrlKey ?? null + }) + } return { ...link, workspaceId: backfill.workspaceId, diff --git a/src/main/runtime/runtime-repository-fork-backfill.ts b/src/main/runtime/runtime-repository-fork-backfill.ts index df9d769f167..4bbee92e640 100644 --- a/src/main/runtime/runtime-repository-fork-backfill.ts +++ b/src/main/runtime/runtime-repository-fork-backfill.ts @@ -1,9 +1,14 @@ -import { getRepoSshConnectionId, LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' +import { + getRepoExecutionHostId, + getRepoSshConnectionId, + LOCAL_EXECUTION_HOST_ID +} from '../../shared/execution-host' import type { GitHubOwnerRepo } from '../../shared/github/pull-request-types' import type { Repo } from '../../shared/repo-types' import { getRepoUpstream } from '../github/client' import { detectGitHubAvatarIcon } from '../repo-icon-autodetect' import type { RuntimeStore } from './runtime-store-contract' +import { isWslMoveRepoBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' export class RuntimeRepositoryForkBackfill { private started = false @@ -32,7 +37,13 @@ export class RuntimeRepositoryForkBackfill { // Why the resolved SSH target and not the raw `connectionId`: this backfill runs `gh`/git // in this process, so any row whose files sit on an SSH host must be skipped — including // one that carries only `executionHostId: ssh:…`, which the raw field reads as local. - if (repo.upstream !== undefined || repo.kind === 'folder' || getRepoSshConnectionId(repo)) { + // A repo a WSL move is taking keeps `upstream` unset, so the next startup retries it. + if ( + repo.upstream !== undefined || + repo.kind === 'folder' || + getRepoSshConnectionId(repo) || + isWslMoveRepoBlocked(repo) + ) { continue } let upstream: GitHubOwnerRepo | null @@ -45,8 +56,14 @@ export class RuntimeRepositoryForkBackfill { upstream && repo.repoIcon?.type === 'image' && repo.repoIcon.source === 'github' ? await detectGitHubAvatarIcon(repo.path, LOCAL_EXECUTION_HOST_ID, upstream) : null - const current = store.getRepos().find((candidate) => candidate.id === repo.id) - if (!current || current.upstream !== undefined) { + // By id and host: a WSL move leaves a hidden local row beside the host's same-id row. + const hostId = getRepoExecutionHostId(repo) + const current = store + .getRepos() + .find( + (candidate) => candidate.id === repo.id && getRepoExecutionHostId(candidate) === hostId + ) + if (!current || current.upstream !== undefined || isWslMoveRepoBlocked(current)) { continue } const updates: Partial = { upstream: upstream ?? null } @@ -57,7 +74,7 @@ export class RuntimeRepositoryForkBackfill { ) { updates.repoIcon = repoIcon } - store.updateRepo(repo.id, updates) + store.updateRepo(repo.id, updates, hostId) changed = true } if (changed) { diff --git a/src/main/runtime/runtime-worktree-filesystem.ts b/src/main/runtime/runtime-worktree-filesystem.ts index 59955e0dfe8..dfde4538a86 100644 --- a/src/main/runtime/runtime-worktree-filesystem.ts +++ b/src/main/runtime/runtime-worktree-filesystem.ts @@ -18,6 +18,7 @@ import { mergeRuntimeFolderWorkspace } from './runtime-folder-workspace' import type { RuntimeStore } from './runtime-store-contract' +import { isWslMoveRepoBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' /** Callers hold a local removal route; an `ssh:` or `runtime:` host is refused before this. */ export async function isLocalRuntimeWorktreePathMissing( @@ -44,6 +45,8 @@ export function listRuntimeFolderWorkspaces( const rootId = getRuntimeFolderWorkspaceRootId(repo) const allMeta = store.getAllWorktreeMeta() const expectedHostId = getRepoExecutionHostId(repo) + // A project a WSL move is taking still lists, without stamping identities it lacks. + const canWrite = !isWslMoveRepoBlocked(repo) const ids = Object.keys(allMeta).filter( (worktreeId) => isRuntimeFolderWorkspaceIdForRepo(repo, worktreeId) && @@ -58,12 +61,12 @@ export function listRuntimeFolderWorkspaces( const existing = getRepoOwnedWorktreeMeta(repo, worktreeId, allMeta, repoOwnerCount) const meta: Partial = existing?.instanceId ? existing - : existing || repoOwnerCount === 1 + : canWrite && (existing || repoOwnerCount === 1) ? store.setWorktreeMeta(worktreeId, { instanceId: getRuntimeFolderWorkspaceInstanceIdentity(repo, worktreeId), ...(existing ? {} : { displayName: repo.displayName, lastActivityAt: Date.now() }) }) - : {} + : (existing ?? {}) return { ...mergeRuntimeFolderWorkspace(repo, worktreeId, meta), hostId: repoOwnerCount === 1 ? (meta.hostId ?? expectedHostId) : expectedHostId diff --git a/src/main/runtime/runtime-worktree-lineage-controller.ts b/src/main/runtime/runtime-worktree-lineage-controller.ts index 93360359fba..de48270a13b 100644 --- a/src/main/runtime/runtime-worktree-lineage-controller.ts +++ b/src/main/runtime/runtime-worktree-lineage-controller.ts @@ -10,6 +10,7 @@ import { sharesResolvedWorktreeLineageBoundary } from '../../shared/resolved-wor import type { OrchestrationDb } from './orchestration/db' import type { RuntimeStore } from './runtime-store-contract' import type { ResolvedWorktree } from './runtime-worktree-path-identity' +import { isWslMoveWorktreeBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' import { RuntimeLineageError, enforceCreateLineageHostBoundary, @@ -154,7 +155,11 @@ export class RuntimeWorktreeLineageController { return } for (const worktree of await this.deps.listResolvedWorktrees()) { - if (store.getWorktreeLineage(worktree.id) || !worktree.instanceId) { + if ( + store.getWorktreeLineage(worktree.id) || + !worktree.instanceId || + isWslMoveWorktreeBlocked(worktree.id, worktree.hostId, worktree.path) + ) { continue } const taskId = worktree.comment?.match(/\btask_[A-Za-z0-9]+\b/)?.[0] diff --git a/src/main/worktree-lineage-pruning.ts b/src/main/worktree-lineage-pruning.ts index fc0068e3779..0e27d98c550 100644 --- a/src/main/worktree-lineage-pruning.ts +++ b/src/main/worktree-lineage-pruning.ts @@ -7,6 +7,7 @@ import { isWorkspaceKey, parseWorkspaceKey, worktreeWorkspaceKey } from '../shar import { splitWorktreeId } from '../shared/worktree/id' import { worktreeRetentionPathComparisonKey } from './worktree-retention-path-comparison' import type { Store } from './persistence' +import { isWslMoveRepoBlocked } from './persistence/moving-wsl-projects/wsl-move-background-backoff' type WorktreeLineagePruningStore = Pick & Partial< @@ -57,7 +58,9 @@ export function pruneLineageForMissingRepoWorktrees( } = {} ): void { const { platform = process.platform, preservedMetadataCandidateIds } = options + // Mid-move a scan can miss paths that still exist; lineage waits for the move to settle. if ( + isWslMoveRepoBlocked(repo) || typeof store.getAllWorktreeLineage !== 'function' || typeof store.removeWorktreeLineage !== 'function' ) { diff --git a/src/main/wsl-move-incidental-writes.test.ts b/src/main/wsl-move-incidental-writes.test.ts new file mode 100644 index 00000000000..58418f1caa8 --- /dev/null +++ b/src/main/wsl-move-incidental-writes.test.ts @@ -0,0 +1,148 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { GitPushTarget } from '../shared/worktree/types' +import type { ExecutionHostId } from '../shared/execution-host' +import { mergeWorktreeMetaForWrite } from './persistence/loading-store/worktree-meta-write-normalization' +import { wslMoveCutoverFixture as cutover } from './persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from './persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + loadWslMoveFence, + resetWslMoveFenceForTests +} from './persistence/moving-wsl-projects/wsl-move-scope-guard' +import type * as LinearCommandDependencies from './runtime/runtime-linear-command-dependencies' +import type { ResolvedWorktree } from './runtime/runtime-worktree-path-identity' + +const { gitExecFileAsyncMock } = vi.hoisted(() => ({ gitExecFileAsyncMock: vi.fn() })) +vi.mock('./git/runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock })) +vi.mock('./runtime/runtime-linear-command-dependencies', async (importOriginal) => ({ + ...(await importOriginal()), + resolveLegacyLinearLinkWorkspace: () => ({ workspaceId: 'ws-1', organizationUrlKey: 'acme' }) +})) + +import { materializeWorktreePushTargetRemote } from './ipc/worktree-remote' +import { RuntimeLinearTeamWriteCommands } from './runtime/runtime-linear-team-write-commands' + +const MOVING_WT = 'repo-1::/ws/repo-1/wt' +const STAYING_WT = 'repo-2::/ws/repo-2/wt' + +let userDataPath: string +beforeEach(() => { + userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-incidental-')) + loadWslMoveFence(userDataPath) + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) + gitExecFileAsyncMock.mockReset() + gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { + if (args[0] === 'remote' && args[1] === 'get-url') { + throw new Error('No such remote') + } + return { stdout: '', stderr: '' } + }) +}) +afterEach(() => { + resetWslMoveFenceForTests() + rmSync(userDataPath, { recursive: true, force: true }) +}) + +describe('push-target bookkeeping under a WSL move', () => { + const target: GitPushTarget = { + remoteName: 'pr-contributor-orca', + branchName: 'contributor/fix', + remoteUrl: 'git@github.com:contributor/orca.git' + } + + async function push(worktreeId: string, hostId?: ExecutionHostId) { + const setWorktreeMeta = vi.fn() + const meta = hostId ? { [worktreeId]: mergeWorktreeMetaForWrite(undefined, { hostId }) } : {} + const store = { getAllWorktreeMeta: () => meta, setWorktreeMeta } + const result = await materializeWorktreePushTargetRemote( + '/ws/repo', + target, + store, + undefined, + {}, + worktreeId + ) + return { result, setWorktreeMeta } + } + + it('still pushes the moving worktree but skips its metadata write', async () => { + const { result, setWorktreeMeta } = await push(MOVING_WT) + expect(result).toEqual({ ...target, remoteCreated: true }) + expect(setWorktreeMeta).not.toHaveBeenCalled() + }) + + it.each<[string, string, ExecutionHostId | undefined]>([ + ['a project the move is not taking', STAYING_WT, undefined], + ['the same repo id on another host', MOVING_WT, 'ssh:target-1'] + ])('records the push target for %s', async (_label, worktreeId, hostId) => { + const { setWorktreeMeta } = await push(worktreeId, hostId) + expect(setWorktreeMeta).toHaveBeenCalledWith(worktreeId, { + pushTarget: { ...target, remoteCreated: true } + }) + }) +}) + +describe('Linear workspace backfill under a WSL move', () => { + function worktree(id: string, hostId: ExecutionHostId): ResolvedWorktree { + const path = id.slice(id.indexOf('::') + 2) + return { + id, + repoId: id.slice(0, id.indexOf('::')), + hostId, + path, + head: 'abc', + branch: 'refs/heads/main', + isBare: false, + isMainWorktree: false, + displayName: 'wt', + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: 'ACME-1', + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0, + parentWorktreeId: null, + childWorktreeIds: [], + lineage: null, + git: { path, head: 'abc', branch: 'refs/heads/main', isBare: false, isMainWorktree: false } + } + } + + async function resolveCurrent(row: ResolvedWorktree) { + const setWorktreeMeta = vi.fn() + const commands = new RuntimeLinearTeamWriteCommands({ + runtimeAvailable: () => true, + showTerminal: async () => { + throw new Error('unused') + }, + resolveWorktreeSelector: async () => row, + listResolvedWorktrees: async () => [row], + setWorktreeMeta, + emitClientEvent: () => {} + }) + const link = await commands.linearResolveCurrentIssue({ cwd: row.path }) + return { link, setWorktreeMeta } + } + + it('answers for the moving worktree without persisting the backfill', async () => { + const { link, setWorktreeMeta } = await resolveCurrent(worktree(MOVING_WT, 'local')) + expect(link.workspaceId).toBe('ws-1') + expect(setWorktreeMeta).not.toHaveBeenCalled() + }) + + it.each<[string, string, ExecutionHostId]>([ + ['a project the move is not taking', STAYING_WT, 'local'], + ['the same repo id on another host', MOVING_WT, 'ssh:target-1'] + ])('persists the backfill for %s', async (_label, id, hostId) => { + const { setWorktreeMeta } = await resolveCurrent(worktree(id, hostId)) + expect(setWorktreeMeta).toHaveBeenCalledWith(id, { + linkedLinearIssueWorkspaceId: 'ws-1', + linkedLinearIssueOrganizationUrlKey: 'acme' + }) + }) +}) diff --git a/src/main/wsl-move-scan-pruning.test.ts b/src/main/wsl-move-scan-pruning.test.ts new file mode 100644 index 00000000000..c1575185260 --- /dev/null +++ b/src/main/wsl-move-scan-pruning.test.ts @@ -0,0 +1,237 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { Repo } from '../shared/repo-types' +import type { ExecutionHostId } from '../shared/execution-host' +import type { WorktreeLineage } from '../shared/worktree/lineage-types' +import type { GitWorktreeInfo } from '../shared/worktree/types' +import { worktreeWorkspaceKey } from '../shared/workspace-scope' +import { closeTestStores, createStore, testState } from './persistence-test-harness' +import type { Store } from './persistence' +import { wslMoveCutoverFixture as cutover } from './persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from './persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + loadWslMoveFence, + resetWslMoveFenceForTests +} from './persistence/moving-wsl-projects/wsl-move-scope-guard' +import { pruneLineageForMissingRepoWorktrees } from './worktree-lineage-pruning' +import { pruneMetadataMissingFromAuthoritativeLocalScan } from './ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning' +import { listFolderWorkspaces } from './ipc/worktrees/listing/folder-workspace-catalog' +import { getFolderWorkspaceRootId } from './ipc/worktrees/folder-workspace-model' +import { getLocalWorktreeScanGeneration } from './local-worktree-scan-generation' +import { listRuntimeFolderWorkspaces } from './runtime/runtime-worktree-filesystem' +import { getRuntimeFolderWorkspaceRootId } from './runtime/runtime-folder-workspace' +import { RuntimeWorktreeLineageController } from './runtime/runtime-worktree-lineage-controller' +import type { ResolvedWorktree } from './runtime/runtime-worktree-path-identity' + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { isEncryptionAvailable: () => false } +})) + +const MOVING = 'repo-1' +const STAYING = 'repo-2' + +function repo(id: string, overrides: Partial = {}): Repo { + return { id, path: `/ws/${id}`, displayName: id, badgeColor: '#000', addedAt: 0, ...overrides } +} +const onSsh: Partial = { connectionId: 'target-1', executionHostId: 'ssh:target-1' } + +function gitRow(path: string): GitWorktreeInfo { + return { path, head: 'abc', branch: 'refs/heads/main', isBare: false, isMainWorktree: false } +} + +function lineage(childId: string, parentId: string): WorktreeLineage { + return { + worktreeId: childId, + worktreeInstanceId: `${childId}#instance`, + parentWorktreeId: parentId, + parentWorktreeInstanceId: `${parentId}#instance`, + origin: 'manual', + capture: { source: 'manual-action', confidence: 'explicit' }, + createdAt: 1 + } +} + +let store: Store +beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'wsl-move-scan-')) + store = createStore() + store.addRepo(repo(MOVING)) + store.addRepo(repo(STAYING)) + loadWslMoveFence(testState.dir) + seedWslMoveCutover(testState.dir, cutover('move-1', 'source-fenced')) +}) +afterEach(async () => { + resetWslMoveFenceForTests() + await closeTestStores() + rmSync(testState.dir, { recursive: true, force: true }) +}) + +describe('lineage pruning under a WSL move', () => { + const pruneMissingChild = (target: Repo): WorktreeLineage | undefined => { + const childId = `${target.id}::/ws/${target.id}/gone` + store.setWorktreeLineage(childId, lineage(childId, `${target.id}::${target.path}`)) + pruneLineageForMissingRepoWorktrees(store, target, [gitRow(target.path)]) + return store.getWorktreeLineage(childId) + } + + it('keeps the moving project lineage a mid-move scan cannot see', () => { + expect(pruneMissingChild(repo(MOVING))).toBeDefined() + }) + + it('prunes a project the move is not taking', () => { + expect(pruneMissingChild(repo(STAYING))).toBeUndefined() + }) + + it('prunes the same repo id on another host', () => { + expect(pruneMissingChild(repo(MOVING, onSsh))).toBeUndefined() + }) +}) + +describe('authoritative metadata pruning under a WSL move', () => { + const pruneMissingRow = async (repoId: string): Promise => { + const target = repo(repoId) + const goneId = `${repoId}::/ws/${repoId}/gone` + store.setWorktreeMeta(goneId, { displayName: 'gone' }) + const result = await pruneMetadataMissingFromAuthoritativeLocalScan({ + store, + repo: target, + gitWorktrees: [gitRow(target.path)], + scan: store.captureNativeLocalWorktreeMetadataScanExpectation(target), + scanGeneration: getLocalWorktreeScanGeneration(repoId), + platform: 'linux', + pathsExistOrAreUnverifiable: async (paths) => new Map(paths.map((path) => [path, false])) + }) + return [...result.removedWorktreeIds] + } + + it('removes nothing from the moving project', async () => { + expect(await pruneMissingRow(MOVING)).toEqual([]) + expect(store.getWorktreeMeta(`${MOVING}::/ws/${MOVING}/gone`)).toBeDefined() + }) + + it('prunes a project the move is not taking', async () => { + expect(await pruneMissingRow(STAYING)).toEqual([`${STAYING}::/ws/${STAYING}/gone`]) + }) +}) + +describe('folder catalog identity stamps under a WSL move', () => { + it('lists the moving folder project without writing its metadata', () => { + const folder = repo(MOVING, { kind: 'folder' }) + expect(listFolderWorkspaces(store, folder).map((row) => row.id)).toEqual([ + getFolderWorkspaceRootId(folder) + ]) + expect(store.getWorktreeMeta(getFolderWorkspaceRootId(folder))).toBeUndefined() + + const runtimeRows = listRuntimeFolderWorkspaces(store, folder) + expect(runtimeRows).toHaveLength(1) + expect(store.getWorktreeMeta(getRuntimeFolderWorkspaceRootId(folder))).toBeUndefined() + }) + + it('gives the moving root row the same identity on every listing', () => { + const folder = repo(MOVING, { kind: 'folder' }) + const first = listFolderWorkspaces(store, folder)[0]?.instanceId + const second = listFolderWorkspaces(store, folder)[0]?.instanceId + expect(first).toBeTruthy() + expect(second).toBe(first) + expect(store.getWorktreeMeta(getFolderWorkspaceRootId(folder))).toBeUndefined() + }) + + it.each([ + ['a project the move is not taking', repo(STAYING, { kind: 'folder' })], + ['the same repo id on another host', repo(MOVING, { kind: 'folder', ...onSsh })] + ])('stamps %s', (_label, folder) => { + listFolderWorkspaces(store, folder) + expect(store.getWorktreeMeta(getFolderWorkspaceRootId(folder))?.instanceId).toBeTruthy() + }) + + it('stamps a runtime folder project the move is not taking', () => { + const folder = repo(STAYING, { kind: 'folder' }) + listRuntimeFolderWorkspaces(store, folder) + expect(store.getWorktreeMeta(getRuntimeFolderWorkspaceRootId(folder))?.instanceId).toBeTruthy() + }) +}) + +describe('inferred lineage hydration under a WSL move', () => { + function resolved(repoId: string, name: string, hostId: ExecutionHostId, taskId?: string) { + const path = `/ws/${repoId}/${name}` + const worktree: ResolvedWorktree = { + id: `${repoId}::${path}`, + repoId, + instanceId: `${repoId}-${hostId}-${name}#instance`, + hostId, + path, + head: 'abc', + branch: `refs/heads/${name}`, + isBare: false, + isMainWorktree: false, + displayName: name, + comment: taskId ? `working on ${taskId}` : '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0, + parentWorktreeId: null, + childWorktreeIds: [], + lineage: null, + git: { path, head: 'abc', branch: `refs/heads/${name}`, isBare: false, isMainWorktree: false } + } + return worktree + } + + it('infers lineage only outside the moving local project', async () => { + const children = [ + resolved(MOVING, 'child', 'local', 'task_moving'), + resolved(STAYING, 'child', 'local', 'task_staying'), + resolved(MOVING, 'child-remote', 'ssh:target-1', 'task_remote') + ] + const parents = new Map([ + ['task_moving', resolved(MOVING, 'parent', 'local')], + ['task_staying', resolved(STAYING, 'parent', 'local')], + ['task_remote', resolved(MOVING, 'parent-remote', 'ssh:target-1')] + ]) + const controller = new RuntimeWorktreeLineageController({ + getStore: () => store, + getCachedWorktrees: () => null, + getDb: () => null, + resolveWorktree: async () => { + throw new Error('unused') + }, + listResolvedWorktrees: async () => children, + showTerminal: async () => { + throw new Error('unused') + } + }) + vi.spyOn(controller, 'resolveTaskCandidate').mockImplementation(async (taskId) => { + const parent = parents.get(taskId) + return parent + ? { + source: 'orchestration-context', + taskId, + parent: { + type: 'worktree', + workspaceKey: worktreeWorkspaceKey(parent.id), + worktree: parent, + instanceId: parent.instanceId ?? null + } + } + : null + }) + + await controller.hydrate() + + expect(store.getWorktreeLineage(children[0].id)).toBeUndefined() + expect(store.getWorktreeLineage(children[1].id)?.parentWorktreeId).toBe( + parents.get('task_staying')?.id + ) + expect(store.getWorktreeLineage(children[2].id)?.parentWorktreeId).toBe( + parents.get('task_remote')?.id + ) + }) +}) From 63780c6fd2c75ec3d3c1d88f530d4b97f223a4a2 Mon Sep 17 00:00:00 2001 From: "Sojiro.O" <53679703+sojiro-o@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:13:56 +0900 Subject: [PATCH 05/53] fix(i18n): correct the Japanese label for the Cleaned browser identity (#28526) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Japanese option read 掃除済み ("housekeeping done"), while the migration notice that points to the same setting already calls it クリーン. Use クリーン in the option and its settings-search keyword. Also restore the literal CLI command in the reset hint, which translation had re-capitalised in ja, zh, and fr so it could not be copied and run. Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> --- src/renderer/src/i18n/locales/fr.json | 2 +- src/renderer/src/i18n/locales/ja.json | 6 +++--- src/renderer/src/i18n/locales/zh.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 8f87352b8c8..136ab58d224 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -272,7 +272,7 @@ "remoteUnsupported": "Gérez l'identité du navigateur sur l'hôte distant avec la CLI Orca.", "loading": "Chargement…", "resetRequired": "Les données d'identité doivent être réinitialisées explicitement avant de pouvoir être modifiées.", - "resetRequiredCommand": "Réinitialisez-le à partir de la ligne de commande : orca Browser Identity Set --mode --reset", + "resetRequiredCommand": "Réinitialisez-le à partir de la ligne de commande : orca browser identity set --mode --reset", "optionClean": "Nettoyé", "optionCleanTooltip": "Supprime les jetons Orca et Electron pour correspondre aux sessions Chrome importées.", "optionNative": "Indigène", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 1f44d2a09f6..0902e56df6e 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -168,8 +168,8 @@ "remoteUnsupported": "Orca CLI を使用して、リモートホスト上のブラウザ ID を管理します。", "loading": "読み込み中…", "resetRequired": "ID データは変更する前に明示的にリセットする必要があります。", - "resetRequiredCommand": "コマンドラインからリセットします: orca Browser Identity set --mode --reset", - "optionClean": "掃除済み", + "resetRequiredCommand": "コマンドラインからリセットします: orca browser identity set --mode --reset", + "optionClean": "クリーン", "optionCleanTooltip": "インポートされた Chrome セッションと一致するように Orca トークンと Electron トークンを削除します。", "optionNative": "ネイティブ", "optionNativeTooltip": "クリーンアップされた ID を拒否するサイトのために Electron の組み込み ID を保持します。 Google サインインはネイティブモードでは利用できません。", @@ -9568,7 +9568,7 @@ "identity": "身元", "userAgent": "ユーザー Agent", "native": "ネイティブ", - "cleaned": "掃除された", + "cleaned": "クリーン", "restart": "再起動" } }, diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 0fd3acacbab..566ad89480e 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -168,7 +168,7 @@ "remoteUnsupported": "使用 Orca CLI 管理远程主机上的浏览器身份。", "loading": "加载中…", "resetRequired": "身份数据必须在更改之前显式重置。", - "resetRequiredCommand": "从命令行重置它:orca browser Identity set --mode --reset", + "resetRequiredCommand": "从命令行重置它:orca browser identity set --mode --reset", "optionClean": "已清理", "optionCleanTooltip": "删除 Orca 和 Electron 令牌以匹配导入的 Chrome 会话。", "optionNative": "原生", From a93a23657719abecb901e957d56f736e6b71e60a Mon Sep 17 00:00:00 2001 From: "Sojiro.O" <53679703+sojiro-o@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:14:09 +0900 Subject: [PATCH 06/53] =?UTF-8?q?fix(i18n):=20use=20=E3=82=AF=E3=83=AA?= =?UTF-8?q?=E3=83=BC=E3=83=B3=E3=82=A2=E3=83=83=E3=83=97=20in=20the=20Japa?= =?UTF-8?q?nese=20VM=20cleanup=20stop=20dialog=20(#28533)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dialog title and cancel button said 掃除 (household cleaning), while its own description and confirm button already say クリーンアップ. Use クリーンアップ throughout. Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> --- src/renderer/src/i18n/locales/ja.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 0902e56df6e..547aa7b3f8b 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -12283,9 +12283,9 @@ } }, "EphemeralVmCleanupStopDialog": { - "title": "掃除をやめますか?", + "title": "クリーンアップを停止しますか?", "description": "VM は実行されたままになり、料金が発生する可能性があります。後でクリーンアップを再試行できます。", - "cancel": "掃除を続ける", + "cancel": "クリーンアップを続ける", "stopping": "停止中…", "confirm": "クリーンアップを停止する" }, From c8f080764b1d764e37933c56bc321488b8a2bdf5 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:15:46 -0400 Subject: [PATCH 07/53] refactor(terminal): publish new tabs together with their startup command (#28848) Seven call sites created a terminal tab and then queued its startup command in a second store write. Pass the command through createTab's existing pendingStartup option instead, so the tab and its command land in one write. Co-authored-by: Claude Opus 5.5 --- .../ipc-events/terminal-request-ipc-bridge.ts | 39 +++++++++-------- ...pcEvents-terminal-create-surfacing.test.ts | 25 ++++++----- .../lib/agent-launch-caller-placement.test.ts | 16 +++---- .../lib/agent-launch-caller-routing.test.ts | 12 +++--- .../lib/agent-launch-funnel-test-harness.ts | 14 +++---- ...h-agent-in-new-tab-host-resolution.test.ts | 8 ++-- .../launch-agent-in-new-tab-placement.test.ts | 11 +++-- ...h-agent-in-new-tab-windows-quoting.test.ts | 29 ++++++------- .../src/lib/launch-agent-in-new-tab.test.ts | 42 ++++++++++--------- .../src/lib/launch-agent-in-new-tab.ts | 39 +++++++++-------- .../src/lib/launch-ai-vault-session.test.ts | 23 ++++++---- .../src/lib/launch-ai-vault-session.ts | 26 ++++++------ .../lib/run-quick-command-in-new-tab.test.ts | 17 ++++---- .../src/lib/run-quick-command-in-new-tab.ts | 7 +--- .../worktree-activation-agent-startup.test.ts | 19 ++++++--- .../worktree-activation-default-tabs.test.ts | 33 ++++++++------- .../worktree-activation-issue-command.test.ts | 7 ++-- .../lib/worktree-activation-reveal.test.ts | 2 - .../worktree-activation-setup-script.test.ts | 41 ++++++++---------- .../lib/worktree-activation-store-contract.ts | 27 ++++++------ ...activation-structured-chat-surface.test.ts | 5 ++- .../lib/worktree-activation-test-harness.ts | 22 ++++++++-- .../worktree-activation-web-runtime.test.ts | 9 ++-- .../src/lib/worktree-default-terminal-tabs.ts | 27 +++++------- .../lib/worktree-initial-terminal-seeding.ts | 21 ++++------ .../lib/worktree-setup-issue-command-queue.ts | 4 +- 26 files changed, 271 insertions(+), 254 deletions(-) diff --git a/src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts b/src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts index 530846c08bc..64898b7ef13 100644 --- a/src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts +++ b/src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts @@ -62,7 +62,28 @@ export function registerTerminalRequestIpcBridge(unsubs: (() => void)[]): void { recordInteraction: false, ...(data.cwd ? { startupCwd: data.cwd } : {}) } - const tab = store.createTab(worktreeId, data.targetGroupId, data.shellOverride, tabOptions) + const pendingStartup = data.command + ? { + command: data.command, + ...(data.env ? { env: data.env } : {}), + ...(data.envToDelete ? { envToDelete: data.envToDelete } : {}), + ...(data.launchConfig ? { launchConfig: data.launchConfig } : {}), + ...(data.resumeProviderSession + ? { resumeProviderSession: data.resumeProviderSession } + : {}), + ...(data.launchToken ? { launchToken: data.launchToken } : {}), + ...(data.launchAgent ? { launchAgent: data.launchAgent } : {}), + ...(data.startupCommandDelivery + ? { startupCommandDelivery: data.startupCommandDelivery } + : {}) + } + : undefined + const tab = store.createTab( + worktreeId, + data.targetGroupId, + data.shellOverride, + pendingStartup ? { ...tabOptions, pendingStartup } : tabOptions + ) if (!shouldActivate) { // Why: renderer-backed Codex startup must mount its new TerminalPane without switching UI or connecting every saved tab. requestBackgroundTerminalWorktreeMount({ worktreeId, tabIds: [tab.id] }) @@ -89,22 +110,6 @@ export function registerTerminalRequestIpcBridge(unsubs: (() => void)[]): void { if (data.title) { store.setTabCustomTitle(tab.id, data.title, { recordInteraction: false }) } - if (data.command) { - store.queueTabStartupCommand(tab.id, { - command: data.command, - ...(data.env ? { env: data.env } : {}), - ...(data.envToDelete ? { envToDelete: data.envToDelete } : {}), - ...(data.launchConfig ? { launchConfig: data.launchConfig } : {}), - ...(data.resumeProviderSession - ? { resumeProviderSession: data.resumeProviderSession } - : {}), - ...(data.launchToken ? { launchToken: data.launchToken } : {}), - ...(data.launchAgent ? { launchAgent: data.launchAgent } : {}), - ...(data.startupCommandDelivery - ? { startupCommandDelivery: data.startupCommandDelivery } - : {}) - }) - } window.api.ui.replyTerminalCreate({ requestId: data.requestId, tabId: tab.id, diff --git a/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts b/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts index 63c3c9122e5..e489341fe5d 100644 --- a/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts @@ -231,7 +231,15 @@ describe('useIpcEvents updater integration', () => { activate: false, recordInteraction: false, launchAgent: 'codex', - startupCwd: '/repo/packages/app' + startupCwd: '/repo/packages/app', + pendingStartup: { + command: 'codex', + launchConfig: { + agentArgs: '--model gpt-5', + agentEnv: { CODEX_PROFILE: 'request' } + }, + launchAgent: 'codex' + } }) expect(setActiveView).not.toHaveBeenCalled() expect(setActiveWorktree).not.toHaveBeenCalled() @@ -251,14 +259,7 @@ describe('useIpcEvents updater integration', () => { expect(setTabCustomTitle).toHaveBeenCalledWith('tab-new', 'Codex', { recordInteraction: false }) - expect(queueTabStartupCommand).toHaveBeenCalledWith('tab-new', { - command: 'codex', - launchConfig: { - agentArgs: '--model gpt-5', - agentEnv: { CODEX_PROFILE: 'request' } - }, - launchAgent: 'codex' - }) + expect(queueTabStartupCommand).not.toHaveBeenCalled() expect(replyTerminalCreate).toHaveBeenCalledWith({ requestId: 'req-renderer-backed', tabId: 'tab-new', @@ -282,7 +283,8 @@ describe('useIpcEvents updater integration', () => { expect(createTab).toHaveBeenCalledWith('wt-2', 'group-left', undefined, { launchAgent: 'codex', - viewMode: 'terminal' + viewMode: 'terminal', + pendingStartup: { command: 'codex', launchAgent: 'codex' } }) expect(replyTerminalCreate).toHaveBeenCalledWith({ requestId: 'req-runtime-session', @@ -444,7 +446,8 @@ describe('useIpcEvents updater integration', () => { expect(createTab).toHaveBeenCalledWith('wt-2', undefined, undefined, { activate: false, - recordInteraction: false + recordInteraction: false, + pendingStartup: { command: 'codex' } }) expect(setActiveView).not.toHaveBeenCalled() expect(setActiveWorktree).not.toHaveBeenCalled() diff --git a/src/renderer/src/lib/agent-launch-caller-placement.test.ts b/src/renderer/src/lib/agent-launch-caller-placement.test.ts index 3b20851e43c..657f1f9b9ac 100644 --- a/src/renderer/src/lib/agent-launch-caller-placement.test.ts +++ b/src/renderer/src/lib/agent-launch-caller-placement.test.ts @@ -12,6 +12,7 @@ import { createdTabOptions, createLaunchFunnelStore, hostLaunchRequest, + queuedStartupCommand, queuedStartupPayload, resetLaunchFunnelStore } from './agent-launch-funnel-test-harness' @@ -143,10 +144,6 @@ describe('agent launch caller placement and telemetry', () => { 'tab-1', profile.args.initialCwd ) - // Ordering matters: the pane snapshots the queued cwd on first render. - expect(store.queueTabInitialCwd.mock.invocationCallOrder[0]).toBeLessThan( - store.queueTabStartupCommand.mock.invocationCallOrder[0] ?? Number.MAX_SAFE_INTEGER - ) return } expect(store.queueTabInitialCwd).not.toHaveBeenCalled() @@ -173,16 +170,15 @@ describe('agent launch caller placement and telemetry', () => { }) }) - it('creates the tab before queueing its startup command', async () => { + it('publishes the tab together with its startup command', async () => { const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') launchAgentInNewTab({ requestId: 'request-2', agent: 'codex', worktreeId: 'wt-1' }) - // Why: the terminal pane snapshots pending startup in useState on first render, so a startup - // queued after mount is never seen. - expect(store.createTab.mock.invocationCallOrder[0]).toBeLessThan( - store.queueTabStartupCommand.mock.invocationCallOrder[0] ?? 0 - ) + // Why: the terminal pane snapshots pending startup in useState on first render, so the tab + // must never exist without its startup command. + expect(store.createTab).toHaveBeenCalledTimes(1) + expect(queuedStartupCommand(store)).toEqual(expect.any(String)) }) it('seeds working status for a Command Code prompt that rides argv', async () => { diff --git a/src/renderer/src/lib/agent-launch-caller-routing.test.ts b/src/renderer/src/lib/agent-launch-caller-routing.test.ts index 2e9f96e8257..600a3c83c46 100644 --- a/src/renderer/src/lib/agent-launch-caller-routing.test.ts +++ b/src/renderer/src/lib/agent-launch-caller-routing.test.ts @@ -11,7 +11,11 @@ import { callerProfileCases, type AgentLaunchCallerProfile } from './agent-launch-caller-profiles-test-harness' -import { createLaunchFunnelStore, resetLaunchFunnelStore } from './agent-launch-funnel-test-harness' +import { + createLaunchFunnelStore, + queuedStartupCommand, + resetLaunchFunnelStore +} from './agent-launch-funnel-test-harness' import type * as LaunchAdmissionModule from './structured-agent-session-launch-admission' const store = createLaunchFunnelStore() @@ -270,10 +274,7 @@ describe('agent launch caller routing', () => { await expect(result?.structuredSettlement).resolves.toEqual({ kind: 'terminal' }) expect(store.createTab).toHaveBeenCalledOnce() expect(store.createTab.mock.calls[0]?.slice(0, 2)).toEqual(['wt-1', 'group-1']) - expect(store.queueTabStartupCommand).toHaveBeenCalledOnce() - expect(store.queueTabStartupCommand.mock.calls[0]?.[1]?.command).toBe( - "claude '--model' 'sonnet'" - ) + expect(queuedStartupCommand(store)).toBe("claude '--model' 'sonnet'") // The typed prompt goes to that terminal once, and the caller hears it arrived. await expect(result?.promptDeliveryResult).resolves.toEqual({ delivered: true, @@ -339,7 +340,6 @@ describe('agent launch caller routing', () => { expect(result).toBeNull() expect(beforeSurfaceOpen).toHaveBeenCalledExactlyOnceWith({ kind: 'local-terminal' }) expect(store.createTab).not.toHaveBeenCalled() - expect(store.queueTabStartupCommand).not.toHaveBeenCalled() }) it('aborts a host-published open when beforeSurfaceOpen refuses', async () => { diff --git a/src/renderer/src/lib/agent-launch-funnel-test-harness.ts b/src/renderer/src/lib/agent-launch-funnel-test-harness.ts index 8ae8b40595e..ec26a151a58 100644 --- a/src/renderer/src/lib/agent-launch-funnel-test-harness.ts +++ b/src/renderer/src/lib/agent-launch-funnel-test-harness.ts @@ -45,7 +45,6 @@ export function createLaunchFunnelStore(): { tabBarOrderByWorktree: Record createTab: ReturnType queueTabInitialCwd: ReturnType - queueTabStartupCommand: ReturnType setActiveTabType: ReturnType setTabBarOrder: ReturnType setAgentStatus: ReturnType @@ -65,7 +64,6 @@ export function createLaunchFunnelStore(): { tabBarOrderByWorktree: {}, createTab: vi.fn(), queueTabInitialCwd: vi.fn(), - queueTabStartupCommand: vi.fn(), setActiveTabType: vi.fn(), setTabBarOrder: vi.fn(), setAgentStatus: vi.fn(), @@ -117,16 +115,18 @@ export function resetLaunchFunnelStore( store.createTab.mockImplementation(createTabInto(store)) } -/** The startup command `queueTabStartupCommand` received, or undefined when no tab was queued. */ +/** The startup command the window planned for the new tab, or undefined when the host plans it. */ export function queuedStartupCommand(store: LaunchFunnelStore): string | undefined { - return store.queueTabStartupCommand.mock.calls[0]?.[1]?.command + return queuedStartupPayload(store)?.command } -/** The whole startup-command payload, for callers that assert more than the command string. */ +/** The whole window-planned startup payload, for callers that assert more than the command string. */ export function queuedStartupPayload( store: LaunchFunnelStore -): Record | undefined { - return store.queueTabStartupCommand.mock.calls[0]?.[1] +): { command?: string; [key: string]: unknown } | undefined { + const options: { pendingStartup?: { command?: string; agentLaunch?: unknown } } = + store.createTab.mock.calls[0]?.[3] ?? {} + return options.pendingStartup?.agentLaunch ? undefined : options.pendingStartup } /** The tab-creation options object (`createTab`'s 4th argument). */ diff --git a/src/renderer/src/lib/launch-agent-in-new-tab-host-resolution.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab-host-resolution.test.ts index bcdc67faeb3..ca7cb21c191 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab-host-resolution.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab-host-resolution.test.ts @@ -4,7 +4,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mockCreateTab = vi.fn() -const mockQueueTabStartupCommand = vi.fn() +/** Startup commands published with the created tabs, in creation order. */ +function queuedStartups() { + return mockCreateTab.mock.calls.flatMap((call) => call[3]?.pendingStartup ?? []) +} type StoreRepo = { id: string @@ -57,7 +60,6 @@ const store = { ptyIdsByTabId: {} as Record, createTab: mockCreateTab, closeTab: vi.fn(), - queueTabStartupCommand: mockQueueTabStartupCommand, setActiveTabType: vi.fn(), setTabBarOrder: vi.fn(), setAgentStatus: vi.fn(), @@ -112,7 +114,7 @@ async function launchOnLinux(): Promise { } function queuedCommand(): string { - return mockQueueTabStartupCommand.mock.calls[0]?.[1]?.command + return queuedStartups()[0]?.command } describe('launchAgentInNewTab execution host resolution', () => { diff --git a/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts index 29db1273f1a..54aa9903bef 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts @@ -6,7 +6,10 @@ import { setLocalRuntimeCapabilitiesForTests } from '@/runtime/local-runtime-cap import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' const mockCreateTab = vi.fn() -const mockQueueTabStartupCommand = vi.fn() +/** Startup commands published with the created tabs, in creation order. */ +function queuedStartups() { + return mockCreateTab.mock.calls.flatMap((call) => call[3]?.pendingStartup ?? []) +} const mockSetActiveTabType = vi.fn() const mockSeedNativeChatAppliedSessionOptions = vi.fn() @@ -42,7 +45,6 @@ const store = { tabBarOrderByWorktree: {}, createTab: mockCreateTab, queueTabInitialCwd: vi.fn(), - queueTabStartupCommand: mockQueueTabStartupCommand, setActiveTabType: mockSetActiveTabType, setTabBarOrder: vi.fn() } @@ -130,7 +132,8 @@ describe('launchAgentInNewTab terminal tab activation', () => { undefined, { launchAgent: 'codex', - quickCommandLabel: undefined + quickCommandLabel: undefined, + pendingStartup: expect.anything() } ) expect(mockSetActiveTabType).toHaveBeenCalledExactlyOnceWith( @@ -138,6 +141,6 @@ describe('launchAgentInNewTab terminal tab activation', () => { FLOATING_TERMINAL_WORKTREE_ID ) // Chat UI model preferences belong to structured chats, not a terminal fallback. - expect(mockQueueTabStartupCommand.mock.calls[0]?.[1]).not.toHaveProperty('sessionOptions') + expect(queuedStartups()[0]).not.toHaveProperty('sessionOptions') }) }) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.ts index 3004cd0d268..48fd355b220 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.ts @@ -7,7 +7,10 @@ const mockIsWebRuntimeSessionActive = vi.fn(() => false) const mockCreateWebRuntimeAgentSessionTerminal = vi.fn() const mockCreateWebRuntimeAgentSessionTerminalWithLaunchDraft = vi.fn() const mockCreateTab = vi.fn() -const mockQueueTabStartupCommand = vi.fn() +/** Startup commands published with the created tabs, in creation order. */ +function queuedStartups() { + return mockCreateTab.mock.calls.flatMap((call) => call[3]?.pendingStartup ?? []) +} const mockPasteDraftWhenAgentReady = vi.fn() const store = { @@ -67,7 +70,6 @@ const store = { ptyIdsByTabId: {} as Record, createTab: mockCreateTab, closeTab: vi.fn(), - queueTabStartupCommand: mockQueueTabStartupCommand, setActiveTabType: vi.fn(), setTabBarOrder: vi.fn(), setAgentStatus: vi.fn(), @@ -202,8 +204,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchPlatform: 'win32' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "claude '--dangerously-skip-permissions' --prefill 'review Bob''s change'" }) @@ -221,8 +222,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchPlatform: 'win32' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: 'claude "--dangerously-skip-permissions"' }) @@ -240,8 +240,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchPlatform: 'win32' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "claude '--dangerously-skip-permissions'" }) @@ -261,8 +260,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchPlatform: 'win32' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: 'codex "--model" "gpt-5" "fix the spinner"', agentArgsOverride: '--model gpt-5' @@ -283,8 +281,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchPlatform: 'win32' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: `claude '--dangerously-skip-permissions' --prefill 'review Bob'"'"'s change'` }) @@ -301,8 +298,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchAgentInNewTab({ requestId: 'request-7', agent: 'claude', worktreeId: 'wt-1' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "claude '--dangerously-skip-permissions'" }) @@ -339,8 +335,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { promptDelivery: 'draft' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: `claude '--dangerously-skip-permissions' --prefill 'review Bob'"'"'s change'` }) @@ -374,7 +369,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchAgentInNewTab({ requestId: 'request-9', agent: 'codex', worktreeId: 'wt-1' }) - const queued = mockQueueTabStartupCommand.mock.calls.at(-1)?.[1] as { command: string } + const queued = queuedStartups().at(-1) expect(queued.command).toContain(`'don'"'"'t'`) expect(queued.command).not.toContain("'don''t'") }) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts index 4cedc331354..51c011a54d3 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts @@ -4,7 +4,10 @@ import { toAppSshPtyId } from '../../../shared/ssh-pty-id' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' const mockCreateTab = vi.fn() -const mockQueueTabStartupCommand = vi.fn() +/** Startup commands published with the created tabs, in creation order. */ +function queuedStartups() { + return mockCreateTab.mock.calls.flatMap((call) => call[3]?.pendingStartup ?? []) +} const mockSetActiveTabType = vi.fn() const mockSetTabViewMode = vi.fn() const mockSetTabBarOrder = vi.fn() @@ -84,7 +87,6 @@ const store = { ptyIdsByTabId: {} as Record, createTab: mockCreateTab, closeTab: vi.fn(), - queueTabStartupCommand: mockQueueTabStartupCommand, setActiveTabType: mockSetActiveTabType, setTabViewMode: mockSetTabViewMode, setTabBarOrder: mockSetTabBarOrder, @@ -215,7 +217,8 @@ describe('launchAgentInNewTab', () => { launchAgentInNewTab({ requestId: 'request-1', agent: 'codex', worktreeId: 'wt-1' }) expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { - launchAgent: 'codex' + launchAgent: 'codex', + pendingStartup: expect.anything() }) }) it('keeps Floating Workspace authority on native Windows beside an active WSL project', async () => { @@ -241,7 +244,7 @@ describe('launchAgentInNewTab', () => { FLOATING_TERMINAL_WORKTREE_ID, undefined, undefined, - { launchAgent: 'codex' } + { launchAgent: 'codex', pendingStartup: expect.anything() } ) }) @@ -265,10 +268,10 @@ describe('launchAgentInNewTab', () => { expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { launchAgent: 'codex', - quickCommandLabel: undefined + quickCommandLabel: undefined, + pendingStartup: expect.anything() }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: expect.not.stringContaining('large generated prompt') }) @@ -302,7 +305,8 @@ describe('launchAgentInNewTab', () => { expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { launchAgent: 'grok', - quickCommandLabel: undefined + quickCommandLabel: undefined, + pendingStartup: expect.anything() }) expect(mockSeedNativeChatLaunchPrompt).toHaveBeenCalledWith({ tabId: 'tab-1', @@ -327,7 +331,8 @@ describe('launchAgentInNewTab', () => { expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { launchAgent: 'grok', - quickCommandLabel: undefined + quickCommandLabel: undefined, + pendingStartup: expect.anything() }) }) @@ -398,7 +403,8 @@ describe('launchAgentInNewTab', () => { expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { launchAgent: 'codex', - quickCommandLabel: 'Review' + quickCommandLabel: 'Review', + pendingStartup: expect.anything() }) }) @@ -427,7 +433,7 @@ describe('launchAgentInNewTab', () => { quickCommandLabel: 'Review' }) - const launch = mockQueueTabStartupCommand.mock.calls[0]?.[1] + const launch = queuedStartups()[0] expect(launch.command).toContain("'--profile' 'team'") expect(launch.command).not.toContain("'-m'") expect(launch.command).not.toContain('model_reasoning_effort=') @@ -459,7 +465,7 @@ describe('launchAgentInNewTab', () => { quickCommandLabel: 'Review' }) - const launch = mockQueueTabStartupCommand.mock.calls[0]?.[1] + const launch = queuedStartups()[0] expect(launch.command).not.toContain("'-m'") expect(launch.command).not.toContain('model_reasoning_effort=') expect(launch.sessionOptions).toBeUndefined() @@ -597,8 +603,7 @@ describe('launchAgentInNewTab', () => { prompt: 'fix the spinner' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "command-code --trust '--yolo' 'fix the spinner'", initialAgentStatus: { @@ -637,8 +642,7 @@ describe('launchAgentInNewTab', () => { }) expect(result).not.toHaveProperty('promptDeliveryResult') - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "claude '--dangerously-skip-permissions'" }) @@ -704,8 +708,7 @@ describe('launchAgentInNewTab', () => { await Promise.resolve() await Promise.resolve() - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "command-code --trust '--yolo'" }) @@ -885,8 +888,7 @@ describe('launchAgentInNewTab', () => { promptDelivery: 'submit-after-ready' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "codex '--model' 'gpt-5.5'" }) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.ts b/src/renderer/src/lib/launch-agent-in-new-tab.ts index bff61b705a3..c8988afffc0 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.ts @@ -265,35 +265,34 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent promptDeliveryResult: launched.promptDeliveryResult } } - // Why: queue startup BEFORE TerminalPane mounts — it snapshots pendingStartupByTabId in useState on first render. const tab = store.createTab(worktreeId, groupId, undefined, { launchAgent: agent, quickCommandLabel, ...(pendingActivationSpawn ? { pendingActivationSpawn: true } : {}), - ...(activate === false ? { activate: false } : {}) + ...(activate === false ? { activate: false } : {}), + pendingStartup: { + command: startupPlan.launchCommand, + ...(startupPlan.env ? { env: startupPlan.env } : {}), + launchConfig: startupPlan.launchConfig, + launchAgent: agent, + ...(agentArgs !== undefined ? { agentArgsOverride: agentArgs } : {}), + ...(startupPlan.startupCommandDelivery + ? { startupCommandDelivery: startupPlan.startupCommandDelivery } + : {}), + ...(agent === 'command-code' && hasPrompt && promptDelivery === 'auto-submit' + ? { initialAgentStatus: { agent, prompt: trimmedPrompt } } + : {}), + telemetry: { + agent_kind: tuiAgentToAgentKind(agent), + launch_source: launchSource ?? 'tab_bar_quick_launch', + request_kind: 'new' + } + } }) if (initialCwd?.trim()) { // Why: queue before mount so local, WSL, and SSH continuations preserve their subdirectory. store.queueTabInitialCwd(tab.id, initialCwd) } - store.queueTabStartupCommand(tab.id, { - command: startupPlan.launchCommand, - ...(startupPlan.env ? { env: startupPlan.env } : {}), - launchConfig: startupPlan.launchConfig, - launchAgent: agent, - ...(agentArgs !== undefined ? { agentArgsOverride: agentArgs } : {}), - ...(startupPlan.startupCommandDelivery - ? { startupCommandDelivery: startupPlan.startupCommandDelivery } - : {}), - ...(agent === 'command-code' && hasPrompt && promptDelivery === 'auto-submit' - ? { initialAgentStatus: { agent, prompt: trimmedPrompt } } - : {}), - telemetry: { - agent_kind: tuiAgentToAgentKind(agent), - launch_source: launchSource ?? 'tab_bar_quick_launch', - request_kind: 'new' - } - }) // Why: fire-and-forget the paste-after-ready delivery so callers keep the synchronous { tabId, startupPlan } signature. // Why: safe to call unconditionally — the helper short-circuits (no paste) for native-prefill agents already holding the draft. if (hasPrompt && promptDelivery === 'draft' && pasteDraftAfterLaunch === null) { diff --git a/src/renderer/src/lib/launch-ai-vault-session.test.ts b/src/renderer/src/lib/launch-ai-vault-session.test.ts index 02f5b83c5f0..e61bccaacf8 100644 --- a/src/renderer/src/lib/launch-ai-vault-session.test.ts +++ b/src/renderer/src/lib/launch-ai-vault-session.test.ts @@ -3,7 +3,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mockCreateTab = vi.fn() const mockCreateEmptySplitGroup = vi.fn() const mockCloseEmptyGroup = vi.fn() -const mockQueueTabStartupCommand = vi.fn() +/** Startup commands published with the created tabs, in creation order. */ +function queuedStartups() { + return mockCreateTab.mock.calls.flatMap((call) => call[3]?.pendingStartup ?? []) +} const mockSetActiveTabType = vi.fn() const mockSetTabBarOrder = vi.fn() const runtimeMocks = vi.hoisted(() => ({ @@ -16,7 +19,6 @@ const mockState = { createTab: mockCreateTab, createEmptySplitGroup: mockCreateEmptySplitGroup, closeEmptyGroup: mockCloseEmptyGroup, - queueTabStartupCommand: mockQueueTabStartupCommand, setActiveTabType: mockSetActiveTabType, setTabBarOrder: mockSetTabBarOrder, tabsByWorktree: {} as Record, @@ -83,8 +85,10 @@ describe('launchAiVaultSessionInNewTab', () => { command: 'claude --resume session-1' }) - expect(mockCreateTab).toHaveBeenCalledWith('wt-1', 'group-1') - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(mockCreateTab).toHaveBeenCalledWith('wt-1', 'group-1', undefined, { + pendingStartup: expect.anything() + }) + expect(queuedStartups()).toContainEqual({ command: 'claude --resume session-1', telemetry: { agent_kind: 'claude', @@ -115,9 +119,10 @@ describe('launchAiVaultSessionInNewTab', () => { }) expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { - startupCwd: 'C:\\Users\\alice\\repo' + startupCwd: 'C:\\Users\\alice\\repo', + pendingStartup: expect.anything() }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(queuedStartups()).toContainEqual({ command: "claude '--dangerously-skip-permissions' '--effort' 'max' '--resume' 'session-1'", env: { ANTHROPIC_BASE_URL: 'https://claude.example.test' }, envToDelete: ['CODEX_HOME'], @@ -146,7 +151,9 @@ describe('launchAiVaultSessionInNewTab', () => { }) expect(mockCreateEmptySplitGroup).toHaveBeenCalledWith('wt-1', 'group-1', 'right') - expect(mockCreateTab).toHaveBeenCalledWith('wt-1', 'group-new') + expect(mockCreateTab).toHaveBeenCalledWith('wt-1', 'group-new', undefined, { + pendingStartup: expect.anything() + }) }) it('creates runtime-hosted resume terminals through the paired host', async () => { @@ -188,7 +195,7 @@ describe('launchAiVaultSessionInNewTab', () => { activate: true }) expect(mockCreateTab).not.toHaveBeenCalled() - expect(mockQueueTabStartupCommand).not.toHaveBeenCalled() + expect(queuedStartups()).toEqual([]) if (result.tabId === null) { await expect(result.runtimeLaunch).resolves.toEqual({ status: 'created' }) diff --git a/src/renderer/src/lib/launch-ai-vault-session.ts b/src/renderer/src/lib/launch-ai-vault-session.ts index a358efb306f..342c5ccc9b4 100644 --- a/src/renderer/src/lib/launch-ai-vault-session.ts +++ b/src/renderer/src/lib/launch-ai-vault-session.ts @@ -69,19 +69,19 @@ export function launchAiVaultSessionInNewTab(args: { } } - const tab = args.cwd - ? store.createTab(args.worktreeId, targetGroupId, undefined, { startupCwd: args.cwd }) - : store.createTab(args.worktreeId, targetGroupId) - store.queueTabStartupCommand(tab.id, { - command: args.command, - ...(args.env ? { env: args.env } : {}), - ...(args.envToDelete ? { envToDelete: args.envToDelete } : {}), - ...(args.launchConfig ? { launchConfig: args.launchConfig, launchAgent: args.agent } : {}), - ...(args.providerSession ? { resumeProviderSession: args.providerSession } : {}), - telemetry: { - agent_kind: tuiAgentToAgentKind(args.agent), - launch_source: 'sidebar', - request_kind: 'resume' + const tab = store.createTab(args.worktreeId, targetGroupId, undefined, { + ...(args.cwd ? { startupCwd: args.cwd } : {}), + pendingStartup: { + command: args.command, + ...(args.env ? { env: args.env } : {}), + ...(args.envToDelete ? { envToDelete: args.envToDelete } : {}), + ...(args.launchConfig ? { launchConfig: args.launchConfig, launchAgent: args.agent } : {}), + ...(args.providerSession ? { resumeProviderSession: args.providerSession } : {}), + telemetry: { + agent_kind: tuiAgentToAgentKind(args.agent), + launch_source: 'sidebar', + request_kind: 'resume' + } } }) store.setActiveTabType('terminal', args.worktreeId) diff --git a/src/renderer/src/lib/run-quick-command-in-new-tab.test.ts b/src/renderer/src/lib/run-quick-command-in-new-tab.test.ts index e5983ec000a..4424300fb53 100644 --- a/src/renderer/src/lib/run-quick-command-in-new-tab.test.ts +++ b/src/renderer/src/lib/run-quick-command-in-new-tab.test.ts @@ -3,7 +3,6 @@ import { runQuickCommandInNewTab } from './run-quick-command-in-new-tab' type MockStoreState = { createTab: ReturnType - queueTabStartupCommand: ReturnType setActiveTabType: ReturnType setTabBarOrder: ReturnType setRecentQuickCommandForGroup: ReturnType @@ -37,7 +36,6 @@ vi.mock('@/lib/launch-agent-in-new-tab', () => ({ function createStoreState(): MockStoreState { return { createTab: vi.fn(() => ({ id: 'tab-new' })), - queueTabStartupCommand: vi.fn(), setActiveTabType: vi.fn(), setTabBarOrder: vi.fn(), setRecentQuickCommandForGroup: vi.fn(), @@ -73,10 +71,8 @@ describe('runQuickCommandInNewTab', () => { expect(result).toEqual({ tabId: 'tab-new' }) expect(mockState.createTab).toHaveBeenCalledWith('wt-1', 'group-1', undefined, { - quickCommandLabel: 'Build' - }) - expect(mockState.queueTabStartupCommand).toHaveBeenCalledWith('tab-new', { - command: 'cd packages; bun run build; cd ..' + quickCommandLabel: 'Build', + pendingStartup: { command: 'cd packages; bun run build; cd ..' } }) expect(mockState.setRecentQuickCommandForGroup).toHaveBeenCalledWith('group-1', 'build') }) @@ -139,8 +135,9 @@ describe('runQuickCommandInNewTab', () => { groupId: 'group-1' }) - expect(mockState.queueTabStartupCommand).toHaveBeenCalledWith('tab-new', { - command: 'git status' + expect(mockState.createTab).toHaveBeenCalledWith('wt-1', 'group-1', undefined, { + quickCommandLabel: 'Status', + pendingStartup: { command: 'git status' } }) }) @@ -174,7 +171,7 @@ describe('runQuickCommandInNewTab', () => { launchSource: 'quick_command', quickCommandLabel: 'Review' }) - expect(mockState.queueTabStartupCommand).not.toHaveBeenCalled() + expect(mockState.createTab).not.toHaveBeenCalled() expect(mockState.setRecentQuickCommandForGroup).toHaveBeenCalledWith('group-1', 'agent-review') }) @@ -330,6 +327,6 @@ describe('runQuickCommandInNewTab', () => { expect(result).toBeNull() expect(mocks.launchAgentInNewTab).not.toHaveBeenCalled() - expect(mockState.queueTabStartupCommand).not.toHaveBeenCalled() + expect(mockState.createTab).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/lib/run-quick-command-in-new-tab.ts b/src/renderer/src/lib/run-quick-command-in-new-tab.ts index 1e20fd0c142..7d730a0f4a6 100644 --- a/src/renderer/src/lib/run-quick-command-in-new-tab.ts +++ b/src/renderer/src/lib/run-quick-command-in-new-tab.ts @@ -103,11 +103,8 @@ export function runQuickCommandInNewTab({ } const store = useAppStore.getState() const tab = store.createTab(worktreeId, targetGroupId, undefined, { - quickCommandLabel: command.label - }) - - store.queueTabStartupCommand(tab.id, { - command: flattenTerminalQuickCommand(command).command + quickCommandLabel: command.label, + pendingStartup: { command: flattenTerminalQuickCommand(command).command } }) // Why: match `+` button's createNewTerminalTab — without this, a worktree diff --git a/src/renderer/src/lib/worktree-activation-agent-startup.test.ts b/src/renderer/src/lib/worktree-activation-agent-startup.test.ts index ca6595d0f43..47fa6113d18 100644 --- a/src/renderer/src/lib/worktree-activation-agent-startup.test.ts +++ b/src/renderer/src/lib/worktree-activation-agent-startup.test.ts @@ -2,7 +2,8 @@ import { describe, expect, it, vi } from 'vitest' import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-seeding' import { createMockStore, - registerWorktreeActivationReset + registerWorktreeActivationReset, + createdTabStartups } from './worktree-activation-test-harness' registerWorktreeActivationReset() @@ -20,10 +21,11 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true }) expect(store.setActiveTab).toHaveBeenCalledWith('tab-1') - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'claude "Fix this bug"' }) expect(store.queueTabSetupSplit).not.toHaveBeenCalled() @@ -49,10 +51,11 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, launchAgent: 'claude' }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'claude', launchAgent: 'claude' }) @@ -82,6 +85,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, launchAgent: 'claude', ...expectedViewMode @@ -114,6 +118,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, launchAgent: 'claude', ...expectedViewMode @@ -141,6 +146,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(createTab).toHaveBeenNthCalledWith(1, 'wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, recordInteraction: false, launchAgent: 'claude' @@ -173,6 +179,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(createTab).toHaveBeenNthCalledWith(1, 'wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, recordInteraction: false, launchAgent: 'claude', @@ -200,10 +207,11 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, launchAgent: 'claude' }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'claude', telemetry: { agent_kind: 'claude-code', @@ -228,10 +236,11 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, launchAgent: 'codex' }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'codex', launchAgent: 'codex' }) diff --git a/src/renderer/src/lib/worktree-activation-default-tabs.test.ts b/src/renderer/src/lib/worktree-activation-default-tabs.test.ts index a6e5e5136a8..e6651635319 100644 --- a/src/renderer/src/lib/worktree-activation-default-tabs.test.ts +++ b/src/renderer/src/lib/worktree-activation-default-tabs.test.ts @@ -2,7 +2,8 @@ import { describe, expect, it, vi } from 'vitest' import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-seeding' import { createMockStore, - registerWorktreeActivationReset + registerWorktreeActivationReset, + createdTabStartups } from './worktree-activation-test-harness' registerWorktreeActivationReset() @@ -17,7 +18,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { pendingActivationSpawn: true }) expect(store.setActiveTab).toHaveBeenCalledWith('tab-1') - expect(store.queueTabStartupCommand).not.toHaveBeenCalled() + expect(createdTabStartups(store)).toEqual({}) expect(store.queueTabSetupSplit).not.toHaveBeenCalled() }) @@ -39,9 +40,10 @@ describe('ensureWorktreeHasInitialTerminal', () => { ensureWorktreeHasInitialTerminal(store, 'wt-1', { command: 'claude "Fix this bug"' }) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { - pendingActivationSpawn: true + pendingActivationSpawn: true, + pendingStartup: expect.anything() }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'claude "Fix this bug"' }) }) @@ -71,7 +73,8 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(createTab).toHaveBeenCalledTimes(2) expect(createTab).toHaveBeenNthCalledWith(1, 'wt-1', undefined, undefined, { pendingActivationSpawn: true, - recordInteraction: false + recordInteraction: false, + pendingStartup: expect.anything() }) expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-1', 'Claude', { recordInteraction: false @@ -81,8 +84,8 @@ describe('ensureWorktreeHasInitialTerminal', () => { }) expect(store.setTabColor).toHaveBeenCalledWith('tab-1', '#f97316') expect(store.setTabColor).toHaveBeenCalledWith('tab-2', '#9ca3af') - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { command: 'claude' }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { command: 'pnpm dev' }) + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'claude' }) + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'pnpm dev' }) expect(store.setActiveTab).toHaveBeenLastCalledWith('tab-1') }) @@ -111,18 +114,20 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(createTab).toHaveBeenNthCalledWith(1, 'wt-1', undefined, undefined, { pendingActivationSpawn: true, recordInteraction: false, - activate: false + activate: false, + pendingStartup: expect.anything() }) expect(createTab).toHaveBeenNthCalledWith(2, 'wt-1', undefined, undefined, { recordInteraction: false, - activate: false + activate: false, + pendingStartup: expect.anything() }) expect(store.setActiveTab).not.toHaveBeenCalled() expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup', { recordInteraction: false }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { command: 'pnpm dev' }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'pnpm dev' }) + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash /tmp/repo/.git/orca/setup-runner.sh', env: { ORCA_WORKTREE_PATH: '/tmp/worktrees/wt-1' } }) @@ -136,7 +141,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { tabs: [{ title: 'Server', command: 'pnpm dev' }] }) - expect(store.queueTabStartupCommand).not.toHaveBeenCalled() + expect(createdTabStartups(store)).toEqual({}) expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-1', 'Server', { recordInteraction: false }) @@ -159,7 +164,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.setTabCustomTitle).not.toHaveBeenCalledWith('tab-1', 'Claude', { recordInteraction: false }) - expect(store.queueTabStartupCommand).not.toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).not.toEqual({ command: 'claude' }) }) @@ -176,7 +181,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.createTab).not.toHaveBeenCalled() expect(store.setActiveTab).not.toHaveBeenCalled() - expect(store.queueTabStartupCommand).not.toHaveBeenCalled() + expect(createdTabStartups(store)).toEqual({}) expect(store.queueTabSetupSplit).not.toHaveBeenCalled() expect(store.queueTabIssueCommandSplit).not.toHaveBeenCalled() }) diff --git a/src/renderer/src/lib/worktree-activation-issue-command.test.ts b/src/renderer/src/lib/worktree-activation-issue-command.test.ts index b84e7569238..07c3337910e 100644 --- a/src/renderer/src/lib/worktree-activation-issue-command.test.ts +++ b/src/renderer/src/lib/worktree-activation-issue-command.test.ts @@ -3,7 +3,8 @@ import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-se import { createMockStore, registerWorktreeActivationReset, - setSetupScriptLaunchMode + setSetupScriptLaunchMode, + createdTabStartups } from './worktree-activation-test-harness' registerWorktreeActivationReset() @@ -67,7 +68,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { } ) - expect(store.queueTabStartupCommand).not.toHaveBeenCalled() + expect(createdTabStartups(store)).toEqual({}) expect(store.queueTabSetupSplit).toHaveBeenCalledWith('tab-1', { command: 'bash /tmp/repo/.git/orca/setup-runner.sh', env: { ORCA_ROOT_PATH: '/tmp/repo' }, @@ -84,7 +85,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { ensureWorktreeHasInitialTerminal(store, 'wt-1') - expect(store.queueTabStartupCommand).not.toHaveBeenCalled() + expect(createdTabStartups(store)).toEqual({}) expect(store.queueTabIssueCommandSplit).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/lib/worktree-activation-reveal.test.ts b/src/renderer/src/lib/worktree-activation-reveal.test.ts index 64818c32a6b..f536936bf12 100644 --- a/src/renderer/src/lib/worktree-activation-reveal.test.ts +++ b/src/renderer/src/lib/worktree-activation-reveal.test.ts @@ -64,7 +64,6 @@ describe('activateAndRevealWorktree', () => { setTabCustomTitle: vi.fn(), setTabColor: vi.fn(), markDefaultTerminalTabsApplied: vi.fn(), - queueTabStartupCommand: vi.fn(), queueTabInitialCwd, queueTabSetupSplit: vi.fn(), queueTabIssueCommandSplit: vi.fn(), @@ -134,7 +133,6 @@ describe('activateAndRevealWorktree', () => { setTabCustomTitle: vi.fn(), setTabColor: vi.fn(), markDefaultTerminalTabsApplied: vi.fn(), - queueTabStartupCommand: vi.fn(), queueTabInitialCwd: vi.fn(), queueTabSetupSplit: vi.fn(), queueTabIssueCommandSplit: vi.fn(), diff --git a/src/renderer/src/lib/worktree-activation-setup-script.test.ts b/src/renderer/src/lib/worktree-activation-setup-script.test.ts index ff9bc0874b8..e7f9491d5c1 100644 --- a/src/renderer/src/lib/worktree-activation-setup-script.test.ts +++ b/src/renderer/src/lib/worktree-activation-setup-script.test.ts @@ -4,7 +4,8 @@ import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-se import { createMockStore, registerWorktreeActivationReset, - setSetupScriptLaunchMode + setSetupScriptLaunchMode, + createdTabStartups } from './worktree-activation-test-harness' registerWorktreeActivationReset() @@ -29,7 +30,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup', { recordInteraction: false }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash /tmp/repo/.git/orca/setup-runner.sh', env: { ORCA_ROOT_PATH: '/tmp/repo', @@ -53,7 +54,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { } }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash /c/repo/.git/orca/setup-runner.sh', env: { ORCA_ROOT_PATH: 'C:\\repo', @@ -83,7 +84,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup', { recordInteraction: false }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash -lc wrapped-setup', env: { ORCA_ROOT_PATH: '/tmp/repo' } }) @@ -112,14 +113,12 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(result).toBe('tab-1') expect(createTab).toHaveBeenCalledTimes(1) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-2', + expect(createdTabStartups(store)['tab-2']).toEqual( expect.objectContaining({ command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh') }) ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-2', + expect(createdTabStartups(store)['tab-2']).toEqual( expect.objectContaining({ command: expect.stringContaining('printf') }) @@ -141,7 +140,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { } }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash /mnt/c/repo/.git/orca/setup-runner.sh', env: { ORCA_ROOT_PATH: 'C:\\repo', @@ -167,8 +166,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { } ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(createdTabStartups(store)['tab-1']).toEqual( expect.objectContaining({ env: expect.objectContaining({ [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining( @@ -177,22 +175,19 @@ describe('ensureWorktreeHasInitialTerminal', () => { }) }) ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(createdTabStartups(store)['tab-1']).toEqual( expect.objectContaining({ env: expect.objectContaining({ [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining('exec claude') }) }) ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-2', + expect(createdTabStartups(store)['tab-2']).toEqual( expect.objectContaining({ command: expect.stringContaining('printf') }) ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-2', + expect(createdTabStartups(store)['tab-2']).toEqual( expect.objectContaining({ command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh') }) @@ -216,10 +211,10 @@ describe('ensureWorktreeHasInitialTerminal', () => { } ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'claude' }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash /tmp/repo/.git/orca/setup-runner.sh', env: { ORCA_ROOT_PATH: '/tmp/repo' } }) @@ -240,8 +235,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { } ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(createdTabStartups(store)['tab-1']).toEqual( expect.objectContaining({ env: expect.objectContaining({ [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining('exec claude') @@ -276,8 +270,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { } ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(createdTabStartups(store)['tab-1']).toEqual( expect.objectContaining({ env: expect.objectContaining({ [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining( @@ -344,7 +337,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup', { recordInteraction: false }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash /tmp/repo/.git/orca/setup-runner.sh', env: { ORCA_ROOT_PATH: '/tmp/repo' } }) diff --git a/src/renderer/src/lib/worktree-activation-store-contract.ts b/src/renderer/src/lib/worktree-activation-store-contract.ts index 32a3f622c6c..d26c131d672 100644 --- a/src/renderer/src/lib/worktree-activation-store-contract.ts +++ b/src/renderer/src/lib/worktree-activation-store-contract.ts @@ -24,6 +24,18 @@ export type WorktreeActivationStore = Partial & { recordInteraction?: boolean viewMode?: Tab['viewMode'] activate?: boolean + pendingStartup?: { + command: string + env?: Record + launchConfig?: SleepingAgentLaunchConfig + resumeProviderSession?: AgentProviderSessionMetadata + launchToken?: string + launchAgent?: TuiAgent + draftPrompt?: string + initialAgentStatus?: { agent: TuiAgent; prompt: string } + showSessionRestoredBanner?: boolean + telemetry?: AgentStartedTelemetry + } } ) => { id: string } setActiveTab: (tabId: string) => void @@ -35,21 +47,6 @@ export type WorktreeActivationStore = Partial & { setTabColor: (tabId: string, color: string | null) => void markDefaultTerminalTabsApplied: (worktreeId: string) => void reconcileWorktreeTabModel: (worktreeId: string) => { renderableTabCount: number } - queueTabStartupCommand: ( - tabId: string, - startup: { - command: string - env?: Record - launchConfig?: SleepingAgentLaunchConfig - resumeProviderSession?: AgentProviderSessionMetadata - launchToken?: string - launchAgent?: TuiAgent - draftPrompt?: string - initialAgentStatus?: { agent: TuiAgent; prompt: string } - showSessionRestoredBanner?: boolean - telemetry?: AgentStartedTelemetry - } - ) => void queueTabSetupSplit: ( tabId: string, startup: { command: string; env?: Record; direction: SetupSplitDirection } diff --git a/src/renderer/src/lib/worktree-activation-structured-chat-surface.test.ts b/src/renderer/src/lib/worktree-activation-structured-chat-surface.test.ts index 33be3888a24..3b855050973 100644 --- a/src/renderer/src/lib/worktree-activation-structured-chat-surface.test.ts +++ b/src/renderer/src/lib/worktree-activation-structured-chat-surface.test.ts @@ -9,7 +9,8 @@ import { import { createMockStore, registerWorktreeActivationReset, - setSetupScriptLaunchMode + setSetupScriptLaunchMode, + createdTabStartups } from './worktree-activation-test-harness' const initialAppStoreState = useAppStore.getState() @@ -49,7 +50,7 @@ describe('seeding beside a caller-provided chat surface', () => { expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-1', 'Setup', { recordInteraction: false }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'bash /tmp/repo/.git/orca/setup-runner.sh', env: setup.envVars }) diff --git a/src/renderer/src/lib/worktree-activation-test-harness.ts b/src/renderer/src/lib/worktree-activation-test-harness.ts index 29a2cbd3227..f7e6958dcc1 100644 --- a/src/renderer/src/lib/worktree-activation-test-harness.ts +++ b/src/renderer/src/lib/worktree-activation-test-harness.ts @@ -46,13 +46,12 @@ export function registerWorktreeActivationReset(): void { export type MockActivationStore = { tabsByWorktree: Record defaultTerminalTabsAppliedByWorktreeId: Record - createTab: Mock<() => { id: string }> + createTab: Mock<(...args: unknown[]) => { id: string }> setActiveTab: Mock<() => void> setTabCustomTitle: Mock<() => void> setTabColor: Mock<() => void> markDefaultTerminalTabsApplied: Mock<() => void> reconcileWorktreeTabModel: Mock<() => { renderableTabCount: number }> - queueTabStartupCommand: Mock<() => void> queueTabInitialCwd: Mock<() => void> queueTabSetupSplit: Mock<() => void> queueTabIssueCommandSplit: Mock<() => void> @@ -68,10 +67,27 @@ export function createMockStore(overrides: Record = {}): MockAc setTabColor: vi.fn(), markDefaultTerminalTabsApplied: vi.fn(), reconcileWorktreeTabModel: vi.fn(() => ({ renderableTabCount: 0 })), - queueTabStartupCommand: vi.fn(), queueTabInitialCwd: vi.fn(), queueTabSetupSplit: vi.fn(), queueTabIssueCommandSplit: vi.fn(), ...overrides } } + +/** The startup command each created tab was published with, keyed by the created tab's id. */ +export function createdTabStartups(store: MockActivationStore): Record { + const startups: Record = {} + store.createTab.mock.calls.forEach((call, index) => { + const result = store.createTab.mock.results[index] + const options = call[3] + if ( + result?.type === 'return' && + options && + typeof options === 'object' && + 'pendingStartup' in options + ) { + startups[result.value.id] = options.pendingStartup + } + }) + return startups +} diff --git a/src/renderer/src/lib/worktree-activation-web-runtime.test.ts b/src/renderer/src/lib/worktree-activation-web-runtime.test.ts index 786fedcd37a..c361bf4a09e 100644 --- a/src/renderer/src/lib/worktree-activation-web-runtime.test.ts +++ b/src/renderer/src/lib/worktree-activation-web-runtime.test.ts @@ -4,7 +4,8 @@ import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-se import type { AppStoreState } from './worktree-activation-test-harness' import { createMockStore, - registerWorktreeActivationReset + registerWorktreeActivationReset, + createdTabStartups } from './worktree-activation-test-harness' import { useAppStore } from '@/store' import { @@ -299,14 +300,12 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup', { recordInteraction: false }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-2', + expect(createdTabStartups(store)['tab-2']).toEqual( expect.objectContaining({ command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh') }) ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-2', + expect(createdTabStartups(store)['tab-2']).toEqual( expect.objectContaining({ command: expect.stringContaining('printf') }) diff --git a/src/renderer/src/lib/worktree-default-terminal-tabs.ts b/src/renderer/src/lib/worktree-default-terminal-tabs.ts index 7104318ff01..d4fbde6ef2b 100644 --- a/src/renderer/src/lib/worktree-default-terminal-tabs.ts +++ b/src/renderer/src/lib/worktree-default-terminal-tabs.ts @@ -41,25 +41,31 @@ export function applyDefaultTerminalTabs( : isStartupTab && startup?.telemetry ? (agentKindToTuiAgent(startup.telemetry.agent_kind) ?? undefined) : undefined + const templateCommand = template.command?.trim() + const pendingStartup = isStartupTab + ? startup + : templateCommand && defaultTabs.runCommands + ? { command: templateCommand } + : undefined const tab = store.createTab(worktreeId, undefined, undefined, { pendingActivationSpawn: true, recordInteraction: false, ...(launchAgent ? { launchAgent } : {}), - ...(opts?.activateCreatedTabs === false ? { activate: false } : {}) + ...(opts?.activateCreatedTabs === false ? { activate: false } : {}), + ...(pendingStartup ? { pendingStartup } : {}) }) if (index === 0) { firstTabId = tab.id } + if (isStartupTab && launchAgent) { + seedNativeChatAppliedSessionOptions(tab.id, launchAgent, startup.sessionOptions) + } if (template.title) { store.setTabCustomTitle(tab.id, template.title, { recordInteraction: false }) } if (template.color) { store.setTabColor(tab.id, template.color) } - const templateCommand = template.command?.trim() - if (templateCommand && defaultTabs.runCommands && !(index === 0 && startup)) { - store.queueTabStartupCommand(tab.id, { command: templateCommand }) - } } if (!firstTabId) { @@ -68,17 +74,6 @@ export function applyDefaultTerminalTabs( if (opts?.activateCreatedTabs !== false) { store.setActiveTab(firstTabId) } - if (startup) { - const startupAgent = - startup.launchAgent ?? - (startup.telemetry - ? (agentKindToTuiAgent(startup.telemetry.agent_kind) ?? undefined) - : undefined) - if (startupAgent) { - seedNativeChatAppliedSessionOptions(firstTabId, startupAgent, startup.sessionOptions) - } - store.queueTabStartupCommand(firstTabId, startup) - } queueSetupAndIssueCommands( store, worktreeId, diff --git a/src/renderer/src/lib/worktree-initial-terminal-seeding.ts b/src/renderer/src/lib/worktree-initial-terminal-seeding.ts index cfd57ac7c98..4acaed2784b 100644 --- a/src/renderer/src/lib/worktree-initial-terminal-seeding.ts +++ b/src/renderer/src/lib/worktree-initial-terminal-seeding.ts @@ -281,22 +281,19 @@ export function ensureWorktreeHasInitialTerminal( const terminalTab = store.createTab(worktreeId, undefined, undefined, { pendingActivationSpawn: true, ...(launchAgent ? { launchAgent } : {}), - ...(opts?.activateCreatedTabs === false ? { activate: false } : {}) + ...(opts?.activateCreatedTabs === false ? { activate: false } : {}), + // Why: the initial pane begins in the requested agent session instead of an idle shell. + ...(sequencedStartup ? { pendingStartup: sequencedStartup } : {}) }) if (opts?.activateCreatedTabs !== false) { store.setActiveTab(terminalTab.id) } - - // Why: queue the seeded startup on the initial pane so the terminal begins in the requested agent session instead of an idle shell. - if (sequencedStartup) { - if (launchAgent) { - seedNativeChatAppliedSessionOptions( - terminalTab.id, - launchAgent, - sequencedStartup.sessionOptions - ) - } - store.queueTabStartupCommand(terminalTab.id, sequencedStartup) + if (sequencedStartup && launchAgent) { + seedNativeChatAppliedSessionOptions( + terminalTab.id, + launchAgent, + sequencedStartup.sessionOptions + ) } queueSetupAndIssueCommands( store, diff --git a/src/renderer/src/lib/worktree-setup-issue-command-queue.ts b/src/renderer/src/lib/worktree-setup-issue-command-queue.ts index 3a66305fb67..b98f4961654 100644 --- a/src/renderer/src/lib/worktree-setup-issue-command-queue.ts +++ b/src/renderer/src/lib/worktree-setup-issue-command-queue.ts @@ -34,7 +34,8 @@ export function queueSetupAndIssueCommands( if (mode === 'new-tab') { const setupTab = store.createTab(worktreeId, undefined, undefined, { recordInteraction: false, - ...(opts?.activateCreatedTabs === false ? { activate: false } : {}) + ...(opts?.activateCreatedTabs === false ? { activate: false } : {}), + pendingStartup: setupCommand }) // Why: createTab auto-activates the new tab; revert so focus stays on the primary terminal while Setup runs in the background. if (opts?.activateCreatedTabs !== false && terminalTabId) { @@ -42,7 +43,6 @@ export function queueSetupAndIssueCommands( } // Why: customTitle overrides the auto "Terminal N" label everywhere the tab renders, so it's the authoritative label source. store.setTabCustomTitle(setupTab.id, 'Setup', { recordInteraction: false }) - store.queueTabStartupCommand(setupTab.id, setupCommand) } else if (terminalTabId) { store.queueTabSetupSplit(terminalTabId, { ...setupCommand, From cc7d03d19c511eed1bbb2d12f5964404d74ad798 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:25:20 -0400 Subject: [PATCH 08/53] fix(daemon): keep every pane in main's layout through a terminal service restart (#28852) Restarting the terminal service ends each session with a synthetic -1 exit. Main read those as ordinary deaths and retired every terminal surface from the saved layout, while the window kept drawing them. Phones and the CLI lost the tabs, and the next launch restored a split as a single pane. The restart now labels those exits as replaced, and main records them in its existing intentional-stop register, the one terminal restarts already use. The runtime keeps the surface and the window receives the same replaced label. Claude-Session: p2-headless --- .../daemon-init-restart-sequence.test.ts | 4 +- src/main/daemon/daemon-provider-restart.ts | 4 +- .../daemon/daemon-pty-listener-emission.ts | 5 +- src/main/daemon/daemon-pty-router-events.ts | 2 + .../daemon/daemon-pty-session-inventory.ts | 8 ++- .../degraded-daemon-pty-provider.test.ts | 8 ++- .../daemon/degraded-daemon-pty-provider.ts | 8 +-- src/main/ipc/pty/provider/bind-listeners.ts | 50 ++++++++++++------- src/main/providers/pty-provider-contract.ts | 2 + .../terminal-intentional-stop-exit.test.ts | 35 +++++++++++-- .../runtime/terminal-intentional-stops.ts | 3 +- .../terminal-parked-pty-watcher.ts | 4 +- ...arked-watcher-sleep-preserved-exit.test.ts | 2 +- .../helpers/terminal-restart-persistence.ts | 5 +- .../workspace-layout-oracle-markers.ts | 16 ++++-- .../workspace-layout-oracle-session.ts | 8 ++- .../workspace-layout-oracle-known-on-main.ts | 19 +++---- 17 files changed, 123 insertions(+), 60 deletions(-) diff --git a/src/main/daemon/daemon-init-restart-sequence.test.ts b/src/main/daemon/daemon-init-restart-sequence.test.ts index df3b48417cb..e9cfcd704e9 100644 --- a/src/main/daemon/daemon-init-restart-sequence.test.ts +++ b/src/main/daemon/daemon-init-restart-sequence.test.ts @@ -74,7 +74,9 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { // killedCount must be 3 — proves the count was taken *before* fanout cleared the set (a swapped-order bug reports 0). expect(result.killedCount).toBe(3) - expect(originalAdapter.fanoutSyntheticExits).toHaveBeenCalledWith(-1) + expect(originalAdapter.fanoutSyntheticExits).toHaveBeenCalledWith(-1, { + replacedByRestart: true + }) // Ordering invariant: synthetic exits must reach the renderer *before* listeners are torn down (Step 1 before 2). expect(order).toEqual(['fanout', 'unbind']) }) diff --git a/src/main/daemon/daemon-provider-restart.ts b/src/main/daemon/daemon-provider-restart.ts index f6d11d868f8..83f476c8396 100644 --- a/src/main/daemon/daemon-provider-restart.ts +++ b/src/main/daemon/daemon-provider-restart.ts @@ -83,9 +83,9 @@ async function runRestartDaemon(expectedDaemonPid?: number): Promise(listeners: readonly T[], emit: (listener: T) export function createPtyExitPayload( id: string, - { code, incarnationId, cause }: Omit + { code, incarnationId, cause, replacedByRestart }: Omit ): DaemonPtyRouterExitEvent { return { id, code, ...(incarnationId ? { incarnationId } : {}), - ...(cause ? { cause } : {}) + ...(cause ? { cause } : {}), + ...(replacedByRestart ? { replacedByRestart } : {}) } } diff --git a/src/main/daemon/daemon-pty-router-events.ts b/src/main/daemon/daemon-pty-router-events.ts index 92b626d36e9..eb12c2d7cfe 100644 --- a/src/main/daemon/daemon-pty-router-events.ts +++ b/src/main/daemon/daemon-pty-router-events.ts @@ -15,4 +15,6 @@ export type DaemonPtyRouterExitEvent = { incarnationId?: PtyIncarnationId /** Absent from a daemon predating exit causes; readers fall back to `unknown`. */ cause?: TerminalExitCause + /** Main ended the session to restart the daemon; the pane stays for its next process. */ + replacedByRestart?: true } diff --git a/src/main/daemon/daemon-pty-session-inventory.ts b/src/main/daemon/daemon-pty-session-inventory.ts index dc3c816f1ec..5f3064dbec6 100644 --- a/src/main/daemon/daemon-pty-session-inventory.ts +++ b/src/main/daemon/daemon-pty-session-inventory.ts @@ -137,7 +137,7 @@ export abstract class DaemonPtySessionInventory extends DaemonPtyProcessInspecti // Why: the daemon's kill-all-and-shutdown path suppresses onExit fanout (session.ts:246-252), so synthesize pty:exit // for every live session before teardown or renderer panes black-hole writes to a disposed adapter forever. - fanoutSyntheticExits(code: number): void { + fanoutSyntheticExits(code: number, exit: { replacedByRestart?: true } = {}): void { const ids = [...this.activeSessionIds] this.activeSessionIds.clear() this.sessionsAwaitingDaemonRecovery.clear() @@ -158,7 +158,11 @@ export abstract class DaemonPtySessionInventory extends DaemonPtyProcessInspecti // Why: don't catch listener throws — matches the natural onExit fanout so synthetic exits keep the same error semantics. emitPtyListeners(this.exitListeners, (listener) => listener( - createPtyExitPayload(id, { code, incarnationId: this.sessionIncarnations.get(id) }) + createPtyExitPayload(id, { + code, + incarnationId: this.sessionIncarnations.get(id), + ...exit + }) ) ) this.sessionIncarnations.delete(id) diff --git a/src/main/daemon/degraded-daemon-pty-provider.test.ts b/src/main/daemon/degraded-daemon-pty-provider.test.ts index eed79501220..161a676d2b8 100644 --- a/src/main/daemon/degraded-daemon-pty-provider.test.ts +++ b/src/main/daemon/degraded-daemon-pty-provider.test.ts @@ -666,10 +666,14 @@ describe('DegradedDaemonPtyProvider', () => { provider.onExit(exitSpy) await provider.discoverDaemonSessions() - provider.fanoutCurrentDaemonSyntheticExits(-1) + provider.fanoutCurrentDaemonSyntheticExits(-1, { replacedByRestart: true }) expect(exitSpy).toHaveBeenCalledOnce() - expect(exitSpy).toHaveBeenCalledWith({ id: 'current-session', code: -1 }) + expect(exitSpy).toHaveBeenCalledWith({ + id: 'current-session', + code: -1, + replacedByRestart: true + }) expect(provider.getSessionIdsRoutedTo('current-daemon')).toEqual([]) expect(provider.hasPty('legacy-session')).toBe(true) }) diff --git a/src/main/daemon/degraded-daemon-pty-provider.ts b/src/main/daemon/degraded-daemon-pty-provider.ts index f1db303ddfd..bf021bce8f8 100644 --- a/src/main/daemon/degraded-daemon-pty-provider.ts +++ b/src/main/daemon/degraded-daemon-pty-provider.ts @@ -33,7 +33,7 @@ export class DegradedDaemonPtyProvider implements IPtyProvider { private ownerRecovery: DegradedDaemonOwnerRecovery private unsubscribers: (() => void)[] = [] private dataListeners: ((payload: PtyDataEvent) => void)[] = [] - private exitListeners: ((payload: { id: string; code: number }) => void)[] = [] + private exitListeners: Parameters[0][] = [] constructor(opts: { current: DaemonPtyAdapter @@ -267,7 +267,7 @@ export class DegradedDaemonPtyProvider implements IPtyProvider { return trackedUnsubscribe } - onExit(callback: (payload: { id: string; code: number }) => void): () => void { + onExit(callback: Parameters[0]): () => void { this.exitListeners.push(callback) return () => { const idx = this.exitListeners.indexOf(callback) @@ -317,13 +317,13 @@ export class DegradedDaemonPtyProvider implements IPtyProvider { return listProviderSessionIds(this.sessionProviders, target) } - fanoutCurrentDaemonSyntheticExits(code: number): void { + fanoutCurrentDaemonSyntheticExits(code: number, exit: { replacedByRestart?: true } = {}): void { for (const id of this.getSessionIdsRoutedTo('current-daemon')) { this.sessionProviders.delete(id) // Why: restart kills listed sessions even when the adapter did not track them active. // oxlint-disable-next-line unicorn/no-useless-spread -- copy-safe: listeners may unsubscribe during iteration for (const listener of [...this.exitListeners]) { - listener({ id, code }) + listener({ id, code, ...exit }) } } } diff --git a/src/main/ipc/pty/provider/bind-listeners.ts b/src/main/ipc/pty/provider/bind-listeners.ts index ec13ad0d36e..5d455b43539 100644 --- a/src/main/ipc/pty/provider/bind-listeners.ts +++ b/src/main/ipc/pty/provider/bind-listeners.ts @@ -88,28 +88,40 @@ export function bindProviderListeners(session: PtyIpcSession): void { return } const syntheticExit = session.consumeSyntheticKillExit(payload.id, payload.incarnationId) - if (!isLocalProvider) { - clearProviderPtyState(payload.id) - ptyOwnership.delete(payload.id) - if (syntheticExit) { - session.runtime?.markPtyStopRequested(payload.id) + // Main restarted the daemon: the pane outlives its process, for the runtime and the window. + const settleRestartStop = payload.replacedByRestart + ? session.runtime?.intentionalPtyStops?.mark( + payload.id, + 'replaced', + payload.incarnationId ?? null + ) + : undefined + try { + if (!isLocalProvider) { + clearProviderPtyState(payload.id) + ptyOwnership.delete(payload.id) + if (syntheticExit) { + session.runtime?.markPtyStopRequested(payload.id) + } + session.runtime?.onPtyExit(payload.id, payload.code, payload.incarnationId, { + providerExitObserved: true, + ...(payload.cause ? { cause: payload.cause } : {}) + }) } - session.runtime?.onPtyExit(payload.id, payload.code, payload.incarnationId, { - providerExitObserved: true, - ...(payload.cause ? { cause: payload.cause } : {}) + // The control reply can overtake stream data; the physical exit must retire that late output. + if (syntheticExit) { + return + } + // Why not the whole payload: the exit cause is a main-process fact for the + // runtime's records; the renderer's pty:exit contract stays as it was. + session.sendPtyExitToRenderer({ + id: payload.id, + code: payload.code, + ...(payload.incarnationId ? { incarnationId: payload.incarnationId } : {}) }) + } finally { + settleRestartStop?.(true) } - // The control reply can overtake stream data; the physical exit must retire that late output. - if (syntheticExit) { - return - } - // Why not the whole payload: the exit cause is a main-process fact for the - // runtime's records; the renderer's pty:exit contract stays as it was. - session.sendPtyExitToRenderer({ - id: payload.id, - code: payload.code, - ...(payload.incarnationId ? { incarnationId: payload.incarnationId } : {}) - }) }) ) } diff --git a/src/main/providers/pty-provider-contract.ts b/src/main/providers/pty-provider-contract.ts index 1f17bc05a44..b16728b4423 100644 --- a/src/main/providers/pty-provider-contract.ts +++ b/src/main/providers/pty-provider-contract.ts @@ -264,6 +264,8 @@ export type IPtyProvider = { incarnationId?: PtyIncarnationId /** Absent when the provider predates exit causes; readers must not infer one from `code`. */ cause?: TerminalExitCause + /** Main ended the session to restart the daemon; the pane stays for its next process. */ + replacedByRestart?: true }) => void ): () => void } diff --git a/src/main/runtime/terminal-intentional-stop-exit.test.ts b/src/main/runtime/terminal-intentional-stop-exit.test.ts index 041d9710936..95cac3c8c5b 100644 --- a/src/main/runtime/terminal-intentional-stop-exit.test.ts +++ b/src/main/runtime/terminal-intentional-stop-exit.test.ts @@ -84,9 +84,7 @@ function createHarness(opts: { lateProviderExit?: boolean; folder?: boolean } = }) ptyOwnership.set(PTY_ID, 'local') ptyIncarnationById.set(PTY_ID, INCARNATION_ID) - let emitProviderExit: - | ((payload: { id: string; code: number; incarnationId?: string }) => void) - | undefined + let emitProviderExit: Parameters[0] | undefined const provider = { onData: () => () => {}, onExit: (listener: typeof emitProviderExit) => { @@ -124,6 +122,14 @@ function createHarness(opts: { lateProviderExit?: boolean; folder?: boolean } = deps, emitProviderExit: (incarnationId: string) => emitProviderExit?.({ id: PTY_ID, code: 0, incarnationId }), + /** The exit main synthesizes for each session when it restarts the daemon. */ + emitDaemonRestartExit: (replacedByRestart?: true) => + emitProviderExit?.({ + id: PTY_ID, + code: -1, + incarnationId: INCARNATION_ID, + ...(replacedByRestart ? { replacedByRestart } : {}) + }), boundPtyId: () => store.getWorkspaceSession().terminalLayoutsByTabId[TAB_ID]?.ptyIdsByLeafId?.[LEAF_ID] ?? null, tabIds: () => (store.getWorkspaceSession().tabsByWorktree[WORKTREE_ID] ?? []).map((t) => t.id), @@ -210,6 +216,29 @@ describe('intentional stops keep the pane through the exit', () => { ]) }) + it('keeps the pane when main restarts the daemon under it', () => { + const harness = createHarness() + + harness.emitDaemonRestartExit(true) + + expect(harness.tabIds()).toEqual([TAB_ID]) + expect(harness.boundPtyId()).toBe(PTY_ID) + expect(harness.rendererExits()).toEqual([ + { id: PTY_ID, code: -1, incarnationId: INCARNATION_ID, replacedByRestart: true } + ]) + }) + + it('retires the pane on a daemon-wide exit that no restart claims', () => { + const harness = createHarness() + + harness.emitDaemonRestartExit() + + expect(harness.boundPtyId()).toBeNull() + expect(harness.rendererExits()).toEqual([ + { id: PTY_ID, code: -1, incarnationId: INCARNATION_ID } + ]) + }) + it('keeps the pane through the synthetic exit and the provider exit that follows it', async () => { const harness = createHarness({ lateProviderExit: true }) diff --git a/src/main/runtime/terminal-intentional-stops.ts b/src/main/runtime/terminal-intentional-stops.ts index 88ac59a40d4..836743fde67 100644 --- a/src/main/runtime/terminal-intentional-stops.ts +++ b/src/main/runtime/terminal-intentional-stops.ts @@ -3,7 +3,8 @@ import { SYNTHETIC_KILL_EXIT_DUPLICATE_WINDOW_MS } from '../ipc/pty/delivery/vis /** * Why main stopped a PTY on purpose. Both kinds keep the pane's binding through the exit: * - `reversible`: sleep or hibernation; the binding is the wake hint. - * - `replaced`: a restart stops the old process, or a reattach finds it gone; a new one takes over. + * - `replaced`: a restart stops the old process, or a reattach finds it gone; a new one takes over, + * at once or (after a daemon restart) when the pane next connects. */ export type TerminalIntentionalStopKind = 'reversible' | 'replaced' diff --git a/src/renderer/src/components/terminal-pane/terminal-parked-pty-watcher.ts b/src/renderer/src/components/terminal-pane/terminal-parked-pty-watcher.ts index 248ed6f9b6d..7541630a278 100644 --- a/src/renderer/src/components/terminal-pane/terminal-parked-pty-watcher.ts +++ b/src/renderer/src/components/terminal-pane/terminal-parked-pty-watcher.ts @@ -61,13 +61,15 @@ export function startParkedPtyWatcher(args: { return } const handlePtyExit = (code: number, { hadPrimary }: { hadPrimary: boolean }): void => { + // Why first: a daemon restart's replacement starts only when the pane next connects, so the + // dead process's title (a 'working' agent pins worktree status) must not wait for it. + useAppStore.getState().clearRuntimePaneTitle(tab.id, pane.paneId) if (isPtyExitReplacedByRestart(ptyId)) { // Why: the pane lives on under its replacement PTY; only this watcher's subscription ends. entry.disposersByPtyId.get(ptyId)?.() entry.disposersByPtyId.delete(ptyId) return } - useAppStore.getState().clearRuntimePaneTitle(tab.id, pane.paneId) // A negative code is a synthetic loss sentinel, not a death certificate. // Preserve the tab so host shutdown/reconnect cannot be mistaken for an // explicit close by either this watcher or the orphan sweep. diff --git a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-sleep-preserved-exit.test.ts b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-sleep-preserved-exit.test.ts index 0349c6aa061..7cf83aa2e21 100644 --- a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-sleep-preserved-exit.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-sleep-preserved-exit.test.ts @@ -149,7 +149,7 @@ describe('sleep-preserved parked exits (sole-owner sidecar)', () => { }) expect(closeTerminalTab).not.toHaveBeenCalled() - expect(mockStoreState.clearRuntimePaneTitle).not.toHaveBeenCalledWith(TAB_ID, 1) + expect(mockStoreState.clearRuntimePaneTitle).toHaveBeenCalledWith(TAB_ID, 1) expect(startedWatcherDisposers[0]).toHaveBeenCalled() expect(entry.disposersByPtyId.size).toBe(0) }) diff --git a/tests/e2e/helpers/terminal-restart-persistence.ts b/tests/e2e/helpers/terminal-restart-persistence.ts index 46ba4efda9f..42d8af829df 100644 --- a/tests/e2e/helpers/terminal-restart-persistence.ts +++ b/tests/e2e/helpers/terminal-restart-persistence.ts @@ -69,7 +69,8 @@ export async function bootstrapFirstLaunch( */ export async function bootstrapRestoredLaunch( page: Page, - expectedWorktreeId: string + expectedWorktreeId: string, + paneCount = 1 ): Promise { await waitForSessionReady(page) await expect @@ -80,7 +81,7 @@ export async function bootstrapRestoredLaunch( // restored terminal surface is what we're about to assert against, so make // sure it exists before any content/layout assertion races. await waitForActiveTerminalManager(page, 30_000) - await waitForPaneCount(page, 1, 30_000) + await waitForPaneCount(page, paneCount, 30_000) } export async function setPaneTitleFromTerminalMenu(page: Page, title: string): Promise { diff --git a/tests/e2e/helpers/workspace-layout-oracle-markers.ts b/tests/e2e/helpers/workspace-layout-oracle-markers.ts index e285e2acbb5..026ff74e61a 100644 --- a/tests/e2e/helpers/workspace-layout-oracle-markers.ts +++ b/tests/e2e/helpers/workspace-layout-oracle-markers.ts @@ -35,12 +35,18 @@ export async function checkPaneMarkers( continue } const marker = `ORACLE_MARK_${Date.now().toString(36)}_${index}` - await client.call('terminal.send', { - terminal: terminal.handle, - text: `echo ${marker}`, - enter: true - }) checked += 1 + try { + await client.call('terminal.send', { + terminal: terminal.handle, + text: `echo ${marker}`, + enter: true + }) + } catch (error) { + // A pane whose process is gone is a finding, not a harness fault. + failures.push(`pane ${terminal.tabId}:${terminal.leafId} refused ${marker}: ${String(error)}`) + continue + } const deadline = Date.now() + 10_000 let shown = false while (!shown && Date.now() < deadline) { diff --git a/tests/e2e/helpers/workspace-layout-oracle-session.ts b/tests/e2e/helpers/workspace-layout-oracle-session.ts index 5978956f9d0..e2cdb58bf08 100644 --- a/tests/e2e/helpers/workspace-layout-oracle-session.ts +++ b/tests/e2e/helpers/workspace-layout-oracle-session.ts @@ -122,8 +122,12 @@ export async function runOracleScenario( run.client = new RuntimeClient(session.userDataDir, 30_000) await subscribeWindowLayout(next.page, feed) await run.oracle.retarget(target(next.page, run.client)) - await (reopen ?? bootstrapRestoredLaunch)(next.page, worktreeId) - await waitForPaneCount(next.page, paneCount, 30_000).catch(() => {}) + if (reopen) { + await reopen(next.page, worktreeId) + await waitForPaneCount(next.page, paneCount, 30_000).catch(() => {}) + } else { + await bootstrapRestoredLaunch(next.page, worktreeId, paneCount) + } const restartsTerminals = coldDaemon || options.terminalsRestart const after = await run.oracle.step( coldDaemon ? 'after cold relaunch' : 'after relaunch', diff --git a/tests/e2e/workspace-layout-oracle-known-on-main.ts b/tests/e2e/workspace-layout-oracle-known-on-main.ts index f5bc67ebda4..bb4aae8c7f0 100644 --- a/tests/e2e/workspace-layout-oracle-known-on-main.ts +++ b/tests/e2e/workspace-layout-oracle-known-on-main.ts @@ -24,20 +24,13 @@ type KnownOnMain = { const TRANSIENT = /\(transient\)$/ export const LAYOUT_ORACLE_KNOWN_ON_MAIN: readonly KnownOnMain[] = [ - // The in-app daemon restart drops the worktree's tabs from main while the window keeps them. - ...(['view', 'expected', 'restart', 'marker'] as const).map((check) => ({ + // The in-app daemon restart keeps every pane, but nothing starts a new process in them. + { scenario: 'live-daemon-restart', - check, - cause: 'daemon restart empties the runtime layout' - })), - // The relaunch after it has no tab bar for the worktree, so its group is minted again (Linux CI). - ...(['rules', 'client'] as const).map((check) => ({ - scenario: 'live-daemon-restart', - check, - step: 'after relaunch', - platform: 'linux' as const, - cause: 'daemon restart empties the runtime layout' - })), + check: 'marker', + step: 'after daemon restart', + cause: 'panes stay without a process after a daemon restart' + }, // A headless host lists a worktree with no saved tab bar (and, after a restart, every worktree) // under a synthesized `headless-terminals:` group, while the layout stream names the same group // by its saved or minted id: same tabs and order, two group ids. One projection in PR 6. From 47b63b4ea1095e044dcd4573b1894a74ea63d278 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:30:53 -0400 Subject: [PATCH 09/53] chore(relay): pin staging c4 to image G for E-pre (#28854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G is main 2de73812 (relay sha256:07b5bf67…), which adds the cell's t_preauth latency metric. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f --- cloud/dev/scripts/relay-staging-c4-refresh-workflow.test.mjs | 4 ++-- cloud/infra/terraform/environments/staging.tfvars | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/dev/scripts/relay-staging-c4-refresh-workflow.test.mjs b/cloud/dev/scripts/relay-staging-c4-refresh-workflow.test.mjs index 619be7f444b..f5e183b4748 100644 --- a/cloud/dev/scripts/relay-staging-c4-refresh-workflow.test.mjs +++ b/cloud/dev/scripts/relay-staging-c4-refresh-workflow.test.mjs @@ -42,9 +42,9 @@ const asiaCells = ['production-gce-c27', 'production-gce-c28', 'production-gce-c const c30Digest = '4158d8a2e18e9caec439d257f0c1e45d92ffea8c0262f057b2f08c76a134bcf0' // C31 launches on the digest the director served when it was declared. const c31Digest = 'f30b5cb1ec52b6b6145efecfa1b8be9e3d309403beffd8abcc64197a2087e269' -// Staging C4 is declared on image F (main 3c1141f2, the six-cell dead-cell rerun); the staging +// Staging C4 is declared on image G (main 2de73812, E-pre with the t_preauth metric); the staging // director deploy binds its digest to this line. -const stagingC4Digest = '41cba995b288cab70579882ebef23e74c5ee426b6f8924cea7c986aba23bcbfe' +const stagingC4Digest = '07b5bf67a7706b2a61ef5e9a6d8ce85ee37c2a08609496a94eb5254881b5b87d' function cellBlock(tfvars, cellId) { const start = tfvars.indexOf(`"${cellId}"`) diff --git a/cloud/infra/terraform/environments/staging.tfvars b/cloud/infra/terraform/environments/staging.tfvars index 4bc485540b4..a4aff0d8600 100644 --- a/cloud/infra/terraform/environments/staging.tfvars +++ b/cloud/infra/terraform/environments/staging.tfvars @@ -86,7 +86,7 @@ relay_gce_cells = { boot_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-stable-121-18867-528-21" capacity_requests = 6000 database_pool_max = 10 - image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:41cba995b288cab70579882ebef23e74c5ee426b6f8924cea7c986aba23bcbfe" + image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:07b5bf67a7706b2a61ef5e9a6d8ce85ee37c2a08609496a94eb5254881b5b87d" initially_enabled = false connection_hard_cap = 3000 connection_unobserved_bound = 60 From f8cbfef4fd4e108272960d9b242e5cc24f60fbc8 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:31:04 -0400 Subject: [PATCH 10/53] test(session): compare what the window shows before and after the shared session loader (#28842) * test(session): compare what the window shows before and after the shared session loader Boots the window's real startup hydration on each saved-profile fixture as stored and as the shared loader rewrites it (load then save), and compares tabs, groups, active tabs, pane trees, terminal bindings, editor and browser tabs. Today's eight differences are listed with the loader rule behind each; the list may only shrink, so any new difference fails. Co-Authored-By: Claude Opus 5.5 * test(session): let the parity test own its store so the harness reads no host focus Co-Authored-By: Claude Opus 5.5 * test(session): restore the parity boot's UUID spy even when the boot throws Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .../app-shell/startup-window-parity.test.ts | 141 ++++++++++++ .../startup-window-view.test-fixture.ts | 212 ++++++++++++++++++ ...pace-layout-legacy-profile.test-fixture.ts | 121 ++++++++++ .../workspace-layout-profile.test-fixture.ts | 43 +++- .../workspace-layout-round-trip.test.ts | 27 +-- 5 files changed, 521 insertions(+), 23 deletions(-) create mode 100644 src/renderer/src/app-shell/startup-window-parity.test.ts create mode 100644 src/renderer/src/app-shell/startup-window-view.test-fixture.ts create mode 100644 src/shared/workspace-layout/workspace-layout-legacy-profile.test-fixture.ts diff --git a/src/renderer/src/app-shell/startup-window-parity.test.ts b/src/renderer/src/app-shell/startup-window-parity.test.ts new file mode 100644 index 00000000000..8fd5d1b5774 --- /dev/null +++ b/src/renderer/src/app-shell/startup-window-parity.test.ts @@ -0,0 +1,141 @@ +// Window parity: for each stored profile, the window's startup hydration of the session as stored +// and of the shared Loader's rewrite of it (load then save) must show the same thing. Where they +// differ today, the difference is listed below with the Loader rule that causes it. + +import { describe, expect, it, vi } from 'vitest' +import type * as AgentStatusModule from '@/lib/agent-status' +import type { ExecutionHostId } from '../../../shared/execution-host' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' +import { LEGACY_WORKSPACE_LAYOUT_PROFILES } from '../../../shared/workspace-layout/workspace-layout-legacy-profile.test-fixture' +import { loadWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-load' +import { WORKSPACE_LAYOUT_PROFILES } from '../../../shared/workspace-layout/workspace-layout-profile.test-fixture' +import { saveWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-save' +import { createStoreSessionMockApi } from '../store/slices/store-session-test-harness' +import { createTestStore } from '../store/slices/store-test-helpers' +import { + bootWindowFromPartitions, + diffStartupWindowViews, + projectStartupWindowView +} from './startup-window-view.test-fixture' + +vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } })) +vi.mock('@/lib/agent-status', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, detectAgentStatusFromTitle: vi.fn().mockReturnValue(null) } +}) + +createStoreSessionMockApi() + +const GIT = 'repo-1::/Users/dev/orca' +const terminal = (tabId: string) => + ['title', 'panes', 'focused pane', 'reattaches'].map( + (field) => `${GIT} terminal ${tabId} ${field}` + ) + +type KnownDifference = { + fixture: string + /** The Loader rule that makes the window show something else. */ + rule: string + /** View paths that differ; see `projectStartupWindowView`. */ + paths: string[] +} + +/** Today's differences. Entries may only be removed: fix the rule, then delete its entry. */ +const KNOWN_DIFFERENCES: readonly KnownDifference[] = [ + { + fixture: 'row without a tab-bar entry or a terminal', + rule: 'ghost tab: the Loader gives every row a tab-bar entry; the window adopts only rows it can reattach', + paths: [`${GIT} group 0 tabs`, `${GIT} tab tab-b`, ...terminal('tab-b')] + }, + { + fixture: 'legacy row repeating a tab-bar tab’s terminal', + rule: 'duplicate tab: the window drops a legacy row whose terminal a tab-bar tab holds; the Loader keeps it as a tab with an unbound pane', + paths: [`${GIT} group 0 tabs`, `${GIT} tab tab-legacy`, ...terminal('tab-legacy')] + }, + { + fixture: 'row terminal differs from its focused pane’s', + rule: 'row.ptyId rewrite: the Loader sets the row terminal to its focused pane’s, which changes what the tab reattaches', + paths: [`${GIT} terminal tab-a reattaches`] + }, + { + fixture: 'row with no terminal while its pane has one', + rule: 'row.ptyId rewrite: the Loader sets the row terminal to its focused pane’s, which changes what the tab reattaches', + paths: [`${GIT} terminal tab-a reattaches`] + }, + { + fixture: 'tab-bar entry declaring a group that does not list it', + rule: 'group owner: the window keeps a tab in the group its entry declares; the Loader moves it to the first group listing it', + paths: [ + `${GIT} groups`, + `${GIT} group 0 tabs`, + `${GIT} group 1 tabs`, + `${GIT} group 1 active tab`, + `${GIT} tab tab-b` + ] + }, + { + fixture: 'session from before the tab bar, with an open file', + rule: 'old sessions: the window builds tabs from rows and open files; the Loader builds them from rows only, losing editor tabs', + paths: [ + `${GIT} group 0 tabs`, + `${GIT} group 0 active tab`, + `${GIT} tab /Users/dev/orca/src/app.ts` + ] + }, + { + fixture: 'legacy surface tombstone on a pane still showing its terminal', + rule: 'tombstones: the Loader closes a pane a legacy surface tombstone names; the window ignores the tombstone and shows the pane', + paths: [`${GIT} group 0 tabs`, `${GIT} tab tab-b`, ...terminal('tab-b')] + }, + { + fixture: 'tab-bar label differing from the row title', + rule: 'label over title: the Loader writes the tab-bar label into the row title the window shows', + paths: [`${GIT} terminal tab-a title`] + } +] + +/** The shared Loader's round trip, as main would persist it. */ +function normalize(hostId: ExecutionHostId, session: WorkspaceSessionState): WorkspaceSessionState { + let next = 0 + const loaded = loadWorkspaceLayout(hostId, structuredClone(session), { + mintId: () => `minted-${++next}`, + mintLeafId: () => `00000000-0000-4000-9000-${String(++next).padStart(12, '0')}` + }) + return saveWorkspaceLayout(loaded) +} + +const PROFILES = [...WORKSPACE_LAYOUT_PROFILES, ...LEGACY_WORKSPACE_LAYOUT_PROFILES] + +describe('the window shows the same session before and after the shared Loader rewrites it', () => { + it.each(PROFILES.map((profile) => [profile.name, profile] as const))( + '%s', + async (name, { hostId, build }) => { + const stored = build() + const storedView = projectStartupWindowView( + await bootWindowFromPartitions(createTestStore(), [{ hostId, session: stored }]) + ) + const normalizedView = projectStartupWindowView( + await bootWindowFromPartitions(createTestStore(), [ + { hostId, session: normalize(hostId, stored) } + ]) + ) + // The window must show the stored workspaces at all, or an empty view hides differences. + for (const [key, rows] of Object.entries(stored.tabsByWorktree)) { + if (rows.length > 0) { + expect(storedView, key).toHaveProperty([`${key} group 0 tabs`]) + } + } + const differences = diffStartupWindowViews(storedView, normalizedView) + const known = KNOWN_DIFFERENCES.filter((entry) => entry.fixture === name) + expect( + differences.map((difference) => difference.path).sort(), + `window differences (stored vs normalized):\n${JSON.stringify(differences, null, 2)}` + ).toEqual(known.flatMap((entry) => entry.paths).sort()) + } + ) + + it('lists differences only for fixtures that exist', () => { + const names = new Set(PROFILES.map((profile) => profile.name)) + expect(KNOWN_DIFFERENCES.filter((entry) => !names.has(entry.fixture))).toEqual([]) + }) +}) diff --git a/src/renderer/src/app-shell/startup-window-view.test-fixture.ts b/src/renderer/src/app-shell/startup-window-view.test-fixture.ts new file mode 100644 index 00000000000..ae0267af904 --- /dev/null +++ b/src/renderer/src/app-shell/startup-window-view.test-fixture.ts @@ -0,0 +1,212 @@ +// Boots a test store through the window's startup session hydration and reduces the result to +// what the window shows, so two stored sessions can be compared by what a user would see. + +import { vi } from 'vitest' +import { getDefaultWorkspaceSession } from '../../../shared/constants' +import { + LOCAL_EXECUTION_HOST_ID, + parseExecutionHostId, + type ExecutionHostId +} from '../../../shared/execution-host' +import type { TabGroupLayoutNode } from '../../../shared/tab-types' +import type { + TerminalLayoutSnapshot, + TerminalPaneLayoutNode +} from '../../../shared/terminal-tab-types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' +import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' +import { fetchWorkspaceSessionWithRuntimeHostOwners } from '@/lib/workspace-session-host-hydration' +import { collectFolderWorkspaceKeysFromSession } from '@/lib/workspace-session-hydration-keys' +import type { AppState } from '../store/types' +import type { createTestStore } from '../store/slices/store-test-helpers' +import { reconcileHydratedWorkspaceTabModels } from './reconcile-hydrated-workspace-tab-models' + +/** One stored session partition and the host it is stored under. */ +export type StoredPartition = { hostId: ExecutionHostId; session: WorkspaceSessionState } + +/** + * The local catalog at first paint: a repo for each git workspace the partitions name, with no + * worktree listing yet. Remote-server repos are absent, as startup skips remote catalogs. + */ +function firstPaintRepos(partitions: readonly StoredPartition[]): AppState['repos'] { + const repos = new Map() + for (const { hostId, session } of partitions) { + const host = parseExecutionHostId(hostId) + if (host?.kind === 'runtime') { + continue + } + for (const key of Object.keys(session.tabsByWorktree)) { + const id = getRepoIdFromWorktreeId(key) + if (!key.startsWith(`${id}::`) || repos.has(id)) { + continue + } + repos.set(id, { + id, + path: key.slice(id.length + 2), + displayName: id, + badgeColor: '#000', + addedAt: 0, + connectionId: host?.kind === 'ssh' ? host.targetId : null, + executionHostId: hostId + }) + } + } + return [...repos.values()] +} + +/** + * Runs the same hydration calls, in the same order, as `use-app-startup-hydration.ts`, on a fresh + * test store the caller creates. + */ +export async function bootWindowFromPartitions( + store: ReturnType, + partitions: readonly StoredPartition[] +): Promise { + let minted = 0 + // Group and tab ids the window mints must match across the two boots being compared. + const randomUUID = vi + .spyOn(globalThis.crypto, 'randomUUID') + .mockImplementation(() => `00000000-0000-4000-8000-${String(++minted).padStart(12, '0')}`) + try { + store.setState({ repos: firstPaintRepos(partitions) }) + const read = async (hostId: ExecutionHostId = LOCAL_EXECUTION_HOST_ID) => + structuredClone( + partitions.find((partition) => partition.hostId === hostId)?.session ?? + getDefaultWorkspaceSession() + ) + const hostIdsOfKind = (kind: 'ssh' | 'runtime') => + partitions + .map((partition) => partition.hostId) + .filter((hostId) => parseExecutionHostId(hostId)?.kind === kind) + const sessionRead = await fetchWorkspaceSessionWithRuntimeHostOwners( + { get: read, listHostIds: async () => hostIdsOfKind('ssh') }, + store.getState().repos, + hostIdsOfKind('runtime') + ) + const actions = store.getState() + const options = { + additionalValidWorkspaceKeys: collectFolderWorkspaceKeysFromSession(sessionRead.session) + } + actions.hydrateWorkspaceSession(sessionRead.session, { + ...options, + runtimeHostIdByWorkspaceSessionKey: sessionRead.runtimeHostIdByWorkspaceSessionKey, + contestedHostWorkspaceSessions: sessionRead.contestedHostWorkspaceSessions, + contestedPrimaryHostBySessionKey: sessionRead.contestedPrimaryHostBySessionKey + }) + actions.hydrateTabsSession(sessionRead.session, options) + actions.hydrateEditorSession(sessionRead.session, options) + actions.hydrateBrowserSession(sessionRead.session, options) + reconcileHydratedWorkspaceTabModels( + sessionRead.session, + store.getState().reconcileWorktreeTabModels + ) + return store.getState() + } finally { + randomUUID.mockRestore() + } +} + +type PaneView = { pane: string | null } | { split: string; first: PaneView; second: PaneView } + +/** Panes by position, each naming the terminal it binds; leaf ids are not shown. */ +function paneView(node: TerminalPaneLayoutNode, layout: TerminalLayoutSnapshot): PaneView { + if (node.type === 'leaf') { + return { pane: layout.ptyIdsByLeafId?.[node.leafId] ?? null } + } + return { + split: node.direction, + first: paneView(node.first, layout), + second: paneView(node.second, layout) + } +} + +function leafPosition( + node: TerminalPaneLayoutNode, + leafId: string | null, + path = '' +): string | null { + if (node.type === 'leaf') { + return node.leafId === leafId ? path || 'root' : null + } + return ( + leafPosition(node.first, leafId, `${path}1`) ?? leafPosition(node.second, leafId, `${path}2`) + ) +} + +type GroupLayoutView = number | { split: string; first: GroupLayoutView; second: GroupLayoutView } + +function groupLayoutView(node: TabGroupLayoutNode, groupIds: string[]): GroupLayoutView { + if (node.type === 'leaf') { + return groupIds.indexOf(node.groupId) + } + return { + split: node.direction, + first: groupLayoutView(node.first, groupIds), + second: groupLayoutView(node.second, groupIds) + } +} + +/** + * What the window shows after startup, as `path -> value`. Groups are named by position, since + * a group id is never shown; tab ids stay, since a tab keeps its id across restarts. + */ +export function projectStartupWindowView(state: AppState): Record { + const view: Record = { + 'active.workspace': state.activeWorktreeId, + 'active.terminal': state.activeTabId + } + const workspaceKeys = new Set([ + ...Object.keys(state.tabsByWorktree), + ...Object.keys(state.unifiedTabsByWorktree), + ...state.openFiles.map((file) => file.worktreeId), + ...Object.keys(state.browserTabsByWorktree) + ]) + for (const key of [...workspaceKeys].sort()) { + const groups = state.groupsByWorktree[key] ?? [] + const groupIds = groups.map((group) => group.id) + const layout = state.layoutByWorktree[key] + view[`${key} groups`] = layout ? groupLayoutView(layout, groupIds) : groupIds.length + groups.forEach((group, index) => { + view[`${key} group ${index} tabs`] = group.tabOrder + view[`${key} group ${index} active tab`] = group.activeTabId + }) + view[`${key} active group`] = groupIds.indexOf(state.activeGroupIdByWorktree[key] ?? '') + view[`${key} active terminal`] = state.activeTabIdByWorktree[key] ?? null + view[`${key} active file`] = state.activeFileIdByWorktree[key] ?? null + view[`${key} active kind`] = state.activeTabTypeByWorktree[key] ?? null + for (const tab of state.unifiedTabsByWorktree[key] ?? []) { + view[`${key} tab ${tab.id}`] = { + kind: tab.contentType, + shows: tab.entityId, + group: groupIds.indexOf(tab.groupId), + label: tab.customLabel ?? tab.label + } + } + for (const row of state.tabsByWorktree[key] ?? []) { + const panes = state.terminalLayoutsByTabId[row.id] + const terminal = `${key} terminal ${row.id}` + view[`${terminal} title`] = row.customTitle ?? row.title + view[`${terminal} panes`] = panes?.root ? paneView(panes.root, panes) : null + view[`${terminal} focused pane`] = panes?.root + ? leafPosition(panes.root, panes.activeLeafId) + : null + view[`${terminal} reattaches`] = state.pendingReconnectPtyIdByTabId[row.id] ?? null + } + view[`${key} open files`] = state.openFiles + .filter((file) => file.worktreeId === key) + .map((file) => file.filePath) + view[`${key} browser tabs`] = (state.browserTabsByWorktree[key] ?? []).map((tab) => tab.url) + } + return view +} + +/** Each path whose value differs between two views, with both values. */ +export function diffStartupWindowViews( + stored: Record, + normalized: Record +): { path: string; stored: unknown; normalized: unknown }[] { + const paths = [...new Set([...Object.keys(stored), ...Object.keys(normalized)])].sort() + return paths + .filter((path) => JSON.stringify(stored[path]) !== JSON.stringify(normalized[path])) + .map((path) => ({ path, stored: stored[path], normalized: normalized[path] })) +} diff --git a/src/shared/workspace-layout/workspace-layout-legacy-profile.test-fixture.ts b/src/shared/workspace-layout/workspace-layout-legacy-profile.test-fixture.ts new file mode 100644 index 00000000000..dadd428053a --- /dev/null +++ b/src/shared/workspace-layout/workspace-layout-legacy-profile.test-fixture.ts @@ -0,0 +1,121 @@ +// Stored partitions older writers leave disagreeing with themselves, one Loader rule each, so a +// test can compare what the window shows for each before and after the Loader rewrites it. + +import { LOCAL_EXECUTION_HOST_ID } from '../execution-host' +import type { WorkspaceSessionState } from '../workspace-session-state-types' +import type { WorkspaceLayoutProfile } from './workspace-layout-profile.test-fixture' +import { addWorkspace, emptySession, GIT_KEY, leaf } from './workspace-layout-session.test-fixture' + +function twoTabs(): WorkspaceSessionState { + return addWorkspace(emptySession(), GIT_KEY, [ + { + id: 'g1', + tabs: [ + { id: 'tab-a', leaves: [[leaf(1), 'pty-a']] }, + { id: 'tab-b', leaves: [[leaf(2), 'pty-b']] } + ] + } + ]) +} + +/** Drops a terminal's tab-bar entry, leaving its row and pane layout. */ +function withoutTabBarEntry(session: WorkspaceSessionState, tabId: string): WorkspaceSessionState { + session.unifiedTabs![GIT_KEY] = session.unifiedTabs![GIT_KEY]!.filter((tab) => tab.id !== tabId) + const group = session.tabGroups![GIT_KEY]![0]! + group.tabOrder = group.tabOrder.filter((id) => id !== tabId) + return session +} + +const local = (name: string, build: () => WorkspaceSessionState): WorkspaceLayoutProfile => ({ + name, + hostId: LOCAL_EXECUTION_HOST_ID, + build +}) + +export const LEGACY_WORKSPACE_LAYOUT_PROFILES: readonly WorkspaceLayoutProfile[] = [ + local('row without a tab-bar entry or a terminal', () => { + const session = withoutTabBarEntry(twoTabs(), 'tab-b') + session.tabsByWorktree[GIT_KEY]![1]!.ptyId = null + session.terminalLayoutsByTabId['tab-b']!.ptyIdsByLeafId = {} + return session + }), + // Control for the case above: the window adopts a row it can reattach, as the Loader does. + local('row without a tab-bar entry, still bound to a terminal', () => + withoutTabBarEntry(twoTabs(), 'tab-b') + ), + local('legacy row repeating a tab-bar tab’s terminal', () => { + const session = twoTabs() + session.tabsByWorktree[GIT_KEY]!.push({ + ...session.tabsByWorktree[GIT_KEY]![0]!, + id: 'tab-legacy', + title: 'Terminal 3', + sortOrder: 2, + createdAt: 1_700_000_009_000 + }) + session.terminalLayoutsByTabId['tab-legacy'] = { + root: { type: 'leaf', leafId: leaf(3) }, + activeLeafId: leaf(3), + expandedLeafId: null, + ptyIdsByLeafId: { [leaf(3)]: 'pty-a' } + } + return session + }), + local('row terminal differs from its focused pane’s', () => { + const session = twoTabs() + session.tabsByWorktree[GIT_KEY]![0]!.ptyId = 'pty-stale' + return session + }), + local('row with no terminal while its pane has one', () => { + const session = twoTabs() + session.tabsByWorktree[GIT_KEY]![0]!.ptyId = null + return session + }), + local('tab-bar entry declaring a group that does not list it', () => { + const session = addWorkspace(emptySession(), GIT_KEY, [ + { id: 'g1', tabs: [{ id: 'tab-a', leaves: [[leaf(1), 'pty-a']] }] }, + { id: 'g2', tabs: [{ id: 'tab-b', leaves: [[leaf(2), 'pty-b']] }] } + ]) + // g1 lists tab-b; its entry still says g2, and g2 lists it too. + session.tabGroups![GIT_KEY]![0]!.tabOrder.push('tab-b') + return session + }), + local('session from before the tab bar, with an open file', () => { + const session = twoTabs() + delete session.unifiedTabs + delete session.tabGroups + delete session.tabGroupLayouts + delete session.activeGroupIdByWorktree + session.openFilesByWorktree = { + [GIT_KEY]: [ + { + filePath: '/Users/dev/orca/src/app.ts', + relativePath: 'src/app.ts', + worktreeId: GIT_KEY, + language: 'typescript' + } + ] + } + session.activeFileIdByWorktree = { [GIT_KEY]: '/Users/dev/orca/src/app.ts' } + session.activeTabTypeByWorktree = { [GIT_KEY]: 'editor' } + return session + }), + local('legacy surface tombstone on a pane still showing its terminal', () => { + const session = twoTabs() + session.terminalSurfaceTombstonesByPaneKey = { + [`tab-b:${leaf(2)}`]: { + worktreeId: GIT_KEY, + parentTabId: 'tab-b', + leafId: leaf(2), + ptyId: 'pty-b', + incarnationId: 'inc', + retiredAt: 1 + } + } + return session + }), + local('tab-bar label differing from the row title', () => { + const session = twoTabs() + session.unifiedTabs![GIT_KEY]![0]!.label = 'vim' + return session + }) +] diff --git a/src/shared/workspace-layout/workspace-layout-profile.test-fixture.ts b/src/shared/workspace-layout/workspace-layout-profile.test-fixture.ts index b6d326fc563..58b009f2576 100644 --- a/src/shared/workspace-layout/workspace-layout-profile.test-fixture.ts +++ b/src/shared/workspace-layout/workspace-layout-profile.test-fixture.ts @@ -1,7 +1,13 @@ // Real-shaped stored partitions: the desktop's own, a relay-fallback SSH host's, a remote server's. import type { SleepingAgentSessionRecord } from '../agent-session-resume' -import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../execution-host' +import { FLOATING_TERMINAL_WORKTREE_ID } from '../constants' +import { + LOCAL_EXECUTION_HOST_ID, + toRuntimeExecutionHostId, + toSshExecutionHostId, + type ExecutionHostId +} from '../execution-host' import { toRemoteRuntimePtyId } from '../remote-runtime-pty-id' import { toAppSshPtyId } from '../ssh-pty-id' import type { WorkspaceSessionState } from '../workspace-session-state-types' @@ -242,3 +248,38 @@ export function serverRuntimeSession(environmentId: string): WorkspaceSessionSta toRuntimeExecutionHostId(environmentId) ) } + +export type WorkspaceLayoutProfile = { + name: string + /** The partition the session is stored under. */ + hostId: ExecutionHostId + build: () => WorkspaceSessionState +} + +/** One stored partition per kind of writer today. */ +export const WORKSPACE_LAYOUT_PROFILES: readonly WorkspaceLayoutProfile[] = [ + { + name: 'local desktop (git worktree, folder workspace, all tab kinds)', + hostId: LOCAL_EXECUTION_HOST_ID, + build: localDesktopSession + }, + { + name: 'floating terminal', + hostId: LOCAL_EXECUTION_HOST_ID, + build: () => + addWorkspace(emptySession(), FLOATING_TERMINAL_WORKTREE_ID, [ + { id: 'group-floating', tabs: [{ id: 'tab-floating', leaves: [[leaf(1), 'floating@@1']] }] } + ]) + }, + { + name: 'relay SSH host partition', + hostId: toSshExecutionHostId('target-1'), + build: () => relaySshSession('target-1') + }, + { + name: 'remote server partition', + hostId: toRuntimeExecutionHostId('env-1'), + build: () => serverRuntimeSession('env-1') + }, + { name: 'empty profile', hostId: LOCAL_EXECUTION_HOST_ID, build: emptySession } +] diff --git a/src/shared/workspace-layout/workspace-layout-round-trip.test.ts b/src/shared/workspace-layout/workspace-layout-round-trip.test.ts index 6cee2624e51..96567a9fe64 100644 --- a/src/shared/workspace-layout/workspace-layout-round-trip.test.ts +++ b/src/shared/workspace-layout/workspace-layout-round-trip.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { FLOATING_TERMINAL_WORKTREE_ID } from '../constants' import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../execution-host' import type { WorkspaceSessionState } from '../workspace-session-state-types' import { loadWorkspaceLayout } from './workspace-layout-load' @@ -8,10 +7,9 @@ import { checkWorkspaceLayoutRules } from './workspace-layout-rules' import { saveWorkspaceLayout } from './workspace-layout-save' import { localDesktopSession, - relaySshSession, - serverRuntimeSession + WORKSPACE_LAYOUT_PROFILES } from './workspace-layout-profile.test-fixture' -import { addWorkspace, emptySession, leaf } from './workspace-layout-session.test-fixture' +import { leaf } from './workspace-layout-session.test-fixture' /** What reaches disk: the profile documents are JSON, so an undefined field is a missing one. */ const onDisk = (session: WorkspaceSessionState): unknown => JSON.parse(JSON.stringify(session)) @@ -34,24 +32,9 @@ function roundTrip(hostId: ExecutionHostId, session: WorkspaceSessionState) { return { loaded, saved: saveWorkspaceLayout(loaded) } } -const PROFILES: [string, ExecutionHostId, () => WorkspaceSessionState][] = [ - [ - 'local desktop (git worktree, folder workspace, all tab kinds)', - LOCAL_EXECUTION_HOST_ID, - localDesktopSession - ], - [ - 'floating terminal', - LOCAL_EXECUTION_HOST_ID, - () => - addWorkspace(emptySession(), FLOATING_TERMINAL_WORKTREE_ID, [ - { id: 'group-floating', tabs: [{ id: 'tab-floating', leaves: [[leaf(1), 'floating@@1']] }] } - ]) - ], - ['relay SSH host partition', 'ssh:target-1', () => relaySshSession('target-1')], - ['remote server partition', 'runtime:env-1', () => serverRuntimeSession('env-1')], - ['empty profile', LOCAL_EXECUTION_HOST_ID, emptySession] -] +const PROFILES = WORKSPACE_LAYOUT_PROFILES.map( + (profile) => [profile.name, profile.hostId, profile.build] as const +) describe('workspace layout Loader and Serializer', () => { it.each(PROFILES)( From ebd4f00453d9a634cb2bcbbf745757009ccfc523 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:37:57 -0700 Subject: [PATCH 11/53] Skip reindexing unchanged transcripts on cold session list reads (#28798) * Skip reindexing unchanged transcripts on cold session list reads A cold session-list read re-parsed every self-contained transcript and rewrote its search rows, even when nothing had changed. That wasted work and churned the index on every list. - When a transcript's stat (mtime, size, inode, device) matches a complete index entry, the list read now skips the rewrite. Agents with sidecar metadata, unknown sizes, or partial or stale entries still reread. - Test fixtures that replay a "changed" transcript now pass a new mtime so they still exercise the replace path. - New tests cover unchanged skips, changed size/mtime/file identity, sidecar refresh, due/failed repair, and append. - Dependent-transcript and Codex title tests check that cold rereads keep search and title data current. * Make the self-contained transcript check exhaustive over every agent - Replace the hand-maintained `Set` of self-contained agents with a `Record`, so adding an agent type fails typecheck until someone decides whether its transcript is self-contained. - Classification is unchanged: the agents that were in the set are `true`, and the rest are `false` with a short note on the sidecar or metadata source they depend on. - Make the replace-generation tests derive the second mtime from the first candidate instead of a hard-coded timestamp, so they stay valid if the default fixture mtime changes. * Guard file-stat reuse with a self-contained transcript test Indexing skips reparsing unchanged transcripts for agents marked self-contained. That is only safe if their parsers read nothing but the transcript itself, and nothing enforced it. A parser that started reading a sidecar or dependency file could serve stale index entries. - Export SELF_CONTAINED_TRANSCRIPT_AGENTS so the test checks the real list - Add a test that parses each self-contained agent's fixture, both direct and cached, and fails if any file other than the transcript is read - Register the test in the SQLite runtime-file include list * Require stored file identity before skipping a transcript rewrite - Rows with a NULL dev or ino were treated as covered, so a file replaced with the same size and mtime could be skipped and never reindexed. - Such rows now count as uncovered, so the rewrite runs and records the identity. - The identity is read through the shared `fileIdentity` helper instead of the raw file fields. * Add regression test for session rows indexed without file identity A session row indexed before file identity (dev/ino) was recorded was never reread once the stat began supplying one, so it could keep stale search content. This test covers that case: the first read with identity replaces the row, and a second read with the same identity leaves it alone. --- .../vitest-sqlite-database-runtime-files.mjs | 3 + ...ssion-search-dependent-transcripts.test.ts | 195 ++++++++++++++++++ .../session-search-file-write.test.ts | 16 +- .../session-search-index-consumer.ts | 38 ++++ .../session-search-list-reparse.test.ts | 123 +++++++++++ .../ai-vault-search/session-search-store.ts | 38 +++- ...ion-search-transcript-dependencies.test.ts | 121 +++++++++++ 7 files changed, 527 insertions(+), 7 deletions(-) create mode 100644 src/main/ai-vault-search/session-search-dependent-transcripts.test.ts create mode 100644 src/main/ai-vault-search/session-search-list-reparse.test.ts create mode 100644 src/main/ai-vault-search/session-search-transcript-dependencies.test.ts diff --git a/config/scripts/vitest-sqlite-database-runtime-files.mjs b/config/scripts/vitest-sqlite-database-runtime-files.mjs index 43701691d05..fcb249648e2 100644 --- a/config/scripts/vitest-sqlite-database-runtime-files.mjs +++ b/config/scripts/vitest-sqlite-database-runtime-files.mjs @@ -1,7 +1,10 @@ // Database opening and lifecycle fixtures exercise Node SQLite behavior. export const SQLITE_DATABASE_RUNTIME_INCLUDE = [ + 'src/main/ai-vault-search/session-search-dependent-transcripts.test.ts', 'src/main/ai-vault-search/session-search-file-write.test.ts', 'src/main/ai-vault-search/session-search-live-transcript.test.ts', + 'src/main/ai-vault-search/session-search-list-reparse.test.ts', + 'src/main/ai-vault-search/session-search-transcript-dependencies.test.ts', 'src/main/ai-vault/session-scanner-devin-contention.test.ts', 'src/main/ai-vault/session-scanner-opencode-native-worker.test.ts', 'src/main/ai-vault/session-scanner-opencode-sqlite-bounds.test.ts', diff --git a/src/main/ai-vault-search/session-search-dependent-transcripts.test.ts b/src/main/ai-vault-search/session-search-dependent-transcripts.test.ts new file mode 100644 index 00000000000..daf8b38877a --- /dev/null +++ b/src/main/ai-vault-search/session-search-dependent-transcripts.test.ts @@ -0,0 +1,195 @@ +import { mkdir, stat, unlink, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { afterEach, beforeEach, expect, it } from 'vitest' +import { + parseAgentSessionFileCached, + resetSessionParseCacheForTests +} from '../ai-vault/session-scanner-parse-cache' +import { resetCodexSessionIndexTitleCacheForTests } from '../ai-vault/session-scanner-codex-title-index' +import type { SessionFileCandidate } from '../ai-vault/session-scanner-types' +import { resetTranscriptConsumersForTests } from '../ai-vault/session-transcript-consumers' +import { registerSessionSearchIndexConsumer } from './session-search-index-consumer' +import { + openSessionSearchIndexFile, + type SessionSearchIndexFile +} from './session-search-index-test-fixture' +import { SessionSearchStore } from './session-search-store' +import { + CODEX_ROLLOUT_FILE, + CODEX_SESSION_ID, + codexRolloutLines, + sessionCandidate +} from './session-search-transcript-fixtures' + +let index: SessionSearchIndexFile +let store: SessionSearchStore +let root: string +let errors: unknown[] +const SESSION_ID = '0b5e1c2a-3d4f-4a5b-8c6d-7e8f9a0b1c2d' + +beforeEach(async () => { + resetTranscriptConsumersForTests() + resetCodexSessionIndexTitleCacheForTests() + index = await openSessionSearchIndexFile('ss-dependent-transcripts') + root = dirname(index.path) + errors = [] + store = new SessionSearchStore(index.path, (error) => errors.push(error)) + registerSessionSearchIndexConsumer(store) +}) + +afterEach(async () => { + resetTranscriptConsumersForTests() + resetSessionParseCacheForTests() + resetCodexSessionIndexTitleCacheForTests() + store.close() + await index.close() +}) + +async function writeSource(path: string, content: string): Promise { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, content) +} + +async function coldRead(candidate: SessionFileCandidate) { + resetSessionParseCacheForTests() + const session = await parseAgentSessionFileCached(candidate, process.platform) + expect(errors).toEqual([]) + expect(session).not.toBeNull() + return session +} + +function matches(term: string): unknown { + return index.db + .prepare( + `SELECT count(*) AS n FROM messages_fts + JOIN messages m ON m.id = messages_fts.rowid + JOIN sessions s ON s.id = m.session_row_id + WHERE messages_fts MATCH ?` + ) + .get(term) +} + +it.each(['cline', 'kiro'] as const)( + 'removes old searchable messages when the %s sidecar is deleted', + async (agent) => { + const path = join(root, agent, SESSION_ID, `${SESSION_ID}.json`) + const sidecarPath = agent === 'cline' ? path.replace(/\.json$/, '.messages.json') : `${path}l` + await writeSource(path, JSON.stringify({ session_id: SESSION_ID, title: 'Conversation' })) + await writeSource( + sidecarPath, + agent === 'cline' + ? JSON.stringify({ messages: [{ role: 'user', content: 'oldsidecarmessage' }] }) + : `${JSON.stringify({ + kind: 'Prompt', + data: { content: [{ kind: 'text', data: 'oldsidecarmessage' }] } + })}\n` + ) + const candidate = await sessionCandidate(agent, path) + const sidecar = await stat(sidecarPath) + candidate.file.sidecar = { + path: sidecarPath, + mtimeMs: sidecar.mtimeMs, + sizeBytes: sidecar.size + } + await coldRead(candidate) + expect(matches('oldsidecarmessage')).toEqual({ n: 1 }) + + await unlink(sidecarPath) + const after = await sessionCandidate(agent, path) + expect(after.file).toEqual({ ...candidate.file, sidecar: undefined }) + after.file.sidecar = 'none' + expect(await coldRead(after)).toMatchObject({ messageCount: 0 }) + expect(matches('oldsidecarmessage')).toEqual({ n: 0 }) + expect(index.db.prepare('SELECT state FROM files').get()).toEqual({ state: 'current' }) + } +) + +it.each(['opencode', 'kimi', 'grok'] as const)( + 'repairs %s search content after a cold reread of unchanged candidate metadata', + async (agent) => { + const source = await multiFileSource(agent) + await writeSource(source.messagesPath, source.message('olddependentmessage')) + const candidate = await sessionCandidate(agent, source.path) + candidate.file.sidecar = 'none' + await coldRead(candidate) + expect(matches('olddependentmessage')).toEqual({ n: 1 }) + + await writeSource(source.messagesPath, source.message('newdependentmessage')) + expect((await sessionCandidate(agent, source.path)).file).toEqual({ + ...candidate.file, + sidecar: undefined + }) + const session = await coldRead(candidate) + expect(session?.previewMessages).toEqual( + expect.arrayContaining([expect.objectContaining({ text: 'newdependentmessage' })]) + ) + expect(matches('newdependentmessage')).toEqual({ n: 1 }) + expect(matches('olddependentmessage')).toEqual({ n: 0 }) + } +) + +async function multiFileSource(agent: 'opencode' | 'kimi' | 'grok') { + if (agent === 'opencode') { + const path = join(root, 'storage', 'session', 'project', 'ses_fixture.json') + await writeSource(path, JSON.stringify({ id: SESSION_ID, title: 'Conversation' })) + return { + path, + messagesPath: join(root, 'storage', 'message', SESSION_ID, 'msg_fixture.json'), + message: (text: string) => JSON.stringify({ role: 'user', content: text }) + } + } + if (agent === 'kimi') { + const directory = join(root, 'kimi', 'sessions', 'wd_app_abc', `session_${SESSION_ID}`) + const path = join(directory, 'state.json') + await writeSource(path, JSON.stringify({ title: 'Conversation' })) + return { + path, + messagesPath: join(directory, 'agents', 'main', 'wire.jsonl'), + message: (text: string) => + `${JSON.stringify({ + type: 'context.append_message', + message: { role: 'user', origin: { kind: 'user' }, content: text } + })}\n` + } + } + const path = join(root, 'grok', SESSION_ID, 'summary.json') + await writeSource( + path, + JSON.stringify({ info: { id: SESSION_ID }, generated_title: 'Conversation' }) + ) + return { + path, + messagesPath: join(dirname(path), 'chat_history.jsonl'), + message: (text: string) => + `${JSON.stringify({ type: 'user', content: `${text}` })}\n` + } +} + +it('updates the indexed Codex title when only session_index.jsonl changes', async () => { + const home = join(root, 'codex') + const path = join(home, 'sessions', CODEX_ROLLOUT_FILE) + await writeSource(path, `${codexRolloutLines(['pwd'], '/repo/app', 'codexmessage').join('\n')}\n`) + const titleIndex = join(home, 'session_index.jsonl') + await writeSource( + titleIndex, + `${JSON.stringify({ id: CODEX_SESSION_ID, thread_name: 'Original' })}\n` + ) + const candidate = await sessionCandidate('codex', path, home) + candidate.file.sidecar = 'none' + await coldRead(candidate) + expect(index.db.prepare('SELECT title FROM sessions').get()).toEqual({ title: 'Original' }) + + await writeSource( + titleIndex, + `${JSON.stringify({ id: CODEX_SESSION_ID, thread_name: 'Renamed conversation' })}\n` + ) + expect((await sessionCandidate('codex', path, home)).file).toEqual({ + ...candidate.file, + sidecar: undefined + }) + expect(await coldRead(candidate)).toMatchObject({ title: 'Renamed conversation' }) + expect(index.db.prepare('SELECT title FROM sessions').get()).toEqual({ + title: 'Renamed conversation' + }) + expect(matches('codexmessage')).toEqual({ n: 1 }) +}) diff --git a/src/main/ai-vault-search/session-search-file-write.test.ts b/src/main/ai-vault-search/session-search-file-write.test.ts index 2c0be88b026..56351c0f538 100644 --- a/src/main/ai-vault-search/session-search-file-write.test.ts +++ b/src/main/ai-vault-search/session-search-file-write.test.ts @@ -491,8 +491,12 @@ it('fences a first-ever read whose file was removed before it committed', () => }) it('replaces the previous generation without ever showing both', async () => { - replayTranscriptRead({ messages: userMessages('firstgeneration', 10) }) - replayTranscriptRead({ messages: userMessages('secondgeneration', 10) }) + const candidate = syntheticCandidate() + replayTranscriptRead({ candidate, messages: userMessages('firstgeneration', 10) }) + replayTranscriptRead({ + candidate: syntheticCandidate({ mtimeMs: candidate.file.mtimeMs + 1 }), + messages: userMessages('secondgeneration', 10) + }) expect(matches(index.db, 'messages_fts', 'firstgeneration')).toBe(0) expect(matches(index.db, 'messages_fts', 'secondgeneration')).toBe(10) @@ -541,8 +545,12 @@ it('replaces a generation by cutting the old one loose, not by deleting it inlin }) it('drains what a replace cut loose without being asked', async () => { - replayTranscriptRead({ messages: userMessages('firstgeneration', 200) }) - replayTranscriptRead({ messages: userMessages('secondgeneration', 3) }) + const candidate = syntheticCandidate() + replayTranscriptRead({ candidate, messages: userMessages('firstgeneration', 200) }) + replayTranscriptRead({ + candidate: syntheticCandidate({ mtimeMs: candidate.file.mtimeMs + 1 }), + messages: userMessages('secondgeneration', 3) + }) // The store schedules the reclaim the way it schedules retention's. Hiding a // generation and never reclaiming it would grow the file by every re-read. diff --git a/src/main/ai-vault-search/session-search-index-consumer.ts b/src/main/ai-vault-search/session-search-index-consumer.ts index c4ff7877453..02e39f93238 100644 --- a/src/main/ai-vault-search/session-search-index-consumer.ts +++ b/src/main/ai-vault-search/session-search-index-consumer.ts @@ -1,3 +1,4 @@ +import type { AiVaultAgent } from '../../shared/ai-vault-types' import { registerTranscriptConsumer, type TranscriptConsumer, @@ -10,6 +11,35 @@ import { fileIdentity } from './session-search-file-cursor' import type { SessionSearchFileWrite } from './session-search-index-writer' import type { SessionSearchStore } from './session-search-store' +// File-read assumptions are checked by session-search-transcript-dependencies.test.ts. +export const SELF_CONTAINED_TRANSCRIPT_AGENTS = { + claude: true, + qoder: true, + codebuddy: true, + codex: false, // session_index.jsonl supplies titles. + gemini: true, + copilot: true, + cursor: false, // Sibling chat metadata enriches the transcript. + antigravity: false, // Workspace history enriches the transcript separately. + opencode: false, // SQLite or a separate message directory. + opencode2: false, + zcode: false, + grok: false, // chat_history.jsonl supplies messages. + hermes: true, + rovo: false, // session_context.json supplies messages. + openclaw: true, + pi: true, + omp: true, + 'prime-agent': true, + droid: true, + cline: false, // A .messages.json sidecar supplies messages. + devin: false, // sessions.db supplies metadata. + kimi: false, // agents/main/wire.jsonl supplies messages. + muse: true, + jcode: true, + kiro: false // A .jsonl sidecar supplies messages. +} satisfies Record + /** * The search index as a consumer of the transcript reader. * @@ -29,6 +59,14 @@ export class SessionSearchIndexConsumer implements TranscriptConsumer { beginRead(start: TranscriptReadStart): TranscriptReadConsumer | null { const { candidate } = start + if ( + start.mode === 'replace' && + SELF_CONTAINED_TRANSCRIPT_AGENTS[candidate.agent] && + (candidate.file.sidecar === undefined || candidate.file.sidecar === 'none') && + this.store.coversTranscript(candidate) + ) { + return null + } if (start.mode === 'append') { const cursor = this.store.indexedFile(candidate.file.path, fileIdentity(candidate.file)) if (!cursor || cursor.byteOffset !== start.previousByteOffset) { diff --git a/src/main/ai-vault-search/session-search-list-reparse.test.ts b/src/main/ai-vault-search/session-search-list-reparse.test.ts new file mode 100644 index 00000000000..ade3b9a6209 --- /dev/null +++ b/src/main/ai-vault-search/session-search-list-reparse.test.ts @@ -0,0 +1,123 @@ +import { afterEach, beforeEach, expect, it } from 'vitest' +import { resetTranscriptConsumersForTests } from '../ai-vault/session-transcript-consumers' +import { registerSessionSearchIndexConsumer } from './session-search-index-consumer' +import { + openSessionSearchIndexFile, + replayTranscriptRead, + syntheticCandidate, + userMessages, + type SessionSearchIndexFile +} from './session-search-index-test-fixture' +import { SessionSearchStore } from './session-search-store' + +let index: SessionSearchIndexFile +let store: SessionSearchStore +const messages = userMessages('Searchable conversation', 2) + +beforeEach(async () => { + index = await openSessionSearchIndexFile('ss-list-reparse') + store = new SessionSearchStore(index.path) + registerSessionSearchIndexConsumer(store) +}) + +afterEach(async () => { + resetTranscriptConsumersForTests() + store.close() + await index.close() +}) + +function sessionRow(): unknown { + return index.db.prepare('SELECT id FROM sessions').get() +} + +it('keeps current search rows when a cold history list rereads the same transcript', () => { + replayTranscriptRead({ messages }) + const before = sessionRow() + replayTranscriptRead({ messages }) + expect(sessionRow()).toEqual(before) + expect(index.db.prepare('SELECT count(*) AS n FROM messages').get()).toEqual({ n: 2 }) +}) + +it('still replaces a changed transcript', () => { + const candidate = syntheticCandidate() + replayTranscriptRead({ candidate, messages }) + const before = sessionRow() + replayTranscriptRead({ + candidate: syntheticCandidate({ mtimeMs: candidate.file.mtimeMs + 1 }), + messages + }) + expect(sessionRow()).not.toEqual(before) +}) + +it('still replaces a transcript whose size changed without a timestamp change', () => { + replayTranscriptRead({ messages }) + const before = sessionRow() + replayTranscriptRead({ candidate: syntheticCandidate({ sizeBytes: 5000 }), messages }) + expect(sessionRow()).not.toEqual(before) +}) + +it('refreshes metadata supplied by a sidecar despite an unchanged transcript', () => { + replayTranscriptRead({ messages }) + const before = sessionRow() + replayTranscriptRead({ + candidate: syntheticCandidate({ + sidecar: { path: 'metadata.json', mtimeMs: 1, sizeBytes: 10 } + }), + messages + }) + expect(sessionRow()).not.toEqual(before) +}) + +it('rereads when the transcript size is unknown', () => { + replayTranscriptRead({ messages }) + const before = sessionRow() + replayTranscriptRead({ candidate: syntheticCandidate({ sizeBytes: undefined }), messages }) + expect(sessionRow()).not.toEqual(before) +}) + +it('still replaces a different file with identical size and timestamp', () => { + replayTranscriptRead({ candidate: syntheticCandidate({ dev: 1, ino: 10 }), messages }) + const before = sessionRow() + replayTranscriptRead({ candidate: syntheticCandidate({ dev: 1, ino: 11 }), messages }) + expect(sessionRow()).not.toEqual(before) +}) + +it('rereads a row indexed without a file identity once the stat supplies one', () => { + replayTranscriptRead({ messages }) + const before = sessionRow() + const identified = syntheticCandidate({ dev: 1, ino: 10 }) + replayTranscriptRead({ candidate: identified, messages }) + expect(sessionRow()).not.toEqual(before) + const repaired = sessionRow() + replayTranscriptRead({ candidate: identified, messages }) + expect(sessionRow()).toEqual(repaired) +}) + +it.each(['due', 'failed'] as const)('repairs an unchanged transcript marked %s', (state) => { + replayTranscriptRead({ messages }) + const before = sessionRow() + store.setFileState(syntheticCandidate().file.path, state) + replayTranscriptRead({ messages }) + expect(sessionRow()).not.toEqual(before) + expect(index.db.prepare('SELECT state FROM files').get()).toEqual({ state: 'current' }) +}) + +it('repairs a partial index even when the stat matches', () => { + replayTranscriptRead({ messages }) + const before = sessionRow() + index.db.exec('UPDATE files SET byte_offset = -1') + replayTranscriptRead({ messages }) + expect(sessionRow()).not.toEqual(before) +}) + +it('still indexes new appended messages', () => { + replayTranscriptRead({ messages }) + replayTranscriptRead({ + candidate: syntheticCandidate({ sizeBytes: 5000 }), + mode: 'append', + previousByteOffset: 4096, + messages, + outcome: { byteOffset: 5000 } + }) + expect(index.db.prepare('SELECT count(*) AS n FROM messages').get()).toEqual({ n: 4 }) +}) diff --git a/src/main/ai-vault-search/session-search-store.ts b/src/main/ai-vault-search/session-search-store.ts index 73da2c2db6f..93cfed0ddff 100644 --- a/src/main/ai-vault-search/session-search-store.ts +++ b/src/main/ai-vault-search/session-search-store.ts @@ -2,9 +2,10 @@ import type SyncDatabase from '../sqlite/sync-database' import { asRecord } from '../ai-vault/session-scanner-record-value' import type { SessionFileCandidate } from '../ai-vault/session-scanner-types' import type { TranscriptSessionIdentity } from '../ai-vault/session-transcript-consumers' -import type { - SessionSearchFileIdentity, - SessionSearchIndexedFile +import { + fileIdentity, + type SessionSearchFileIdentity, + type SessionSearchIndexedFile } from './session-search-file-cursor' import { SESSION_SEARCH_COMMIT_CHARS, @@ -171,6 +172,37 @@ export class SessionSearchStore { } } + /** A cold list read must not rewrite a complete, unchanged search entry. */ + coversTranscript(candidate: SessionFileCandidate): boolean { + const file = candidate.file + if (this.closed || typeof file.sizeBytes !== 'number') { + return false + } + const identity = fileIdentity(file) + try { + return ( + this.db + .prepare( + // A row without a stored identity is uncovered so the rewrite records one. + `SELECT 1 FROM files WHERE path = ? AND state = 'current' + AND mtime_ms = ? AND size_bytes = ? AND byte_offset = size_bytes + AND (? IS NULL OR (CAST(dev AS REAL) = ? AND CAST(ino AS REAL) = ?))` + ) + .get( + file.path, + file.mtimeMs, + file.sizeBytes, + identity?.dev ?? null, + identity?.dev ?? null, + identity?.ino ?? null + ) !== undefined + ) + } catch (error) { + this.onError(error) + return false + } + } + /** Null when this read cannot extend the index, or when the store refuses writes. */ beginWrite( candidate: SessionFileCandidate, diff --git a/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts b/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts new file mode 100644 index 00000000000..5550d4fa670 --- /dev/null +++ b/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts @@ -0,0 +1,121 @@ +import * as fs from 'node:fs' +import * as fsPromises from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterAll, afterEach, beforeAll, expect, it, vi } from 'vitest' +import { AI_VAULT_AGENTS } from '../../shared/ai-vault-types' +import * as transcriptFs from '../native-chat/wsl-transcript-fs-access' +import { parseAgentSessionFile } from '../ai-vault/session-scanner-agent-parser' +import { AI_VAULT_AGENT_SOURCES } from '../ai-vault/session-scanner-agent-sources' +import { discoverFiles } from '../ai-vault/session-scanner-discovery' +import { + writeEveryAgentVault, + type EveryAgentVault +} from '../ai-vault/session-scanner-every-agent-fixture' +import { geminiJsonlFixture } from '../ai-vault/session-scanner-incremental-fixtures' +import { + parseAgentSessionFileCached, + resetSessionParseCacheForTests +} from '../ai-vault/session-scanner-parse-cache' +import { + registerTranscriptConsumer, + resetTranscriptConsumersForTests, + type TranscriptMessage +} from '../ai-vault/session-transcript-consumers' +import { SELF_CONTAINED_TRANSCRIPT_AGENTS } from './session-search-index-consumer' + +vi.mock('node:fs', { spy: true }) +vi.mock('node:fs/promises', { spy: true }) +vi.mock('../native-chat/wsl-transcript-fs-access', { spy: true }) + +let root: string +let vault: EveryAgentVault + +beforeAll(async () => { + root = await fsPromises.mkdtemp(join(tmpdir(), 'ss-transcript-dependencies-')) + vault = await writeEveryAgentVault(root) + const gemini = geminiJsonlFixture() + await fsPromises.writeFile( + join(vault.roots.geminiSessionsDir, gemini.fileName), + `${gemini.seedLines.join('\n')}\n` + ) +}) + +afterEach(() => { + vi.clearAllMocks() + resetTranscriptConsumersForTests() + resetSessionParseCacheForTests() +}) + +afterAll(async () => { + await fsPromises.rm(root, { recursive: true, force: true }) +}) + +function contentReadPaths(): unknown[] { + return [ + ...vi.mocked(fsPromises.readFile).mock.calls.map(([path]) => path), + ...vi.mocked(fsPromises.open).mock.calls.map(([path]) => path), + ...vi.mocked(fs.readFile).mock.calls.map(([path]) => path), + ...vi.mocked(fs.readFileSync).mock.calls.map(([path]) => path), + ...vi.mocked(fs.open).mock.calls.map(([path]) => path), + ...vi.mocked(fs.openSync).mock.calls.map(([path]) => path), + ...vi.mocked(fs.createReadStream).mock.calls.map(([path]) => path), + ...vi.mocked(transcriptFs.wslGatedReadFile).mock.calls.map(([path]) => path), + ...vi.mocked(transcriptFs.openTranscriptFile).mock.calls.map(([path]) => path), + ...vi.mocked(transcriptFs.openTranscriptReadStream).mock.calls.map(([path]) => path), + ...vi.mocked(transcriptFs.readTranscriptSlice).mock.calls.map(([path]) => path) + ] +} + +it.each(AI_VAULT_AGENTS.filter((agent) => SELF_CONTAINED_TRANSCRIPT_AGENTS[agent]))( + '%s reads indexed content only from its candidate transcript', + async (agent) => { + const source = AI_VAULT_AGENT_SOURCES[agent] + if (!source) { + throw new Error(`${agent} needs a real transcript fixture before enabling file-stat reuse`) + } + expect(source.contentDependencyPath).toBeUndefined() + const discoveries = await Promise.all( + source.rootDirs(vault.roots, []).map((rootDir) => + discoverFiles({ + ...source, + rootDir, + agent, + limit: 10, + issues: [], + extensions: [...source.extensions] + }) + ) + ) + const files = discoveries.flatMap((discovery) => discovery.files) + expect(files.length).toBeGreaterThan(0) + + for (const file of files) { + const candidate = { agent, file, codexHome: null } + for (const read of ['direct', 'cached'] as const) { + const messages: TranscriptMessage[] = [] + const sink = { active: true, push: (message: TranscriptMessage) => messages.push(message) } + resetTranscriptConsumersForTests() + resetSessionParseCacheForTests() + registerTranscriptConsumer({ + beginRead: () => ({ message: sink.push, finish: () => undefined }) + }) + vi.clearAllMocks() + + const session = await (read === 'direct' + ? parseAgentSessionFile(candidate, process.platform, sink) + : parseAgentSessionFileCached(candidate, process.platform)) + + expect(session).not.toBeNull() + expect(messages.length).toBeGreaterThan(0) + const paths = contentReadPaths() + expect(paths).toContain(file.path) + // Include failed optional reads: an absent sidecar can appear on a later scan. + expect( + paths.filter((path) => path !== file.path), + `${read}: ${file.path}` + ).toEqual([]) + } + } + } +) From 27c0275e3ab90859678b40468dbaf351f601057f Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:42:22 -0400 Subject: [PATCH 12/53] feat(mobile): log OTA shell lifecycle to the connection diagnostics (#28840) * feat(mobile): log OTA shell lifecycle to the connection diagnostics Exported connection logs showed nothing about the web shell, so a phone stuck on "Opening workspace" could not say whether it stalled activating the download, loading the page, or waiting for its first paint. Record sparse shell milestones, page signals, one-shot 15s stall warnings and bridge diagnostics into the per-host connection log. Diagnostics only. Refs STA-10545, #28626 * fix(mobile): cut shell log volume and pinpoint each waiting screen Every page route mounts its own shell, so four lines per healthy open evicted connection history from the 200-entry log. A healthy open now writes one line, on paint, carrying source, build and elapsed. The 15s stall line now also covers "Checking host" (gate verdict and whether the cache read answered) and a download with no progress. The "page has not drawn" line counts the page's ready asks and whether the document loaded, telling a page that keeps refusing init from one that took it and died. Frames dropped with no bridge host, an unreadable host profile, post-failed error names and route-refused issues are now logged; path masking catches quoted, bracketed and Windows paths. The exported report header names the shell build and bridge protocol. Diagnostics only: no behaviour, UI or wire change. Refs STA-10545 * fix(mobile): dedupe flapping shell lines and mask spaced paths A host with no cache flapping between unreachable and dialling wrote "checking host" and "host unreachable" every cycle per mounted shell; each is now written once per host until a different shell line lands. Path masking keeps going across a space while the next word still holds a separator, and accepts a comma before the path. The paint line's elapsed time is renamed activatedMs, which is what it measures. A route-only session rebuild re-arms the stall timer. Refs STA-10545 * refactor(mobile): split the shell log module by responsibility shell-lifecycle-log.ts passed the 300-line lint cap. The line type, recorder and flapping dedupe move to shell-lifecycle-line-recorder.ts, bridge diagnostic lines to shell-bridge-log-lines.ts, and user-path masking to shell-log-path-masking.ts, with tests split to match. No behaviour change. Refs STA-10545 --- .../connection-diagnostics-report.test.ts | 23 ++ .../connection-diagnostics-report.ts | 5 + .../native-diagnostics-operations.ts | 5 +- .../MobileWebShellScreen.test.tsx | 5 + .../mobile-web-shell/MobileWebShellScreen.tsx | 10 + .../bridge-diagnostic-log.test.ts | 10 + .../mobile-web-shell/bridge-diagnostic-log.ts | 6 +- .../shell-bridge-log-lines.test.ts | 45 ++ .../shell-bridge-log-lines.ts | 61 +++ .../shell-lifecycle-line-recorder.ts | 39 ++ .../shell-lifecycle-log.test.ts | 383 ++++++++++++++++++ .../mobile-web-shell/shell-lifecycle-log.ts | 272 +++++++++++++ .../shell-log-path-masking.test.ts | 21 + .../shell-log-path-masking.ts | 15 + .../use-mobile-web-shell-bridge.test.ts | 19 + .../use-mobile-web-shell-bridge.ts | 28 +- .../use-mobile-web-shell-session.test.ts | 92 ++++- .../use-mobile-web-shell-session.ts | 32 ++ mobile/src/transport/types.ts | 5 + 19 files changed, 1071 insertions(+), 5 deletions(-) create mode 100644 mobile/src/mobile-web-shell/shell-bridge-log-lines.test.ts create mode 100644 mobile/src/mobile-web-shell/shell-bridge-log-lines.ts create mode 100644 mobile/src/mobile-web-shell/shell-lifecycle-line-recorder.ts create mode 100644 mobile/src/mobile-web-shell/shell-lifecycle-log.test.ts create mode 100644 mobile/src/mobile-web-shell/shell-lifecycle-log.ts create mode 100644 mobile/src/mobile-web-shell/shell-log-path-masking.test.ts create mode 100644 mobile/src/mobile-web-shell/shell-log-path-masking.ts diff --git a/mobile/src/diagnostics/connection-diagnostics-report.test.ts b/mobile/src/diagnostics/connection-diagnostics-report.test.ts index 991ec01f549..88794135209 100644 --- a/mobile/src/diagnostics/connection-diagnostics-report.test.ts +++ b/mobile/src/diagnostics/connection-diagnostics-report.test.ts @@ -56,6 +56,29 @@ describe('buildConnectionDiagnosticsReport', () => { expect(report).toContain('No connection events recorded.') }) + it('names the shell build and bridge version under the app line when given', () => { + const args = { + hostName: 'Host 3', + endpoint: 'ws://192.168.1.50:6768', + state: 'connected' as const, + reconnectAttempts: 0, + lastConnectedAt: null, + platform: 'ios 26', + appVersion: '0.0.60', + entries: [], + nowMs: NOW + } + const lines = buildConnectionDiagnosticsReport({ + ...args, + shell: { kind: 'ota', bridgeProtocolVersion: 1 } + }).split('\n') + expect(lines.slice(2, 4)).toEqual([ + 'App: Orca Mobile 0.0.60 · ios 26', + 'Shell: ota · bridge protocol 1' + ]) + expect(buildConnectionDiagnosticsReport(args)).not.toContain('Shell:') + }) + it('explains the most likely cause and redacts credentials before copying', () => { const report = buildConnectionDiagnosticsReport({ hostName: 'Host 3', diff --git a/mobile/src/diagnostics/connection-diagnostics-report.ts b/mobile/src/diagnostics/connection-diagnostics-report.ts index 41c22014471..25c78e224b9 100644 --- a/mobile/src/diagnostics/connection-diagnostics-report.ts +++ b/mobile/src/diagnostics/connection-diagnostics-report.ts @@ -28,6 +28,8 @@ export function buildConnectionDiagnosticsReport(args: { entries: readonly ConnectionLogEntry[] activePath?: MobileConnectionDiagnosticPath pendingPath?: MobileConnectionDiagnosticPath | null + /** Which shell this binary is and the page bridge version it speaks; omitted by older callers. */ + shell?: { kind: 'ota' | 'native'; bridgeProtocolVersion: number } nowMs?: number }): string { const now = args.nowMs ?? Date.now() @@ -43,6 +45,9 @@ export function buildConnectionDiagnosticsReport(args: { lines.push('Orca Mobile connection diagnostics') lines.push(`Generated: ${new Date(now).toISOString()}`) lines.push(`App: Orca Mobile ${args.appVersion} · ${args.platform}`) + if (args.shell) { + lines.push(`Shell: ${args.shell.kind} · bridge protocol ${args.shell.bridgeProtocolVersion}`) + } const desktopAppVersion = normalizeHostAppVersion(args.desktopAppVersion) lines.push(`Host Orca version: ${desktopAppVersion ?? 'unknown'}`) lines.push(`Host: ${redactConnectionLogText(args.hostName)}`) diff --git a/mobile/src/diagnostics/native-diagnostics-operations.ts b/mobile/src/diagnostics/native-diagnostics-operations.ts index 2b7b7fe6f0b..39e614b43ce 100644 --- a/mobile/src/diagnostics/native-diagnostics-operations.ts +++ b/mobile/src/diagnostics/native-diagnostics-operations.ts @@ -9,6 +9,8 @@ import { getReportableConnectionIncidentId } from './connection-diagnostics-anal import { buildConnectionDiagnosticsReport } from './connection-diagnostics-report' import { submitConnectionDiagnostics } from './connection-diagnostics-submission' import type { DiagnosticsDeviceOperations } from './diagnostics-device-operations' +import { mobileShellBuildKind } from '../storage/preferences' +import { BRIDGE_PROTOCOL_VERSION } from '../mobile-web-shell/bridge/bridge-frame-fields' export function createNativeDiagnosticsOperations( host: HostProfile, @@ -33,7 +35,8 @@ export function createNativeDiagnosticsOperations( desktopAppVersion, entries: snapshot.entries, activePath: snapshot.activePath, - pendingPath: snapshot.pendingPath + pendingPath: snapshot.pendingPath, + shell: { kind: mobileShellBuildKind(), bridgeProtocolVersion: BRIDGE_PROTOCOL_VERSION } }), appVersion, platform, diff --git a/mobile/src/mobile-web-shell/MobileWebShellScreen.test.tsx b/mobile/src/mobile-web-shell/MobileWebShellScreen.test.tsx index f285ec7076e..69fa1bbaceb 100644 --- a/mobile/src/mobile-web-shell/MobileWebShellScreen.test.tsx +++ b/mobile/src/mobile-web-shell/MobileWebShellScreen.test.tsx @@ -47,6 +47,7 @@ import { readBridgeHostMessage } from './bridge/bridge-envelope' import { MobileWebShellScreen } from './MobileWebShellScreen' +import { connectionLogStore } from '../transport/persisted-connection-log-store' import type { MobileWebShellSessionState } from './mobile-web-shell-session-contract' import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer' @@ -244,8 +245,12 @@ describe('the hybrid shell screen', () => { // page re-posting `ready` on its backoff for as long as the screen is open. dependencies.snapshotUnreadable = true const warned = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + const logged = () => connectionLogStore.get('host-1').map((entry) => entry.message) + const before = logged().length await renderScreen(readyState('session-one')) expect(dependencies.reportShellFailure.mock.calls).toEqual([['document-load-failed']]) + // Named in the exported log, not only as the generic failure it causes. + expect(logged().slice(before)).toEqual(['Web shell: host profile unreadable']) warned.mockRestore() }) diff --git a/mobile/src/mobile-web-shell/MobileWebShellScreen.tsx b/mobile/src/mobile-web-shell/MobileWebShellScreen.tsx index 34c150921ba..ea58f9876e3 100644 --- a/mobile/src/mobile-web-shell/MobileWebShellScreen.tsx +++ b/mobile/src/mobile-web-shell/MobileWebShellScreen.tsx @@ -38,6 +38,8 @@ import { storageReadParamsSchema, type BridgeNativeVerb } from './bridge/bridge- import { SHELL_OPENING_LABEL, ShellPageCover, ShellWaitingFrame } from './ShellWaitingFrame' import { ShellDownloadProgress } from './ShellDownloadProgress' import { pageSafeAreaInsets, usePublishedSafeAreaInsets } from './page-safe-area-insets' +import { hostProfileUnreadableLine, pageFaultLine } from './shell-lifecycle-log' +import { recordShellLifecycleLine } from './shell-lifecycle-line-recorder' function failureMessage(reason: MobileWebShellFailureCause): string { switch (reason) { @@ -245,6 +247,7 @@ export function MobileWebShellScreen({ // handler on the other, and neither has anywhere to put a throw. onPageFault: (error) => { console.warn('[web-shell] the page faulted', error) + recordShellLifecycleLine(hostId, pageFaultLine(error)) reportShellFailure('document-load-failed') }, // The `init` this ready is answered with is already built from the app's writes, which reach @@ -266,6 +269,7 @@ export function MobileWebShellScreen({ // `document-load-failed` because that is what happens: the document loads and the page refuses // the session, so no tree is ever built. The refetch it costs is wasted on a route this shell // produced, and the second report is terminal, which is the failure screen this deserves. + // Logged by the host's `route-refused` diagnostic, which carries the issue, in the same call. onRouteRefused: (issue) => { console.warn('[web-shell] refused to open this screen', issue) reportShellFailure('document-load-failed') @@ -341,6 +345,12 @@ export function MobileWebShellScreen({ reportShellFailure('document-load-failed') } }, [reportShellFailure, unreadable]) + // Log-only and separate, so the failure effect above keeps its own dependencies. + useEffect(() => { + if (unreadable) { + recordShellLifecycleLine(hostId, hostProfileUnreadableLine()) + } + }, [hostId, unreadable]) if (state.kind === 'native-route') { return fallback diff --git a/mobile/src/mobile-web-shell/bridge-diagnostic-log.test.ts b/mobile/src/mobile-web-shell/bridge-diagnostic-log.test.ts index c7f96afedf4..84c9c021050 100644 --- a/mobile/src/mobile-web-shell/bridge-diagnostic-log.test.ts +++ b/mobile/src/mobile-web-shell/bridge-diagnostic-log.test.ts @@ -18,6 +18,16 @@ function lines(): string[] { } describe('the bridge diagnostic log', () => { + it('hands the connection log the same once-per-key diagnostics as the console', () => { + const reported: string[] = [] + const report = createBridgeDiagnosticReporter((diagnostic) => reported.push(diagnostic.kind)) + report({ kind: 'frame-after-close' }) + report({ kind: 'frame-after-close' }) + report({ kind: 'notify-refused', name: 'fault', why: 'ungranted' }) + expect(reported).toEqual(['frame-after-close', 'notify-refused']) + expect(lines()).toHaveLength(2) + }) + it('names the notification it refused and why, for each refusal', () => { const report = createBridgeDiagnosticReporter() report({ kind: 'notify-refused', name: 'fault', why: 'before-ready' }) diff --git a/mobile/src/mobile-web-shell/bridge-diagnostic-log.ts b/mobile/src/mobile-web-shell/bridge-diagnostic-log.ts index 34ae56fa24b..6c6793008cc 100644 --- a/mobile/src/mobile-web-shell/bridge-diagnostic-log.ts +++ b/mobile/src/mobile-web-shell/bridge-diagnostic-log.ts @@ -24,7 +24,10 @@ function diagnosticKey(diagnostic: BridgeHostDiagnostic): string { * says why. The host already holds `post-failed` to one; this is the same bound for the kinds it * does not, and a new host starts the count over because a new page is new evidence. */ -export function createBridgeDiagnosticReporter(): (diagnostic: BridgeHostDiagnostic) => void { +export function createBridgeDiagnosticReporter( + // Gets the same once-per-key diagnostics as the console, for the exported connection log. + onReported?: (diagnostic: BridgeHostDiagnostic) => void +): (diagnostic: BridgeHostDiagnostic) => void { const reported = new Set() return (diagnostic) => { const key = diagnosticKey(diagnostic) @@ -32,6 +35,7 @@ export function createBridgeDiagnosticReporter(): (diagnostic: BridgeHostDiagnos return } reported.add(key) + onReported?.(diagnostic) if (diagnostic.kind === 'refused') { console.warn('[web-shell-bridge] refused a page frame', diagnostic.refusal) return diff --git a/mobile/src/mobile-web-shell/shell-bridge-log-lines.test.ts b/mobile/src/mobile-web-shell/shell-bridge-log-lines.test.ts new file mode 100644 index 00000000000..243dda5d9b5 --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-bridge-log-lines.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' +import { bridgeDiagnosticLine, frameWithoutHostLine } from './shell-bridge-log-lines' + +describe('shell bridge log lines', () => { + it('skips routine bridge kinds and names the specific failure for the rest', () => { + expect( + bridgeDiagnosticLine({ + kind: 'terminal-backlog', + id: 't', + coalescedFrames: 0, + deliveredFrames: 0, + peakPendingBytes: 0, + ended: null + }) + ).toBeNull() + expect( + bridgeDiagnosticLine({ kind: 'notify-refused', name: 'fault', why: 'ungranted' }) + ).toEqual({ + level: 'warn', + code: 'web-shell-bridge', + message: 'Web shell bridge: notify-refused', + detail: 'ungranted' + }) + const gone = new Error('view gone') + gone.name = 'BridgeViewGoneError' + expect(bridgeDiagnosticLine({ kind: 'post-failed', error: gone })?.detail).toBe( + 'error=BridgeViewGoneError' + ) + expect(bridgeDiagnosticLine({ kind: 'post-failed', error: 'nope' })?.detail).toBe( + 'error=string' + ) + expect( + bridgeDiagnosticLine({ kind: 'route-refused', issue: 'Invalid path /Users/bob/wt' })?.detail + ).toBe('issue=Invalid path [path]') + }) + + it('says which half a frame with no bridge host was waiting on', () => { + expect(frameWithoutHostLine({ clientReady: false, snapshotReady: true })).toEqual({ + level: 'warn', + code: 'web-shell-page', + message: 'Web shell: page frame with no bridge host', + detail: 'client=null snapshot=ok' + }) + }) +}) diff --git a/mobile/src/mobile-web-shell/shell-bridge-log-lines.ts b/mobile/src/mobile-web-shell/shell-bridge-log-lines.ts new file mode 100644 index 00000000000..54d950f1ccf --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-bridge-log-lines.ts @@ -0,0 +1,61 @@ +import type { BridgeHostDiagnostic } from './bridge-host-contract' +import type { ShellLifecycleLine } from './shell-lifecycle-line-recorder' +import { maskUserText } from './shell-log-path-masking' + +/** Bridge-side shell log lines: host diagnostics and frames that found no host to answer them. */ + +/** Null for routine kinds; the caller already bounds each key to one line per host. */ +export function bridgeDiagnosticLine(diagnostic: BridgeHostDiagnostic): ShellLifecycleLine | null { + // Retired terminal streams are routine and one per stream; they would crowd out the log. + if (diagnostic.kind === 'terminal-backlog') { + return null + } + const detail = bridgeDiagnosticDetail(diagnostic) + return { + level: 'warn', + code: 'web-shell-bridge', + message: `Web shell bridge: ${diagnostic.kind}`, + ...(detail === undefined ? {} : { detail }) + } +} + +/** A page frame arrived while no bridge host was built, so it was dropped unanswered. */ +export function frameWithoutHostLine(facts: { + clientReady: boolean + snapshotReady: boolean +}): ShellLifecycleLine { + return { + level: 'warn', + code: 'web-shell-page', + message: 'Web shell: page frame with no bridge host', + detail: `client=${facts.clientReady ? 'ok' : 'null'} snapshot=${facts.snapshotReady ? 'ok' : 'null'}` + } +} + +function bridgeDiagnosticDetail(diagnostic: BridgeHostDiagnostic): string | undefined { + switch (diagnostic.kind) { + case 'refused': + return diagnostic.refusal + case 'notify-refused': + case 'navigate-back-refused': + return diagnostic.why + case 'post-failed': + return `error=${maskUserText(errorName(diagnostic.error))}` + case 'route-refused': + return `issue=${maskUserText(diagnostic.issue)}` + case 'notify-failed': + case 'frame-after-dispose': + case 'frame-after-close': + case 'storage-refused': + case 'route-update-refused': + case 'binary-lane-refused': + case 'binary-frame-dropped': + case 'terminal-backlog': + // Their payloads name keys, ids or schema text that is not needed to place a stall. + return undefined + } +} + +function errorName(error: unknown): string { + return error instanceof Error ? error.name : typeof error +} diff --git a/mobile/src/mobile-web-shell/shell-lifecycle-line-recorder.ts b/mobile/src/mobile-web-shell/shell-lifecycle-line-recorder.ts new file mode 100644 index 00000000000..a2ed1b4743a --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-lifecycle-line-recorder.ts @@ -0,0 +1,39 @@ +import { connectionLogStore } from '../transport/persisted-connection-log-store' +import type { ConnectionLogEntry } from '../transport/types' + +/** + * Sparse OTA shell milestones for the per-host connection log, so an exported report says where a + * workspace open stalled. Diagnostics only: nothing here feeds back into the session. + * + * The store holds 200 entries shared with connection events and every page route mounts its own + * shell, so a healthy open writes one line (the paint); the rest are failures and one-shot stall + * warnings. Route paths, generation directories and error stacks stay out: they can name user paths. + */ +export type ShellLifecycleLine = Required> & + Pick + +let lineSequence = 0 + +// Lines a host flapping between unreachable and dialling would repeat every cycle per mounted shell. +const FLAPPING_MESSAGES: ReadonlySet = new Set([ + 'Web shell: checking host', + 'Web shell: host unreachable and no app cached' +]) +// Per host, the flapping lines already written since the last other shell line for it. +const flappingWritten = new Map>() + +export function recordShellLifecycleLine(hostId: string, line: ShellLifecycleLine): void { + if (FLAPPING_MESSAGES.has(line.message)) { + const written = flappingWritten.get(hostId) ?? new Set() + if (written.has(line.message)) { + return + } + written.add(line.message) + flappingWritten.set(hostId, written) + } else { + flappingWritten.delete(hostId) + } + const ts = Date.now() + lineSequence += 1 + connectionLogStore.append(hostId, { id: `web-shell-${ts}-${lineSequence}`, ts, ...line }) +} diff --git a/mobile/src/mobile-web-shell/shell-lifecycle-log.test.ts b/mobile/src/mobile-web-shell/shell-lifecycle-log.test.ts new file mode 100644 index 00000000000..a7c13b20039 --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-lifecycle-log.test.ts @@ -0,0 +1,383 @@ +import { describe, expect, it, vi } from 'vitest' +import { buildConnectionDiagnosticsReport } from '../diagnostics/connection-diagnostics-report' +import type { ConnectionLogEntry } from '../transport/types' +import { + CACHED, + MANIFEST, + createMobileWebShellSession, + gates, + ROUTE, + stamp +} from './mobile-web-shell-session-test-fixtures' +import { reduceMobileWebShellSession } from './mobile-web-shell-session' +import type { + MobileWebShellSession, + MobileWebShellSessionEvent +} from './mobile-web-shell-session-contract' +import { connectionLogStore } from '../transport/persisted-connection-log-store' +import { bridgeDiagnosticLine, frameWithoutHostLine } from './shell-bridge-log-lines' +import { recordShellLifecycleLine, type ShellLifecycleLine } from './shell-lifecycle-line-recorder' +import { + EMPTY_SHELL_LOG_TRACE, + pageFaultLine, + shellSessionStepLines, + shellStallLine, + traceShellStep, + type ShellLogTrace +} from './shell-lifecycle-log' + +vi.mock('@react-native-async-storage/async-storage', () => ({ + default: { getItem: vi.fn(async () => null), setItem: vi.fn(async () => undefined) } +})) + +type Pending = Parameters[1] + +/** Replays events through the real reducer, collecting what each step would log. */ +type Replayed = { + session: MobileWebShellSession + trace: ShellLogTrace + lines: ShellLifecycleLine[] +} + +function replay( + events: readonly Pending[], + from: Omit = { + session: createMobileWebShellSession(ROUTE), + trace: EMPTY_SHELL_LOG_TRACE + } +): Replayed { + let { session, trace } = from + const lines: ShellLifecycleLine[] = [] + for (const pending of events) { + const event: MobileWebShellSessionEvent = stamp(session.flow, pending) + const next = reduceMobileWebShellSession(session, event).session + trace = traceShellStep(trace, session, event, next) + lines.push(...shellSessionStepLines(session, event, next, trace)) + session = next + } + return { session, trace, lines } +} + +function stall(replayed: Omit): ShellLifecycleLine | null { + return shellStallLine(replayed.session.state, replayed.session, replayed.trace) +} + +const ACTIVATED = { + type: 'activated', + generationDirectory: '/Users/someone/Library/Caches/gen-1', + sessionId: 'session-one', + buildId: MANIFEST.buildId, + totalBytes: MANIFEST.totalBytes, + elapsedMs: 1234 +} as const + +const DOWNLOAD_OPEN: readonly Pending[] = [ + { type: 'gates-changed', gates: gates() }, + { type: 'cache-read', generation: null }, + { type: 'manifest-read', manifest: MANIFEST }, + { + type: 'fetch-progress', + completedAssets: 1, + totalAssets: MANIFEST.totalAssets, + receivedBytes: 10, + totalBytes: MANIFEST.totalBytes + }, + { + type: 'fetch-progress', + completedAssets: 2, + totalAssets: MANIFEST.totalAssets, + receivedBytes: 20, + totalBytes: MANIFEST.totalBytes + }, + { type: 'download-staged' }, + ACTIVATED +] + +function present(line: ShellLifecycleLine | null): ShellLifecycleLine[] { + expect(line).not.toBeNull() + return line === null ? [] : [line] +} + +function summary(lines: readonly ShellLifecycleLine[]): string[] { + return lines.map( + (line) => `${line.code} ${line.message}${line.detail ? ` | ${line.detail}` : ''}` + ) +} + +const PAGE_LOAD: readonly Pending[] = [ + { type: 'document-started' }, + { type: 'document-loaded' }, + { type: 'page-ready' } +] + +const BUILD = MANIFEST.buildId.slice(0, 8) + +describe('shell lifecycle log lines', () => { + it('writes exactly one line for a healthy cached open: the paint', () => { + const { lines } = replay([ + { type: 'gates-changed', gates: gates() }, + { type: 'cache-read', generation: CACHED }, + { type: 'manifest-read', manifest: MANIFEST }, + ACTIVATED, + ...PAGE_LOAD, + { type: 'page-ready' }, + { type: 'page-painted' }, + { type: 'page-painted' }, + { type: 'page-back-claim', claimed: true } + ]) + expect(summary(lines)).toEqual([ + `web-shell-page Web shell: page painted | source=cache build=${BUILD} activatedMs=1234` + ]) + }) + + it('adds only the download milestones to a downloading open, and no progress ticks', () => { + const { lines } = replay([...DOWNLOAD_OPEN, ...PAGE_LOAD, { type: 'page-painted' }]) + expect(summary(lines)).toEqual([ + `web-shell-flow Web shell: downloading app | ${MANIFEST.totalAssets} assets, ${MANIFEST.totalBytes} bytes`, + 'web-shell-flow Web shell: activating app | source=download', + `web-shell-page Web shell: page painted | source=download build=${BUILD} activatedMs=1234` + ]) + // The generation directory names a device path and never reaches the log. + expect(JSON.stringify(lines)).not.toContain('/Users/someone') + }) + + it('logs nothing for a route that stays native, the normal case for native screens', () => { + const { session, lines } = replay([ + { type: 'gates-changed', gates: gates({ hostCapabilities: [] }) } + ]) + expect(session.state.kind).toBe('native-route') + expect(lines).toEqual([]) + }) + + it('logs a missed ready deadline, the failure it causes and the recovery flow', () => { + const opened = replay(DOWNLOAD_OPEN) + const { lines } = replay([{ type: 'document-loaded' }, { type: 'page-ready-deadline' }], opened) + expect(summary(lines)).toEqual([ + 'web-shell-page Web shell: page never said ready before the deadline', + 'web-shell-flow Web shell: checking host' + ]) + }) + + it('logs a view failure but not the remount it earns', () => { + const opened = replay(DOWNLOAD_OPEN) + const { lines } = replay( + [ + { type: 'shell-failed', reason: 'render-process-gone' }, + { type: 'remounted', sessionId: 'session-two' } + ], + opened + ) + expect(summary(lines)).toEqual([ + 'web-shell-page Web shell: page load failed | reason=render-process-gone' + ]) + }) + + it('ignores results from a superseded flow', () => { + const opened = replay(DOWNLOAD_OPEN) + const { lines } = replay( + [ + { type: 'page-ready-deadline', flow: opened.session.flow - 1 }, + { ...ACTIVATED, flow: 0 } + ], + opened + ) + expect(lines).toEqual([]) + }) + + it('logs a failed download with its closed reason code', () => { + const { lines } = replay([ + { type: 'gates-changed', gates: gates() }, + { type: 'cache-read', generation: null }, + { type: 'download-failed', cause: { reason: 'connection-lost', hostCode: null } } + ]) + expect(summary(lines)).toEqual([ + 'web-shell-flow Web shell: app download failed | reason=connection-lost', + 'web-shell-flow Web shell: failed | reason=download-failed retried=no' + ]) + }) +}) + +describe('shell stall lines', () => { + it('names what a checking session waits on: the gates, then the cache', () => { + const fresh = replay([]) + expect(stall(fresh)).toMatchObject({ + code: 'web-shell-stalled', + message: 'Web shell: still checking host after 15s', + detail: 'gates=none cache=unread' + }) + expect( + stall(replay([{ type: 'gates-changed', gates: gates({ reachability: 'connecting' }) }])) + ).toMatchObject({ detail: 'gates=dialling cache=unread' }) + const opened = replay([{ type: 'gates-changed', gates: gates() }]) + expect(opened.session.state.kind).toBe('checking') + expect(stall(opened)).toMatchObject({ detail: 'gates=open cache=unread' }) + // An empty cache leaves `cached` null, so only the trace can tell the manifest wait apart. + const awaitingManifest = replay([{ type: 'cache-read', generation: null }], opened) + expect(stall(awaitingManifest)).toMatchObject({ detail: 'gates=open cache=read' }) + }) + + it('names how far a stalled download got', () => { + const fetching = replay(DOWNLOAD_OPEN.slice(0, 4)) + expect(stall(fetching)).toMatchObject({ + message: 'Web shell: download made no progress for 15s', + detail: `assets=1/${MANIFEST.totalAssets} bytes=10/${MANIFEST.totalBytes}` + }) + }) + + it('names an activation that never finished', () => { + expect(stall(replay(DOWNLOAD_OPEN.slice(0, 6)))).toMatchObject({ + message: 'Web shell: still activating after 15s', + detail: 'source=download' + }) + }) + + it('tells a page that keeps refusing init from one that took it and never drew', () => { + const opened = replay(DOWNLOAD_OPEN) + expect(stall(opened)).toMatchObject({ + message: 'Web shell: page has not drawn after 15s', + detail: `build=${BUILD} page ready=no readyAsks=0 documentLoaded=no` + }) + const refusing = replay([...PAGE_LOAD, { type: 'page-ready' }, { type: 'page-ready' }], opened) + expect(stall(refusing)).toMatchObject({ + detail: `build=${BUILD} page ready=yes readyAsks=3 documentLoaded=yes` + }) + // A replacement document starts its own count. + const reloaded = replay([{ type: 'document-started' }, { type: 'page-ready' }], refusing) + expect(stall(reloaded)).toMatchObject({ + detail: `build=${BUILD} page ready=yes readyAsks=1 documentLoaded=no` + }) + expect(stall(replay([{ type: 'page-painted' }], reloaded))).toBeNull() + }) + + it('stays quiet for states nobody waits in', () => { + expect( + stall(replay([{ type: 'gates-changed', gates: gates({ hostCapabilities: [] }) }])) + ).toBeNull() + }) +}) + +describe('shell failure details', () => { + it('keeps device paths out of a page fault', () => { + expect( + pageFaultLine({ + category: 'TypeError', + message: 'x is undefined at file:///var/mobile/app/index.js:1\n at /Users/me/secret.js', + isRpcDeliveryUnknown: false + }).detail + ).toBe('TypeError: x is undefined at [path]') + }) +}) + +describe('recording shell lines', () => { + it('writes a flapping host line once until a different shell line is written', () => { + const host = 'flapping-host' + const checking = replay([ + { type: 'gates-changed', gates: gates({ reachability: 'unreachable' }) }, + { type: 'cache-read', generation: null }, + { type: 'gates-changed', gates: gates({ reachability: 'connecting' }) } + ]).lines + expect(summary(checking)).toEqual([ + 'web-shell-flow Web shell: host unreachable and no app cached', + 'web-shell-flow Web shell: checking host' + ]) + for (let cycle = 0; cycle < 3; cycle += 1) { + for (const line of checking) { + recordShellLifecycleLine(host, line) + } + } + const messages = () => connectionLogStore.get(host).map((entry) => entry.message) + expect(messages()).toEqual([ + 'Web shell: host unreachable and no app cached', + 'Web shell: checking host' + ]) + recordShellLifecycleLine(host, frameWithoutHostLine({ clientReady: true, snapshotReady: true })) + recordShellLifecycleLine(host, checking[1]!) + recordShellLifecycleLine('other-host', checking[1]!) + expect(messages().slice(2)).toEqual([ + 'Web shell: page frame with no bridge host', + 'Web shell: checking host' + ]) + expect(connectionLogStore.get('other-host')).toHaveLength(1) + }) +}) + +describe('shell lifecycle lines in the exported report', () => { + function entries(lines: readonly ShellLifecycleLine[]): ConnectionLogEntry[] { + return lines.map((line, index) => ({ id: `e${index}`, ts: 1_000 + index * 1_000, ...line })) + } + + it('shows where a connected session stalled without changing the connection verdict', () => { + const opened = replay(DOWNLOAD_OPEN) + const page = replay(PAGE_LOAD, opened) + const report = buildConnectionDiagnosticsReport({ + hostName: 'Desk', + endpoint: 'ws://192.168.1.2:6768', + state: 'connected', + reconnectAttempts: 0, + lastConnectedAt: 500, + platform: 'ios', + appVersion: '0.0.56', + entries: entries([...opened.lines, ...page.lines, ...present(stall(page))]), + shell: { kind: 'ota', bridgeProtocolVersion: 1 }, + nowMs: 60_000 + }) + expect(report).toContain('Shell: ota · bridge protocol 1') + expect(report).toContain('Likely cause: Connection is healthy.') + expect(report).toContain('[web-shell-flow] Web shell: activating app') + expect(report).toContain( + `[warn] [web-shell-stalled] Web shell: page has not drawn after 15s — build=${BUILD} page ready=yes readyAsks=1 documentLoaded=yes` + ) + }) + + it('never reads shell lines as a connection failure', () => { + const failed = replay([ + { type: 'gates-changed', gates: gates() }, + { type: 'cache-read', generation: null }, + { type: 'download-failed', cause: { reason: 'connection-lost', hostCode: null } } + ]) + const report = buildConnectionDiagnosticsReport({ + hostName: 'Desk', + endpoint: 'ws://192.168.1.2:6768', + state: 'disconnected', + reconnectAttempts: 1, + lastConnectedAt: null, + platform: 'ios', + appVersion: '0.0.56', + entries: entries([ + ...failed.lines, + ...present(stall(replay(DOWNLOAD_OPEN.slice(0, 6)))), + ...present(stall(replay([]))), + ...present(bridgeDiagnosticLine({ kind: 'refused', refusal: 'oversized' })), + frameWithoutHostLine({ clientReady: false, snapshotReady: false }) + ]), + nowMs: 60_000 + }) + expect(report).toContain( + 'Likely cause: No single failure cause can be determined from the recorded events.' + ) + }) + + it('leaves an earlier connection failure as the likely cause', () => { + const timeout: ConnectionLogEntry = { + id: 'timeout', + ts: 500, + level: 'error', + code: 'connect-timeout', + message: 'WebSocket connect timeout' + } + const report = buildConnectionDiagnosticsReport({ + hostName: 'Desk', + endpoint: 'ws://192.168.1.2:6768', + state: 'disconnected', + reconnectAttempts: 1, + lastConnectedAt: null, + platform: 'ios', + appVersion: '0.0.56', + entries: [timeout, ...entries(replay(DOWNLOAD_OPEN).lines)], + nowMs: 60_000 + }) + expect(report).toContain( + 'Likely cause: The saved direct endpoint did not answer before the connection timeout.' + ) + }) +}) diff --git a/mobile/src/mobile-web-shell/shell-lifecycle-log.ts b/mobile/src/mobile-web-shell/shell-lifecycle-log.ts new file mode 100644 index 00000000000..0ee08fdd508 --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-lifecycle-log.ts @@ -0,0 +1,272 @@ +import type { BridgeErrorCapture } from './bridge/bridge-error-capture' +import { gateVerdict } from './mobile-web-shell-gates' +import type { + MobileWebShellSession, + MobileWebShellSessionEvent, + MobileWebShellSessionState +} from './mobile-web-shell-session-contract' +import type { ShellLifecycleLine } from './shell-lifecycle-line-recorder' +import { maskUserText } from './shell-log-path-masking' + +/** Session-side shell log lines: reducer steps, one-shot stalls and page-reported failures. */ + +const BUILD_ID_PREFIX_LENGTH = 8 + +/** + * How long a parked waiting state lasts before the log says so. Armed per state object, so for + * `ready` it starts at activation and outlasts the 10 s ready deadline, which starts at document load. + */ +export const SHELL_STALL_LOG_MS = 15_000 + +/** What the reducer does not keep but a log line needs; tracked beside the session, log-only. */ +export type ShellLogTrace = { + /** The flow whose cache read answered, so a stall can say whether it waits on the cache. */ + readonly cacheReadFlow: number | null + readonly openedFrom: 'download' | 'cache' | null + /** The page's `ready` asks for this document: more than one means it keeps refusing `init`. */ + readonly readyAsks: number + readonly documentLoaded: boolean +} + +export const EMPTY_SHELL_LOG_TRACE: ShellLogTrace = { + cacheReadFlow: null, + openedFrom: null, + readyAsks: 0, + documentLoaded: false +} + +export function traceShellStep( + trace: ShellLogTrace, + before: MobileWebShellSession, + event: MobileWebShellSessionEvent, + after: MobileWebShellSession +): ShellLogTrace { + if ('flow' in event && event.flow !== before.flow) { + return trace + } + switch (event.type) { + case 'cache-read': + return { ...trace, cacheReadFlow: event.flow } + case 'activated': + return { + ...trace, + readyAsks: 0, + documentLoaded: false, + openedFrom: before.state.kind === 'activating' ? before.state.source : null + } + case 'remounted': + case 'document-started': + return { ...trace, readyAsks: 0, documentLoaded: false } + case 'document-loaded': + return after.state.kind === 'ready' ? { ...trace, documentLoaded: true } : trace + case 'page-ready': + return after.state.kind === 'ready' ? { ...trace, readyAsks: trace.readyAsks + 1 } : trace + case 'gates-changed': + case 'manifest-read': + case 'fetch-progress': + case 'download-staged': + case 'download-failed': + case 'shell-failed': + case 'retry-pressed': + case 'page-painted': + case 'page-back-claim': + case 'page-ready-deadline': + case 'layout-changed': + return trace + } +} + +/** What one reducer step is worth logging; empty for most steps. `trace` is the post-step one. */ +export function shellSessionStepLines( + before: MobileWebShellSession, + event: MobileWebShellSessionEvent, + after: MobileWebShellSession, + trace: ShellLogTrace +): ShellLifecycleLine[] { + // A superseded flow's result changed nothing and says nothing about this one. + if ('flow' in event && event.flow !== before.flow) { + return [] + } + const lines = [...eventLines(before, event, trace)] + const transition = transitionLine(before.state, after.state) + if (transition !== null) { + lines.push(transition) + } + return lines +} + +/** The one-shot warning for a session parked in a waiting state, or null when not parked. */ +export function shellStallLine( + state: MobileWebShellSessionState, + session: Pick, + trace: ShellLogTrace +): ShellLifecycleLine | null { + const seconds = `${SHELL_STALL_LOG_MS / 1000}s` + switch (state.kind) { + case 'checking': { + const gates = session.gates === null ? 'none' : gateVerdict(session.gates).kind + const cache = trace.cacheReadFlow === session.flow ? 'read' : 'unread' + return stallLine( + `Web shell: still checking host after ${seconds}`, + `gates=${gates} cache=${cache}` + ) + } + case 'fetching': + // Each progress tick is a new state and re-arms the timer, so this means no progress. + return stallLine( + `Web shell: download made no progress for ${seconds}`, + `assets=${state.completedAssets}/${state.totalAssets} bytes=${state.receivedBytes}/${state.totalBytes}` + ) + case 'activating': + return stallLine(`Web shell: still activating after ${seconds}`, `source=${state.source}`) + case 'ready': + return session.pagePainted + ? null + : stallLine( + `Web shell: page has not drawn after ${seconds}`, + `build=${buildPrefix(state.buildId)} page ready=${yesNo(session.pageReady)} readyAsks=${trace.readyAsks} documentLoaded=${yesNo(trace.documentLoaded)}` + ) + case 'native-route': + case 'wall': + case 'failed': + case 'offline': + return null + } +} + +export function pageFaultLine(error: BridgeErrorCapture): ShellLifecycleLine { + const firstLine = error.message.split(/\r?\n/, 1)[0] ?? '' + return pageLine( + 'warn', + 'Web shell: page faulted', + `${error.category}: ${maskUserText(firstLine)}` + ) +} + +/** The profile read behind `init` rejected, so no bridge host can be built for this screen. */ +export function hostProfileUnreadableLine(): ShellLifecycleLine { + return pageLine('warn', 'Web shell: host profile unreadable') +} + +function eventLines( + before: MobileWebShellSession, + event: MobileWebShellSessionEvent, + trace: ShellLogTrace +): ShellLifecycleLine[] { + if (event.type === 'download-failed') { + const code = event.cause.hostCode === null ? '' : ` code=${event.cause.hostCode}` + return [ + flowLine('warn', 'Web shell: app download failed', `reason=${event.cause.reason}${code}`) + ] + } + // Every page signal below is only accepted while a generation is on screen. + if (before.state.kind !== 'ready') { + return [] + } + switch (event.type) { + case 'shell-failed': + return [pageLine('warn', 'Web shell: page load failed', `reason=${event.reason}`)] + case 'page-painted': + // The one line a healthy open writes; `activatedMs` is flow start to activation, not to paint. + return before.pagePainted + ? [] + : [ + pageLine( + 'success', + 'Web shell: page painted', + `source=${trace.openedFrom ?? 'unknown'} build=${buildPrefix(before.state.buildId)} activatedMs=${before.state.elapsedMs}` + ) + ] + case 'page-ready-deadline': + return before.pageReady + ? [] + : [pageLine('warn', 'Web shell: page never said ready before the deadline')] + case 'document-started': + case 'document-loaded': + case 'page-ready': + case 'gates-changed': + case 'cache-read': + case 'manifest-read': + case 'fetch-progress': + case 'download-staged': + case 'activated': + case 'remounted': + case 'page-back-claim': + case 'layout-changed': + case 'retry-pressed': + // Healthy-path signals; the paint line or a stall line's detail stands in for them. + return [] + } +} + +function transitionLine( + before: MobileWebShellSessionState, + after: MobileWebShellSessionState +): ShellLifecycleLine | null { + if (before.kind === after.kind) { + return null + } + switch (after.kind) { + case 'checking': + return flowLine('info', 'Web shell: checking host') + case 'offline': + return flowLine('warn', 'Web shell: host unreachable and no app cached') + case 'fetching': + return flowLine( + 'info', + 'Web shell: downloading app', + `${after.totalAssets} assets, ${after.totalBytes} bytes` + ) + case 'activating': + // A cache activation is immediate and shows up as the paint line's source. + return after.source === 'download' + ? flowLine('info', 'Web shell: activating app', 'source=download') + : null + case 'ready': + case 'native-route': + // `ready` is logged at paint; native is every native screen's normal outcome. + return null + case 'wall': { + const side = 'side' in after.verdict ? ` side=${after.verdict.side}` : '' + return flowLine( + 'warn', + 'Web shell: blocked by version check', + `reason=${after.verdict.reason}${side}` + ) + } + case 'failed': + return flowLine( + 'error', + 'Web shell: failed', + `reason=${after.reason} retried=${after.retriedOnce ? 'yes' : 'no'}` + ) + } +} + +function buildPrefix(buildId: string): string { + return buildId.slice(0, BUILD_ID_PREFIX_LENGTH) +} + +function yesNo(value: boolean): string { + return value ? 'yes' : 'no' +} + +function flowLine( + level: ShellLifecycleLine['level'], + message: string, + detail?: string +): ShellLifecycleLine { + return { level, code: 'web-shell-flow', message, ...(detail === undefined ? {} : { detail }) } +} + +function pageLine( + level: ShellLifecycleLine['level'], + message: string, + detail?: string +): ShellLifecycleLine { + return { level, code: 'web-shell-page', message, ...(detail === undefined ? {} : { detail }) } +} + +function stallLine(message: string, detail: string): ShellLifecycleLine { + return { level: 'warn', code: 'web-shell-stalled', message, detail } +} diff --git a/mobile/src/mobile-web-shell/shell-log-path-masking.test.ts b/mobile/src/mobile-web-shell/shell-log-path-masking.test.ts new file mode 100644 index 00000000000..2e1986f9880 --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-log-path-masking.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' +import { maskUserText } from './shell-log-path-masking' + +describe('shell log path masking', () => { + it('masks absolute paths wherever they start, including spaced and Windows ones', () => { + expect(maskUserText("open '/var/mobile/Containers/a.db' failed")).toBe("open '[path]' failed") + expect(maskUserText('at f (/Users/me/secret.js:1)')).toBe('at f ([path])') + expect(maskUserText('bad `/Users/me/x.js` here')).toBe('bad `[path]` here') + expect(maskUserText('path="/home/bob/x" [/Users/bob/y]')).toBe('path="[path]" [[path]]') + expect(maskUserText('read C:\\Users\\bob\\x.txt and \\\\srv\\share\\y')).toBe( + 'read [path] and [path]' + ) + expect(maskUserText('key:~/notes.txt')).toBe('key:[path]') + expect(maskUserText('open C:\\Users\\John Doe\\proj\\x.ts failed')).toBe('open [path] failed') + expect(maskUserText('at /Users/Jane Q/My Proj/a.js and more')).toBe('at [path] and more') + expect(maskUserText('a,/Users/x/y')).toBe('a,[path]') + expect(maskUserText('Cannot read properties of undefined')).toBe( + 'Cannot read properties of undefined' + ) + }) +}) diff --git a/mobile/src/mobile-web-shell/shell-log-path-masking.ts b/mobile/src/mobile-web-shell/shell-log-path-masking.ts new file mode 100644 index 00000000000..4d0350757b7 --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-log-path-masking.ts @@ -0,0 +1,15 @@ +const MASKED_TEXT_MAX_CHARS = 160 + +/** Absolute paths and file URLs masked, then clamped: page and schema text can quote user paths. */ +export function maskUserText(text: string): string { + const masked = text + .replace(/file:\/\/[^\s)'"`\]]+/g, '[path]') + // A following word that still holds a separator is the same path (`C:\Users\John Doe\x`). + .replace( + /(^|[\s'"`([=:,])(?:~?\/|[A-Za-z]:[\\/]|\\\\)[^\s)'"`\]]+(?: [^\s)'"`\]]*[\\/][^\s)'"`\]]*)*/g, + '$1[path]' + ) + return masked.length > MASKED_TEXT_MAX_CHARS + ? `${masked.slice(0, MASKED_TEXT_MAX_CHARS)}…` + : masked +} diff --git a/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.test.ts b/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.test.ts index 67bc07fba2c..13876aa0d53 100644 --- a/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.test.ts +++ b/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.test.ts @@ -45,6 +45,7 @@ import { useMobileWebShellBridge, type MobileWebShellBridgeView } from './use-mobile-web-shell-bridge' +import { connectionLogStore } from '../transport/persisted-connection-log-store' const ID = bridgeId(1) const DIRECTORY = '/caches/mobile-web/deadbeef/generations/a1b2' @@ -425,6 +426,24 @@ describe('the bridge channel', () => { await mounted.deliver(clientFrame({ type: 'ready' })) expect(mounted.posted).toEqual([]) }) + + it('logs a frame it dropped for want of a host once per session, naming what is missing', async () => { + doubles.client = null + const dropped = () => + connectionLogStore + .get('host-1') + .filter((entry) => entry.message === 'Web shell: page frame with no bridge host') + const before = dropped().length + const mounted = await mount(readyState('session-one')) + await mounted.deliver(clientFrame({ type: 'ready' })) + await mounted.deliver(clientFrame({ type: 'ready' })) + expect(dropped().slice(before)).toEqual([ + expect.objectContaining({ detail: 'client=null snapshot=ok' }) + ]) + await mounted.update(readyState('session-two')) + await mounted.deliver(clientFrame({ type: 'ready' })) + expect(dropped().slice(before)).toHaveLength(2) + }) }) describe('teardown', () => { diff --git a/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.ts b/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.ts index 1f20a1b81b8..0db8fd8af2b 100644 --- a/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.ts +++ b/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.ts @@ -5,6 +5,8 @@ import type { } from '../../modules/orca-mobile-web-shell/src' import { useHostClient } from '../transport/client-context' import { createBridgeDiagnosticReporter } from './bridge-diagnostic-log' +import { bridgeDiagnosticLine, frameWithoutHostLine } from './shell-bridge-log-lines' +import { recordShellLifecycleLine } from './shell-lifecycle-line-recorder' import type { BridgeInitRoute } from './bridge/bridge-envelope' import type { BridgeClearableRouteParam } from './bridge/bridge-route-update' import type { BridgeSafeAreaInsets } from './bridge/bridge-safe-area-insets' @@ -169,6 +171,10 @@ export function useMobileWebShellBridge(args: MobileWebShellBridgeArgs): MobileW // Held rather than depended on, for the reason every prop above is: the identity settles with the // client, and taking it as a dependency would tear a live page session down to re-read a string. const clientIdRef = useRef(clientId) + // Read only by the dropped-frame log line, which says which of the two a host waits on. + const clientRef = useRef(client) + // The session a dropped frame was last logged for, so a page re-asking `ready` logs once. + const droppedFrameLoggedRef = useRef(null) // Commit-phase and declared above the host's effect, so the host is built against what this // render passed: a native frame can land between a commit and a passive effect. // @@ -178,6 +184,7 @@ export function useMobileWebShellBridge(args: MobileWebShellBridgeArgs): MobileW useLayoutEffect(() => { argsRef.current = args clientIdRef.current = clientId + clientRef.current = client }) const snapshot = args.snapshot @@ -214,7 +221,12 @@ export function useMobileWebShellBridge(args: MobileWebShellBridgeArgs): MobileW ? Promise.reject(new BridgeViewGoneError()) : mounted.handle.postBridgeMessage(json) }, - onDiagnostic: createBridgeDiagnosticReporter(), + onDiagnostic: createBridgeDiagnosticReporter((diagnostic) => { + const line = bridgeDiagnosticLine(diagnostic) + if (line !== null) { + recordShellLifecycleLine(latest.hostId, line) + } + }), onNavigate: (href) => argsRef.current.onNavigate(href), onNavigateBack: () => argsRef.current.onNavigateBack(), onExternalLink: (url) => argsRef.current.onExternalLink(url), @@ -256,6 +268,20 @@ export function useMobileWebShellBridge(args: MobileWebShellBridgeArgs): MobileW (event: MobileWebShellBridgeMessageEvent) => { const mounted = hostRef.current if (mounted === null || mounted.sessionId !== sessionId) { + if ( + mounted === null && + sessionId !== null && + droppedFrameLoggedRef.current !== sessionId + ) { + droppedFrameLoggedRef.current = sessionId + recordShellLifecycleLine( + argsRef.current.hostId, + frameWithoutHostLine({ + clientReady: clientRef.current !== null, + snapshotReady: argsRef.current.snapshot !== null + }) + ) + } return } mounted.host.receive(event.nativeEvent.json) diff --git a/mobile/src/mobile-web-shell/use-mobile-web-shell-session.test.ts b/mobile/src/mobile-web-shell/use-mobile-web-shell-session.test.ts index df01ca7e70d..31502505594 100644 --- a/mobile/src/mobile-web-shell/use-mobile-web-shell-session.test.ts +++ b/mobile/src/mobile-web-shell/use-mobile-web-shell-session.test.ts @@ -67,6 +67,9 @@ const doubles = vi.hoisted((): Doubles => { }) vi.mock('expo-crypto', () => ({ getRandomBytes: (length: number) => new Uint8Array(length) })) +vi.mock('@react-native-async-storage/async-storage', () => ({ + default: { getItem: async () => null, setItem: async () => undefined } +})) vi.mock('expo-file-system', () => ({ Directory: class {}, File: class {}, Paths: { cache: '' } })) vi.mock('../transport/mobile-endpoint-supervisor-support', () => ({ encodeBase64Url: () => 'session-id' @@ -95,6 +98,8 @@ vi.mock('../transport/mobile-web-bundle-fetch', () => ({ import { BRIDGE_READY_RETRY_MAX_MS } from './bridge/bridge-client-init-handshake' import { PAGE_READY_DEADLINE_MS } from './mobile-web-shell-runtime' +import { SHELL_STALL_LOG_MS } from './shell-lifecycle-log' +import { connectionLogStore } from '../transport/persisted-connection-log-store' import { useMobileWebShellSession } from './use-mobile-web-shell-session' const HOST_ID = 'host-1' @@ -214,21 +219,28 @@ type Mounted = { handshakes: () => readonly boolean[] documentLoaded: () => void pageReady: () => void + pagePainted: () => void timers: ReturnType } +function live(timers: readonly ArmedTimer[]): ArmedTimer[] { + return timers.filter((timer) => !timer.cancelled) +} + async function mount(store: GenerationStore): Promise { const timers = createTimerSeam() const handle: { retry: () => void documentLoaded: () => void pageReady: () => void + pagePainted: () => void states: MobileWebShellSessionState[] handshakes: boolean[] } = { retry: () => {}, documentLoaded: () => {}, pageReady: () => {}, + pagePainted: () => {}, states: [], handshakes: [] } @@ -246,6 +258,7 @@ async function mount(store: GenerationStore): Promise { handle.retry = session.retry handle.documentLoaded = session.reportDocumentLoaded handle.pageReady = session.reportPageReady + handle.pagePainted = session.reportPagePainted handle.states.push(session.state) handle.handshakes.push(session.pageReady) return null @@ -266,6 +279,7 @@ async function mount(store: GenerationStore): Promise { handshakes: () => handle.handshakes, documentLoaded: () => handle.documentLoaded(), pageReady: () => handle.pageReady(), + pagePainted: () => handle.pagePainted(), timers } } @@ -471,7 +485,11 @@ describe('the wait for the page to speak', () => { await act(async () => { mounted.documentLoaded() }) - expect(mounted.timers.armed.map((timer) => timer.delayMs)).toEqual([PAGE_READY_DEADLINE_MS]) + // The other live timer is the log-only stall warning; the checking one was cancelled. + expect(live(mounted.timers.armed).map((timer) => timer.delayMs)).toEqual([ + SHELL_STALL_LOG_MS, + PAGE_READY_DEADLINE_MS + ]) await act(async () => { mounted.timers.fire() }) @@ -523,7 +541,9 @@ describe('the wait for the page to speak', () => { }) // The race is real on a device: the page's first frame crosses the bridge while the WebView's // own load callback is still in the native queue. - expect(mounted.timers.armed).toHaveLength(0) + expect( + mounted.timers.armed.filter((timer) => timer.delayMs === PAGE_READY_DEADLINE_MS) + ).toEqual([]) await act(async () => { mounted.tree.unmount() }) @@ -579,6 +599,74 @@ describe('the wait for the page to speak', () => { }) }) + it('logs one stall warning for a page that never draws, without moving the session', async () => { + const logged = () => connectionLogStore.get(HOST_ID) + const before = logged().length + const mounted = await ready() + await act(async () => { + mounted.documentLoaded() + mounted.pageReady() + }) + const stallTimers = live(mounted.timers.armed).filter( + (timer) => timer.delayMs === SHELL_STALL_LOG_MS + ) + // A page signal that leaves the cover up re-arms nothing. + expect(stallTimers).toHaveLength(1) + await act(async () => { + stallTimers[0]?.run() + }) + expect(mounted.states().at(-1)?.kind).toBe('ready') + expect(logged().slice(before)).toEqual([ + expect.objectContaining({ + message: 'Web shell: page has not drawn after 15s', + detail: expect.stringContaining('page ready=yes readyAsks=1 documentLoaded=yes') + }) + ]) + }) + + it('writes one line for a healthy open, on paint', async () => { + const before = connectionLogStore.get(HOST_ID).length + const mounted = await ready() + await act(async () => { + mounted.documentLoaded() + mounted.pageReady() + mounted.pagePainted() + }) + expect( + connectionLogStore + .get(HOST_ID) + .slice(before) + .map((entry) => `${entry.message} | ${entry.detail}`) + ).toEqual([expect.stringMatching(/^Web shell: page painted \| source=cache build=\S{1,8} /)]) + expect(live(mounted.timers.armed).filter((t) => t.delayMs === SHELL_STALL_LOG_MS)).toEqual([]) + await act(async () => { + mounted.tree.unmount() + }) + }) + + it("logs a stall for the first open's checking state, which no transition line covers", async () => { + const before = connectionLogStore.get(HOST_ID).length + const fake = createFakeStore() + const mounted = await mount(fake.store) + expect(mounted.states().at(-1)?.kind).toBe('checking') + const stallTimers = live(mounted.timers.armed).filter( + (timer) => timer.delayMs === SHELL_STALL_LOG_MS + ) + expect(stallTimers).toHaveLength(1) + await act(async () => { + stallTimers[0]?.run() + }) + expect(connectionLogStore.get(HOST_ID).slice(before)).toEqual([ + expect.objectContaining({ + message: 'Web shell: still checking host after 15s', + detail: 'gates=open cache=unread' + }) + ]) + await act(async () => { + mounted.tree.unmount() + }) + }) + it('cancels the armed deadline when the session it belongs to is torn down', async () => { const mounted = await ready() await act(async () => { diff --git a/mobile/src/mobile-web-shell/use-mobile-web-shell-session.ts b/mobile/src/mobile-web-shell/use-mobile-web-shell-session.ts index dca736d415c..0d917fdd564 100644 --- a/mobile/src/mobile-web-shell/use-mobile-web-shell-session.ts +++ b/mobile/src/mobile-web-shell/use-mobile-web-shell-session.ts @@ -12,6 +12,14 @@ import { } from './mobile-web-shell-runtime' import { readMobileWebShellReachability } from './mobile-web-shell-reachability' import { shellPageBackClaimed } from './shell-page-back-claim' +import { recordShellLifecycleLine } from './shell-lifecycle-line-recorder' +import { + EMPTY_SHELL_LOG_TRACE, + SHELL_STALL_LOG_MS, + shellSessionStepLines, + shellStallLine, + traceShellStep +} from './shell-lifecycle-log' import { shellPageFrame, type ShellPageFrame } from './shell-page-frame' import { createMobileWebShellSession, @@ -102,6 +110,9 @@ export function useMobileWebShellSession(args: { // Cancelled on the same bump, for the same reason: an armed deadline belongs to the generation it // was armed under, and the epoch check alone would leave a real timer alive until it fired. const timersRef = useRef void>>(new Set()) + // The log's host for `dispatch`, which is stable; moved with the session rebuild below. + const hostIdRef = useRef(hostId) + const logTraceRef = useRef(EMPTY_SHELL_LOG_TRACE) const runEffectRef = useRef< ((epoch: number, flow: number, effect: MobileWebShellSessionEffect) => void) | null >(null) @@ -111,6 +122,11 @@ export function useMobileWebShellSession(args: { return } const stepped = reduceMobileWebShellSession(sessionRef.current, event) + const trace = traceShellStep(logTraceRef.current, sessionRef.current, event, stepped.session) + logTraceRef.current = trace + for (const line of shellSessionStepLines(sessionRef.current, event, stepped.session, trace)) { + recordShellLifecycleLine(hostIdRef.current, line) + } sessionRef.current = stepped.session setState(stepped.session.state) setPageReady(stepped.session.pageReady) @@ -218,6 +234,8 @@ export function useMobileWebShellSession(args: { useEffect(() => { // A new host is a new session: the old one's latches, cache handle and in-flight work all go. invalidate() + hostIdRef.current = hostId + logTraceRef.current = EMPTY_SHELL_LOG_TRACE sessionRef.current = createMobileWebShellSession(routePathname, wide) startedAtRef.current = runtime.now() setState(sessionRef.current.state) @@ -226,6 +244,20 @@ export function useMobileWebShellSession(args: { return invalidate }, [hostId, invalidate, routePathname, runtime]) + // Log-only: one warning per parked state object, cancelled by any state, paint, host or route change. + // `CHECKING` is one shared object, so checking-to-checking steps keep the first open's timer. + useEffect(() => { + if (shellStallLine(state, sessionRef.current, logTraceRef.current) === null) { + return + } + return runtime.setTimer(() => { + const line = shellStallLine(state, sessionRef.current, logTraceRef.current) + if (line !== null) { + recordShellLifecycleLine(hostIdRef.current, line) + } + }, SHELL_STALL_LOG_MS) + }, [hostId, pageFrame, routePathname, runtime, state]) + useEffect(() => { dispatch(epochRef.current, { type: 'layout-changed', wide }) }, [dispatch, wide]) diff --git a/mobile/src/transport/types.ts b/mobile/src/transport/types.ts index d9c9271c79d..4d83a5cb762 100644 --- a/mobile/src/transport/types.ts +++ b/mobile/src/transport/types.ts @@ -64,6 +64,11 @@ export type ConnectionDiagnosticCode = | 'direct-connected' | 'relay-credential-unavailable' | 'host-open-failed' + // OTA web shell lifecycle; diagnostics only, never read as connection failures. + | 'web-shell-flow' + | 'web-shell-page' + | 'web-shell-stalled' + | 'web-shell-bridge' export type ConnectionLogEntry = { id: string From e53b20095d3ca98df120cd0a3ef4814e945a9779 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:54:46 -0400 Subject: [PATCH 13/53] fix(terminal): reject a requested tab id that is already taken (#28841) * fix(terminal): reject a requested tab id that is already taken createTab silently minted a fresh id when the caller's requested id was already in use. The caller's PTY routes agent status by the id it asked for, so the new tab never received its agent status and the status went to the other tab instead. Now the create throws and changes nothing. Co-Authored-By: Claude Opus 5.5 * refactor(agent-launch): drop the unreachable tab id mismatch guard Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .../src/lib/agent-launch-tab-publication.ts | 5 ---- .../slices/store-create-tab-id-hint.test.ts | 28 ++++++++----------- .../store/terminals/terminal-tab-creation.ts | 12 ++++---- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/renderer/src/lib/agent-launch-tab-publication.ts b/src/renderer/src/lib/agent-launch-tab-publication.ts index 20116563991..4c9959ea2d1 100644 --- a/src/renderer/src/lib/agent-launch-tab-publication.ts +++ b/src/renderer/src/lib/agent-launch-tab-publication.ts @@ -103,11 +103,6 @@ export function publishAgentLaunchTab( if (request.prompt) { rememberAgentLaunchPanePrompt(tabId, request.prompt) } - if (tab.id !== tabId) { - // createTab mints a fresh id on a collision it saw and we did not; that tab would never attach. - useAppStore.getState().closeTab(tab.id, { recordInteraction: false }) - throw new Error('agent_launch_tab_id_taken') - } if (placement.anchorTabId) { const unifiedTabId = useAppStore .getState() diff --git a/src/renderer/src/store/slices/store-create-tab-id-hint.test.ts b/src/renderer/src/store/slices/store-create-tab-id-hint.test.ts index 6dbda55207c..643b9454e97 100644 --- a/src/renderer/src/store/slices/store-create-tab-id-hint.test.ts +++ b/src/renderer/src/store/slices/store-create-tab-id-hint.test.ts @@ -47,7 +47,7 @@ describe('createTab tabId hint', () => { expect(tab.id).toBe(hintedId) }) - it('falls back to a fresh id on collision and warns', () => { + it('rejects a taken id instead of minting a fresh one', () => { const store = createTestStore() const wt = 'repo1::/path/wt-collision' const existingId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' @@ -63,14 +63,12 @@ describe('createTab tabId hint', () => { unifiedTabsByWorktree: {} }) - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - try { - const tab = store.getState().createTab(wt, undefined, undefined, { id: existingId }) - expect(tab.id).not.toBe(existingId) - expect(warn).toHaveBeenCalledWith(expect.stringContaining(existingId)) - } finally { - warn.mockRestore() - } + const before = store.getState().tabsByWorktree + + expect(() => store.getState().createTab(wt, undefined, undefined, { id: existingId })).toThrow( + `Terminal tab ${existingId} already exists` + ) + expect(store.getState().tabsByWorktree).toBe(before) }) it('treats tab ids as global and rejects hints that collide in another worktree', () => { @@ -93,14 +91,10 @@ describe('createTab tabId hint', () => { unifiedTabsByWorktree: {} }) - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - try { - const tab = store.getState().createTab(wtA, undefined, undefined, { id: existingId }) - expect(tab.id).not.toBe(existingId) - expect(warn).toHaveBeenCalledWith(expect.stringContaining(existingId)) - } finally { - warn.mockRestore() - } + expect(() => store.getState().createTab(wtA, undefined, undefined, { id: existingId })).toThrow( + `Terminal tab ${existingId} already exists` + ) + expect(store.getState().tabsByWorktree[wtA]).toBeUndefined() }) it('ignores empty string hints instead of persisting an unusable tab id', () => { diff --git a/src/renderer/src/store/terminals/terminal-tab-creation.ts b/src/renderer/src/store/terminals/terminal-tab-creation.ts index 5e12d3b45a8..511498965d1 100644 --- a/src/renderer/src/store/terminals/terminal-tab-creation.ts +++ b/src/renderer/src/store/terminals/terminal-tab-creation.ts @@ -52,22 +52,20 @@ export function createTerminalTabCreationActions( const existing = (s.tabsByWorktree[worktreeId] ?? []).filter( (entry) => !orphanTerminalIds.has(entry.id) ) - // Why: honor a caller-supplied tab id but mint on collision — aliasing two PTYs to one id corrupts agent-status routing. See docs/cli-terminal-hook-pane-key.md. // Why: only honor a non-empty trimmed hint; useIpcEvents spreads `id` whenever tabId !== undefined, so a stray '' would break paneKey routing. const trimmedHint = typeof options?.id === 'string' ? options.id.trim() : '' const hintedId = trimmedHint.length > 0 && isValidHostTerminalTabId(trimmedHint) ? trimmedHint : undefined - const idCollides = + // Why: the caller's PTY routes agent status by this id, so a fresh id would orphan it and aliasing would corrupt another tab's. + if ( hintedId !== undefined && Object.values(s.tabsByWorktree).some((tabs) => tabs.some((entry) => entry.id === hintedId) ) - if (idCollides) { - console.warn( - `[createTab] tabId hint ${hintedId} already exists; minting a fresh id (hook attribution will degrade for this terminal)` - ) + ) { + throw new Error(`Terminal tab ${hintedId} already exists`) } - const id = hintedId !== undefined && !idCollides ? hintedId : createBrowserUuid() + const id = hintedId ?? createBrowserUuid() const requestedInitialLeafId = options?.initialLeafId && isTerminalLeafId(options.initialLeafId) ? options.initialLeafId From e18e7fcf10c2c1bc0db8215edd73011e82b72118 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:28:05 -0400 Subject: [PATCH 14/53] chore(relay): pin staging c1, c5, c6 and c7 to image G for E-pre (#28872) Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f --- cloud/infra/terraform/environments/staging.tfvars | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/infra/terraform/environments/staging.tfvars b/cloud/infra/terraform/environments/staging.tfvars index a4aff0d8600..d74abd85b75 100644 --- a/cloud/infra/terraform/environments/staging.tfvars +++ b/cloud/infra/terraform/environments/staging.tfvars @@ -48,8 +48,8 @@ relay_gce_cells = { boot_disk_gb = 30 boot_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-stable-121-18867-528-7" capacity_requests = 4000 - # Step-5 image F (main 3c1141f2). Never 2d0f6e6: it cannot report capped telemetry. - image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:41cba995b288cab70579882ebef23e74c5ee426b6f8924cea7c986aba23bcbfe" + # Step-5 image G (main 2de73812, reports preAuthLatencyMs). Never 2d0f6e6: it cannot report capped telemetry. + image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:07b5bf67a7706b2a61ef5e9a6d8ce85ee37c2a08609496a94eb5254881b5b87d" # Step-5 staging e2e: c1 is the second US reserve cell, so it needs a cap and rehome trust. connection_hard_cap = 600 connection_unobserved_bound = 60 @@ -100,7 +100,7 @@ relay_gce_cells = { boot_disk_gb = 30 boot_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-stable-121-18867-528-7" capacity_requests = 4000 - image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:41cba995b288cab70579882ebef23e74c5ee426b6f8924cea7c986aba23bcbfe" + image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:07b5bf67a7706b2a61ef5e9a6d8ce85ee37c2a08609496a94eb5254881b5b87d" connection_hard_cap = 600 connection_unobserved_bound = 60 } @@ -111,7 +111,7 @@ relay_gce_cells = { boot_disk_gb = 30 boot_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-stable-121-18867-528-7" capacity_requests = 4000 - image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:41cba995b288cab70579882ebef23e74c5ee426b6f8924cea7c986aba23bcbfe" + image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:07b5bf67a7706b2a61ef5e9a6d8ce85ee37c2a08609496a94eb5254881b5b87d" connection_hard_cap = 600 connection_unobserved_bound = 60 } @@ -122,7 +122,7 @@ relay_gce_cells = { boot_disk_gb = 30 boot_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-stable-121-18867-528-7" capacity_requests = 4000 - image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:41cba995b288cab70579882ebef23e74c5ee426b6f8924cea7c986aba23bcbfe" + image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:07b5bf67a7706b2a61ef5e9a6d8ce85ee37c2a08609496a94eb5254881b5b87d" connection_hard_cap = 600 connection_unobserved_bound = 60 } From 7244bf3e6f0f6e0adba835213da8331181addf8e Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:47:02 -0700 Subject: [PATCH 15/53] Reclaim idle worktree space automatically on dev startup (#28864) * Run safe worktree cleanup on dev and add interactive pnpm reclaim * Rename storage cleanup command to reclaim-space * Keep real stream constructors in transcript dependency tests * Detach cleanup output and align historical cache previews --- config/scripts/dev-storage-reclaim.mjs | 41 +++++++++ config/scripts/dev-storage-reclaim.test.mjs | 84 ++++++++++++++++++ config/scripts/reclaim-electron-dists.mjs | 22 +++++ config/scripts/reclaim-electron-dists.test.ts | 35 +++++++- .../scripts/reclaim-release-test-caches.mjs | 2 +- config/scripts/reclaim-worktree-command.mjs | 83 ++++++++++++++++++ .../scripts/reclaim-worktree-command.test.mjs | 87 +++++++++++++++++++ config/scripts/reclaim-worktree-space.mjs | 55 +++--------- .../scripts/reclaim-worktree-space.test.mjs | 44 ++++++---- config/scripts/run-electron-vite-dev.mjs | 7 ++ docs/readme/release-source-sharing.md | 9 +- docs/readme/worktree-storage-maintenance.md | 31 +++++-- package.json | 4 +- ...ion-search-transcript-dependencies.test.ts | 13 ++- .../startup/run-electron-vite-dev-web.test.ts | 2 + .../startup/run-electron-vite-dev.test.ts | 2 +- 16 files changed, 440 insertions(+), 81 deletions(-) create mode 100644 config/scripts/dev-storage-reclaim.mjs create mode 100644 config/scripts/dev-storage-reclaim.test.mjs create mode 100644 config/scripts/reclaim-worktree-command.mjs create mode 100644 config/scripts/reclaim-worktree-command.test.mjs diff --git a/config/scripts/dev-storage-reclaim.mjs b/config/scripts/dev-storage-reclaim.mjs new file mode 100644 index 00000000000..5b2123678af --- /dev/null +++ b/config/scripts/dev-storage-reclaim.mjs @@ -0,0 +1,41 @@ +import { resolve } from 'node:path' +import { spawnProcess } from '@orca/process-host' + +export function startDevStorageReclaim({ + repoRoot, + env = process.env, + spawn = spawnProcess, + report = console.error +}) { + if (env.ORCA_SKIP_DEV_STORAGE_RECLAIM === '1') { + return + } + try { + // The worker owns its lock after dev exits and must not keep captured dev output open. + const child = spawn({ + program: process.execPath, + args: [ + resolve(import.meta.dirname, 'reclaim-worktree-space.mjs'), + '--repo', + repoRoot, + '--exclude-worktree', + repoRoot, + '--automatic', + '--apply' + ], + cwd: repoRoot, + env, + detached: true, + stdio: 'ignore' + }) + child.on('error', (error) => report(`[orca-dev] Storage cleanup skipped: ${error.message}`)) + child.on('exit', (code) => { + if (code !== 0) { + report('[orca-dev] Storage cleanup did not finish; retry with pnpm reclaim-space.') + } + }) + child.unref() + } catch (error) { + report(`[orca-dev] Storage cleanup skipped: ${error.message}`) + } +} diff --git a/config/scripts/dev-storage-reclaim.test.mjs b/config/scripts/dev-storage-reclaim.test.mjs new file mode 100644 index 00000000000..390b686d7cf --- /dev/null +++ b/config/scripts/dev-storage-reclaim.test.mjs @@ -0,0 +1,84 @@ +import { EventEmitter } from 'node:events' +import { tmpdir } from 'node:os' +import { resolve } from 'node:path' +import { pathToFileURL } from 'node:url' +import { runProcess } from '@orca/process-host' +import { expect, it, vi } from 'vitest' +import { startDevStorageReclaim } from './dev-storage-reclaim.mjs' + +function fixture() { + const child = new EventEmitter() + child.unref = vi.fn() + const spawn = vi.fn(() => child) + return { child, spawn, report: vi.fn(), repoRoot: '/dev-worktree', env: {} } +} + +it('starts one non-interactive worker without waiting for its exit', () => { + const options = fixture() + startDevStorageReclaim(options) + expect(options.spawn).toHaveBeenCalledTimes(1) + expect(options.spawn.mock.calls[0][0]).toMatchObject({ + args: expect.arrayContaining([ + '--automatic', + '--apply', + '--exclude-worktree', + options.repoRoot + ]), + detached: true, + stdio: 'ignore' + }) + expect(options.child.unref).toHaveBeenCalledTimes(1) + expect(options.report).not.toHaveBeenCalled() +}) + +it('allows tests and developers to skip the repository-wide sweep', () => { + const options = fixture() + startDevStorageReclaim({ ...options, env: { ORCA_SKIP_DEV_STORAGE_RECLAIM: '1' } }) + expect(options.spawn).not.toHaveBeenCalled() +}) + +it('reports cleanup failure without throwing into dev startup', () => { + const options = fixture() + startDevStorageReclaim(options) + options.child.emit('exit', 1) + expect(options.report).toHaveBeenCalledWith(expect.stringContaining('pnpm reclaim-space')) +}) + +it.each([true, false])('handles both sync and async spawn errors (sync: %j)', (synchronous) => { + const options = fixture() + if (synchronous) { + options.spawn.mockImplementation(() => { + throw new Error('spawn failed') + }) + } + expect(() => startDevStorageReclaim(options)).not.toThrow() + if (!synchronous) { + options.child.emit('error', new Error('spawn failed')) + } + expect(options.report).toHaveBeenCalledWith(expect.stringContaining('spawn failed')) +}) + +it('closes captured dev output before the detached worker exits', async () => { + const entry = pathToFileURL(resolve(import.meta.dirname, 'dev-storage-reclaim.mjs')).href + const result = await runProcess({ + program: process.env.ORCA_TEST_NODE_EXECUTABLE ?? process.execPath, + args: [ + '--input-type=module', + '-e', + `import { startDevStorageReclaim } from ${JSON.stringify(entry)}; + import { spawnProcess } from '@orca/process-host'; + startDevStorageReclaim({ + repoRoot: ${JSON.stringify(tmpdir())}, + env: { ...process.env, ORCA_SKIP_DEV_STORAGE_RECLAIM: '0' }, + spawn: (spec) => spawnProcess({ ...spec, args: ['-e', 'setTimeout(() => {}, 5000)'] }) + }); + process.exit(0);` + ], + cwd: resolve(import.meta.dirname, '../..'), + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }, + timeoutMs: 2_000, + terminationBarrier: true + }) + expect(result.timedOut).toBe(false) + expect(result.code, result.stderr).toBe(0) +}) diff --git a/config/scripts/reclaim-electron-dists.mjs b/config/scripts/reclaim-electron-dists.mjs index 03e7b539d54..b4116c7441b 100644 --- a/config/scripts/reclaim-electron-dists.mjs +++ b/config/scripts/reclaim-electron-dists.mjs @@ -11,6 +11,7 @@ import { existsSync, readFileSync, readdirSync, + realpathSync, renameSync, rmSync, statSync, @@ -32,6 +33,24 @@ const repoRoot = process.argv.includes('--repo') ? path.resolve(process.argv[process.argv.indexOf('--repo') + 1]) : process.cwd() +const excludeIndex = process.argv.indexOf('--exclude-worktree') +const excludeArgument = excludeIndex === -1 ? undefined : process.argv[excludeIndex + 1] +if (excludeIndex !== -1 && (!excludeArgument || excludeArgument.startsWith('-'))) { + throw new Error('--exclude-worktree requires a directory') +} +const excludeWorktree = excludeArgument ? realpathSync(excludeArgument) : null + +function isExcluded(worktree) { + if (!excludeWorktree) { + return false + } + try { + return realpathSync(worktree) === excludeWorktree + } catch { + return path.resolve(worktree) === excludeWorktree + } +} + function listWorktrees(root) { const raw = execFileSync('git', ['-C', root, 'worktree', 'list', '--porcelain'], { encoding: 'utf8' @@ -88,6 +107,9 @@ function main() { let skipped = 0 for (const worktree of listWorktrees(repoRoot)) { + if (isExcluded(worktree)) { + continue + } const electronPackageDir = path.join(worktree, 'node_modules', 'electron') const distPath = path.join(electronPackageDir, 'dist') if (!existsSync(path.join(electronPackageDir, 'package.json')) || !existsSync(distPath)) { diff --git a/config/scripts/reclaim-electron-dists.test.ts b/config/scripts/reclaim-electron-dists.test.ts index b1b3e08e094..fbac962ca5e 100644 --- a/config/scripts/reclaim-electron-dists.test.ts +++ b/config/scripts/reclaim-electron-dists.test.ts @@ -1,4 +1,4 @@ -import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { chmod, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { afterEach, expect, it } from 'vitest' @@ -29,14 +29,15 @@ async function fixture() { await writeFile(join(pkg, 'package.json'), JSON.stringify({ version: '99.1.0' })) await writeFile(join(pkg, 'dist', 'version'), '99.1.0\n') await writeFile(executable, 'fake electron executable\n') - const run = (apply = false) => + const run = (apply = false, flags: string[] = []) => runProcess({ program: process.env.ORCA_TEST_NODE_EXECUTABLE ?? process.execPath, args: [ resolve(import.meta.dirname, 'reclaim-electron-dists.mjs'), '--repo', root, - ...(apply ? ['--apply'] : []) + ...(apply ? ['--apply'] : []), + ...flags ], cwd: root, env: { ...process.env, CI: '', ORCA_BACKGROUND_LAUNCH: '1' }, @@ -84,3 +85,31 @@ it('shares a writable install and preserves its executable bytes', async () => { const repeated = await run() expect(repeated.stdout).toContain('Would share 0 worktree(s)') }) + +it('protects the launching worktree, including when selected through a filesystem alias', async () => { + const { root, pkg, executable, run } = await fixture() + const alias = join(root, 'alias') + await symlink(root, alias, process.platform === 'win32' ? 'junction' : 'dir') + for (const exclusion of [root, alias]) { + for (const apply of [false, true]) { + const result = await run(apply, ['--exclude-worktree', exclusion]) + expect(result.code, result.stderr).toBe(0) + expect(result.stdout).toContain('0 worktree(s)') + expect(await readFile(executable, 'utf8')).toBe('fake electron executable\n') + await expect(readFile(join(pkg, '.orca-shared-dist'))).rejects.toMatchObject({ + code: 'ENOENT' + }) + } + } +}) + +it.each([['--exclude-worktree'], ['--exclude-worktree', '--apply']])( + 'rejects invalid exclusions before changing an install: %j', + async (...flags) => { + const { pkg, run } = await fixture() + const result = await run(true, flags) + expect(result.code).not.toBe(0) + expect(result.stderr).toContain('--exclude-worktree requires a directory') + await expect(readFile(join(pkg, '.orca-shared-dist'))).rejects.toMatchObject({ code: 'ENOENT' }) + } +) diff --git a/config/scripts/reclaim-release-test-caches.mjs b/config/scripts/reclaim-release-test-caches.mjs index 55f90a1b149..b064ba453ff 100644 --- a/config/scripts/reclaim-release-test-caches.mjs +++ b/config/scripts/reclaim-release-test-caches.mjs @@ -278,7 +278,7 @@ if (process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename const argument = process.argv[index] if (argument === '--help') { console.log( - 'Usage: pnpm run reclaim:worktree-space [--repo DIRECTORY] [--release-cache-min-age-days 30] [--apply --tests-stopped]' + 'Usage: pnpm reclaim-space [--repo DIRECTORY] [--release-cache-min-age-days 30] [--apply --tests-stopped]' ) process.exit(0) } diff --git a/config/scripts/reclaim-worktree-command.mjs b/config/scripts/reclaim-worktree-command.mjs new file mode 100644 index 00000000000..b4bac4116fa --- /dev/null +++ b/config/scripts/reclaim-worktree-command.mjs @@ -0,0 +1,83 @@ +import { createInterface } from 'node:readline/promises' +import { resolve } from 'node:path' +import { reclaimWorktreeSpace } from './reclaim-worktree-space.mjs' + +export function parseReclaimArguments(args) { + const options = { apply: false, automatic: false, testsStopped: false } + for (let index = 0; index < args.length; index += 1) { + const argument = args[index] + if (argument === '--apply') { + options.apply = true + } else if (argument === '--automatic') { + options.automatic = true + } else if (argument === '--tests-stopped') { + options.testsStopped = true + } else if (argument === '--help' || argument === '-h') { + options.help = true + } else if ( + argument === '--repo' || + argument === '--release-cache-min-age-days' || + argument === '--exclude-worktree' + ) { + const value = args[++index] + if (!value || value.startsWith('-')) { + throw new Error(`${argument} requires a value`) + } + if (argument === '--repo') { + options.repoRoot = resolve(value) + } else if (argument === '--exclude-worktree') { + options.excludeWorktree = resolve(value) + } else { + options.releaseCacheMinAgeDays = Number(value) + if ( + !Number.isFinite(options.releaseCacheMinAgeDays) || + options.releaseCacheMinAgeDays < 0 + ) { + throw new Error('--release-cache-min-age-days requires a non-negative number') + } + } + } else { + throw new Error(`Unknown argument: ${argument}`) + } + } + return options +} + +export async function runReclaimCommand( + args, + { input = process.stdin, output = process.stdout, reclaim = reclaimWorktreeSpace } = {} +) { + const options = parseReclaimArguments(args) + if (options.help) { + output.write( + 'Usage: pnpm reclaim-space [--apply] [--repo DIRECTORY] [--tests-stopped] [--release-cache-min-age-days 30]\n' + ) + output.write( + 'Shares Electron installs and removes idle dev bundles across local Git worktrees.\n' + ) + output.write('Without --apply, previews cleanup and offers [y/N] in an interactive terminal.\n') + output.write( + 'Add --tests-stopped to include old test caches after stopping cross-version tests.\n' + ) + return + } + if (!options.testsStopped && !options.automatic) { + output.write( + 'Historical test caches retained. Add --tests-stopped after stopping cross-version tests to include them.\n' + ) + } + await reclaim(options) + if (options.apply || options.automatic || !input.isTTY || !output.isTTY) { + return + } + + const prompt = createInterface({ input, output }) + try { + const answer = await prompt.question('Apply this cleanup? [y/N] ') + if (/^y(?:es)?$/i.test(answer.trim())) { + await reclaim({ ...options, apply: true }) + } + } finally { + prompt.close() + } +} diff --git a/config/scripts/reclaim-worktree-command.test.mjs b/config/scripts/reclaim-worktree-command.test.mjs new file mode 100644 index 00000000000..6b3f05eded8 --- /dev/null +++ b/config/scripts/reclaim-worktree-command.test.mjs @@ -0,0 +1,87 @@ +import { PassThrough } from 'node:stream' +import { expect, it, vi } from 'vitest' +import { parseReclaimArguments, runReclaimCommand } from './reclaim-worktree-command.mjs' + +function terminal(answer, tty = true) { + const input = new PassThrough() + const output = new PassThrough() + input.isTTY = tty + output.isTTY = tty + let text = '' + output.on('data', (chunk) => { + text += chunk.toString() + if (chunk.toString().includes('[y/N]')) { + input.write(`${answer}\n`) + } + }) + return { input, output, text: () => text } +} + +it.each(['Y', 'y', 'yes', ' YES '])( + 'applies after preview when the user answers %j', + async (answer) => { + const streams = terminal(answer) + const reclaim = vi.fn().mockResolvedValue({ completed: true }) + await runReclaimCommand(['--tests-stopped', '--release-cache-min-age-days', '7'], { + ...streams, + reclaim + }) + expect(reclaim.mock.calls.map(([options]) => options.apply)).toEqual([false, true]) + expect(reclaim.mock.calls[1][0]).toMatchObject({ + testsStopped: true, + releaseCacheMinAgeDays: 7 + }) + } +) + +it.each(['', 'n', 'no', 'sure'])('leaves files alone after preview on %j', async (answer) => { + const streams = terminal(answer) + const reclaim = vi.fn().mockResolvedValue({ completed: true }) + await runReclaimCommand([], { ...streams, reclaim }) + expect(reclaim).toHaveBeenCalledTimes(1) + expect(reclaim.mock.calls[0][0].apply).toBe(false) +}) + +it.each(['--apply', '--automatic'])('never prompts with %s', async (flag) => { + const streams = terminal('Y') + const reclaim = vi.fn().mockResolvedValue({ completed: true }) + await runReclaimCommand([flag], { ...streams, reclaim }) + expect(reclaim).toHaveBeenCalledTimes(1) + expect(streams.text()).not.toContain('[y/N]') +}) + +it('does not interpret piped input as approval', async () => { + const streams = terminal('Y', false) + streams.input.write('Y\n') + const reclaim = vi.fn().mockResolvedValue({ completed: true }) + await runReclaimCommand([], { ...streams, reclaim }) + expect(reclaim).toHaveBeenCalledTimes(1) + expect(streams.text()).not.toContain('[y/N]') +}) + +it('does not offer to apply a failed preview', async () => { + const streams = terminal('Y') + const reclaim = vi.fn().mockRejectedValue(new Error('preview failed')) + await expect(runReclaimCommand([], { ...streams, reclaim })).rejects.toThrow('preview failed') + expect(streams.text()).not.toContain('[y/N]') +}) + +it.each([ + ['--repo'], + ['--repo', '--apply'], + ['--exclude-worktree'], + ['--release-cache-min-age-days'], + ['--release-cache-min-age-days', 'NaN'], + ['--release-cache-min-age-days', '-1'], + ['--unknown'] +])('rejects invalid flags before any sweep: %j', (...args) => { + expect(() => parseReclaimArguments(args)).toThrow() +}) + +it('shows the single command without starting cleanup for help', async () => { + const streams = terminal('Y') + const reclaim = vi.fn() + await runReclaimCommand(['--help'], { ...streams, reclaim }) + expect(streams.text()).toContain('pnpm reclaim-space') + expect(reclaim).not.toHaveBeenCalled() +}) diff --git a/config/scripts/reclaim-worktree-space.mjs b/config/scripts/reclaim-worktree-space.mjs index e99b791845a..b9e8e7ea72a 100644 --- a/config/scripts/reclaim-worktree-space.mjs +++ b/config/scripts/reclaim-worktree-space.mjs @@ -12,11 +12,9 @@ export async function reclaimWorktreeSpace({ acquireLock = lock, commonDirectory, testsStopped = false, + excludeWorktree, releaseCacheMinAgeDays = 30 } = {}) { - if (apply && !automatic && !testsStopped) { - throw new Error('Stop cross-version tests, then pass --tests-stopped with --apply') - } if (!Number.isFinite(releaseCacheMinAgeDays) || releaseCacheMinAgeDays < 0) { throw new Error('--release-cache-min-age-days requires a non-negative number') } @@ -46,7 +44,7 @@ export async function reclaimWorktreeSpace({ let cleanupError try { const scripts = ['reclaim-electron-dists.mjs', 'reclaim-dev-electron-bundles.mjs'] - if (!automatic) { + if (!automatic && testsStopped) { scripts.unshift('reclaim-release-test-caches.mjs') } for (const script of scripts) { @@ -54,6 +52,9 @@ export async function reclaimWorktreeSpace({ if (apply) { args.push('--apply') } + if (excludeWorktree && script !== 'reclaim-release-test-caches.mjs') { + args.push('--exclude-worktree', excludeWorktree) + } if (script === 'reclaim-release-test-caches.mjs') { args.unshift('--import', import.meta.resolve('tsx')) args.push('--min-age-days', String(releaseCacheMinAgeDays)) @@ -94,44 +95,10 @@ export async function reclaimWorktreeSpace({ } if (process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename)) { - if (process.argv.includes('--help')) { - console.log( - 'Usage: pnpm run reclaim:worktree-space [--repo DIRECTORY] [--release-cache-min-age-days 30] [--apply --tests-stopped]' - ) - console.log( - 'Previews historical test cache cleanup, Electron sharing, and idle dev bundle removal.' - ) - process.exit(0) - } - const repoIndex = process.argv.indexOf('--repo') - const ageIndex = process.argv.indexOf('--release-cache-min-age-days') - for (let index = 2; index < process.argv.length; index += 1) { - const argument = process.argv[index] - if (argument === '--repo' || argument === '--release-cache-min-age-days') { - index += 1 - } else if (argument !== '--apply' && argument !== '--tests-stopped') { - throw new Error(`Unknown argument: ${argument}`) - } - } - if ( - repoIndex !== -1 && - (!process.argv[repoIndex + 1] || process.argv[repoIndex + 1].startsWith('--')) - ) { - throw new Error('--repo requires a directory') - } - if ( - ageIndex !== -1 && - (!process.argv[ageIndex + 1] || process.argv[ageIndex + 1].startsWith('--')) - ) { - throw new Error('--release-cache-min-age-days requires a number') - } - await reclaimWorktreeSpace({ - repoRoot: repoIndex !== -1 ? resolve(process.argv[repoIndex + 1]) : process.cwd(), - apply: process.argv.includes('--apply'), - testsStopped: process.argv.includes('--tests-stopped'), - releaseCacheMinAgeDays: ageIndex === -1 ? 30 : Number(process.argv[ageIndex + 1]) - }).catch((error) => { - console.error(error.message) - process.exitCode = 1 - }) + import('./reclaim-worktree-command.mjs') + .then(({ runReclaimCommand }) => runReclaimCommand(process.argv.slice(2))) + .catch((error) => { + console.error(error.message) + process.exitCode = 1 + }) } diff --git a/config/scripts/reclaim-worktree-space.test.mjs b/config/scripts/reclaim-worktree-space.test.mjs index 0b5967a0b41..37babcf3750 100644 --- a/config/scripts/reclaim-worktree-space.test.mjs +++ b/config/scripts/reclaim-worktree-space.test.mjs @@ -19,7 +19,7 @@ async function fixture() { } it('previews historical test caches and both Electron cleanup operations', async () => { - const options = await fixture() + const options = { ...(await fixture()), testsStopped: true } await reclaimWorktreeSpace(options) expect(options.execute).toHaveBeenCalledTimes(3) expect(options.execute.mock.calls.flatMap(([spec]) => spec.args)).not.toContain('--apply') @@ -42,22 +42,36 @@ it('forwards apply and repository selection to all cleanup operations', async () } }) -it('rejects manual apply before any operation unless cross-version tests are stopped', async () => { +it('applies Electron cleanup while retaining unacknowledged test caches', async () => { const options = { ...(await fixture()), apply: true } - await expect(reclaimWorktreeSpace(options)).rejects.toThrow('--tests-stopped') - expect(options.execute).not.toHaveBeenCalled() -}) - -it('leaves historical caches out of automatic invocations', async () => { - const options = { ...(await fixture()), automatic: true, apply: true } await reclaimWorktreeSpace(options) + expect(options.execute).toHaveBeenCalledTimes(2) expect(options.execute.mock.calls.flatMap(([spec]) => spec.args).join(' ')).not.toContain( 'reclaim-release-test-caches' ) }) +it('leaves historical caches out of automatic invocations', async () => { + const options = { + ...(await fixture()), + automatic: true, + apply: true, + testsStopped: true, + excludeWorktree: '/dev-worktree' + } + await reclaimWorktreeSpace(options) + expect(options.execute.mock.calls.flatMap(([spec]) => spec.args).join(' ')).not.toContain( + 'reclaim-release-test-caches' + ) + for (const [spec] of options.execute.mock.calls) { + expect(spec.args).toEqual( + expect.arrayContaining(['--exclude-worktree', options.excludeWorktree]) + ) + } +}) + it('forwards an age override only to historical test cache cleanup', async () => { - const options = { ...(await fixture()), releaseCacheMinAgeDays: 7 } + const options = { ...(await fixture()), releaseCacheMinAgeDays: 7, testsStopped: true } await reclaimWorktreeSpace(options) expect(options.execute.mock.calls[0][0].args).toEqual( expect.arrayContaining(['--min-age-days', '7']) @@ -73,7 +87,7 @@ it('rejects an invalid cache age before starting cleanup', async () => { expect(options.execute).not.toHaveBeenCalled() }) -it('runs the combined CLI from outside a disposable repository and requires acknowledgement', async () => { +it('runs the CLI outside the repository and retains historical caches until acknowledged', async () => { const options = await fixture() const init = await runProcess({ program: 'git', @@ -110,13 +124,13 @@ it('runs the combined CLI from outside a disposable repository and requires ackn timeoutMs: 20_000, terminationBarrier: true }) - const preview = await run() + const preview = await run(['--tests-stopped']) expect(preview.code, preview.stderr).toBe(0) expect(preview.stdout).toContain('Would remove 1 cache(s)') expect(await readFile(marker, 'utf8')).toContain(commit) - const blocked = await run(['--apply']) - expect(blocked.code).not.toBe(0) - expect(blocked.stderr).toContain('--tests-stopped') + const safe = await run(['--apply']) + expect(safe.code, safe.stderr).toBe(0) + expect(safe.stdout).toContain('Historical test caches retained') expect(await readFile(marker, 'utf8')).toContain(commit) const applied = await run(['--apply', '--tests-stopped']) expect(applied.code, applied.stderr).toBe(0) @@ -168,7 +182,7 @@ it('preserves the cleanup failure and reports a concurrent lock release failure }) it('fails if cleanup succeeds but the repository lock cannot be released', async () => { - const options = await fixture() + const options = { ...(await fixture()), testsStopped: true } const failure = new Error('lock release failure') const release = vi.fn().mockRejectedValue(failure) await expect(reclaimWorktreeSpace({ ...options, acquireLock: async () => release })).rejects.toBe( diff --git a/config/scripts/run-electron-vite-dev.mjs b/config/scripts/run-electron-vite-dev.mjs index c520083cb6a..66565ab0186 100644 --- a/config/scripts/run-electron-vite-dev.mjs +++ b/config/scripts/run-electron-vite-dev.mjs @@ -23,6 +23,7 @@ import { isDevBundleInUse, selectStaleDevBundleDirs } from './dev-electron-bundle-cache.mjs' +import { startDevStorageReclaim } from './dev-storage-reclaim.mjs' import { copyPrivateTree } from './space-sharing-copy.mjs' import { DEV_BUNDLE_ID, @@ -656,6 +657,12 @@ const child = spawn(process.execPath, [electronViteCli, ...forwardedArgs], { detached: process.platform !== 'win32' }) +child.once('spawn', () => { + if (!isHelpOrVersion) { + startDevStorageReclaim({ repoRoot }) + } +}) + let isShuttingDown = false let forcedKillTimer = null diff --git a/docs/readme/release-source-sharing.md b/docs/readme/release-source-sharing.md index 4ccf3449ef4..f115d423ce1 100644 --- a/docs/readme/release-source-sharing.md +++ b/docs/readme/release-source-sharing.md @@ -44,17 +44,18 @@ Folder contexts without a Git repository cannot provide a historical release che ## Cleaning up older historical caches -Preview cleanup across this repository's registered local worktrees and shared source cache: +First stop cross-version tests in all this repository's worktrees, then preview historical-cache +cleanup across the registered local worktrees and shared source cache: ```sh -pnpm run reclaim:worktree-space +pnpm reclaim-space --tests-stopped ``` The default selects stamped release checkouts older than 30 days, including previous extraction -formats. To apply the preview, first stop cross-version tests in all this repository's worktrees: +formats. Answer `Y` after the preview to apply it, or run the same selection directly: ```sh -pnpm run reclaim:worktree-space --apply --tests-stopped +pnpm reclaim-space --apply --tests-stopped ``` Use `--release-cache-min-age-days 7` for a shorter retention window, or `--release-cache-min-age-days 0` to select all diff --git a/docs/readme/worktree-storage-maintenance.md b/docs/readme/worktree-storage-maintenance.md index 536a5b2a31d..636d6d65358 100644 --- a/docs/readme/worktree-storage-maintenance.md +++ b/docs/readme/worktree-storage-maintenance.md @@ -1,21 +1,27 @@ # Worktree storage maintenance -Run from the repository to preview maintenance across its locally registered Git worktrees: +Run one command from the repository to preview maintenance across its locally registered Git worktrees: ```sh -pnpm run reclaim:worktree-space +pnpm reclaim-space ``` -After stopping cross-version tests in all this repository's worktrees, apply cleanup with: +In an interactive terminal, the preview offers `Apply this cleanup? [y/N]`; enter `Y` to apply it. +Redirected input or output remains preview-only. To apply without a prompt, use `pnpm reclaim-space --apply`. + +Historical test caches are retained by default. After stopping cross-version tests in all this +repository's worktrees, preview the same selection with: ```sh -pnpm run reclaim:worktree-space --apply --tests-stopped +pnpm reclaim-space --tests-stopped ``` +Answer `Y` to apply that preview, or use `pnpm reclaim-space --apply --tests-stopped` to apply directly. + The command reuses the historical test cache, Electron distribution, and dev bundle cleanup scripts. It shares complete Electron distributions through the repository cache and removes idle -`out/electron-dev` bundles, which development rebuilds on demand. It also removes stamped historical -test checkouts and shared seeds older than 30 days; tests regenerate them when needed. It leaves source files, +`out/electron-dev` bundles, which development rebuilds on demand. With `--tests-stopped`, it also +removes stamped historical test checkouts and shared seeds older than 30 days; tests regenerate them when needed. It leaves source files, dependency installs, worktree registrations, and other build output alone. Reported sizes are logical file data; APFS clones and hardlinks mean actual disk savings can be smaller. @@ -23,7 +29,7 @@ Use `--release-cache-min-age-days 7` to shorten historical-cache retention, or ` recognised completed historical checkouts. This age option applies only to test caches. Local checkout age uses directory modification time, which ordinary reuse does not refresh. The `--tests-stopped` flag acknowledges stopped tests; older harnesses cannot prove a completed -checkout is unused. Missing acknowledgement rejects manual apply before any cleanup begins. +checkout is unused. Without that flag, historical caches are excluded from both preview and apply. Cache cleanup skips symlinks, malformed stamps, future formats, and publication/copy locks. See [release source sharing](./release-source-sharing.md) for the cache and locking details. @@ -39,8 +45,15 @@ without waiting for an Electron upgrade. The manual command migrates older insta Read-only Electron package directories are retained and reported as skipped. Cache publication or copying failures are also reported as skipped rather than silently omitted. -Historical test cache cleanup runs only in this explicit manual command. Automatic invocations -exclude it because tests may still be importing source. +`pnpm dev` and `pnpm dev-stable-name` start an automatic repository-wide Electron cleanup worker +in the background after the dev process starts. It never prompts or waits before launching the app. +The worktree being launched is excluded from both Electron sharing and dev bundle removal; active +sibling apps and recent unfinished builds remain protected by the existing bundle checks. +Background sweep output is suppressed; worker start or exit failures are reported without stopping dev. Set `ORCA_SKIP_DEV_STORAGE_RECLAIM=1` to opt out. +The worker finishes independently if the dev app exits, and each cleanup operation has a ten-minute limit. + +Historical test cache cleanup runs only in the explicit manual command with `--tests-stopped`. +Automatic invocations exclude it because tests may still be importing source. The repository lock prevents competing cleanup commands. Dev bundle removal checks active processes and recent unfinished builds, then rechecks eligibility immediately before deletion. An unreadable process table prevents removal. diff --git a/package.json b/package.json index c444b918881..b6707fa2677 100644 --- a/package.json +++ b/package.json @@ -115,9 +115,7 @@ "build:release:parallel": "pnpm run build:wsl-guest && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite:parallel && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", "postinstall": "pnpm run build:packages && node config/scripts/rebuild-native-deps.mjs", "rebuild:electron": "node config/scripts/rebuild-native-deps.mjs", - "reclaim:electron-dists": "node config/scripts/reclaim-electron-dists.mjs", - "reclaim:dev-bundles": "node config/scripts/reclaim-dev-electron-bundles.mjs", - "reclaim:worktree-space": "node config/scripts/reclaim-worktree-space.mjs", + "reclaim-space": "node config/scripts/reclaim-worktree-space.mjs", "rebuild:node": "pnpm rebuild node-pty", "build:unpack": "pnpm run build && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --dir", "build:win": "pnpm run build:desktop && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --win", diff --git a/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts b/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts index 5550d4fa670..367a19c96c6 100644 --- a/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts +++ b/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts @@ -24,7 +24,18 @@ import { } from '../ai-vault/session-transcript-consumers' import { SELF_CONTAINED_TRANSCRIPT_AGENTS } from './session-search-index-consumer' -vi.mock('node:fs', { spy: true }) +// Spying on every fs export also mocks ReadStream internals and stalls real reads. +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + readFile: vi.fn(actual.readFile), + readFileSync: vi.fn(actual.readFileSync), + open: vi.fn(actual.open), + openSync: vi.fn(actual.openSync), + createReadStream: vi.fn(actual.createReadStream) + } +}) vi.mock('node:fs/promises', { spy: true }) vi.mock('../native-chat/wsl-transcript-fs-access', { spy: true }) diff --git a/src/main/startup/run-electron-vite-dev-web.test.ts b/src/main/startup/run-electron-vite-dev-web.test.ts index ada8bb3c9fa..9b94938bcc9 100644 --- a/src/main/startup/run-electron-vite-dev-web.test.ts +++ b/src/main/startup/run-electron-vite-dev-web.test.ts @@ -156,6 +156,7 @@ describe('run-electron-vite-dev web client prepare', () => { ORCA_ELECTRON_VITE_CLI: fakeCliPath, ORCA_VITE_CLI: fakeVitePath, ORCA_SKIP_DEV_CLI_PREPARE: '1', + ORCA_SKIP_DEV_STORAGE_RECLAIM: '1', ORCA_SKIP_DEV_ELECTRON_APP_PREPARE: '1', ORCA_DEV_WRAPPER_TEST_PID_FILE: pidFile, ORCA_DEV_WRAPPER_TEST_ENV_FILE: envFile, @@ -207,6 +208,7 @@ describe('run-electron-vite-dev web client prepare', () => { ORCA_ELECTRON_VITE_CLI: fakeCliPath, ORCA_VITE_CLI: fakeVitePath, ORCA_SKIP_DEV_CLI_PREPARE: '1', + ORCA_SKIP_DEV_STORAGE_RECLAIM: '1', ORCA_SKIP_DEV_ELECTRON_APP_PREPARE: '1', ORCA_DEV_WEB_PREPARE: '1', ORCA_DEV_WRAPPER_TEST_PID_FILE: pidFile, diff --git a/src/main/startup/run-electron-vite-dev.test.ts b/src/main/startup/run-electron-vite-dev.test.ts index 73d1bb21cdc..1c0469f7664 100644 --- a/src/main/startup/run-electron-vite-dev.test.ts +++ b/src/main/startup/run-electron-vite-dev.test.ts @@ -102,7 +102,7 @@ function devWrapperTestEnv(extra: NodeJS.ProcessEnv): NodeJS.ProcessEnv { } } delete env.ELECTRON_EXEC_PATH - return { ...env, ...extra } + return { ...env, ORCA_SKIP_DEV_STORAGE_RECLAIM: '1', ...extra } } /** From 850e71e6726401215bcfd12613812297af462762 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:06:50 -0700 Subject: [PATCH 16/53] Resume in New CLI for OpenCode and OMP chats (#28075) * feat(chat): Resume in New CLI for OpenCode and OMP chats OpenCode's and OMP's CLIs have no copy flag, but their chat protocol copies a conversation (ACP session/fork). For a fork of one of their chats, the chat's host now has a short-lived agent, launched exactly as the chat's own, copy the conversation; the terminal then opens the copy with a plain resume. The chat keeps its own conversation, so the CLI is never a second writer on it. The copy travels on the existing aiVault.prepareSessionResume fork request as an optional forkedSessionId; a host that predates it answers without one, and the client asks for an Orca update instead of resuming the original. * fix(chat): open an OpenCode 2.x chat's CLI copy with opencode, not opencode2 The copy kept the row's agent, so Resume in New CLI ran `opencode2 --standalone --session `, which OpenCode 2.x doesn't install. The chat's host now answers the copy with the agent whose CLI opens it: the chat's own. * fix(chat): type the copied conversation's agent as a history agent * fix: preserve independent chat copies through CLI launch * test: keep only the tests that pin user-visible behavior * fix(chat): validate OpenCode copies on their execution host * test(chat): expect copied OMP forks after main integration --- .../scripts/vitest-sqlite-runtime-files.mjs | 1 + src/main/acp/acp-conversation-copies.ts | 37 +++++ src/main/acp/acp-conversation-copy.test.ts | 137 ++++++++++++++++ src/main/acp/acp-conversation-copy.ts | 152 ++++++++++++++++++ src/main/acp/acp-session-runtime.ts | 12 +- src/main/acp/acp-structured-connection.ts | 1 + .../acp-structured-session-adapter-deps.ts | 2 + .../acp/acp-structured-session-adapter.ts | 27 ++-- .../ai-vault/runtime-session-scanner.test.ts | 23 +++ src/main/ai-vault/runtime-session-scanner.ts | 10 +- .../session-list-result-validation.ts | 2 +- .../structured-chat-conversation-copy.test.ts | 83 ++++++++++ .../structured-chat-conversation-copy.ts | 55 +++++++ ...-session-ownership-client-audience.test.ts | 32 ++-- ...ructured-session-ownership.test-support.ts | 31 ++++ .../ai-vault/structured-session-ownership.ts | 5 +- .../ipc/ai-vault-resume-chat-copy.test.ts | 52 ++++++ src/main/ipc/ai-vault-resume.test.ts | 10 +- src/main/ipc/ai-vault-resume.ts | 6 +- ...structured-agent-session-adapter-router.ts | 8 + .../structured-agent-session-adapter.ts | 9 ++ .../structured-agent-configured-args.test.ts | 2 +- .../structured-agent-configured-args.ts | 4 +- src/main/runtime/rpc/methods/ai-vault.test.ts | 47 ++++++ src/main/runtime/rpc/methods/ai-vault.ts | 5 +- .../structured-agent-runtime-registrations.ts | 1 + .../ai-vault-session-cli-fork.test.ts | 14 +- .../ai-vault-session-cli-fork.ts | 20 ++- .../ai-vault-session-launch-actions.test.ts | 78 ++++++++- .../ai-vault-session-launch-actions.ts | 84 ++++------ .../ai-vault-session-launch-feedback.ts | 48 ++++++ .../tab-session-history-switch.test.ts | 15 +- .../src/lib/ai-vault-omp-cold-resume.test.ts | 38 +++++ .../src/lib/ai-vault-resume-command.ts | 9 +- .../src/lib/ai-vault-session-fork-startup.ts | 85 ++++++++++ ...i-vault-session-resume-preparation.test.ts | 20 ++- .../ai-vault-session-resume-preparation.ts | 43 ++++- src/shared/agent-resume-argv.ts | 18 ++- src/shared/ai-vault-resume-preparation.ts | 11 +- src/shared/opencode-launch-command.ts | 16 ++ 40 files changed, 1123 insertions(+), 130 deletions(-) create mode 100644 src/main/acp/acp-conversation-copies.ts create mode 100644 src/main/acp/acp-conversation-copy.test.ts create mode 100644 src/main/acp/acp-conversation-copy.ts create mode 100644 src/main/ai-vault/structured-chat-conversation-copy.test.ts create mode 100644 src/main/ai-vault/structured-chat-conversation-copy.ts create mode 100644 src/main/ipc/ai-vault-resume-chat-copy.test.ts create mode 100644 src/renderer/src/components/right-sidebar/ai-vault-session-launch-feedback.ts diff --git a/config/scripts/vitest-sqlite-runtime-files.mjs b/config/scripts/vitest-sqlite-runtime-files.mjs index 7c74900029d..c21975ecdc0 100644 --- a/config/scripts/vitest-sqlite-runtime-files.mjs +++ b/config/scripts/vitest-sqlite-runtime-files.mjs @@ -21,6 +21,7 @@ export const SQLITE_RUNTIME_INCLUDE = [ 'src/main/acp/acp-structured-prompt-failure.test.ts', 'src/main/acp/acp-timeline-turn-failures.test.ts', 'src/main/acp/acp-structured-session-adapter.test.ts', + 'src/main/acp/acp-conversation-copy.test.ts', 'src/main/pi/rpc-turn-races.test.ts', 'src/main/acp/acp-structured-child-session-end.test.ts', 'src/main/acp/acp-structured-child-stop.test.ts', diff --git a/src/main/acp/acp-conversation-copies.ts b/src/main/acp/acp-conversation-copies.ts new file mode 100644 index 00000000000..ed453a3f82c --- /dev/null +++ b/src/main/acp/acp-conversation-copies.ts @@ -0,0 +1,37 @@ +import { randomUUID } from 'node:crypto' +import type { StructuredAgentSessionAdapter } from '../native-chat/agent-session-wire/structured-agent-session-adapter' +import { ProviderAcquisitionStarts } from '../provider-process/provider-acquisition-starts' +import { copyAcpConversation } from './acp-conversation-copy' +import type { AcpStructuredConnection } from './acp-structured-connection' +import type { AcpStructuredSessionAdapterDeps } from './acp-structured-session-adapter-deps' + +/** Copy children have their own cleanup lifecycle; they never gate the original chat's start. */ +export class AcpConversationCopies { + private readonly children = new ProviderAcquisitionStarts() + private readonly shutdown = new AbortController() + private readonly pending = new Set>() + + constructor(private readonly deps: AcpStructuredSessionAdapterDeps) {} + + copy: NonNullable = async (input) => { + const copy = copyAcpConversation(this.deps, input, { + signal: this.shutdown.signal, + track: (connection) => this.children.retainFailed(randomUUID(), connection) + }) + this.pending.add(copy) + try { + return await copy + } finally { + this.pending.delete(copy) + } + } + + async closeAll(): Promise { + this.shutdown.abort() + await Promise.allSettled(this.pending) + const closed = await Promise.all( + this.children.failedSessionIds().map((id) => this.children.stopFailed(id)) + ) + return closed.every(Boolean) + } +} diff --git a/src/main/acp/acp-conversation-copy.test.ts b/src/main/acp/acp-conversation-copy.test.ts new file mode 100644 index 00000000000..25fc9eed346 --- /dev/null +++ b/src/main/acp/acp-conversation-copy.test.ts @@ -0,0 +1,137 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { acpLaunchSpecFor } from './acp-launch-specs' +import { copyAcpConversation } from './acp-conversation-copy' +import { + FakeAcpChild, + openAcpAdapterRig, + type AcpAdapterRig, + PROVIDER_SESSION +} from './acp-structured-adapter.test-support' + +vi.mock('../native-chat/session-file-resolver', () => ({ + resolveSessionFilePath: async () => '/account-a/sessions/copy-1.jsonl' +})) +const FORKS = { agentCapabilities: { loadSession: true, sessionCapabilities: { fork: {} } } } +const input = { + identity: { + sessionId: 'orca-chat-1', + workspaceId: 'workspace-1', + hostId: 'local', + agent: 'omp', + providerHandle: null + }, + providerSessionId: PROVIDER_SESSION, + accountHome: { variable: 'PI_CODING_AGENT_DIR', path: '/account-a' } +} +const rigs: AcpAdapterRig[] = [] +afterEach(async () => { + await Promise.all(rigs.splice(0).map((rig) => rig.adapter.closeAll())) +}) + +async function open(agent: 'omp' | 'opencode' = 'omp') { + const rig = await openAcpAdapterRig({ + spec: acpLaunchSpecFor(agent)!, + initialize: FORKS, + script: (agent) => + agent.on('session/fork', (frame) => agent.reply(frame, { sessionId: 'copy-1' })) + }) + rigs.push(rig) + return rig +} + +describe('copying a chat conversation for the CLI', () => { + it.each(['omp', 'opencode'] as const)( + 'copies a stopped and running %s chat without loading or prompting its original', + async (agent) => { + const rig = await open(agent) + const request = { + ...input, + identity: { ...input.identity, agent }, + accountHome: + agent === 'omp' + ? input.accountHome + : { kind: 'opencode' as const, locator: { kind: 'unmanaged' as const } } + } + const expected = { + sessionId: 'copy-1', + ...(agent === 'omp' ? { filePath: '/account-a/sessions/copy-1.jsonl' } : {}) + } + await expect(rig.adapter.copyConversation(request)).resolves.toEqual(expected) + expect(rig.child().agent.frames.map((frame) => frame.method)).toEqual([ + 'initialize', + 'session/fork' + ]) + expect(rig.sent('session/fork')[0]?.params).toEqual({ + sessionId: PROVIDER_SESSION, + cwd: '/workspace/project', + mcpServers: [] + }) + expect(rig.child().closes).toBe(1) + await rig.acquire() + const chat = rig.child() + const frames = [...chat.agent.frames] + await expect(rig.adapter.copyConversation(request)).resolves.toEqual(expected) + expect(chat.agent.frames).toEqual(frames) + expect(chat.closes).toBe(0) + expect(rig.child()).not.toBe(chat) + expect(rig.child().closes).toBe(1) + } + ) + + it('permits another copy after unproven cleanup and retries the retained child at shutdown', async () => { + const rig = await openAcpAdapterRig({ spec: acpLaunchSpecFor('omp')!, initialize: FORKS }) + rigs.push(rig) + const copying = rig.adapter.copyConversation(input) + await vi.waitFor(() => expect(rig.sent('session/fork')).toHaveLength(1)) + const child = rig.child() + child.proveClose = async () => false + child.agent.reply(rig.sent('session/fork')[0]!, { sessionId: 'copy-1' }) + await expect(copying).resolves.toHaveProperty('sessionId', 'copy-1') + const next = rig.adapter.copyConversation(input) + await vi.waitFor(() => { + expect(rig.child()).not.toBe(child) + expect(rig.sent('session/fork')).toHaveLength(1) + }) + rig.child().agent.reply(rig.sent('session/fork')[0]!, { sessionId: 'copy-2' }) + await expect(next).resolves.toHaveProperty('sessionId', 'copy-2') + child.proveClose = async () => { + child.exit() + return true + } + await rig.adapter.closeAll() + expect(child.closes).toBe(2) + }) + + it('bounds an unanswered fork and closes its temporary process', async () => { + const children: FakeAcpChild[] = [] + const spec = acpLaunchSpecFor('omp')! + await expect( + copyAcpConversation( + { + spec, + readLaunch: async () => ({ + spec, + command: 'fake', + args: [], + cwd: '/repo', + env: {}, + envToDelete: [], + fullAccess: false, + resume: null + }), + connect: (...args: ConstructorParameters) => { + const child = new FakeAcpChild(...args) + child.agent.on('initialize', (frame) => + child.agent.reply(frame, { protocolVersion: 1, ...FORKS }) + ) + children.push(child) + return child + } + }, + input, + { timeoutMs: 10, track: () => {} } + ) + ).rejects.toThrow('OMP could not copy this chat') + expect(children.map((child) => child.closes)).toEqual([1]) + }) +}) diff --git a/src/main/acp/acp-conversation-copy.ts b/src/main/acp/acp-conversation-copy.ts new file mode 100644 index 00000000000..afe9ddee32a --- /dev/null +++ b/src/main/acp/acp-conversation-copy.ts @@ -0,0 +1,152 @@ +import { normalizeAgentProviderSession } from '../../shared/agent-session-resume' +import { + assertOpenCodeCopyUsesLocalStore, + OpenCodeCopyServerRouteError +} from '../../shared/opencode-launch-command' +// The chat protocol saves an independent conversation; its ordinary CLI opens that copy. +import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' +import { + agentSessionAccountHomesEqual, + type AgentSessionAccountHome +} from '../../shared/agent-session-account-home' +import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason' +import type { AiVaultConversationCopy } from '../../shared/ai-vault-resume-preparation' +import { resolveSessionFilePath } from '../native-chat/session-file-resolver' +import { resolveOmpSessionsDir } from '../ai-vault/omp-session-root' +import { AcpRpcError } from './acp-errors' +import { acpAgentName } from './acp-structured-agent-definitions' +import type { AcpStructuredConnection } from './acp-structured-connection' +import type { AcpStructuredSessionAdapterDeps } from './acp-structured-session-adapter-deps' + +class AcpCopyAccountMismatchError extends Error { + constructor() { + super('Select the account this chat uses before opening its copy in a CLI.') + } +} + +export const ACP_CONVERSATION_COPY_TIMEOUT_MS = 60_000 + +export async function copyAcpConversation( + deps: Pick< + AcpStructuredSessionAdapterDeps, + 'spec' | 'readLaunch' | 'resolveLaunchArgs' | 'connect' | 'logger' + >, + input: { + identity: AgentSessionJournalIdentity + providerSessionId: string + accountHome: AgentSessionAccountHome + }, + options: { + timeoutMs?: number + signal?: AbortSignal + track: (connection: AcpStructuredConnection) => void + } +): Promise { + const name = acpAgentName(deps.spec.agent) + const timeout = AbortSignal.timeout(options.timeoutMs ?? ACP_CONVERSATION_COPY_TIMEOUT_MS) + const signal = options.signal ? AbortSignal.any([timeout, options.signal]) : timeout + let connection: AcpStructuredConnection | null = null + try { + signal.throwIfAborted() + if (deps.spec.agent === 'opencode') { + const args = await waitForPromiseWithSignal( + Promise.resolve(deps.resolveLaunchArgs?.() ?? []), + signal + ) + assertOpenCodeCopyUsesLocalStore(args) + } + signal.throwIfAborted() + const launch = await waitForPromiseWithSignal(deps.readLaunch(input.identity), signal) + signal.throwIfAborted() + await assertCopyAccountSelected(deps.spec, input.accountHome, launch.env, signal) + signal.throwIfAborted() + connection = deps.connect( + { + command: launch.command, + args: launch.args, + cwd: launch.cwd, + env: launch.env, + envToDelete: launch.envToDelete + }, + { clientInfo: { name: 'orca', version: '1' } } + ) + options.track(connection) + const sessionId = await waitForPromiseWithSignal( + forkAdvertised(connection, input.providerSessionId, launch.cwd), + signal + ) + await assertCopyAccountSelected(deps.spec, input.accountHome, launch.env, signal) + const providerSession = normalizeAgentProviderSession({ key: 'session_id', id: sessionId }) + if (!providerSession || providerSession.id !== sessionId) { + throw new Error('The agent returned an invalid copied conversation identity.') + } + if (sessionId === input.providerSessionId) { + throw new Error('The agent returned the original conversation instead of a copy.') + } + const filePath = + deps.spec.agent === 'omp' + ? await resolveSessionFilePath( + 'omp', + sessionId, + { + ompSessionsDir: resolveOmpSessionsDir({ env: launch.env }) + }, + signal + ) + : undefined + if (deps.spec.agent === 'omp' && !filePath) { + throw new Error('OMP did not save the copied conversation in its pinned session directory.') + } + return { + sessionId, + ...(filePath ? { filePath } : {}) + } + } catch (error) { + if ( + error instanceof AcpCopyAccountMismatchError || + error instanceof OpenCodeCopyServerRouteError + ) { + throw error + } + throw new Error( + error instanceof AcpRpcError && error.code === -32601 + ? `This ${name} version cannot copy a chat. Update ${name} to resume this chat in a new CLI.` + : `${name} could not copy this chat.`, + { cause: error } + ) + } finally { + if (connection && !(await connection.close().catch(() => false))) { + deps.logger?.warn('ACP conversation copy child exit is unproven; retained for cleanup', { + scope: 'acp-conversation-copy', + sessionId: input.identity.sessionId + }) + } + } +} + +async function forkAdvertised( + connection: AcpStructuredConnection, + sessionId: string, + cwd: string +): Promise { + const { agentCapabilities } = await connection.initialize() + if (agentCapabilities?.sessionCapabilities?.fork == null) { + throw new AcpRpcError(-32601, 'session/fork is not advertised') + } + return connection.forkSession(sessionId, cwd) +} + +/** Rederive selection at the fork boundaries; ordinary terminals still own their normal routing. */ +async function assertCopyAccountSelected( + spec: AcpStructuredSessionAdapterDeps['spec'], + home: AgentSessionAccountHome, + launchEnv: Record, + signal: AbortSignal +): Promise { + if ('kind' in home && home.kind === 'opencode') { + const selected = await waitForPromiseWithSignal(spec.account.resolve({ launchEnv }), signal) + if (!agentSessionAccountHomesEqual(home, selected)) { + throw new AcpCopyAccountMismatchError() + } + } +} diff --git a/src/main/acp/acp-session-runtime.ts b/src/main/acp/acp-session-runtime.ts index 5f362d0fe7d..8cfd4bbaf42 100644 --- a/src/main/acp/acp-session-runtime.ts +++ b/src/main/acp/acp-session-runtime.ts @@ -1,5 +1,5 @@ import type { Readable, Writable } from 'node:stream' -import type { z } from 'zod' +import { z } from 'zod' import { AcpAgentError, AcpAuthRequiredError, @@ -28,6 +28,7 @@ import { SetSessionModeResponseSchema, SetSessionModelResponseSchema, SetSessionConfigOptionResponseSchema, + SessionIdSchema, type InitializeRequest, type InitializeResponse, type AuthenticateResponse, @@ -42,6 +43,8 @@ import { type SetSessionModelResponse } from './generated/acp-protocol.generated' +const ForkSessionResponseSchema = z.looseObject({ sessionId: SessionIdSchema }) + type Meta = PromptRequest['_meta'] const withMeta = (meta: Meta): { _meta?: Meta } => (meta ? { _meta: meta } : {}) @@ -142,6 +145,13 @@ export class AcpSessionRuntime { return this.peer.request(method, params, { timeoutMs: null }) } + /** Copies saved session `sessionId` into a new one the agent saves (`session/fork`) and answers + * its id; this connection starts no session of its own. Ask only an agent that advertises it. */ + async forkSession(sessionId: string, cwd: string): Promise { + const params = { sessionId, cwd, mcpServers: [] } + return (await this.call('session/fork', params, ForkSessionResponseSchema)).sessionId + } + start(options: AcpSessionStartOptions): Promise { if (this.started) { return Promise.resolve(this.started) diff --git a/src/main/acp/acp-structured-connection.ts b/src/main/acp/acp-structured-connection.ts index 525189b2003..7a03b539855 100644 --- a/src/main/acp/acp-structured-connection.ts +++ b/src/main/acp/acp-structured-connection.ts @@ -9,6 +9,7 @@ import type { ProviderProcessLaunch } from '../provider-process/provider-process export type AcpStructuredConnection = Pick< AcpAgentConnection, | 'initialize' + | 'forkSession' | 'start' | 'prompt' | 'cancel' diff --git a/src/main/acp/acp-structured-session-adapter-deps.ts b/src/main/acp/acp-structured-session-adapter-deps.ts index 64f4d120791..ec2802e38f1 100644 --- a/src/main/acp/acp-structured-session-adapter-deps.ts +++ b/src/main/acp/acp-structured-session-adapter-deps.ts @@ -25,6 +25,8 @@ export type AcpStructuredSessionAdapterDeps = { }) => Promise /** The same launch for a read that starts no child (restart recovery): the agent's env, unsealed. */ readLaunch: (identity: AgentSessionJournalIdentity) => Promise + /** Current host arguments before protocol-specific filtering; copies open through the CLI. */ + resolveLaunchArgs?: () => Promise | string[] /** The chat's journal as it stands, read without opening it; null when it has none. */ readJournal?: (sessionId: string) => JournalLoad | null /** Starts the agent's process and owns its protocol: `createAcpAgentConnection` in production. */ diff --git a/src/main/acp/acp-structured-session-adapter.ts b/src/main/acp/acp-structured-session-adapter.ts index dbaec32b2e4..8b48a97b963 100644 --- a/src/main/acp/acp-structured-session-adapter.ts +++ b/src/main/acp/acp-structured-session-adapter.ts @@ -43,18 +43,25 @@ import { import { readAcpLiveOptions, writeAcpSessionOption } from './acp-structured-options' import { readAcpRecoveryHistory } from './acp-recovery-history' import { stopAcpChildren, acpChildStopCapabilities } from './acp-structured-child-stop' +import { AcpConversationCopies } from './acp-conversation-copies' export class AcpStructuredSessionAdapter implements StructuredAgentSessionAdapter { /** Live children, and ones whose exit is not yet proven; a proven exit removes its entry. */ private readonly sessions = new Map() private readonly starts = new ProviderAcquisitionStarts() + private readonly copies: AcpConversationCopies - constructor(private readonly deps: AcpStructuredSessionAdapterDeps) {} + constructor(private readonly deps: AcpStructuredSessionAdapterDeps) { + this.copies = new AcpConversationCopies(deps) + } /** Restart recovery's evidence; null for an agent whose own store Orca cannot read. */ - providerHistoryWindow: NonNullable = ({ - identity - }) => readAcpRecoveryHistory(this.deps, identity) + providerHistoryWindow: NonNullable = ( + input + ) => readAcpRecoveryHistory(this.deps, input.identity) + + copyConversation: NonNullable = (input) => + this.copies.copy(input) // The child runs on this runtime's own machine; Windows needs process start-time proof. supportsLocation = (location: AgentSessionExecutionLocation): boolean => @@ -248,10 +255,11 @@ export class AcpStructuredSessionAdapter implements StructuredAgentSessionAdapte forceCloseSession = (sessionId: string): Promise => this.stop(sessionId, false) async closeAll(): Promise { + const copiedChildren = this.copies.closeAll() const ids = new Set([...this.sessions.keys(), ...this.starts.failedSessionIds()]) // A start still under way is the host's to abort: its teardown does, before this runs. const proven = await Promise.all([...ids].map((sessionId) => this.stop(sessionId, true))) - if (proven.includes(false)) { + if (!(await copiedChildren) || proven.includes(false)) { throw new Error('an ACP agent child could not be proven stopped') } } @@ -352,11 +360,6 @@ export class AcpStructuredSessionAdapter implements StructuredAgentSessionAdapte return session } - private stopGraceMs(): number { - return this.deps.stopGraceMs ?? ACP_STOP_GRACE_MS - } - - private now(): number { - return (this.deps.now ?? Date.now)() - } + private readonly stopGraceMs = (): number => this.deps.stopGraceMs ?? ACP_STOP_GRACE_MS + private readonly now = (): number => (this.deps.now ?? Date.now)() } diff --git a/src/main/ai-vault/runtime-session-scanner.test.ts b/src/main/ai-vault/runtime-session-scanner.test.ts index db718a4380d..9fe60aa0972 100644 --- a/src/main/ai-vault/runtime-session-scanner.test.ts +++ b/src/main/ai-vault/runtime-session-scanner.test.ts @@ -188,6 +188,29 @@ describe('runtime AI Vault session scanner', () => { }) }) + it.each([ + { agent: 'opencode' as const, sessionId: 'opencode-copy' }, + { agent: 'omp' as const, sessionId: 'omp-copy', filePath: '/pinned-omp/sessions/copy.jsonl' } + ])( + 'preserves the $agent copy identity and transcript across the paired-host response', + async (forkedSession) => { + mocks.callRuntimeEnvironment.mockResolvedValueOnce({ + ok: true, + result: { useRealCodexHome: false, forkedSession } + }) + await expect( + prepareRuntimeAiVaultSessionResume('/user-data', 'env-1', { + agent: forkedSession.agent === 'opencode' ? 'opencode2' : 'omp', + sessionId: 'source', + filePath: '/source', + codexHome: null, + executionHostId: 'runtime:env-1', + fork: true + }) + ).resolves.toEqual({ useRealCodexHome: false, forkedSession }) + } + ) + it('fails retryably when runtime preparation returns an invalid result', async () => { mocks.callRuntimeEnvironment.mockResolvedValueOnce({ ok: true, result: {} }) diff --git a/src/main/ai-vault/runtime-session-scanner.ts b/src/main/ai-vault/runtime-session-scanner.ts index e81e29e9160..b60c71ddce6 100644 --- a/src/main/ai-vault/runtime-session-scanner.ts +++ b/src/main/ai-vault/runtime-session-scanner.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { + AI_VAULT_AGENTS, AI_VAULT_SCOPE_PATHS_MAX_COUNT, type AiVaultListArgs, type AiVaultListResult, @@ -33,7 +34,14 @@ export type RuntimeAiVaultScanOptions = { // parent would silently drop it and resume under the wrong account's home. const aiVaultPrepareSessionResumeResultSchema = z.object({ useRealCodexHome: z.boolean(), - substituteCodexHome: z.string().optional() + substituteCodexHome: z.string().optional(), + forkedSession: z + .object({ + sessionId: z.string(), + agent: z.enum(AI_VAULT_AGENTS), + filePath: z.string().optional() + }) + .optional() }) export function getSavedRuntimeAiVaultHostInfos( diff --git a/src/main/ai-vault/session-list-result-validation.ts b/src/main/ai-vault/session-list-result-validation.ts index 5b005eb60bf..fcae149b68a 100644 --- a/src/main/ai-vault/session-list-result-validation.ts +++ b/src/main/ai-vault/session-list-result-validation.ts @@ -10,7 +10,7 @@ import { normalizeExecutionHostId } from '../../shared/execution-host' const aiVaultAgentSet = new Set(AI_VAULT_AGENTS) -function isAiVaultAgent(agent: string): agent is AiVaultAgent { +export function isAiVaultAgent(agent: string): agent is AiVaultAgent { return aiVaultAgentSet.has(agent) } diff --git a/src/main/ai-vault/structured-chat-conversation-copy.test.ts b/src/main/ai-vault/structured-chat-conversation-copy.test.ts new file mode 100644 index 00000000000..d14df1807ff --- /dev/null +++ b/src/main/ai-vault/structured-chat-conversation-copy.test.ts @@ -0,0 +1,83 @@ +import { + agentSessionRecordFixture, + agentSessionLeaseFixture +} from '../../shared/agent-session-record.test-fixture' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { AgentSessionProviderHandle } from '../../shared/agent-session-provider-handle' +import type { AiVaultPrepareSessionResumeArgs } from '../../shared/ai-vault-resume-preparation' +import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' +import { copyStructuredChatConversation } from './structured-chat-conversation-copy' +import { installCopyingChat as installChat } from './structured-session-ownership.test-support' + +const OMP_ID = '019fd611-2b3c-7d4e-9f50-6b7c8d9e0f1a' +const OPENCODE_ID = 'ses_2f9aB3cD4eF5gH6iJ7kL8mN9oP' +const OMP_FILE = `/sessions/${OMP_ID}.jsonl` + +const acp = (agent: 'opencode' | 'omp', nativeId: string): AgentSessionProviderHandle => ({ + transport: 'acp', + agent, + nativeId +}) + +const fork = ( + overrides: Partial +): AiVaultPrepareSessionResumeArgs => ({ + agent: 'omp', + sessionId: OMP_ID, + filePath: OMP_FILE, + codexHome: null, + executionHostId: 'local', + fork: true, + ...overrides +}) + +afterEach(() => setStructuredAgentSessionHost(null)) + +describe('copying a chat conversation for Resume in New CLI', () => { + it.each([ + { agent: 'omp' as const, handle: acp('omp', OMP_ID), sessionId: OMP_ID }, + { agent: 'opencode2' as const, handle: acp('opencode', OPENCODE_ID), sessionId: OPENCODE_ID } + ])( + "has the chat's host copy a $agent row's conversation", + async ({ agent, handle, sessionId }) => { + const copy = vi.fn(async () => ({ + sessionId: 'copy-1', + filePath: '/sessions/copy-1.jsonl' + })) + installChat(handle, copy) + + await expect( + copyStructuredChatConversation(fork({ agent, sessionId, filePath: `/x/${sessionId}` })) + ).resolves.toEqual({ + useRealCodexHome: false, + // The copy opens with the CLI that ran the chat: `opencode` for an OpenCode 2.x row. + forkedSession: { + sessionId: 'copy-1', + filePath: '/sessions/copy-1.jsonl', + agent: handle.agent + } + }) + } + ) + + it.each(['source', 'owned'])('rejects a %s fork id on the execution host', async (returned) => { + const lease = agentSessionLeaseFixture() + const fixture = agentSessionRecordFixture(lease) + const copy = vi.fn(async () => ({ + sessionId: returned === 'source' ? OMP_ID : 'other-owned' + })) + installChat(acp('omp', OMP_ID), copy, [ + { + ...fixture, + sessionId: 'other-chat', + provider: 'omp', + providerHandleChain: [ + { ...fixture.providerHandleChain[0]!, handle: acp('omp', 'other-owned') } + ] + } + ]) + await expect(copyStructuredChatConversation(fork({}))).rejects.toThrow( + 'already owned by a chat' + ) + }) +}) diff --git a/src/main/ai-vault/structured-chat-conversation-copy.ts b/src/main/ai-vault/structured-chat-conversation-copy.ts new file mode 100644 index 00000000000..275f8f1d691 --- /dev/null +++ b/src/main/ai-vault/structured-chat-conversation-copy.ts @@ -0,0 +1,55 @@ +import { agentForksThroughChat } from '../../shared/agent-resume-argv' +import { agentSessionProviderHandleChainHead } from '../../shared/agent-session-provider-handle' +import type { + AiVaultPrepareSessionResumeArgs, + AiVaultPrepareSessionResumeResult +} from '../../shared/ai-vault-resume-preparation' +import { getStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' +import { isAiVaultAgent } from './session-list-result-validation' +import { findResumeOwnership } from './structured-session-ownership' + +/** + * A fork of a chat's conversation, for an agent whose chat makes the copy: the chat's host copies + * it and answers the copy, which the terminal resumes and no chat owns. Null for any other prepare. + * Runs on the host that runs the chat, as every resume preparation does. + */ +export async function copyStructuredChatConversation( + args: AiVaultPrepareSessionResumeArgs +): Promise { + if (!args.fork || !agentForksThroughChat(args.agent)) { + return null + } + const host = getStructuredAgentSessionHost() + const ownership = findResumeOwnership(args) + const record = ownership ? host?.deps.store.getRecord(ownership.sessionId) : undefined + const copy = host?.deps.adapter.copyConversation + if (!host || !ownership || !record || !copy) { + throw new Error('This chat is no longer in Orca, so it cannot be copied to a new CLI.') + } + const forkedSession = await copy.call(host.deps.adapter, { + identity: { + sessionId: record.sessionId, + workspaceId: record.location.workspaceId, + hostId: record.location.executionHostId, + agent: record.provider, + providerHandle: + agentSessionProviderHandleChainHead(record.providerHandleChain)?.handle ?? null + }, + providerSessionId: ownership.providerSessionId, + accountHome: record.accountHome + }) + if ( + forkedSession.sessionId === ownership.providerSessionId || + findResumeOwnership({ agent: args.agent, sessionId: forkedSession.sessionId, filePath: '' }) + ) { + throw new Error('The agent returned a conversation already owned by a chat instead of a copy.') + } + return { + useRealCodexHome: false, + forkedSession: { + ...forkedSession, + // The chat's own agent, not the row's: an OpenCode 2 row's chat runs `opencode`. + agent: isAiVaultAgent(record.provider) ? record.provider : args.agent + } + } +} diff --git a/src/main/ai-vault/structured-session-ownership-client-audience.test.ts b/src/main/ai-vault/structured-session-ownership-client-audience.test.ts index 830bc8b2520..32d91c7e2b9 100644 --- a/src/main/ai-vault/structured-session-ownership-client-audience.test.ts +++ b/src/main/ai-vault/structured-session-ownership-client-audience.test.ts @@ -6,7 +6,7 @@ import { buildAiVaultResumeShellCommand } from '../../shared/ai-vault-resume-command' import { buildAgentResumeStartupPlan } from '../../shared/tui-agent-resume-startup' -import { agentForksConversations } from '../../shared/agent-resume-argv' +import { agentForksConversations, agentForksThroughChat } from '../../shared/agent-resume-argv' import { isResumableTuiAgent } from '../../shared/agent-session-resume' import type { AgentStartupShell } from '../../shared/tui-agent-startup-shell' import { @@ -220,19 +220,21 @@ describe('resume commands Orca builds for an owned row', () => { } }) - // "Resume in New CLI" opens a copy, so it is no second writer of the chat's conversation. - it.each(everyAgent.filter(({ rowAgent }) => agentForksConversations(rowAgent)))( - 'allows each copy it opens for the $rowAgent row', - async (conversation) => { - installOwnership({ handle: conversation.handle }) - const commands = builtCommands(conversation, true) - expect(commands.length).toBe(7) - for (const command of commands) { - await expect( - assertLegacyAiVaultResumeCommandAllowed(command, async () => undefined), - command - ).resolves.toBeUndefined() - } + // "Resume in New CLI" opens a copy, so it is no second writer of the chat's conversation. A copy + // the chat makes is opened by a plain resume of the copy's own id, which no chat owns. + it.each( + everyAgent.filter( + ({ rowAgent }) => agentForksConversations(rowAgent) && !agentForksThroughChat(rowAgent) + ) + )('allows each copy it opens for the $rowAgent row', async (conversation) => { + installOwnership({ handle: conversation.handle }) + const commands = builtCommands(conversation, true) + expect(commands.length).toBe(7) + for (const command of commands) { + await expect( + assertLegacyAiVaultResumeCommandAllowed(command, async () => undefined), + command + ).resolves.toBeUndefined() } - ) + }) }) diff --git a/src/main/ai-vault/structured-session-ownership.test-support.ts b/src/main/ai-vault/structured-session-ownership.test-support.ts index f8c0c2ec6f7..f1b8048549b 100644 --- a/src/main/ai-vault/structured-session-ownership.test-support.ts +++ b/src/main/ai-vault/structured-session-ownership.test-support.ts @@ -1,9 +1,11 @@ +import type { AgentSessionRecord } from '../../shared/agent-session-record' import { agentSessionLeaseFixture, agentSessionRecordFixture } from '../../shared/agent-session-record.test-fixture' import type { AgentSessionProviderHandle } from '../../shared/agent-session-provider-handle' import type { AiVaultListResult, AiVaultSession } from '../../shared/ai-vault-types' +import type { StructuredAgentSessionAdapter } from '../native-chat/agent-session-wire/structured-agent-session-adapter' import type { StructuredProviderSessionOwnership } from '../native-chat/agent-session-wire/structured-provider-session-ownership' import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' import { @@ -59,6 +61,35 @@ export function installOwnership( } as never) } +/** A host whose one chat, `chat-1`, owns `handle`'s conversation, and whose adapter copies a + * conversation through `copy`. */ +export function installCopyingChat( + handle: AgentSessionProviderHandle, + copy: StructuredAgentSessionAdapter['copyConversation'], + otherRecords: AgentSessionRecord[] = [] +): void { + const lease = { ...agentSessionLeaseFixture(), sessionId: 'chat-1' } + const fixture = agentSessionRecordFixture(lease) + const record = { + ...fixture, + sessionId: 'chat-1', + provider: handle.agent, + location: { ...fixture.location, workspaceId: 'workspace-1' }, + providerHandleChain: [{ ...fixture.providerHandleChain[0]!, handle }], + lease + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a copy reads only `deps.store` and `deps.adapter.copyConversation`; the rest of the host is never reached. + setStructuredAgentSessionHost({ + deps: { + store: { + listRecords: () => [record, ...otherRecords], + getRecord: (sessionId: string) => (sessionId === record.sessionId ? record : null) + }, + adapter: { copyConversation: copy } + } + } as never) +} + export function listResult(): AiVaultListResult { const session: AiVaultSession = { id: `local:codex:${PROVIDER_SESSION}`, diff --git a/src/main/ai-vault/structured-session-ownership.ts b/src/main/ai-vault/structured-session-ownership.ts index 2499f9c50b2..3836b4613e8 100644 --- a/src/main/ai-vault/structured-session-ownership.ts +++ b/src/main/ai-vault/structured-session-ownership.ts @@ -175,8 +175,9 @@ export async function assertLegacyAiVaultResumeCommandAllowed( } } -function findResumeOwnership( - args: AiVaultPrepareSessionResumeArgs +/** The chat that owns the row a resume or fork names; null when none does. */ +export function findResumeOwnership( + args: Pick ): StructuredProviderSessionOwnership | null { const owner = historyRowOwner(args.agent) if (!owner) { diff --git a/src/main/ipc/ai-vault-resume-chat-copy.test.ts b/src/main/ipc/ai-vault-resume-chat-copy.test.ts new file mode 100644 index 00000000000..9f38fbf24e2 --- /dev/null +++ b/src/main/ipc/ai-vault-resume-chat-copy.test.ts @@ -0,0 +1,52 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' +import { installCopyingChat } from '../ai-vault/structured-session-ownership.test-support' +import type { AiVaultPrepareSessionResumeArgs } from '../../shared/ai-vault-resume-preparation' + +vi.mock('electron', () => ({ ipcMain: { handle: vi.fn() } })) + +const { prepareAiVaultSessionResume } = await import('./ai-vault-resume') + +const OMP_ID = '019fd611-2b3c-7d4e-9f50-6b7c8d9e0f1a' +const forkArgs = ( + executionHostId: AiVaultPrepareSessionResumeArgs['executionHostId'] +): AiVaultPrepareSessionResumeArgs => ({ + agent: 'omp', + sessionId: OMP_ID, + filePath: `/Users/me/.omp/agent/sessions/--repo--/x_${OMP_ID}.jsonl`, + codexHome: null, + executionHostId, + fork: true +}) + +afterEach(() => setStructuredAgentSessionHost(null)) + +describe('Resume in New CLI preparation for a chat whose host makes the copy', () => { + it('leaves the copy to the server that runs a remote chat', async () => { + const copy = vi.fn(async () => ({ + sessionId: 'local-copy' + })) + installCopyingChat({ transport: 'acp', agent: 'omp', nativeId: 'unrelated' }, copy) + const prepareRuntimeSessionResume = vi.fn(async () => ({ + useRealCodexHome: false, + forkedSession: { + sessionId: 'remote-copy', + filePath: '/sessions/remote-copy.jsonl', + agent: 'omp' as const + } + })) + + await expect( + prepareAiVaultSessionResume(forkArgs('runtime:env-1'), { prepareRuntimeSessionResume }) + ).resolves.toEqual({ + useRealCodexHome: false, + forkedSession: { + sessionId: 'remote-copy', + filePath: '/sessions/remote-copy.jsonl', + agent: 'omp' + } + }) + expect(prepareRuntimeSessionResume).toHaveBeenCalledWith('env-1', forkArgs('runtime:env-1')) + expect(copy).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/ipc/ai-vault-resume.test.ts b/src/main/ipc/ai-vault-resume.test.ts index 89187a0593b..de1952a298a 100644 --- a/src/main/ipc/ai-vault-resume.test.ts +++ b/src/main/ipc/ai-vault-resume.test.ts @@ -1,7 +1,7 @@ import { afterEach, expect, it, vi } from 'vitest' import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' import { - installOwnership, + installCopyingChat, PROVIDER_SESSION } from '../ai-vault/structured-session-ownership.test-support' import { prepareAiVaultSessionResume } from './ai-vault-resume' @@ -10,7 +10,10 @@ vi.mock('electron', () => ({ ipcMain: { handle: vi.fn() } })) afterEach(() => setStructuredAgentSessionHost(null)) it('refuses the native owner while preserving forks and other execution hosts', async () => { - installOwnership({ handle: { transport: 'acp', agent: 'omp', nativeId: PROVIDER_SESSION } }) + installCopyingChat( + { transport: 'acp', agent: 'omp', nativeId: PROVIDER_SESSION }, + vi.fn(async () => ({ sessionId: 'copy-1', filePath: '/sessions/copy-1.jsonl' })) + ) const local = { agent: 'omp' as const, sessionId: PROVIDER_SESSION, @@ -26,7 +29,8 @@ it('refuses the native owner while preserving forks and other execution hosts', ) expect(prepareSessionResume).not.toHaveBeenCalled() await expect(prepareAiVaultSessionResume({ ...local, fork: true }, options)).resolves.toEqual({ - useRealCodexHome: false + useRealCodexHome: false, + forkedSession: { agent: 'omp', sessionId: 'copy-1', filePath: '/sessions/copy-1.jsonl' } }) const paired = { ...local, executionHostId: 'runtime:paired' as const } await expect(prepareAiVaultSessionResume(paired, options)).resolves.toEqual({ diff --git a/src/main/ipc/ai-vault-resume.ts b/src/main/ipc/ai-vault-resume.ts index 25469b1d244..c46eeaecaa2 100644 --- a/src/main/ipc/ai-vault-resume.ts +++ b/src/main/ipc/ai-vault-resume.ts @@ -8,6 +8,7 @@ import { isUnresolvedOwnerHostId, parseExecutionHostId } from '../../shared/exec import { isWslUncPath } from '../../shared/wsl-paths' import { assertLegacyAiVaultResumeAllowed } from '../ai-vault/structured-session-ownership' import { tryEnsureStructuredAgentSessionHostForResume } from '../runtime/structured-agent-session-host-refusal' +import { copyStructuredChatConversation } from '../ai-vault/structured-chat-conversation-copy' export type AiVaultResumeHandlerOptions = { ensureStructuredSessionOwnership?: () => Promise @@ -47,5 +48,8 @@ export async function prepareAiVaultSessionResume( } assertLegacyAiVaultResumeAllowed(args) } - return options.prepareSessionResume?.(args) ?? { useRealCodexHome: false } + return ( + (await copyStructuredChatConversation(args)) ?? + options.prepareSessionResume?.(args) ?? { useRealCodexHome: false } + ) } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts index 25e70096078..cb842d04c84 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts @@ -200,6 +200,14 @@ export class StructuredAgentSessionAdapterRouter implements StructuredAgentSessi this.requireAgent(input.identity).adapter.providerHistoryWindow?.(input) ?? Promise.resolve(null) + copyConversation: NonNullable = (input) => { + const { adapter } = this.requireAgent(input.identity) + if (!adapter.copyConversation) { + return Promise.reject(new Error(`${input.identity.agent} cannot copy a conversation`)) + } + return adapter.copyConversation(input) + } + closeSession = (sessionId: string): Promise => this.stopSession(sessionId, (adapter) => adapter.closeSession) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts index bddcd06c964..664cb423838 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts @@ -1,3 +1,4 @@ +import type { AiVaultConversationCopy } from '../../../shared/ai-vault-resume-preparation' import type { AgentSessionAccountKind, AgentSessionUnavailable @@ -439,6 +440,14 @@ export type StructuredAgentSessionAdapter = StructuredAgentSessionAdapterStop & identity: AgentSessionJournalIdentity accountHome: AgentSessionAccountHome }): Promise + /** Copies provider conversation `providerSessionId` of the chat into a new one the agent saves, + * answering the copy's id; the chat keeps its own. Runs whether or not the chat's agent is + * running. Absent where the agent copies through its CLI instead (`agentForksThroughChat`). */ + copyConversation?(input: { + identity: AgentSessionJournalIdentity + providerSessionId: string + accountHome: AgentSessionAccountHome + }): Promise /** Gracefully stops the structured owner after its event stream is drained. */ /** Returns true only after the provider child exit is proven. A root-exit or processless verdict * is thrown only once the session is finalized; read it through `stopAgentSessionProviderRoot`. */ diff --git a/src/main/native-chat/structured-agent-configured-args.test.ts b/src/main/native-chat/structured-agent-configured-args.test.ts index 58823a40608..4daf3556527 100644 --- a/src/main/native-chat/structured-agent-configured-args.test.ts +++ b/src/main/native-chat/structured-agent-configured-args.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { structuredAgentConfiguredArgs } from './structured-agent-configured-args' describe('structured chat configured Arguments', () => { - it.each(['claude', 'codex'] as const)( + it.each(['claude', 'codex', 'opencode', 'omp', 'grok'] as const)( 'reads the existing %s setting and preserves quoted values', (agent) => { expect( diff --git a/src/main/native-chat/structured-agent-configured-args.ts b/src/main/native-chat/structured-agent-configured-args.ts index 3b60c764268..49aac2e398b 100644 --- a/src/main/native-chat/structured-agent-configured-args.ts +++ b/src/main/native-chat/structured-agent-configured-args.ts @@ -1,4 +1,4 @@ -import type { AgentSessionHandleProvider } from '../../shared/agent-session-provider-handle' +import type { TuiAgent } from '../../shared/tui-agent' import type { GlobalSettings } from '../../shared/global-settings-types' import { resolveTuiAgentLaunchArgs } from '../../shared/tui-agent-launch-defaults' import { tokenizeStartupCommand } from '../../shared/tui-agent-startup-shell' @@ -6,7 +6,7 @@ import { resolveLocalWindowsAgentStartupShell } from '../../shared/windows-termi /** Use the same grouping rules as terminal launches before the provider filters its owned flags. */ export function structuredAgentConfiguredArgs( - agent: AgentSessionHandleProvider, + agent: TuiAgent, settings: Partial>, platform: NodeJS.Platform = process.platform ): string[] { diff --git a/src/main/runtime/rpc/methods/ai-vault.test.ts b/src/main/runtime/rpc/methods/ai-vault.test.ts index f9d76f4acdd..cea3f989ed5 100644 --- a/src/main/runtime/rpc/methods/ai-vault.test.ts +++ b/src/main/runtime/rpc/methods/ai-vault.test.ts @@ -31,6 +31,8 @@ import { } from './ai-vault' import { agentSessionRefusalError } from '../../../../shared/agent-session-wire-refusals' import { recordStructuredAgentSessionHostInstallRefusal } from '../../structured-agent-session-host-refusal' +import { setStructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-registry' +import { installCopyingChat } from '../../../ai-vault/structured-session-ownership.test-support' import { configureAiVaultSessionSources, listAiVaultSessions, @@ -252,6 +254,51 @@ describe('aiVault.prepareSessionResume', () => { }) }) +describe('aiVault.prepareSessionResume for a chat whose host makes the copy', () => { + afterEach(() => setStructuredAgentSessionHost(null)) + + it("has the server's chat copy an OMP conversation and answers the copy", async () => { + const providerSessionId = '019fd611-2b3c-7d4e-9f50-6b7c8d9e0f1a' + const copy = vi.fn(async () => ({ + sessionId: 'copy-1', + filePath: '/sessions/copy-1.jsonl' + })) + installCopyingChat({ transport: 'acp', agent: 'omp', nativeId: providerSessionId }, copy) + const prepareAiVaultSessionResume = vi.fn() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handler reads only these runtime members. + const runtime = { + getRuntimeId: () => 'test-runtime', + ensureStructuredAgentSessionHost: vi.fn(async () => undefined), + prepareAiVaultSessionResume + } as unknown as OrcaRuntimeService + const dispatcher = new RpcDispatcher({ runtime, methods: AI_VAULT_METHODS }) + + const response = await dispatcher.dispatch( + makeRequest('aiVault.prepareSessionResume', { + agent: 'omp', + sessionId: providerSessionId, + filePath: `/home/me/.omp/agent/sessions/--repo--/x_${providerSessionId}.jsonl`, + codexHome: null, + fork: true + }) + ) + + expect(response).toMatchObject({ + ok: true, + result: { + useRealCodexHome: false, + forkedSession: { + sessionId: 'copy-1', + filePath: '/sessions/copy-1.jsonl', + agent: 'omp' + } + } + }) + expect(copy).toHaveBeenCalledWith(expect.objectContaining({ providerSessionId })) + expect(prepareAiVaultSessionResume).not.toHaveBeenCalled() + }) +}) + // Session history and terminal resume are not chats: a process whose chats are refused still // serves them, while any other host failure still fails the request. describe('aiVault methods without a structured host', () => { diff --git a/src/main/runtime/rpc/methods/ai-vault.ts b/src/main/runtime/rpc/methods/ai-vault.ts index a5e6d6a5890..effaec21648 100644 --- a/src/main/runtime/rpc/methods/ai-vault.ts +++ b/src/main/runtime/rpc/methods/ai-vault.ts @@ -10,6 +10,7 @@ import { import { defineMethod } from '../core' import { conditionalAiVaultListResult } from '../../../ai-vault/session-list-fingerprint' import { restampAiVaultListResult } from '../../../ai-vault/session-list-results' +import { copyStructuredChatConversation } from '../../../ai-vault/structured-chat-conversation-copy' import type { AiVaultPrepareSessionResumeArgs } from '../../../../shared/ai-vault-resume-preparation' import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' import { isWslUncPath } from '../../../../shared/wsl-paths' @@ -144,7 +145,9 @@ export const AI_VAULT_METHODS = [ : ensureStructuredAgentSessionHostUnlessRefused(ensureHost)) assertLegacyAiVaultResumeAllowed(args) } - return runtime.prepareAiVaultSessionResume(args) + return ( + (await copyStructuredChatConversation(args)) ?? runtime.prepareAiVaultSessionResume(args) + ) } }) ] diff --git a/src/main/runtime/structured-agent-runtime-registrations.ts b/src/main/runtime/structured-agent-runtime-registrations.ts index a770451e54a..a062e762094 100644 --- a/src/main/runtime/structured-agent-runtime-registrations.ts +++ b/src/main/runtime/structured-agent-runtime-registrations.ts @@ -241,6 +241,7 @@ function acpRegistration(spec: AcpLaunchSpec): StructuredAgentRuntimeRegistratio readJournal, resolveLaunch: composeStructuredLaunch(definition, sources, part), readLaunch: composeStructuredLaunchRead(definition, sources, part), + resolveLaunchArgs: () => deps.resolveLaunchArgs(spec.agent), connect: (launch, options) => createAcpAgentConnection(launch, options), onChildWorkEvidence: (sessionId, evidence) => context.host()?.publishChildWorkEvidence(sessionId, evidence), diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.test.ts index 9977da1a7ac..3ed7f3c7294 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.test.ts @@ -1,3 +1,4 @@ +import { OpenCodeCopyServerRouteError } from '../../../../shared/opencode-launch-command' import { describe, expect, it } from 'vitest' import { aiVaultSessionCliForkWorktreeId, @@ -31,11 +32,11 @@ describe('Resume in New CLI eligibility', () => { }) it.each(['opencode', 'opencode2', 'omp'] as const)( - 'withholds it from a %s chat row, whose CLI cannot open a copy', + 'offers it for a %s chat row, whose chat makes the copy', (agent) => { expect( aiVaultSessionCliForkWorktreeId({ agent, ...ROW, structuredSession: OWNED }, RESUMABLE) - ).toBeNull() + ).toBe('repo-1::worktree-2') } ) @@ -81,6 +82,15 @@ describe('Resume in New CLI failure text', () => { expect(describeAiVaultCliForkFailure(message)).toBe(UPDATE) }) + it('keeps server refusal feedback plain when Electron wraps the host error', () => { + const message = new OpenCodeCopyServerRouteError().message + expect( + describeAiVaultCliForkFailure( + `Error invoking remote method 'aiVault:prepareSessionResume': Error: ${message}` + ) + ).toBe(message) + }) + it('passes any other failure through unchanged', () => { const message = 'The session host is unavailable. Reconnect it and retry resume.' expect(describeAiVaultCliForkFailure(message)).toBe(message) diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.ts index f5c86c938fa..f2cf3221488 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.ts @@ -1,14 +1,16 @@ -import { translate } from '@/i18n/i18n' +import { compactIpcErrorMessage } from '@/lib/ipc-error' import { isAiVaultOwnershipRefusal } from '@/lib/ai-vault-resume-refusal' +import { aiVaultCliForkHostTooOldMessage } from '@/lib/ai-vault-session-resume-preparation' import type { AiVaultSession } from '../../../../shared/ai-vault-types' import { getAgentForkArgv, isResumableTuiAgent } from '../../../../shared/agent-session-resume' +import { agentForksThroughChat } from '../../../../shared/agent-resume-argv' import { getAiVaultAgentProviderSession } from '@/lib/ai-vault-resume-command' /** * Where "Resume in New CLI" opens its copy of a conversation native chat owns, or null when the row - * does not offer it. A row no chat owns already resumes in the CLI through plain Resume, and a row - * offers it only when its CLI's copy command can be built (Pi's needs the session file, which a - * remote search hit doesn't carry). + * does not offer it. A row no chat owns already resumes in the CLI through plain Resume. A row offers + * it when its chat makes the copy (OpenCode, OMP), or when its CLI's copy command can be built (Pi's + * needs the session file, which a remote search hit doesn't carry). */ export function aiVaultSessionCliForkWorktreeId( session: Pick, @@ -20,7 +22,7 @@ export function aiVaultSessionCliForkWorktreeId( const providerSession = getAiVaultAgentProviderSession(session) return providerSession && isResumableTuiAgent(session.agent) && - getAgentForkArgv(session.agent, providerSession) + (agentForksThroughChat(session.agent) || getAgentForkArgv(session.agent, providerSession)) ? resume.worktreeId : null } @@ -29,10 +31,6 @@ export function aiVaultSessionCliForkWorktreeId( * the fork as a second writer, so that refusal asks for the update instead of naming a code. */ export function describeAiVaultCliForkFailure(message: string): string { // The desktop preparation's refusal arrives inside Electron's IPC wrapper. - return isAiVaultOwnershipRefusal(message) - ? translate( - 'auto.components.right.sidebar.AiVaultPanel.resumeInNewCliHostTooOld', - 'Update Orca on the host that runs this chat to resume it in a new CLI.' - ) - : message + const code = compactIpcErrorMessage(message) + return isAiVaultOwnershipRefusal(message) ? aiVaultCliForkHostTooOldMessage() : (code ?? message) } diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.test.ts index a17cfb36890..36b9ba0dc51 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.test.ts @@ -1,3 +1,4 @@ +import type * as ForkStartup from '@/lib/ai-vault-session-fork-startup' // @vitest-environment happy-dom import { renderHook, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -6,6 +7,7 @@ import type { WebRuntimeTerminalCreateOutcome } from '@/runtime/web-runtime-sess const mocks = vi.hoisted(() => ({ buildAiVaultForkStartupForWorktree: vi.fn(), + getState: vi.fn(), buildAiVaultResumeStartupForWorktree: vi.fn(), prepareAiVaultSessionForFork: vi.fn(), prepareAiVaultSessionForResume: vi.fn(), @@ -16,10 +18,16 @@ const mocks = vi.hoisted(() => ({ toastSuccess: vi.fn() })) -vi.mock('@/lib/ai-vault-session-fork-startup', () => ({ +vi.mock('@/lib/ai-vault-session-fork-startup', async (importOriginal) => ({ + ...(await importOriginal()), + buildAiVaultCopiedSessionStartupForWorktree: (args: { session: AiVaultSession }) => ({ + ...mocks.buildAiVaultResumeStartupForWorktree(args), + agent: args.session.agent + }), buildAiVaultForkStartupForWorktree: mocks.buildAiVaultForkStartupForWorktree })) vi.mock('@/lib/ai-vault-resume-command', () => ({ + resolveAiVaultResumeHost: () => ({ platform: 'linux', codexHome: null, liveShell: undefined }), buildAiVaultResumeStartupForWorktree: mocks.buildAiVaultResumeStartupForWorktree, buildAiVaultResumeCopyCommandForWorktree: vi.fn() })) @@ -27,7 +35,9 @@ vi.mock('@/lib/ai-vault-session-resume-preparation', () => ({ prepareAiVaultSessionForFork: mocks.prepareAiVaultSessionForFork, prepareAiVaultSessionForResume: mocks.prepareAiVaultSessionForResume, prepareAiVaultSessionForTerminalResume: mocks.prepareAiVaultSessionForTerminalResume, - dropDeletedSshResumeCwd: async (session: unknown) => session + dropDeletedSshResumeCwd: async (session: unknown) => session, + aiVaultCliForkHostTooOldMessage: () => + 'Update Orca on the host that runs this chat to resume it in a new CLI.' })) vi.mock('@/lib/launch-ai-vault-session', () => ({ launchAiVaultSessionInNewTab: mocks.launchAiVaultSessionInNewTab @@ -46,7 +56,7 @@ vi.mock('./ai-vault-session-resume-in-chat-launch', () => ({ })) vi.mock('sonner', () => ({ toast: { error: mocks.toastError, success: mocks.toastSuccess } })) vi.mock('@/store', () => ({ - useAppStore: { getState: () => ({ activeWorktreeId: 'worktree-1', settings: {} }) } + useAppStore: { getState: mocks.getState } })) import { useAiVaultSessionLaunchActions } from './ai-vault-session-launch-actions' @@ -77,11 +87,12 @@ const owned: AiVaultSession = { const FORK_STARTUP = { command: "claude --resume 'provider-1' --fork-session", cwd: '/repo' } -function renderActions() { +function renderActions(agentCmdOverrides?: { opencode?: string }) { return renderHook(() => useAiVaultSessionLaunchActions({ activeWorktree: null, activeWorktreeId: 'worktree-1', + agentCmdOverrides, // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked launch target never reads it. targetState: {} as never }) @@ -91,11 +102,38 @@ function renderActions() { describe('Resume in New CLI launch', () => { beforeEach(() => { vi.clearAllMocks() - mocks.prepareAiVaultSessionForFork.mockImplementation(async (session) => session) + mocks.getState.mockReturnValue({ activeWorktreeId: 'worktree-1', settings: {} }) + mocks.prepareAiVaultSessionForFork.mockImplementation(async (session) => ({ + session, + copied: false + })) mocks.buildAiVaultForkStartupForWorktree.mockReturnValue(FORK_STARTUP) mocks.launchAiVaultSessionInNewTab.mockReturnValue({ tabId: 'tab-1' }) }) + it.each(['command', 'arguments'] as const)( + 'refuses effective OpenCode %s server routing before asking for any copy', + async (location) => { + const chat = { ...owned, agent: 'opencode2' as const } + mocks.getState.mockReturnValue({ + activeWorktreeId: 'worktree-1', + settings: { + agentDefaultArgs: + location === 'arguments' ? { opencode: '--server=http://synthetic-server' } : {} + } + }) + renderActions( + location === 'command' + ? { opencode: 'opencode --server http://synthetic-server' } + : undefined + ).handleResumeInNewCli(chat, 'worktree-2') + await waitFor(() => expect(mocks.toastError).toHaveBeenCalledTimes(1)) + expect(mocks.toastError).toHaveBeenCalledWith(expect.stringContaining('Remove --server')) + expect(mocks.prepareAiVaultSessionForFork).not.toHaveBeenCalled() + expect(mocks.launchAiVaultSessionInNewTab).not.toHaveBeenCalled() + } + ) + it('opens the fork plan in the chosen workspace, never the plain resume plan', async () => { renderActions().handleResumeInNewCli(owned, 'worktree-2') @@ -115,6 +153,36 @@ describe('Resume in New CLI launch', () => { expect(mocks.toastError).not.toHaveBeenCalled() }) + it.each(['omp', 'opencode2'] as const)( + 'opens the %s copy with the actual CLI in the chosen workspace', + async (agent) => { + const chat = { ...owned, agent, executionHostId: 'runtime:paired-host' as const } + const copy = { + ...chat, + agent: agent === 'omp' ? ('omp' as const) : ('opencode' as const), + sessionId: 'copy-1' + } + const startup = { + command: `${copy.agent} --resume copy-1`, + providerSession: { key: 'session_id', id: 'copy-1' } + } + mocks.prepareAiVaultSessionForFork.mockResolvedValue({ session: copy, copied: true }) + mocks.buildAiVaultResumeStartupForWorktree.mockReturnValue(startup) + renderActions({ opencode: 'opencode --server http://synthetic-server' }).handleResumeInNewCli( + chat, + 'worktree-2' + ) + await waitFor(() => expect(mocks.launchAiVaultSessionInNewTab).toHaveBeenCalledTimes(1)) + expect(mocks.launchAiVaultSessionInNewTab).toHaveBeenCalledWith({ + agent: copy.agent, + worktreeId: 'worktree-2', + ...startup + }) + expect(mocks.buildAiVaultForkStartupForWorktree).not.toHaveBeenCalled() + expect(mocks.toastError).not.toHaveBeenCalled() + } + ) + it('shows one toast and opens nothing when the agent cannot fork', async () => { mocks.buildAiVaultForkStartupForWorktree.mockReturnValue(null) diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts index 08421b670b8..d31800c40fa 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts @@ -1,3 +1,8 @@ +import { + notifyAiVaultSessionPreparationFailure, + notifyAiVaultSessionQueued, + resolveAiVaultSessionLaunchTargetOrNotify +} from './ai-vault-session-launch-feedback' import { useCallback, useState } from 'react' import { toast } from 'sonner' import { @@ -6,7 +11,11 @@ import { type AiVaultResumeCommandSession, type AiVaultResumeStartup } from '@/lib/ai-vault-resume-command' -import { buildAiVaultForkStartupForWorktree } from '@/lib/ai-vault-session-fork-startup' +import { + assertAiVaultCopiedSessionLaunchAllowed, + buildAiVaultCopiedSessionStartupForWorktree, + buildAiVaultForkStartupForWorktree +} from '@/lib/ai-vault-session-fork-startup' import { launchAiVaultSessionInNewTab } from '@/lib/launch-ai-vault-session' import { useAppStore } from '@/store' import type { AiVaultAgent, AiVaultSession } from '../../../../shared/ai-vault-types' @@ -31,11 +40,7 @@ import { activateAiVaultResumeWorkspace, resumeAiVaultSessionInNewChat } from './ai-vault-session-resume-in-chat-launch' -import { - aiVaultResumeUnsupportedMessage, - resolveAiVaultSessionLaunchTarget, - resolveAiVaultTargetWorkspacePath -} from './ai-vault-session-launch-target' +import { resolveAiVaultTargetWorkspacePath } from './ai-vault-session-launch-target' export function useAiVaultSessionLaunchActions({ activeWorktree, @@ -111,15 +116,6 @@ export function useAiVaultSessionLaunchActions({ if (!targetId) { return } - const showQueuedToast = (): void => { - toast.success( - translate( - 'auto.components.right.sidebar.AiVaultPanel.agentSessionQueued', - '{{value0}} session queued', - { value0: agentLabel(session.agent) } - ) - ) - } void prepareStartup(targetId.worktreeId) .then((startup) => { const launchResult = launchAiVaultSessionInNewTab({ @@ -143,14 +139,14 @@ export function useAiVaultSessionLaunchActions({ if (useAppStore.getState().activeWorktreeId !== targetId.worktreeId) { activateAiVaultResumeWorkspace(targetId.worktreeId) } - showQueuedToast() + notifyAiVaultSessionQueued(session.agent) }) return } if (useAppStore.getState().activeWorktreeId !== targetId.worktreeId) { activateAiVaultResumeWorkspace(targetId.worktreeId) } - showQueuedToast() + notifyAiVaultSessionQueued(session.agent) }) .catch((error: unknown) => notifyAiVaultSessionPreparationFailure(error, session.agent, describeFailure) @@ -182,12 +178,31 @@ export function useAiVaultSessionLaunchActions({ const handleResumeInNewCli = useCallback( (session: AiVaultSession, targetWorktreeId: string): void => { const prepareStartup = async (worktreeId: string): Promise => { - const startup = buildAiVaultForkStartupForWorktree({ + assertAiVaultCopiedSessionLaunchAllowed({ state: useAppStore.getState(), worktreeId, - session: await prepareAiVaultSessionForFork(session).then(dropDeletedSshResumeCwd), - commandOverride: agentCmdOverrides?.[session.agent] + session, + commandOverride: agentCmdOverrides?.opencode }) + const prepared = await prepareAiVaultSessionForFork(session) + const target = await dropDeletedSshResumeCwd(prepared.session) + // A copy the chat's host already made is the terminal's own conversation: a plain resume. + const startup = prepared.copied + ? buildAiVaultCopiedSessionStartupForWorktree( + { + state: useAppStore.getState(), + worktreeId, + session: target, + commandOverride: agentCmdOverrides?.[target.agent] + }, + session.agent + ) + : buildAiVaultForkStartupForWorktree({ + state: useAppStore.getState(), + worktreeId, + session: target, + commandOverride: agentCmdOverrides?.[session.agent] + }) if (!startup) { throw new Error( translate( @@ -282,32 +297,3 @@ export function useAiVaultSessionLaunchActions({ handleContinuationDialogOpenChange } } - -function notifyAiVaultSessionPreparationFailure( - error: unknown, - agent: AiVaultAgent, - describeFailure: (message: string) => string = (message) => message -): void { - const described = error instanceof Error ? new Error(describeFailure(error.message)) : error - toast.error(describeAiVaultResumeFailure(described, agent)) -} - -function resolveAiVaultSessionLaunchTargetOrNotify( - args: Parameters[0] -): Extract, { status: 'ready' }> | null { - const target = resolveAiVaultSessionLaunchTarget(args) - if (target.status === 'missing') { - toast.error( - translate( - 'auto.components.right.sidebar.AiVaultPanel.openWorkspaceBeforeResuming', - 'Open a workspace before resuming a session.' - ) - ) - return null - } - if (target.status === 'unsupported') { - toast.error(aiVaultResumeUnsupportedMessage(target.targetStatus)) - return null - } - return target -} diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-launch-feedback.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-feedback.ts new file mode 100644 index 00000000000..832cec2a84b --- /dev/null +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-feedback.ts @@ -0,0 +1,48 @@ +import { + aiVaultResumeUnsupportedMessage, + resolveAiVaultSessionLaunchTarget +} from './ai-vault-session-launch-target' +import { toast } from 'sonner' +import { describeAiVaultResumeFailure } from '@/lib/ai-vault-resume-refusal' +import { translate } from '@/i18n/i18n' +import { agentLabel } from './ai-vault-session-filters' +import type { AiVaultAgent } from '../../../../shared/ai-vault-types' + +export function notifyAiVaultSessionQueued(agent: AiVaultAgent): void { + toast.success( + translate( + 'auto.components.right.sidebar.AiVaultPanel.agentSessionQueued', + '{{value0}} session queued', + { value0: agentLabel(agent) } + ) + ) +} + +export function notifyAiVaultSessionPreparationFailure( + error: unknown, + agent: AiVaultAgent, + describeFailure: (message: string) => string = (message) => message +): void { + const described = error instanceof Error ? new Error(describeFailure(error.message)) : error + toast.error(describeAiVaultResumeFailure(described, agent)) +} + +export function resolveAiVaultSessionLaunchTargetOrNotify( + args: Parameters[0] +): Extract, { status: 'ready' }> | null { + const target = resolveAiVaultSessionLaunchTarget(args) + if (target.status === 'missing') { + toast.error( + translate( + 'auto.components.right.sidebar.AiVaultPanel.openWorkspaceBeforeResuming', + 'Open a workspace before resuming a session.' + ) + ) + return null + } + if (target.status === 'unsupported') { + toast.error(aiVaultResumeUnsupportedMessage(target.targetStatus)) + return null + } + return target +} diff --git a/src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts b/src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts index 6b7ab2ce318..57c31c14722 100644 --- a/src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts +++ b/src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts @@ -192,24 +192,15 @@ describe('resolveTabSessionHistorySubject', () => { ).toEqual({ ...chatSubject }) }) - it('finds a chat of any agent whose CLI can open a copy', () => { - expect( - resolveTabSessionHistorySubject(makeState(), { - tab: { id: 'unified-1', worktreeId: WORKTREE_ID }, - nativeChat: { sessionId: 'orca-chat-1', agent: 'grok' } - }) - ).toEqual({ ...chatSubject }) - }) - - it.each(['opencode', 'omp'] as const)( - 'skips a %s chat, whose CLI cannot open a copy', + it.each(['grok', 'opencode', 'omp'] as const)( + 'finds a %s chat, whose conversation can be copied to its CLI', (agent) => { expect( resolveTabSessionHistorySubject(makeState(), { tab: { id: 'unified-1', worktreeId: WORKTREE_ID }, nativeChat: { sessionId: 'orca-chat-1', agent } }) - ).toBeNull() + ).toEqual({ ...chatSubject }) } ) diff --git a/src/renderer/src/lib/ai-vault-omp-cold-resume.test.ts b/src/renderer/src/lib/ai-vault-omp-cold-resume.test.ts index 256858d3ad2..07cf99624a7 100644 --- a/src/renderer/src/lib/ai-vault-omp-cold-resume.test.ts +++ b/src/renderer/src/lib/ai-vault-omp-cold-resume.test.ts @@ -1,3 +1,4 @@ +import { buildAiVaultCopiedSessionStartupForWorktree } from './ai-vault-session-fork-startup' import { describe, expect, it } from 'vitest' import type { AppState } from '@/store/types' import { createTestStore, makeTab } from '@/store/slices/store-test-helpers' @@ -5,6 +6,43 @@ import { buildAgentResumeStartupPlan } from './tui-agent-startup' import { buildAiVaultResumeStartupForWorktree } from './ai-vault-resume-command' describe('AI Vault OMP cold resume', () => { + it('preserves local OpenCode standalone mode and existing custom host settings', () => { + const store = createTestStore() + const state = store.getState() + Object.assign(state, { + settings: { + agentDefaultArgs: { opencode: '--model synthetic-model' }, + agentDefaultEnv: { + opencode: { + OPENCODE_AUTH_CONTENT: 'synthetic-local-auth', + HOME: '/host-home', + OPENCODE_CONFIG_DIR: '/host-config' + } + } + } + }) + const startup = buildAiVaultCopiedSessionStartupForWorktree( + { + state, + session: { + agent: 'opencode', + sessionId: 'copy-session', + cwd: '/repo', + codexHome: null, + executionHostId: 'local' + } + }, + 'opencode2' + ) + expect(startup.command).toContain("'--standalone'") + expect(startup.command).toContain("'--model' 'synthetic-model'") + expect(startup.env).toMatchObject({ + OPENCODE_AUTH_CONTENT: 'synthetic-local-auth', + HOME: '/host-home', + OPENCODE_CONFIG_DIR: '/host-config' + }) + }) + it('keeps the custom session store after the resumed process reports its id', () => { const store = createTestStore() store.setState({ diff --git a/src/renderer/src/lib/ai-vault-resume-command.ts b/src/renderer/src/lib/ai-vault-resume-command.ts index 4a736d19c12..3adf2897646 100644 --- a/src/renderer/src/lib/ai-vault-resume-command.ts +++ b/src/renderer/src/lib/ai-vault-resume-command.ts @@ -36,6 +36,7 @@ export type AiVaultResumeCommandSession = Pick< > export type AiVaultResumeStartup = { + agent?: AiVaultSession['agent'] command: string cwd?: string env?: Record @@ -59,6 +60,7 @@ export type AiVaultResumeWorktreeArgs = { worktreeId?: string | null session: AiVaultResumeCommandSession commandOverride?: string | null + agentArgs?: string } export function buildAiVaultResumeCopyCommandForWorktree(args: AiVaultResumeWorktreeArgs): string { @@ -173,10 +175,9 @@ function buildAiVaultResumeForWorktree( }, platform, shell: liveShell, - agentArgs: resolveTuiAgentLaunchArgs( - args.session.agent, - args.state.settings?.agentDefaultArgs - ), + agentArgs: + args.agentArgs ?? + resolveTuiAgentLaunchArgs(args.session.agent, args.state.settings?.agentDefaultArgs), agentEnv: resolveTuiAgentLaunchEnv(args.session.agent, args.state.settings?.agentDefaultEnv), resumeInLaunchCwd: true, ...(args.session.agent === 'omp' && resumeFilePath diff --git a/src/renderer/src/lib/ai-vault-session-fork-startup.ts b/src/renderer/src/lib/ai-vault-session-fork-startup.ts index c94c08d5acc..43774321b28 100644 --- a/src/renderer/src/lib/ai-vault-session-fork-startup.ts +++ b/src/renderer/src/lib/ai-vault-session-fork-startup.ts @@ -1,3 +1,17 @@ +import { parseExecutionHostId } from '../../../shared/execution-host' +import { assertOpenCodeCopyUsesLocalStore } from '../../../shared/opencode-launch-command' +import { findOptionOccurrence } from '../../../shared/command-option-occurrence' +import { + getTuiAgentLaunchCommand, + TUI_AGENT_CONFIG, + isTuiAgent +} from '../../../shared/tui-agent-config' +import { + tokenizeStartupCommand, + resolveStartupShell, + quoteStartupArg +} from '../../../shared/tui-agent-startup-shell' +import type { AiVaultAgent } from '../../../shared/ai-vault-types' import { normalizeAiVaultResumeFilePath } from '../../../shared/ai-vault-resume-path' import { isResumableTuiAgent } from '../../../shared/agent-session-resume' import { @@ -11,6 +25,7 @@ import { import { buildAgentResumeStartupPlan } from '@/lib/tui-agent-startup' import { getAiVaultAgentProviderSession, + buildAiVaultResumeStartupForWorktree, resolveAiVaultResumeHost, type AiVaultResumeStartup, type AiVaultResumeWorktreeArgs @@ -66,3 +81,73 @@ export function buildAiVaultForkStartupForWorktree( launchConfig: plan.launchConfig } } + +/** Keeps the history row's launch mode while the copied conversation opens with its actual CLI. */ +export function buildAiVaultCopiedSessionStartupForWorktree( + args: AiVaultResumeWorktreeArgs, + sourceAgent: AiVaultAgent +): AiVaultResumeStartup { + const { platform, liveShell } = resolveAiVaultResumeHost(args) + const shell = resolveStartupShell(platform, liveShell) + const configuredArgs = isTuiAgent(args.session.agent) + ? resolveTuiAgentLaunchArgs(args.session.agent, args.state.settings?.agentDefaultArgs) + : '' + const configured = tokenizeStartupCommand(configuredArgs, shell) + if (!configured.ok) { + throw new Error('This session cannot be opened in the CLI.') + } + const commandOverride = + args.commandOverride?.trim() || + args.state.settings?.agentCmdOverrides?.[args.session.agent]?.trim() + const mode = + sourceAgent !== args.session.agent && + isTuiAgent(sourceAgent) && + !commandOverride && + !findOptionOccurrence(configured.tokens, ['--standalone'], true) + ? tokenizeStartupCommand( + getTuiAgentLaunchCommand(TUI_AGENT_CONFIG[sourceAgent], platform), + shell + ) + : null + if (mode && !mode.ok) { + throw new Error('This session cannot be opened in the CLI.') + } + const agentArgs = [ + mode?.ok + ? mode.tokens + .slice(1) + .map((token) => quoteStartupArg(token, shell)) + .join(' ') + : '', + configuredArgs + ] + .filter(Boolean) + .join(' ') + return { + ...buildAiVaultResumeStartupForWorktree({ ...args, agentArgs }), + agent: args.session.agent + } +} + +/** Runtime hosts validate their own launch settings when making the copy. */ +export function assertAiVaultCopiedSessionLaunchAllowed(args: AiVaultResumeWorktreeArgs): void { + if (args.session.agent !== 'opencode' && args.session.agent !== 'opencode2') { + return + } + if (parseExecutionHostId(args.session.executionHostId)?.kind === 'runtime') { + return + } + const { platform, liveShell } = resolveAiVaultResumeHost(args) + const command = + args.commandOverride?.trim() || + args.state.settings?.agentCmdOverrides?.opencode || + getTuiAgentLaunchCommand(TUI_AGENT_CONFIG.opencode, platform) + const parsed = tokenizeStartupCommand( + `${command} ${resolveTuiAgentLaunchArgs('opencode', args.state.settings?.agentDefaultArgs)}`, + resolveStartupShell(platform, liveShell) + ) + if (!parsed.ok) { + throw new Error('This session cannot be opened in the CLI.') + } + assertOpenCodeCopyUsesLocalStore(parsed.tokens.slice(1)) +} diff --git a/src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts b/src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts index b29f32fba47..b4ae9067377 100644 --- a/src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts +++ b/src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts @@ -138,7 +138,8 @@ describe('prepareAiVaultSessionForFork', () => { const prepared = await prepareAiVaultSessionForFork(owned) - expect(prepared.codexHome).toBe('/tmp/orca/codex-accounts/account-2/home') + expect(prepared.copied).toBe(false) + expect(prepared.session.codexHome).toBe('/tmp/orca/codex-accounts/account-2/home') expect(prepareSessionResume).toHaveBeenCalledWith({ agent: 'codex', sessionId: owned.sessionId, @@ -158,9 +159,24 @@ describe('prepareAiVaultSessionForFork', () => { structuredSession: { sessionId: 'session-1', workspaceId: 'worktree-1' } }) - await expect(prepareAiVaultSessionForFork(owned)).resolves.toBe(owned) + await expect(prepareAiVaultSessionForFork(owned)).resolves.toEqual({ + session: owned, + copied: false + }) expect(prepareSessionResume).not.toHaveBeenCalled() }) + + it("refuses a host that made no copy rather than resuming the chat's own conversation", async () => { + stubPreparation(vi.fn().mockResolvedValue({ useRealCodexHome: false })) + const owned = session({ + agent: 'omp', + structuredSession: { sessionId: 'session-1', workspaceId: 'worktree-1' } + }) + + await expect(prepareAiVaultSessionForFork(owned)).rejects.toThrow( + 'Update Orca on the host that runs this chat to resume it in a new CLI.' + ) + }) }) describe('dropDeletedSshResumeCwd', () => { diff --git a/src/renderer/src/lib/ai-vault-session-resume-preparation.ts b/src/renderer/src/lib/ai-vault-session-resume-preparation.ts index 04bef7974be..794718ef4d0 100644 --- a/src/renderer/src/lib/ai-vault-session-resume-preparation.ts +++ b/src/renderer/src/lib/ai-vault-session-resume-preparation.ts @@ -14,6 +14,8 @@ import { } from '../../../shared/execution-host' import { readIpcErrorMessage } from './ipc-error' import type { AiVaultResumeCommandSession } from './ai-vault-resume-command' +import { agentForksThroughChat } from '../../../shared/agent-resume-argv' +import { translate } from '@/i18n/i18n' export function prepareAiVaultSessionForResume(session: AiVaultSession): Promise { return session.structuredSession || !aiVaultSessionNeedsResumePreparation(session) @@ -78,9 +80,44 @@ function isMissingPairedResumePreparation(error: unknown): boolean { ) } -/** Picks the Codex home a fork of `session` runs under, the same way a resume picks it. */ -export function prepareAiVaultSessionForFork(session: AiVaultSession): Promise { - return prepareAiVaultSessionHome(session, true) +/** + * The conversation "Resume in New CLI" opens. Most agents' fork command makes the copy, so this + * only picks the Codex home it runs under, as a resume does (`copied: false`). An agent whose chat + * makes the copy (`agentForksThroughChat`) has the chat's host make it here; the result is that + * copy, which a plain resume opens and no chat owns (`copied: true`). + */ +export async function prepareAiVaultSessionForFork( + session: AiVaultSession +): Promise<{ session: AiVaultSession; copied: boolean }> { + if (!agentForksThroughChat(session.agent)) { + return { session: await prepareAiVaultSessionHome(session, true), copied: false } + } + const result = await window.api.aiVault.prepareSessionResume({ + agent: session.agent, + sessionId: session.sessionId, + filePath: session.filePath, + codexHome: session.codexHome, + executionHostId: session.executionHostId, + fork: true + }) + if (!result.forkedSession || (session.agent === 'omp' && !result.forkedSession.filePath)) { + // A host that predates chat copies makes none; resuming the chat's own would be a second writer. + throw new Error(aiVaultCliForkHostTooOldMessage()) + } + const { structuredSession: _owner, ...copy } = session + const { sessionId, agent, filePath } = result.forkedSession + return { + session: { ...copy, agent, sessionId, filePath: filePath ?? '', resumeCommand: '' }, + copied: true + } +} + +/** Only a host that predates a fork refuses or ignores it, so the failure asks for the update. */ +export function aiVaultCliForkHostTooOldMessage(): string { + return translate( + 'auto.components.right.sidebar.AiVaultPanel.resumeInNewCliHostTooOld', + 'Update Orca on the host that runs this chat to resume it in a new CLI.' + ) } async function prepareAiVaultSessionHome( diff --git a/src/shared/agent-resume-argv.ts b/src/shared/agent-resume-argv.ts index c165ec83385..9ba961cfbe4 100644 --- a/src/shared/agent-resume-argv.ts +++ b/src/shared/agent-resume-argv.ts @@ -91,8 +91,24 @@ const FORK_ARGV: Partial< pi: ({ transcriptPath }) => (transcriptPath ? ['pi', '--fork', transcriptPath] : null) } +/** Agents whose CLI cannot copy a conversation but whose chat protocol can (ACP `session/fork`, as + * OpenCode 2.0.14 and OMP 17.0.5 advertise): the chat's host makes the copy, and the terminal opens + * it with a plain resume. A host whose agent does not advertise the copy refuses it. OpenCode 2.x + * lists its chats' conversations as `opencode2` rows. */ +const FORKS_THROUGH_CHAT: ReadonlySet = new Set([ + 'opencode', + 'opencode2', + 'omp' +]) + +/** Whether "Resume in New CLI" can open a copy of this agent's conversations at all. A row's own + * gate (`aiVaultSessionCliForkWorktreeId`) also checks that its copy can be made. */ export function agentForksConversations(agent: string): boolean { - return Object.hasOwn(FORK_ARGV, agent) + return Object.hasOwn(FORK_ARGV, agent) || FORKS_THROUGH_CHAT.has(agent) +} + +export function agentForksThroughChat(agent: string): boolean { + return FORKS_THROUGH_CHAT.has(agent) } export function getAgentForkArgv( diff --git a/src/shared/ai-vault-resume-preparation.ts b/src/shared/ai-vault-resume-preparation.ts index 93e15accf08..2281e624b2d 100644 --- a/src/shared/ai-vault-resume-preparation.ts +++ b/src/shared/ai-vault-resume-preparation.ts @@ -1,4 +1,9 @@ -import type { AiVaultSession } from './ai-vault-types' +import type { AiVaultAgent, AiVaultSession } from './ai-vault-types' + +export type AiVaultConversationCopy = { + sessionId: string + filePath?: string +} export type AiVaultPrepareSessionResumeArgs = Pick< AiVaultSession, @@ -16,6 +21,10 @@ export type AiVaultPrepareSessionResumeResult = { // homes, so the owning host repins resume to the selected account's home. // Absent (older hosts included) means resume keeps the session's own home. substituteCodexHome?: string + /** The copy a fork made of a chat's conversation, for an agent whose chat makes it + * (`agentForksThroughChat`): its id, and the agent whose CLI opens it, the chat's (an OpenCode + * 2.x chat lists as `opencode2` but runs `opencode`). Absent from older hosts, which make none. */ + forkedSession?: AiVaultConversationCopy & { agent: AiVaultAgent } } export type AiVaultSessionResumePreparation = ( diff --git a/src/shared/opencode-launch-command.ts b/src/shared/opencode-launch-command.ts index 86c39837081..ad3617fc772 100644 --- a/src/shared/opencode-launch-command.ts +++ b/src/shared/opencode-launch-command.ts @@ -1,5 +1,21 @@ import type { TuiAgent } from './tui-agent' import { getAgentLaunchExecutableName } from './agent-command-line-intent' +import { findOptionOccurrence } from './command-option-occurrence' + +export class OpenCodeCopyServerRouteError extends Error { + constructor() { + super( + 'Remove --server from the OpenCode command or arguments in Settings to open a copy in a CLI.' + ) + } +} + +/** A configured server may not hold the copy saved by this execution host. */ +export function assertOpenCodeCopyUsesLocalStore(args: readonly string[]): void { + if (findOptionOccurrence(args, ['--server'], true)) { + throw new OpenCodeCopyServerRouteError() + } +} function openCodeCommandAgent(launchCommand: string | undefined): 'opencode' | 'opencode2' | null { const binary = getAgentLaunchExecutableName(launchCommand) From bd6f1ca32ad38979a150066ff249b4f62c069e6a Mon Sep 17 00:00:00 2001 From: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:13:22 +0000 Subject: [PATCH 17/53] fix(automations): sleep a finished run's terminal instead of closing it (#28838) * fix(terminal): keep a hibernating pane's resume record through its stop The agent dies before its shell, so the shell's command-finished mark read as the agent exiting and deleted the record the hibernation had just written. * fix(automations): sleep a finished run's terminal instead of closing it Closing it left the run's own workspace empty, so opening it later showed a blank chat or shell instead of the run's conversation. Fixes #28645 --- .../automation-run-open-target.test.ts | 35 +++++- .../automations/automation-run-open-target.ts | 23 ++-- .../automation-run-view-state.test.ts | 2 +- .../automations/automation-run-view-state.ts | 2 +- ...onnection-command-finished-cleanup.test.ts | 21 +++- .../pty-connection/pane-agent-identity.ts | 5 +- .../hooks/automation-dispatch-completion.ts | 29 ++--- .../src/hooks/automation-dispatch-handler.ts | 4 +- ...omation-dispatch-unverifiable-loss.test.ts | 7 +- .../hooks/useAutomationDispatchEvents.test.ts | 108 +++++++++++------- .../lib/automation-terminal-ownership.test.ts | 105 ++++++++++++++--- .../src/lib/automation-terminal-ownership.ts | 55 +++++++-- 12 files changed, 290 insertions(+), 106 deletions(-) diff --git a/src/renderer/src/components/automations/automation-run-open-target.test.ts b/src/renderer/src/components/automations/automation-run-open-target.test.ts index 40e23c904ed..93938cc4342 100644 --- a/src/renderer/src/components/automations/automation-run-open-target.test.ts +++ b/src/renderer/src/components/automations/automation-run-open-target.test.ts @@ -114,13 +114,38 @@ describe('automation run open target', () => { ).toBeNull() }) - it('rejects a run without an exact PTY identity', () => { + it('targets the pane a successful run slept in place, before and after it wakes', () => { + // Why: hibernation drops only the run's PTY id; the pane key still names the run's own pane. + const sleptRun = run({ terminalPtyId: null }) + const asleepLayout = { ...runLeafLayout, ptyIdsByLeafId: { [leafId]: 'pty-run' } } + const asleep = resolveAutomationRunOpenTarget({ + run: sleptRun, + terminalTabExists: true, + currentLayout: asleepLayout, + livePtyIds: [] + }) + + expect(asleep).toEqual({ tabId: 'tab-1', paneKey, leafId, ptyId: null }) + if (!asleep) { + throw new Error('Expected target.') + } + expect( + buildAutomationRunOpenLayout({ target: asleep, currentLayout: asleepLayout }).ptyIdsByLeafId + ).toBe(asleepLayout.ptyIdsByLeafId) + expect( + resolveAutomationRunOpenTarget({ + run: sleptRun, + terminalTabExists: true, + currentLayout: { ...runLeafLayout, ptyIdsByLeafId: { [leafId]: 'pty-resumed' } }, + livePtyIds: ['pty-resumed'] + }) + ).toEqual({ tabId: 'tab-1', paneKey, leafId, ptyId: 'pty-resumed' }) expect( canOpen({ - run: run({ terminalPtyId: null }), - terminalTabExists: true, - currentLayout: runLeafLayout, - livePtyIds + run: sleptRun, + terminalTabExists: false, + currentLayout: asleepLayout, + livePtyIds: [] }) ).toBe(false) }) diff --git a/src/renderer/src/components/automations/automation-run-open-target.ts b/src/renderer/src/components/automations/automation-run-open-target.ts index 4a24aecbe22..f70a8ad9d94 100644 --- a/src/renderer/src/components/automations/automation-run-open-target.ts +++ b/src/renderer/src/components/automations/automation-run-open-target.ts @@ -15,7 +15,8 @@ export type AutomationRunPaneTarget = { tabId: string paneKey: string leafId: string - ptyId: string + /** Null while a hibernated run pane waits for its tab to wake it. */ + ptyId: string | null } function getAutomationRunOpenTabId(run: Pick): string | null { @@ -41,16 +42,25 @@ export function resolveAutomationRunOpenTarget({ livePtyIds: readonly string[] }): AutomationRunPaneTarget | null { const parsed = parsePaneKey(run.terminalPaneKey ?? '') - if (!terminalTabExists || !parsed || !run.terminalPtyId || !currentLayout?.root) { + if (!terminalTabExists || !parsed || !currentLayout?.root) { return null } if (!terminalLayoutContainsLeaf(currentLayout.root, parsed.leafId)) { return null } + const layoutPtyId = currentLayout.ptyIdsByLeafId?.[parsed.leafId] + if (!run.terminalPtyId) { + // Why: a slept run pane keeps its key but drops its PTY id; the pane, not a process, is the target. + return { + tabId: parsed.tabId, + paneKey: run.terminalPaneKey!, + leafId: parsed.leafId, + ptyId: layoutPtyId && livePtyIds.includes(layoutPtyId) ? layoutPtyId : null + } + } if (!livePtyIds.includes(run.terminalPtyId)) { return null } - const layoutPtyId = currentLayout.ptyIdsByLeafId?.[parsed.leafId] if (layoutPtyId !== undefined && layoutPtyId !== run.terminalPtyId) { return null } @@ -125,10 +135,9 @@ export function buildAutomationRunOpenLayout({ ...currentLayout, activeLeafId: target.leafId, expandedLeafId: currentLayout.expandedLeafId === target.leafId ? target.leafId : null, - ptyIdsByLeafId: { - ...currentLayout.ptyIdsByLeafId, - [target.leafId]: target.ptyId - } + ptyIdsByLeafId: target.ptyId + ? { ...currentLayout.ptyIdsByLeafId, [target.leafId]: target.ptyId } + : currentLayout.ptyIdsByLeafId } } diff --git a/src/renderer/src/components/automations/automation-run-view-state.test.ts b/src/renderer/src/components/automations/automation-run-view-state.test.ts index bc007f51c62..dc50ad0b319 100644 --- a/src/renderer/src/components/automations/automation-run-view-state.test.ts +++ b/src/renderer/src/components/automations/automation-run-view-state.test.ts @@ -100,7 +100,7 @@ describe('automation run view state', () => { }) ).toMatchObject({ availability: 'workspace', - actionLabel: 'Resume workspace', + actionLabel: 'Open workspace', statusLabel: 'Workspace is available.' }) }) diff --git a/src/renderer/src/components/automations/automation-run-view-state.ts b/src/renderer/src/components/automations/automation-run-view-state.ts index 4f35e3f8f37..c8066c0146b 100644 --- a/src/renderer/src/components/automations/automation-run-view-state.ts +++ b/src/renderer/src/components/automations/automation-run-view-state.ts @@ -92,7 +92,7 @@ export function getAutomationRunViewState({ if (run.workspaceId && workspaceExists && run.dispatchedAt !== null) { return { availability: 'workspace', - actionLabel: 'Resume workspace', + actionLabel: 'Open workspace', statusLabel: 'Workspace is available.', canOpen: true } diff --git a/src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts index 35bb4fd7a4e..290d7861464 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts @@ -336,7 +336,12 @@ describe('connectPanePty', () => { expect(window.api.agentStatus.reconcileEndedProcess).toHaveBeenCalledWith(paneKey) }) - it('clears stale sleeping identity after a confirmed return to shell', async () => { + it.each([ + ['clears stale sleeping identity after a confirmed return to shell', false], + // Why: a hibernation records the pane's resume before its kill; the agent dying first + // surfaces the shell, which is Orca's own stop rather than the agent exiting. + ['keeps sleeping identity when the shell surfaces during an intentional stop', true] + ])('%s', async (_label, stoppingOnPurpose) => { vi.useFakeTimers() const { connectPanePty } = await import('./pty-connection') vi.mocked(window.api.pty.confirmForegroundProcess).mockResolvedValue('zsh') @@ -369,12 +374,22 @@ describe('connectPanePty', () => { capturedAt: 1, updatedAt: 1 } + if (stoppingOnPurpose) { + mockStoreState.suppressedPtyExitIds[ptyId] = true + } dataCallbackRef.current?.('\x1b]133;D;0\x07') await vi.advanceTimersByTimeAsync(350) - expect(mockStoreState.clearSleepingAgentSession).toHaveBeenCalledExactlyOnceWith(paneKey) - expect(mockStoreState.sleepingAgentSessionsByPaneKey[paneKey]).toBeUndefined() + if (stoppingOnPurpose) { + expect(mockStoreState.clearSleepingAgentSession).not.toHaveBeenCalled() + expect(mockStoreState.sleepingAgentSessionsByPaneKey[paneKey]).toMatchObject({ + agent: 'codex' + }) + } else { + expect(mockStoreState.clearSleepingAgentSession).toHaveBeenCalledExactlyOnceWith(paneKey) + expect(mockStoreState.sleepingAgentSessionsByPaneKey[paneKey]).toBeUndefined() + } expect(mockStoreState.paneForegroundAgentByPaneKey[paneKey]).toEqual({ agent: null, shellForeground: true diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts b/src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts index 0425194d893..bd5437ba430 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts @@ -207,7 +207,10 @@ export function installPaneAgentIdentity(session: ConnectPanePtySession): void { // otherwise the tab resolver can repaint the exited agent from sleeping occupancy. const state = useAppStore.getState() const sleepingRecord = session.getSleepingRecordForPane(state) - if (sleepingRecord) { + const ptyId = session.transport.getPtyId() + // Why: a shell surfacing while Orca stops this PTY is that stop, not the agent exiting; a + // hibernation records the pane's resume before its kill, so this would delete it. + if (sleepingRecord && !(ptyId && state.suppressedPtyExitIds[ptyId])) { session.clearSleepingRecordProviderDuplicates(state, sleepingRecord) } session.clearStaleAgentTabTitleOnConfirmedShell() diff --git a/src/renderer/src/hooks/automation-dispatch-completion.ts b/src/renderer/src/hooks/automation-dispatch-completion.ts index 933f3666ae1..08f043c5cc3 100644 --- a/src/renderer/src/hooks/automation-dispatch-completion.ts +++ b/src/renderer/src/hooks/automation-dispatch-completion.ts @@ -15,6 +15,7 @@ import { } from './automation-agent-status-entry-change' import type { Worktree } from '../../../shared/worktree/types' import { isProvenProcessExit } from '../../../shared/terminal-exit-cause' +import type { AutomationTerminalOwnership } from '@/lib/automation-terminal-ownership' type MarkDispatchResult = (result: AutomationDispatchResult) => Promise @@ -24,7 +25,7 @@ export function createAutomationDispatchCompletion(args: { precheckResult: AutomationPrecheckResult | null markDispatchResult: MarkDispatchResult releaseTerminalOwnership: () => void - finalizeTerminalOwnership: () => boolean + finalizeTerminalOwnership: AutomationTerminalOwnership['finalize'] }) { const outputSnapshotBuffer = createAutomationRunOutputSnapshotBuffer() let latestAssistantMessage: string | null = null @@ -66,21 +67,23 @@ export function createAutomationDispatchCompletion(args: { args.releaseTerminalOwnership() throw error } - if (args.finalizeTerminalOwnership()) { - await clearRetiredRunTerminalIdentity() - } + await retireRunTerminal() } - const clearRetiredRunTerminalIdentity = async (): Promise => { - // Why: the owned terminal was just retired, so the run's pane/pty - // pointers now reference a closed tab. Drop them (best-effort) so - // "View run" resolves to the workspace/snapshot instead of dead-ending - // on an unavailable terminal. + const retireRunTerminal = async (): Promise => { + const retirement = await args.finalizeTerminalOwnership({ + // Why: only a workspace made for this run is its own; a shared one would gather a sleeping tab per run. + preservePane: args.worktree.automationProvenance?.automationRunId === args.run.id + }) + if (!retirement) { + return + } + // Why: drop pointers to the stopped process (best-effort) so "View run" never dead-ends; + // a slept pane keeps its tab and pane key, which reopen and resume it. try { await args.markDispatchResult({ runId: args.run.id, status: 'completed', - terminalSessionId: null, - terminalPaneKey: null, + ...(retirement === 'closed' ? { terminalSessionId: null, terminalPaneKey: null } : {}), terminalPtyId: null }) } catch (error) { @@ -134,9 +137,7 @@ export function createAutomationDispatchCompletion(args: { throw error } if (code === 0) { - if (args.finalizeTerminalOwnership()) { - await clearRetiredRunTerminalIdentity() - } + await retireRunTerminal() } else { args.releaseTerminalOwnership() } diff --git a/src/renderer/src/hooks/automation-dispatch-handler.ts b/src/renderer/src/hooks/automation-dispatch-handler.ts index 82ee3f65c6a..af03f844839 100644 --- a/src/renderer/src/hooks/automation-dispatch-handler.ts +++ b/src/renderer/src/hooks/automation-dispatch-handler.ts @@ -51,10 +51,10 @@ export async function handleAutomationDispatchRequest({ terminalOwnership = null ownership?.release() } - const finalizeTerminalOwnership = (): boolean => { + const finalizeTerminalOwnership: AutomationTerminalOwnership['finalize'] = async (options) => { const ownership = terminalOwnership terminalOwnership = null - return ownership?.finalize() ?? false + return (await ownership?.finalize(options)) ?? null } if (!resolved.repo) { diff --git a/src/renderer/src/hooks/automation-dispatch-unverifiable-loss.test.ts b/src/renderer/src/hooks/automation-dispatch-unverifiable-loss.test.ts index 87adc2860e8..85f74af44cf 100644 --- a/src/renderer/src/hooks/automation-dispatch-unverifiable-loss.test.ts +++ b/src/renderer/src/hooks/automation-dispatch-unverifiable-loss.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { AutomationDispatchResult } from '../../../shared/automations-types' +import type { AutomationTerminalOwnership } from '@/lib/automation-terminal-ownership' vi.mock('@/store', () => ({ useAppStore: { @@ -10,7 +11,7 @@ vi.mock('@/store', () => ({ const markDispatchResult = vi.fn<(result: AutomationDispatchResult) => Promise>() const releaseTerminalOwnership = vi.fn() -const finalizeTerminalOwnership = vi.fn(() => false) +const finalizeTerminalOwnership = vi.fn(async () => null) async function createCompletion() { const { createAutomationDispatchCompletion } = await import('./automation-dispatch-completion') @@ -37,7 +38,7 @@ describe('automation dispatch completion on an unverifiable loss', () => { beforeEach(() => { vi.clearAllMocks() markDispatchResult.mockResolvedValue(undefined) - finalizeTerminalOwnership.mockReturnValue(false) + finalizeTerminalOwnership.mockResolvedValue(null) }) it('records no result, so the run keeps its non-final dispatched status', async () => { @@ -58,7 +59,7 @@ describe('automation dispatch completion on an unverifiable loss', () => { it('still completes and finalizes a genuinely exited process', async () => { const completion = await createCompletion() - finalizeTerminalOwnership.mockReturnValue(true) + finalizeTerminalOwnership.mockResolvedValue('closed') completion.handleExit(0) await vi.waitFor(() => expect(finalizeTerminalOwnership).toHaveBeenCalledOnce()) diff --git a/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts b/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts index 2eb82ec4b8c..2a69c1708ba 100644 --- a/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts +++ b/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts @@ -422,59 +422,79 @@ describe('useAutomationDispatchEvents setup launch', () => { ) }) - it('finalizes a fresh non-reuse terminal only after completed result persistence', async () => { - const order: string[] = [] - let launchArgs: { onAgentStatus?: (payload: { state: string }) => void } = {} - mockMarkDispatchResult.mockImplementation( - async (result: { status: string; terminalPaneKey?: string | null }) => { + it.each([ + { + workspace: 'a shared workspace', + provenanceRunId: undefined, + retirement: 'closed', + cleared: { terminalSessionId: null, terminalPaneKey: null, terminalPtyId: null } + }, + { + // Why: the slept pane still holds the run's session, so only its stopped process is dropped. + workspace: 'a workspace made for this run', + provenanceRunId: 'run-1', + retirement: 'hibernated', + cleared: { terminalPtyId: null } + } + ])( + 'retires the terminal in $workspace only after completed result persistence', + async ({ provenanceRunId, retirement, cleared }) => { + const order: string[] = [] + let launchArgs: { onAgentStatus?: (payload: { state: string }) => void } = {} + mockCreateWorktree.mockResolvedValue({ + worktree: provenanceRunId + ? { ...createdWorktree, automationProvenance: { automationRunId: provenanceRunId } } + : createdWorktree + }) + mockMarkDispatchResult.mockImplementation(async (result: { status: string }) => { // The retirement clear reuses status 'completed' but nulls the terminal // identity; label it distinctly so ordering stays legible. order.push( - result.status === 'completed' && result.terminalPaneKey === null + result.status === 'completed' && Object.hasOwn(result, 'terminalPtyId') ? 'clear-terminal-identity' : `persist:${result.status}` ) - } - ) - mockFinalizeTerminalOwnership.mockImplementation(() => { - order.push('finalize') - return true - }) - mockLaunchAgentBackgroundSession.mockImplementation(async (args) => { - launchArgs = args - return { - tabId: 'agent-tab', - paneKey: 'agent-tab:7c6fb4e5-3bf1-4ff4-8259-03f7ae81c40d', - ptyId: 'agent-pty', - startupPlan: {}, - terminalOwnership: { - finalize: mockFinalizeTerminalOwnership, - release: mockReleaseTerminalOwnership + }) + mockFinalizeTerminalOwnership.mockImplementationOnce(async () => { + order.push('finalize') + return retirement + }) + mockLaunchAgentBackgroundSession.mockImplementation(async (args) => { + launchArgs = args + return { + tabId: 'agent-tab', + paneKey: 'agent-tab:7c6fb4e5-3bf1-4ff4-8259-03f7ae81c40d', + ptyId: 'agent-pty', + startupPlan: {}, + terminalOwnership: { + finalize: mockFinalizeTerminalOwnership, + release: mockReleaseTerminalOwnership + } } - } - }) + }) - await registerAndDispatch() - launchArgs.onAgentStatus?.({ state: 'done' }) - await vi.waitFor(() => expect(mockFinalizeTerminalOwnership).toHaveBeenCalledOnce()) + await registerAndDispatch() + launchArgs.onAgentStatus?.({ state: 'done' }) + await vi.waitFor(() => expect(mockFinalizeTerminalOwnership).toHaveBeenCalledOnce()) - expect(order).toEqual([ - 'persist:dispatched', - 'persist:completed', - 'finalize', - 'clear-terminal-identity' - ]) - expect(mockReleaseTerminalOwnership).not.toHaveBeenCalled() - // Why: the retired terminal is gone; the run must drop its pane/pty pointers - // so "View run" resolves to the workspace/snapshot, not an unavailable terminal. - expect(mockMarkDispatchResult).toHaveBeenLastCalledWith({ - runId: expect.any(String), - status: 'completed', - terminalSessionId: null, - terminalPaneKey: null, - terminalPtyId: null - }) - }) + expect(order).toEqual([ + 'persist:dispatched', + 'persist:completed', + 'finalize', + 'clear-terminal-identity' + ]) + expect(mockReleaseTerminalOwnership).not.toHaveBeenCalled() + expect(mockFinalizeTerminalOwnership).toHaveBeenCalledWith({ + preservePane: provenanceRunId !== undefined + }) + // Why: the retired process is gone; "View run" must not dead-end on a pointer to it. + expect(mockMarkDispatchResult).toHaveBeenLastCalledWith({ + runId: expect.any(String), + status: 'completed', + ...cleared + }) + } + ) it('ignores a session-boundary done so a connecting agent cannot complete the run (STA-3386)', async () => { let launchArgs: { diff --git a/src/renderer/src/lib/automation-terminal-ownership.test.ts b/src/renderer/src/lib/automation-terminal-ownership.test.ts index 9528bbdf384..83658228690 100644 --- a/src/renderer/src/lib/automation-terminal-ownership.test.ts +++ b/src/renderer/src/lib/automation-terminal-ownership.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { AppState } from '@/store/types' import { singlePaneLayoutSnapshot } from '@/store/slices/terminal-helpers' +import type { AgentStatusEntry } from '../../../shared/agent-status-types' import { createAutomationTerminalOwnership, type AutomationTerminalOwnershipStore @@ -22,11 +23,30 @@ type OwnershipState = Pick< | 'ptyIdsByTabId' | 'terminalLayoutsByTabId' | 'lastTerminalInputAtByPaneKey' + | 'agentStatusByPaneKey' + | 'retainedAgentsByPaneKey' + | 'sleepingAgentSessionsByPaneKey' + | 'agentLaunchConfigByPaneKey' | 'closeTab' + | 'shutdownCompletedAgentPaneForHibernation' > +// A finished Claude turn whose hook reported its conversation id, as a live run leaves it. +const RESUMABLE_DONE_ENTRY: AgentStatusEntry = { + state: 'done', + prompt: 'Reply with pineapple', + updatedAt: 300, + stateStartedAt: 300, + paneKey: PANE_KEY, + worktreeId: WORKTREE_ID, + agentType: 'claude', + providerSession: { key: 'session_id', id: 'c569e233-595e-4085-8660-5af742f81e59' }, + stateHistory: [] +} + function createStore() { const closeTab = vi.fn() + const shutdownCompletedAgentPaneForHibernation = vi.fn(async () => {}) let state: OwnershipState = { activeWorktreeId: 'other-worktree', activeTabId: 'other-tab', @@ -50,7 +70,12 @@ function createStore() { [TAB_ID]: singlePaneLayoutSnapshot(LEAF_ID, PTY_ID) }, lastTerminalInputAtByPaneKey: {}, - closeTab + agentStatusByPaneKey: { [PANE_KEY]: RESUMABLE_DONE_ENTRY }, + retainedAgentsByPaneKey: {}, + sleepingAgentSessionsByPaneKey: {}, + agentLaunchConfigByPaneKey: {}, + closeTab, + shutdownCompletedAgentPaneForHibernation } const listeners = new Set<(state: AppState, previousState: AppState) => void>() const store: AutomationTerminalOwnershipStore = { @@ -67,7 +92,13 @@ function createStore() { listener(state as unknown as AppState, previousState as unknown as AppState) } } - return { closeTab, getState: () => state, store, update } + return { + closeTab, + getState: () => state, + shutdownCompletedAgentPaneForHibernation, + store, + update + } } function own( @@ -89,7 +120,7 @@ function own( describe('automation terminal ownership', () => { beforeEach(() => vi.clearAllMocks()) - it('closes the exact fresh desktop tab once after the PTY exit binding is cleared', () => { + it('closes the exact fresh desktop tab once after the PTY exit binding is cleared', async () => { const { closeTab, store, update } = createStore() const ownership = own(store) update({ @@ -99,8 +130,8 @@ describe('automation terminal ownership', () => { } }) - expect(ownership.finalize()).toBe(true) - expect(ownership.finalize()).toBe(false) + expect(await ownership.finalize({ preservePane: false })).toBe('closed') + expect(await ownership.finalize({ preservePane: false })).toBeNull() expect(closeTab).toHaveBeenCalledTimes(1) expect(closeTab).toHaveBeenCalledWith(TAB_ID, { recordInteraction: false, @@ -108,24 +139,24 @@ describe('automation terminal ownership', () => { }) }) - it('preserves a tab activated after launch even when focus later moves away', () => { + it('preserves a tab activated after launch even when focus later moves away', async () => { const { closeTab, store, update } = createStore() const ownership = own(store) update({ activeWorktreeId: WORKTREE_ID, activeTabId: TAB_ID }) update({ activeWorktreeId: 'other-worktree', activeTabId: 'other-tab' }) - expect(ownership.finalize()).toBe(false) + expect(await ownership.finalize({ preservePane: false })).toBeNull() expect(closeTab).not.toHaveBeenCalled() }) - it('preserves a tab that received user input after launch', () => { + it('preserves a tab that received user input after launch', async () => { const { closeTab, store, update } = createStore() const ownership = own(store) update({ lastTerminalInputAtByPaneKey: { [PANE_KEY]: 200 } }) - expect(ownership.finalize()).toBe(false) + expect(await ownership.finalize({ preservePane: false })).toBeNull() expect(closeTab).not.toHaveBeenCalled() }) @@ -139,7 +170,7 @@ describe('automation terminal ownership', () => { 'pane layout', { tabsByWorktree: null, ptyIdsByTabId: undefined, layoutPty: 'pty-replacement' } ] - ])('refuses a replacement identity in the %s binding', (_label, drift) => { + ])('refuses a replacement identity in the %s binding', async (_label, drift) => { const { closeTab, getState, store, update } = createStore() const ownership = own(store) const tab = getState().tabsByWorktree[WORKTREE_ID]![0]! @@ -160,11 +191,11 @@ describe('automation terminal ownership', () => { : {}) }) - expect(ownership.finalize()).toBe(false) + expect(await ownership.finalize({ preservePane: false })).toBeNull() expect(closeTab).not.toHaveBeenCalled() }) - it('refuses a tab recreated with the same id', () => { + it('refuses a tab recreated with the same id', async () => { const { closeTab, getState, store, update } = createStore() const ownership = own(store) update({ @@ -175,28 +206,68 @@ describe('automation terminal ownership', () => { } }) - expect(ownership.finalize()).toBe(false) + expect(await ownership.finalize({ preservePane: false })).toBeNull() expect(closeTab).not.toHaveBeenCalled() }) it.each([ ['remote runtime', { runtimeKind: 'environment' as const }], ['remote PTY identity', { ptyId: 'remote:env-1@@terminal-1' }] - ])('never owns a %s terminal', (_label, overrides) => { + ])('never owns a %s terminal', async (_label, overrides) => { const { closeTab, store } = createStore() const ownership = own(store, overrides) - expect(ownership.finalize()).toBe(false) + expect(await ownership.finalize({ preservePane: false })).toBeNull() expect(closeTab).not.toHaveBeenCalled() }) - it('release consumes ownership without closing the terminal', () => { + it('sleeps a resumable pane in a workspace the run owns instead of closing it', async () => { + const { closeTab, shutdownCompletedAgentPaneForHibernation, store } = createStore() + const ownership = own(store) + + expect(await ownership.finalize({ preservePane: true })).toBe('hibernated') + expect(shutdownCompletedAgentPaneForHibernation).toHaveBeenCalledExactlyOnceWith(WORKTREE_ID, { + paneKey: PANE_KEY, + tabId: TAB_ID, + leafId: LEAF_ID, + ptyId: PTY_ID + }) + expect(closeTab).not.toHaveBeenCalled() + }) + + it.each([ + ['the agent reported no conversation id', { providerSession: undefined }, [PTY_ID]], + ['the run ended by its process exiting', {}, []] + ])('closes the tab when %s', async (_label, entryPatch, livePtyIds) => { + const { closeTab, shutdownCompletedAgentPaneForHibernation, store, update } = createStore() + const ownership = own(store) + update({ + agentStatusByPaneKey: { [PANE_KEY]: { ...RESUMABLE_DONE_ENTRY, ...entryPatch } }, + ptyIdsByTabId: { [TAB_ID]: livePtyIds } + }) + + expect(await ownership.finalize({ preservePane: true })).toBe('closed') + expect(shutdownCompletedAgentPaneForHibernation).not.toHaveBeenCalled() + expect(closeTab).toHaveBeenCalledTimes(1) + }) + + it('keeps the terminal running when its stop fails', async () => { + const { closeTab, shutdownCompletedAgentPaneForHibernation, store } = createStore() + shutdownCompletedAgentPaneForHibernation.mockRejectedValueOnce(new Error('kill failed')) + vi.spyOn(console, 'error').mockImplementation(() => {}) + const ownership = own(store) + + expect(await ownership.finalize({ preservePane: true })).toBeNull() + expect(closeTab).not.toHaveBeenCalled() + }) + + it('release consumes ownership without closing the terminal', async () => { const { closeTab, store } = createStore() const ownership = own(store) ownership.release() - expect(ownership.finalize()).toBe(false) + expect(await ownership.finalize({ preservePane: false })).toBeNull() expect(closeTab).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/lib/automation-terminal-ownership.ts b/src/renderer/src/lib/automation-terminal-ownership.ts index 07753434220..95af2610651 100644 --- a/src/renderer/src/lib/automation-terminal-ownership.ts +++ b/src/renderer/src/lib/automation-terminal-ownership.ts @@ -3,6 +3,7 @@ import { useAppStore } from '@/store' import type { AppState } from '@/store/types' import type { TerminalTab } from '../../../shared/terminal-tab-types' import { parsePaneKey } from '../../../shared/stable-pane-id' +import { collectSleepingAgentSessionRecordsForWorktree } from '@/store/slices/agent-status' import { singlePaneLayoutSnapshot } from '@/store/slices/terminal-helpers' import { readLastTerminalInputAt } from './terminal-input-activity-coalescing' @@ -11,8 +12,12 @@ export type AutomationTerminalOwnershipStore = { subscribe: (listener: (state: AppState, previousState: AppState) => void) => () => void } +/** How finalize retired the run's terminal; null when it left the terminal as it was. */ +export type AutomationTerminalRetirement = 'closed' | 'hibernated' | null + export type AutomationTerminalOwnership = { - finalize: () => boolean + /** `preservePane` sleeps a resumable pane in place instead of closing its tab. */ + finalize: (options: { preservePane: boolean }) => Promise release: () => void } @@ -70,6 +75,22 @@ function isOwnedTabIdentityCurrent( return layoutPtyId === undefined || layoutPtyId === args.ptyId } +function canHibernateOwnedPane( + state: AppState, + args: Pick +): boolean { + // Why: a run that ended by exiting has no process left to stop in place. + if (!(state.ptyIdsByTabId[args.tabId] ?? []).includes(args.ptyId)) { + return false + } + return Boolean( + collectSleepingAgentSessionRecordsForWorktree(state, args.worktreeId, { + paneKeys: [args.paneKey], + captureMode: 'completed-agent-hibernation' + })[args.paneKey] + ) +} + export function createAutomationTerminalOwnership( args: CreateAutomationTerminalOwnershipArgs ): AutomationTerminalOwnership { @@ -105,9 +126,9 @@ export function createAutomationTerminalOwnership( return { release, - finalize: () => { + finalize: async ({ preservePane }) => { if (consumed) { - return false + return null } // Why: completion and exit can race; consume before any identity check so // only the first successful-result path can retire this exact session. @@ -115,11 +136,29 @@ export function createAutomationTerminalOwnership( unsubscribe() observeTakeover() if (args.runtimeKind !== 'desktop' || isRemoteRuntimePtyId(args.ptyId) || userTookOver) { - return false + return null } const state = args.store.getState() - if (!isOwnedTabIdentityCurrent(state, args)) { - return false + const leafId = parsePaneKey(args.paneKey)?.leafId + if (!leafId || !isOwnedTabIdentityCurrent(state, args)) { + return null + } + if (preservePane && canHibernateOwnedPane(state, args)) { + try { + // Why: sleeping keeps the tab and its resume record, so opening the run resumes it. + await state.shutdownCompletedAgentPaneForHibernation(args.worktreeId, { + paneKey: args.paneKey, + tabId: args.tabId, + leafId, + ptyId: args.ptyId + }) + } catch (error) { + // Why: a failed stop rolls itself back with the agent still running; closing + // now would throw away the session the stop just kept. + console.error('[automations] Failed to hibernate owned automation terminal:', error) + return null + } + return 'hibernated' } try { // Why: closeTab centrally owns provider shutdown and pane removal; a @@ -129,9 +168,9 @@ export function createAutomationTerminalOwnership( // Why: a throwing close leaves the terminal alive; report not-closed so // the run keeps its (still-valid) terminal identity and no stale clear runs. console.error('[automations] Failed to close owned automation terminal:', error) - return false + return null } - return true + return 'closed' } } } From 6607c54984378a8b32662ec1bb08a13f8ba297e9 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:14:27 -0700 Subject: [PATCH 18/53] perf(ci): cut redundant tests and reuse expensive setup (#28669) * perf(ci): reduce test overhead and reuse expensive build work * test(ci): keep worker startup outside failure-case deadlines * test: isolate push fixture advisory locks * test(ci): preserve fixed drain retry allowance * Align CI contracts with current triggers and guide content * Stabilize consolidated reveal scenarios and retain behavioral CI contracts * Fix upstream lint blockers while retaining behavior and assertions * Classify global Jira preferences in the WSL migration inventory * Align Jira test fixtures with startup and build safety checks * Include package-test routing dependency in CI checkouts and fixtures * Keep native stream classes intact in transcript dependency tests --- .../prepare-orcad-prebuilds/action.yml | 4 +- .../prepare-wayland-clipboard/action.yml | 53 +++ .../ci-cache-warmup-platform-jobs.yml | 23 ++ .../workflows/ci-cache-warmup-platforms.yml | 6 + .github/workflows/ci-cache-warmup.yml | 8 + .github/workflows/mobile.yml | 1 + .github/workflows/node-server-tests.yml | 12 +- .github/workflows/pr.yml | 55 +-- ...urable-push-fixture-lock-isolation.test.ts | 42 ++ .../src/durable-push-store.test-fixture.ts | 35 +- ...lease-cell-row-contention-postgres.test.ts | 3 +- ...ublic-assignment-drain-return-lane.test.ts | 19 +- .../src/reserve-placement-simulation.test.ts | 7 - config/patches/i18next-cli@1.74.2.patch | 358 +++++++++++++++++- .../scripts/build-wayland-clipboard.test.mjs | 46 +++ .../scripts/check-node-runtime-pin.test.mjs | 3 +- ...ectron-builder-mac-channel-config.test.mjs | 16 - ...enerated-artifact-line-ending-pin.test.mjs | 4 + config/scripts/happy-dom-offscreen-canvas.ts | 2 +- .../scripts/locale-ko-key-overrides.test.mjs | 14 - ...calization-extraction-call-syntax.test.mjs | 43 +++ .../mobile-release-check-scope.test.mjs | 4 +- config/scripts/mobile-test-change-scope.mjs | 1 + .../scripts/mobile-test-change-scope.test.mjs | 1 + ...-web-app-browser-pane-text-inputs.test.mjs | 108 ------ .../mobile-web-app-module-closure.test.mjs | 20 - ...-web-app-session-terminal-closure.test.mjs | 18 +- ...obile-web-app-session-text-inputs.test.mjs | 76 ---- ...eb-app-terminal-live-input-render.test.mjs | 2 +- ...bile-web-app-text-input-discovery.test.mjs | 22 ++ config/scripts/node-server-change-scope.mjs | 1 + .../scripts/node-server-change-scope.test.mjs | 2 +- .../orcad-windows-prebuild-workflow.test.mjs | 5 +- ...wright-focused-workflow-selection.test.mjs | 51 +-- .../pr-auxiliary-preflight-admission.test.mjs | 2 + config/scripts/pr-code-change-scope.mjs | 85 +---- config/scripts/pr-code-change-scope.test.mjs | 8 +- config/scripts/pr-e2e-gate-contract.test.mjs | 34 +- config/scripts/pr-e2e-source-routing.mjs | 18 +- config/scripts/pr-package-test-paths.mjs | 78 ++++ .../scripts/process-host-build-dist.test.mjs | 34 +- .../shebang-script-line-ending-pin.test.mjs | 70 ---- config/scripts/vitest-dom-setup.ts | 7 + config/scripts/vitest-host-ports-setup.ts | 17 +- .../workflow-ref-mirror-case-safety.test.mjs | 33 -- .../workflow-ref-reachability.test.mjs | 2 + config/tsconfig.node.json | 3 + config/vitest.config.ts | 3 +- ...native-chat-merged-snapshot-parity.test.ts | 4 +- .../rpc-recording/golden-value-pool.ts | 5 +- pnpm-lock.yaml | 6 +- ...ser-cookie-credentials-empty-value.test.ts | 117 ------ ...e.test.ts => browser-empty-values.test.ts} | 79 ++++ src/cli/index.test.ts | 258 +------------ src/cli/specs/file.test.ts | 31 -- src/cli/specs/search.test.ts | 129 ------- src/cli/terminal-format-draft.test.ts | 21 - src/cli/terminal-format.test.ts | 20 + .../server-transport-interference.test.ts | 33 +- ...ion-search-transcript-dependencies.test.ts | 18 +- .../browser-popup-background-cdp-probe.ts | 24 +- .../daemon-pty-startup-delivery.test.ts | 31 +- .../slow-daemon-session-verification.test.ts | 30 +- .../git/repo-ref-maintenance-real-git.test.ts | 36 +- ...rktree-rebase-update-refs-real-git.test.ts | 48 +-- src/main/git/worktree-safety-real-git.test.ts | 59 +-- .../hook-termination-real-process.test.ts | 4 +- src/main/ipc/jira.ts | 8 +- .../transcript-opencode2-sqlite.test.ts | 7 +- .../profile-state-backup-worker.test.ts | 5 +- ...der-process-supervisor.integration.test.ts | 42 +- ...orca-runtime-files-artifact-facade.test.ts | 110 ++---- ...cad-state-mutation-fence-heartbeat.test.ts | 29 +- ...ChatMessageList.rail-history-jump.test.tsx | 14 +- .../onboarding/OnboardingFlow.test.tsx | 8 +- .../OnboardingInlineCommandTerminal.test.ts | 31 -- .../components/settings/GeneralPane.test.ts | 6 + .../browser-cookie-import-label.test.ts | 53 --- .../settings/osc52-clipboard-copy.test.ts | 45 --- .../settings/setting-ownership.test.ts | 39 -- .../sidebar/wsl-host-copy-catalog.test.ts | 27 -- .../tab-session-history-switch.test.ts | 5 +- ...-technical-literal-mistranslations.test.ts | 67 ---- .../ko-ui-semantic-mistranslations.test.ts | 68 ---- .../i18n/locale-english-regression.test.ts | 97 ----- .../src/i18n/native-chat-locales.test.ts | 90 ----- ...elative-duration-copy-localization.test.ts | 70 ---- ...settings-status-label-localization.test.ts | 168 -------- .../technical-literal-catalog-values.test.ts | 71 ---- ...-technical-literal-mistranslations.test.ts | 139 ------- src/shared/constants.test.ts | 148 +------- src/shared/contextual-tours.test.ts | 187 +-------- src/shared/feature-tips.test.ts | 35 -- src/shared/git-binary-compatibility.test.ts | 66 +--- src/shared/git-binary.test-fixture.ts | 114 ++++++ ...it-branch-line-total-soft-deadline.test.ts | 52 ++- src/shared/native-chat-types.test.ts | 45 --- tests/e2e/browser-tab.spec.ts | 127 ------- .../e2e-completed-onboarding-profile.ts | 3 +- tests/e2e/onboarding.spec.ts | 34 +- tests/e2e/tabs.spec.ts | 165 +------- tests/e2e/terminal-tui-wheel-drain.spec.ts | 69 ++-- .../e2e/worktree-reveal-host-sections.spec.ts | 293 +++++++------- 103 files changed, 1673 insertions(+), 3276 deletions(-) create mode 100644 .github/actions/prepare-wayland-clipboard/action.yml create mode 100644 cloud/apps/push/src/durable-push-fixture-lock-isolation.test.ts delete mode 100644 config/scripts/locale-ko-key-overrides.test.mjs delete mode 100644 config/scripts/mobile-web-app-browser-pane-text-inputs.test.mjs delete mode 100644 config/scripts/mobile-web-app-module-closure.test.mjs delete mode 100644 config/scripts/mobile-web-app-session-text-inputs.test.mjs create mode 100644 config/scripts/mobile-web-app-text-input-discovery.test.mjs create mode 100644 config/scripts/pr-package-test-paths.mjs delete mode 100644 config/scripts/shebang-script-line-ending-pin.test.mjs create mode 100644 config/scripts/vitest-dom-setup.ts delete mode 100644 src/cli/browser-cookie-credentials-empty-value.test.ts rename src/cli/{browser-storage-empty-value.test.ts => browser-empty-values.test.ts} (61%) delete mode 100644 src/cli/specs/file.test.ts delete mode 100644 src/cli/specs/search.test.ts delete mode 100644 src/cli/terminal-format-draft.test.ts delete mode 100644 src/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.test.ts delete mode 100644 src/renderer/src/components/settings/browser-cookie-import-label.test.ts delete mode 100644 src/renderer/src/components/settings/osc52-clipboard-copy.test.ts delete mode 100644 src/renderer/src/components/settings/setting-ownership.test.ts delete mode 100644 src/renderer/src/components/sidebar/wsl-host-copy-catalog.test.ts delete mode 100644 src/renderer/src/i18n/ja-technical-literal-mistranslations.test.ts delete mode 100644 src/renderer/src/i18n/ko-ui-semantic-mistranslations.test.ts delete mode 100644 src/renderer/src/i18n/locale-english-regression.test.ts delete mode 100644 src/renderer/src/i18n/native-chat-locales.test.ts delete mode 100644 src/renderer/src/i18n/relative-duration-copy-localization.test.ts delete mode 100644 src/renderer/src/i18n/settings-status-label-localization.test.ts delete mode 100644 src/renderer/src/i18n/technical-literal-catalog-values.test.ts delete mode 100644 src/renderer/src/i18n/zh-technical-literal-mistranslations.test.ts create mode 100644 src/shared/git-binary.test-fixture.ts delete mode 100644 src/shared/native-chat-types.test.ts diff --git a/.github/actions/prepare-orcad-prebuilds/action.yml b/.github/actions/prepare-orcad-prebuilds/action.yml index 635fb9d6d54..126e2b60f40 100644 --- a/.github/actions/prepare-orcad-prebuilds/action.yml +++ b/.github/actions/prepare-orcad-prebuilds/action.yml @@ -63,5 +63,5 @@ runs: fi pnpm build:orcad-prebuilds fi - pnpm build:orcad-prebuilds --require-slots "$(node config/scripts/build-orcad-prebuilds.mjs --print-slot)" - pnpm build:orcad-prebuilds --smoke + node config/scripts/build-orcad-prebuilds.mjs --require-slots "$(node config/scripts/build-orcad-prebuilds.mjs --print-slot)" + node config/scripts/build-orcad-prebuilds.mjs --smoke diff --git a/.github/actions/prepare-wayland-clipboard/action.yml b/.github/actions/prepare-wayland-clipboard/action.yml new file mode 100644 index 00000000000..78c622855bd --- /dev/null +++ b/.github/actions/prepare-wayland-clipboard/action.yml @@ -0,0 +1,53 @@ +name: Prepare Wayland clipboard helper +description: Reuse the source-matched Ubuntu 20.04 helper, validating its architecture and glibc requirements every time. + +inputs: + save-cache: + description: Publish a validated helper from the default branch. + default: 'false' + +runs: + using: composite + steps: + - name: Resolve Wayland clipboard build identity + id: identity + shell: bash + run: | + node --input-type=module <<'NODE' + import { appendFileSync } from 'node:fs' + import { waylandClipboardFingerprint } from './config/scripts/build-wayland-clipboard.mjs' + const fingerprint = waylandClipboardFingerprint(process.cwd(), process.arch) + // Immutable invalid hits need a version bump here before publishing their repair. + appendFileSync(process.env.GITHUB_OUTPUT, `key=wayland-clipboard-v1-${process.arch}-${fingerprint}\n`) + NODE + + - name: Restore Wayland clipboard helper + id: cache + uses: actions/cache/restore@v5 + continue-on-error: true + with: + path: native/wayland-clipboard/.build + key: ${{ steps.identity.outputs.key }} + + - name: Build or validate Wayland clipboard helper + shell: bash + env: + CACHE_HIT: ${{ steps.cache.outputs.cache-hit }} + run: | + if node config/scripts/build-wayland-clipboard.mjs; then + exit 0 + fi + if [ "$CACHE_HIT" != true ]; then + exit 1 + fi + echo 'Discarding an unusable Wayland clipboard cache and rebuilding from source.' + rm -rf -- native/wayland-clipboard/.build + node config/scripts/build-wayland-clipboard.mjs + + - name: Save validated Wayland clipboard helper from main + if: inputs.save-cache == 'true' && steps.cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + uses: actions/cache/save@v5 + continue-on-error: true + with: + path: native/wayland-clipboard/.build + key: ${{ steps.identity.outputs.key }} diff --git a/.github/workflows/ci-cache-warmup-platform-jobs.yml b/.github/workflows/ci-cache-warmup-platform-jobs.yml index 47d1238909d..b4a7be987d0 100644 --- a/.github/workflows/ci-cache-warmup-platform-jobs.yml +++ b/.github/workflows/ci-cache-warmup-platform-jobs.yml @@ -53,6 +53,29 @@ jobs: - name: Verify native cache is usable run: node config/scripts/ensure-native-runtime.mjs --check-only + - name: Cache Windows CLI launcher + uses: actions/cache@v5 + with: + path: | + native/windows-cli-launcher/.build + native/windows-cli-launcher/target + native/windows-agent-hook/.build + native/windows-agent-hook/target + ~/.cargo/registry + key: windows-cli-launcher-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/windows-cli-launcher/**', 'native/windows-agent-hook/**', 'config/scripts/build-windows-cli-launcher.mjs', 'config/scripts/build-windows-agent-hook.mjs', 'config/scripts/windows-rust-executable-build.mjs', 'resources/build/icon.ico', 'package.json') }} + + - name: Build the Windows native executables + shell: pwsh + run: | + if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { + rustup toolchain install stable --profile minimal + rustup default stable + } + node config/scripts/build-windows-agent-hook.mjs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + node config/scripts/build-windows-cli-launcher.mjs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + warm-linux-package-fixtures: concurrency: group: ci-cache-warmup-linux-package-fixtures-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/ci-cache-warmup-platforms.yml b/.github/workflows/ci-cache-warmup-platforms.yml index 7643747da79..8f49c2a63aa 100644 --- a/.github/workflows/ci-cache-warmup-platforms.yml +++ b/.github/workflows/ci-cache-warmup-platforms.yml @@ -34,6 +34,12 @@ on: - 'config/scripts/space-sharing-copy.mjs' - 'config/patches/**' - 'native/windows-registry/**' + - 'native/windows-cli-launcher/**' + - 'native/windows-agent-hook/**' + - 'config/scripts/build-windows-cli-launcher.mjs' + - 'config/scripts/build-windows-agent-hook.mjs' + - 'config/scripts/windows-rust-executable-build.mjs' + - 'resources/build/icon.ico' pull_request: paths: - '.github/workflows/ci-cache-warmup.yml' diff --git a/.github/workflows/ci-cache-warmup.yml b/.github/workflows/ci-cache-warmup.yml index 848cf7ef42a..b44598f7ac6 100644 --- a/.github/workflows/ci-cache-warmup.yml +++ b/.github/workflows/ci-cache-warmup.yml @@ -18,6 +18,9 @@ on: - '.github/actions/prepare-headless-compiler/**' - 'config/scripts/headless-detector-compiler-cache.mjs' - '.github/actions/prepare-linux-package-fixture/**' + - '.github/actions/prepare-wayland-clipboard/**' + - 'native/wayland-clipboard/**' + - 'config/scripts/build-wayland-clipboard.mjs' - 'config/docker/headless-serve-shutdown/**' - 'config/docker/cli-launch-contract/**' - 'config/docker/daemon-shutdown-descendants/**' @@ -48,6 +51,7 @@ on: - '.github/workflows/ci-cache-warmup-platforms.yml' - '.github/actions/prepare-headless-compiler/**' - 'config/scripts/headless-detector-compiler-cache.mjs' + - '.github/actions/prepare-wayland-clipboard/**' permissions: contents: read @@ -83,6 +87,10 @@ jobs: - uses: ./.github/actions/prepare-git-compatibility + - uses: ./.github/actions/prepare-wayland-clipboard + with: + save-cache: 'true' + warm-platforms: if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' uses: ./.github/workflows/ci-cache-warmup-platform-jobs.yml diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index f0edb9c52ee..f6265737195 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -36,6 +36,7 @@ on: - 'config/scripts/mobile-release-check-scope*' - 'config/scripts/mobile-test-change-scope*' - 'config/scripts/pr-code-change-scope.mjs' + - 'config/scripts/pr-package-test-paths.mjs' - 'config/scripts/pr-layout-oracle-route.mjs' # Why main too: two pull requests can each pass against their own base and disagree once both # land, and `verify` never runs on main. The same source paths as above, less the CI inputs only diff --git a/.github/workflows/node-server-tests.yml b/.github/workflows/node-server-tests.yml index 54d8b40e16b..457382b1b91 100644 --- a/.github/workflows/node-server-tests.yml +++ b/.github/workflows/node-server-tests.yml @@ -27,7 +27,7 @@ on: description: Also merge every lane's slot into the desktop orcad template artifact type: boolean default: false - # Release packaging calls this to build the orcad template it ships (design D2). + # Release and dev builds qualify every platform for the template they ship. workflow_call: inputs: ref: @@ -38,11 +38,14 @@ on: description: Upload each lane's release slot and merge them into the orcad-template artifact type: boolean default: false + # Requalify fresh builds daily; publish missing immutable Windows cache keys. + schedule: + - cron: '30 11 * * *' permissions: contents: read -# Main pushes must finish detection before they can supersede relevant qualification. +# Keep template callers independent while replacing superseded PR qualification. concurrency: group: node-server-${{ (inputs.build_template || github.event_name == 'push') && format('run-{0}', github.run_id) || github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ !inputs.build_template && github.event_name != 'push' }} @@ -117,9 +120,8 @@ jobs: concurrency: group: node-server-persistence-${{ matrix.os }}-${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' && github.ref || github.run_id }} cancel-in-progress: ${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' }} - # Missing/failed detection runs the full matrix; manual runs remain unconditional. - # A draft carries no platform verdict; readiness re-triggers this workflow. Spelled against - # the event name so the push and schedule paths do not rest on a null property comparison. + # Missing/failed detection runs the full matrix; scheduled and manual runs stay unconditional. + # Drafts re-trigger at readiness; inherited caller events must not depend on PR fields. if: >- ${{ !cancelled() && needs.changes.outputs.should_run != 'false' && (github.event_name != 'pull_request' || github.event.pull_request.draft != true) }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ca5c9079cce..894469dadb9 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -79,6 +79,7 @@ jobs: /config/.bun-version /config/scripts/check-readme-local-links.mjs /config/scripts/pr-code-change-scope.mjs + /config/scripts/pr-package-test-paths.mjs /config/scripts/pr-layout-oracle-route.mjs /config/scripts/pr-e2e-source-routing.mjs /config/scripts/ci-e2e-job-selection.mjs @@ -219,6 +220,7 @@ jobs: with: bun-version-file: config/.bun-version + # Run gate scripts directly to avoid repeated package-manager startup. - name: Check process-host imports if: '!cancelled()' id: process-host-imports @@ -239,7 +241,7 @@ jobs: PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - bun --bun run check:main-sync-spawn-ratchet + bun config/scripts/check-main-sync-spawn-ratchet.mjs # Keep each check in its own log while sharing this runner. - name: Lint @@ -251,7 +253,7 @@ jobs: PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - pnpm exec oxlint --format github + node node_modules/oxlint/bin/oxlint --format github - name: Reject low-evidence patterns if: needs.code_paths.outputs.static_analysis == 'true' @@ -270,11 +272,11 @@ jobs: PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - pnpm run audit:code-quality:native + node node_modules/oxlint/bin/oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings - name: Enforce type-aware code-quality baseline if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run audit:code-quality:type-aware + run: node node_modules/oxlint/bin/oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests --deny-warnings # Mobile installation changes import resolution for the native cycle check. - wait: native-code-quality @@ -294,11 +296,11 @@ jobs: PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - pnpm run check:code-quality:changed -- "${{ github.event.pull_request.base.sha }}" + node config/scripts/check-changed-code-quality.mjs "${{ github.event.pull_request.base.sha }}" - name: Enforce React Doctor on changed lines if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:react-doctor:changed -- "${{ github.event.pull_request.base.sha }}" + run: node config/scripts/check-react-doctor-changed.mjs "${{ github.event.pull_request.base.sha }}" - wait: changed-code-quality @@ -316,15 +318,15 @@ jobs: - name: Check Zustand selector fan-out budget if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:zustand-selector-fanout + run: node config/scripts/zustand-selector-fanout-benchmark.mjs --check - name: Check reliability gate manifest if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:reliability-gates + run: node config/scripts/check-reliability-gates.mjs - name: Enforce dead design-system classes if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:dead-classes + run: node node_modules/oxlint/bin/oxlint --config config/oxlint-dead-classes.json src/renderer - name: Check VM runtime rollback compatibility if: needs.code_paths.outputs.static_analysis == 'true' @@ -350,19 +352,19 @@ jobs: - name: Enforce max-lines ratchet if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:max-lines-ratchet + run: node config/scripts/check-max-lines-ratchet.mjs - name: Enforce ts-nocheck ratchet if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:ts-nocheck-ratchet + run: node config/scripts/check-ts-nocheck-ratchet.mjs - name: Enforce runtime Electron-import ratchet if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:runtime-electron-ratchet + run: node config/scripts/check-runtime-electron-ratchet.mjs - name: Enforce focus-read ratchet if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:focus-read-ratchet + run: node config/scripts/check-focus-read-ratchet.mjs - name: Enforce layout-writer ratchets if: needs.code_paths.outputs.static_analysis == 'true' @@ -370,7 +372,7 @@ jobs: - name: Check Node runtime pin if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:node-runtime-pin + run: node config/scripts/check-node-runtime-pin.mjs # Why: extraction writes sorted evidence to an isolated temporary path, # so feature PRs need one normalized AST pass rather than a three-OS matrix. @@ -414,19 +416,19 @@ jobs: - name: Verify the generated RPC params catalog if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run verify:rpc-params-catalog + run: node config/scripts/generate-rpc-params-catalog.mjs --check - name: Verify the generated ACP protocol schema if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run verify:acp-protocol + run: node config/scripts/acp/generate-protocol.mjs --check - name: Verify bundled skill guides if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run verify:bundled-skill-guides + run: node config/scripts/generate-bundled-skill-guides.mjs --check - name: Verify skill freshness manifest if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run verify:skill-bundle-manifest + run: node config/scripts/generate-skill-bundle-manifest.mjs - name: Verify localization catalogs if: '!cancelled()' @@ -437,7 +439,7 @@ jobs: PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - pnpm run verify:localization-catalogs + bun config/scripts/verify-localization-catalogs.mjs - name: Verify localization coverage if: needs.code_paths.outputs.static_analysis == 'true' @@ -465,11 +467,11 @@ jobs: - name: Check feature wall asset budget if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm check:feature-wall-assets + run: node config/scripts/check-feature-wall-assets.mjs - name: Verify macOS entitlements if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm verify:macos-entitlements + run: node config/scripts/verify-macos-entitlements.mjs # Planning shares setup and stays off the compiler's critical path. - name: Plan unit selection @@ -1103,6 +1105,12 @@ jobs: src/main/browser/browser-popup-native-compatibility.electron.test.ts src/main/browser/browser-popup-background-compatibility.electron.test.ts + # The isolated glibc-floor build writes only the Wayland helper, so JavaScript can overlap. + - name: Build native components + id: linux-native + background: true + uses: ./.github/actions/prepare-wayland-clipboard + # Restore independent fixtures during builds; keep native lifecycle probes serial. - uses: ./.github/actions/prepare-linux-package-fixture id: shutdown-fixture-cache @@ -1157,10 +1165,7 @@ jobs: - name: Build mobile web bundle run: pnpm run build:mobile-web - - name: Build native components - run: pnpm run build:native - - - wait: [linux-package-tools, web-client] + - wait: [linux-native, linux-package-tools, web-client] - name: Package unpacked app env: diff --git a/cloud/apps/push/src/durable-push-fixture-lock-isolation.test.ts b/cloud/apps/push/src/durable-push-fixture-lock-isolation.test.ts new file mode 100644 index 00000000000..be3a10802c4 --- /dev/null +++ b/cloud/apps/push/src/durable-push-fixture-lock-isolation.test.ts @@ -0,0 +1,42 @@ +import { afterEach, expect, it } from 'vitest' +import { DurablePushStore } from './durable-push-store.js' +import { + cleanupDurablePushFixtures, + DEVICE_HEAD_SQL, + durablePushTestDatabaseUrl, + fixture, + notification, + pauseAfter, + within +} from './durable-push-store.test-fixture.js' + +afterEach(cleanupDurablePushFixtures) + +it.skipIf(!durablePushTestDatabaseUrl)( + 'claims the same device in another schema while a fixture holds its device lock', + async () => { + const first = await fixture() + const second = await fixture() + const [firstScope] = await first.db.query( + 'SELECT current_database() AS database, current_schema() AS schema' + ) + const [secondScope] = await second.db.query( + 'SELECT current_database() AS database, current_schema() AS schema' + ) + expect(firstScope?.database).toBe(secondScope?.database) + expect(firstScope?.schema).not.toBe(secondScope?.schema) + await first.store.accept('host', 'phone', notification(1)) + await second.store.accept('host', 'phone', notification(2)) + const paused = pauseAfter(first.db, DEVICE_HEAD_SQL) + const held = new DurablePushStore(paused.wrapped, first.clock).claim() + await paused.atCandidate + try { + expect(await within(first.store.claim(), 2_000)).toBeNull() + expect((await within(second.store.claim(), 2_000))?.notification.notificationSeq).toBe(2) + } finally { + paused.release() + await held + } + expect((await held)?.notification.notificationSeq).toBe(1) + } +) diff --git a/cloud/apps/push/src/durable-push-store.test-fixture.ts b/cloud/apps/push/src/durable-push-store.test-fixture.ts index 417931878d1..d4fedd46074 100644 --- a/cloud/apps/push/src/durable-push-store.test-fixture.ts +++ b/cloud/apps/push/src/durable-push-store.test-fixture.ts @@ -11,7 +11,10 @@ const cleanups: (() => Promise)[] = [] export async function cleanupDurablePushFixtures(): Promise { await Promise.all(cleanups.splice(0).map((cleanup) => cleanup())) } -export const notification = (seq: number, kind: 'alert' | 'dismiss' = 'alert'): PushNotification => ({ +export const notification = ( + seq: number, + kind: 'alert' | 'dismiss' = 'alert' +): PushNotification => ({ notificationId: `notification-${seq}`, notificationEpoch: 'epoch', notificationSeq: seq, @@ -21,11 +24,26 @@ export const notification = (seq: number, kind: 'alert' | 'dismiss' = 'alert'): body: '', kind }) -// ownDatabase: advisory locks are database-wide, so a test that holds a global key must not share one. + +// Schemas isolate rows, but advisory locks need the same fixture boundary. +function scopeFixtureLocks(database: PushDatabase, scope: string): PushDatabase { + return { + dialect: database.dialect, + query: (sql, params) => database.query(sql, params), + transaction: (run) => database.transaction((tx) => run(scopeFixtureLocks(tx, scope))), + lockQuotaScope: (key) => database.lockQuotaScope(`${scope}:${key}`), + tryLockScope: (key) => database.tryLockScope(`${scope}:${key}`), + tryLockSharedScope: (key) => database.tryLockSharedScope(`${scope}:${key}`), + close: () => database.close() + } +} + +// ownDatabase keeps production lock keys for mixed-revision tests. export async function fixture({ ownDatabase = false } = {}) { const databaseUrl = durablePushTestDatabaseUrl - if (databaseUrl && !process.env.CI && new URL(databaseUrl).port !== '55440') + if (databaseUrl && !process.env.CI && new URL(databaseUrl).port !== '55440') { throw new Error('isolated_postgres_port_required') + } let db: PushDatabase if (databaseUrl) { const admin = new pg.Client({ connectionString: databaseUrl }) @@ -38,7 +56,9 @@ export async function fixture({ ownDatabase = false } = {}) { } finally { try { await admin.query( - ownDatabase ? `DROP DATABASE IF EXISTS ${name}` : `DROP SCHEMA IF EXISTS ${name} CASCADE` + ownDatabase + ? `DROP DATABASE IF EXISTS ${name}` + : `DROP SCHEMA IF EXISTS ${name} CASCADE` ) } finally { await admin.end() @@ -53,7 +73,8 @@ export async function fixture({ ownDatabase = false } = {}) { await admin.query(`CREATE SCHEMA ${name}`) url.searchParams.set('options', `-c search_path=${name}`) } - db = scoped = await openPushDatabase({ databaseUrl: url.toString(), dataDir: '', poolMax: 4 }) + scoped = await openPushDatabase({ databaseUrl: url.toString(), dataDir: '', poolMax: 4 }) + db = ownDatabase ? scoped : scopeFixtureLocks(scoped, name) } else { db = await openInMemoryPushDatabase() cleanups.push(() => db.close()) @@ -117,7 +138,9 @@ export async function within(operation: Promise, ms: number): Promise { try { return await Promise.race([ operation, - new Promise((_, reject) => (timer = setTimeout(() => reject(new Error('claim_blocked')), ms))) + new Promise( + (_, reject) => (timer = setTimeout(() => reject(new Error('claim_blocked')), ms)) + ) ]) } finally { clearTimeout(timer) diff --git a/cloud/apps/relay/src/drain-release-cell-row-contention-postgres.test.ts b/cloud/apps/relay/src/drain-release-cell-row-contention-postgres.test.ts index a484fc81246..6e2b0974f16 100644 --- a/cloud/apps/relay/src/drain-release-cell-row-contention-postgres.test.ts +++ b/cloud/apps/relay/src/drain-release-cell-row-contention-postgres.test.ts @@ -32,7 +32,8 @@ const describePostgres = databaseUrl ? describe : describe.skip // asia-east2 cell to the us-central1 database, measured in production. const ASIA_ROUND_TRIP_MS = 171 const LOCAL_ROUND_TRIP_MS = 1 -const WINDOW_MS = 10_000 +// Keep the measured contention rates; repeating them for longer only repeats the same locks. +const WINDOW_MS = 5_000 // Reconnecting hosts dialling the directors during the window. Above the // ~6-15/s the production tail sustained, so the director side is never idle. const DIAL_RATE_PER_SECOND = 20 diff --git a/cloud/apps/relay/src/public-assignment-drain-return-lane.test.ts b/cloud/apps/relay/src/public-assignment-drain-return-lane.test.ts index cd1344dc5b6..361dcad7575 100644 --- a/cloud/apps/relay/src/public-assignment-drain-return-lane.test.ts +++ b/cloud/apps/relay/src/public-assignment-drain-return-lane.test.ts @@ -233,17 +233,17 @@ describe('drain-return lane', () => { expect(redial.headers.get('retry-after')).toBe('2') }) - it('admits a 2,500-host drain across five directors without a sticky or placement rejection', async () => { + it('admits a paced drain across five directors without a sticky or placement rejection', async () => { vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'Date'] }) vi.spyOn(console, 'warn').mockImplementation(() => {}) // Five directors behind a load balancer that sends each request to any of - // them. 2,500 drained hosts over the 300 s pace window, beside ordinary - // reconnects (c29 baseline ~13/s) and a trickle of new placements. + // them. A shorter cohort preserves the production arrival rates: ~8 drained + // hosts/s, ~13 ordinary reconnects/s, and a trickle of new placements. const DIRECTORS = 5 - const DRAINED = 2_500 - const PACE_WINDOW_MS = 300_000 - const ORDINARY = 3_900 - const PLACEMENTS = 30 + const DRAINED = 250 + const PACE_WINDOW_MS = 30_000 + const ORDINARY = 390 + const PLACEMENTS = 3 // Measured c29 2026-10-01: ~5.8 re-placements/s fleet-wide, bound by the // region's inventory lock, so ~172 ms of lock per re-placement. const INVENTORY_LOCK_MS = 172 @@ -348,7 +348,7 @@ describe('drain-return lane', () => { } let elapsedMs = 0 - while (inFlight > 0 && elapsedMs < 1_200_000) { + while (inFlight > 0 && elapsedMs < 120_000) { await vi.advanceTimersByTimeAsync(20) elapsedMs += 20 // Signing the lease completes off the timer queue. @@ -363,10 +363,11 @@ describe('drain-return lane', () => { expect(outcomes.get('sticky')).toBe(ORDINARY) expect(outcomes.get('placement')).toBe(PLACEMENTS) expect(outcomes.get('drain-return')).toBe(DRAINED) + expect(outcomes.get('drain-return-deferred') ?? 0).toBeGreaterThan(0) // Paced returns: fewer than two deferrals per drained host, none past the cap. expect(outcomes.get('drain-return-deferred') ?? 0).toBeLessThan(2 * DRAINED) expect(Math.max(...retryAfters)).toBeLessThanOrEqual(300) - // The cohort finishes at the inventory lock's rate, not minutes behind it. + // Retry rounding and off-thread signing keep their original fixed allowance. expect(elapsedMs).toBeLessThan(DRAINED * INVENTORY_LOCK_MS + 60_000) }, 300_000) }) diff --git a/cloud/apps/relay/src/reserve-placement-simulation.test.ts b/cloud/apps/relay/src/reserve-placement-simulation.test.ts index 44f2cafa24e..13bd867b93c 100644 --- a/cloud/apps/relay/src/reserve-placement-simulation.test.ts +++ b/cloud/apps/relay/src/reserve-placement-simulation.test.ts @@ -317,13 +317,6 @@ describe('step 5 deterministic simulation', () => { // A director whose map has not seen the leave yet may re-place level with the row; the // WRONG_CELL row read keeps that a small share (the rest are repaired by one more reconcile). expect(report.badFirstReseats / report.rowAheadDemotions).toBeLessThan(0.1) - const withoutRead = await runReservePlacementSimulation({ ...config, faults: { noWrongCellRowRead: true } }) - process.stderr.write( - `row_ahead_repair ${JSON.stringify({ - withRead: { demotions: report.rowAheadDemotions, badFirstReseats: report.badFirstReseats, perHost: report.rowAheadDemotionsPerHost }, - withoutRead: { demotions: withoutRead.rowAheadDemotions, badFirstReseats: withoutRead.badFirstReseats, perHost: withoutRead.rowAheadDemotionsPerHost } - })}\n` - ) }, 240_000) // Every director misses its feed polls for 150 s: each reserve cell's dead-man trips (60-120 s) diff --git a/config/patches/i18next-cli@1.74.2.patch b/config/patches/i18next-cli@1.74.2.patch index e4eb976d1ec..307096dd25c 100644 --- a/config/patches/i18next-cli@1.74.2.patch +++ b/config/patches/i18next-cli@1.74.2.patch @@ -1,5 +1,5 @@ diff --git a/dist/cjs/extractor/core/extractor.js b/dist/cjs/extractor/core/extractor.js -index 6d61ade2471c20bf1a253e3784bfadcb01440545..c1b001e78a215f89a8d4da1756a87562a5a6e723 100644 +index 6d61ade2471..23ad9f5ba49 100644 --- a/dist/cjs/extractor/core/extractor.js +++ b/dist/cjs/extractor/core/extractor.js @@ -143,6 +143,8 @@ const extractionSiteRegexes = new WeakMap(); @@ -20,8 +20,185 @@ index 6d61ade2471c20bf1a253e3784bfadcb01440545..c1b001e78a215f89a8d4da1756a87562 extractionSiteRegexes.set(config, re); } return re.test(code); +@@ -185,13 +187,15 @@ function mayContainExtractionSite(code, config) { + * + * @internal + */ +-async function processFile(file, plugins, astVisitors, pluginContext, config, logger$1 = new logger.ConsoleLogger(), fileErrors) { ++async function processFile(file, plugins, astVisitors, pluginContext, config, logger$1 = new logger.ConsoleLogger(), fileErrors, parsedFiles) { + try { + // Skip directories that happen to match file-extension globs (e.g. a directory named "Foo.tsx") + const fileStat = await promises.stat(file); + if (fileStat.isDirectory()) + return; +- let code = await promises.readFile(file, 'utf-8'); ++ const cached = parsedFiles?.get(file); ++ parsedFiles?.delete(file); ++ let code = cached?.code ?? await promises.readFile(file, 'utf-8'); + // Run onLoad hooks from plugins with error handling. + // Track whether any plugin actually transformed the code so we can make + // the right parse decision for non-native file types (e.g. .svelte, .vue). +@@ -241,59 +245,61 @@ async function processFile(file, plugins, astVisitors, pluginContext, config, lo + const isTypeScriptFile = effectiveExt === '.ts' || effectiveExt === '.tsx' || effectiveExt === '.mts' || effectiveExt === '.cts'; + const isTSX = effectiveExt === '.tsx'; + const isJSX = effectiveExt === '.jsx'; +- let ast; +- try { +- ast = await core.parse(code, { +- syntax: isTypeScriptFile ? 'typescript' : 'ecmascript', +- tsx: isTSX, +- jsx: isJSX, +- decorators: true, +- dynamicImport: true, +- comments: true, +- }); +- } +- catch (err) { +- // Fallback for .ts files with JSX (already present) +- if (effectiveExt === '.ts' && !isTSX) { +- try { +- ast = await core.parse(code, { +- syntax: 'typescript', +- tsx: true, +- decorators: true, +- dynamicImport: true, +- comments: true, +- }); +- logger$1.info?.(`Parsed ${file} using TSX fallback`); +- } +- catch (err2) { +- throw new validation.ExtractorError('Failed to process file', file, err2); +- } +- // Fallback for .js files with JSX ++ let ast = cached?.ast; ++ if (!cached) { ++ try { ++ ast = await core.parse(code, { ++ syntax: isTypeScriptFile ? 'typescript' : 'ecmascript', ++ tsx: isTSX, ++ jsx: isJSX, ++ decorators: true, ++ dynamicImport: true, ++ comments: true, ++ }); + } +- else if (effectiveExt === '.js' && !isJSX) { +- try { +- ast = await core.parse(code, { +- syntax: 'ecmascript', +- jsx: true, +- decorators: true, +- dynamicImport: true, +- comments: true, +- }); +- logger$1.info?.(`Parsed ${file} using JSX fallback`); ++ catch (err) { ++ // Fallback for .ts files with JSX (already present) ++ if (effectiveExt === '.ts' && !isTSX) { ++ try { ++ ast = await core.parse(code, { ++ syntax: 'typescript', ++ tsx: true, ++ decorators: true, ++ dynamicImport: true, ++ comments: true, ++ }); ++ logger$1.info?.(`Parsed ${file} using TSX fallback`); ++ } ++ catch (err2) { ++ throw new validation.ExtractorError('Failed to process file', file, err2); ++ } ++ // Fallback for .js files with JSX + } +- catch (err2) { +- throw new validation.ExtractorError('Failed to process file', file, err2); ++ else if (effectiveExt === '.js' && !isJSX) { ++ try { ++ ast = await core.parse(code, { ++ syntax: 'ecmascript', ++ jsx: true, ++ decorators: true, ++ dynamicImport: true, ++ comments: true, ++ }); ++ logger$1.info?.(`Parsed ${file} using JSX fallback`); ++ } ++ catch (err2) { ++ throw new validation.ExtractorError('Failed to process file', file, err2); ++ } ++ } ++ else { ++ throw new validation.ExtractorError('Failed to process file', file, err); + } + } +- else { +- throw new validation.ExtractorError('Failed to process file', file, err); +- } ++ // Normalize SWC span offsets so every span is a file-relative (0-based) ++ // character index. SWC accumulates byte offsets across successive parse() ++ // calls and Module.span.start points to the first token, NOT to byte 0 of ++ // the source. ++ astUtils.normalizeSpansToCharIndices(ast, code); + } +- // Normalize SWC span offsets so every span is a file-relative (0-based) +- // character index. SWC accumulates byte offsets across successive parse() +- // calls and Module.span.start points to the first token, NOT to byte 0 of +- // the source. +- astUtils.normalizeSpansToCharIndices(ast, code); + // "Wire up" the visitor's scope method to the context. + // This avoids a circular dependency while giving plugins access to the scope. + pluginContext.getVarFromScope = astVisitors.getVarFromScope.bind(astVisitors); +@@ -344,7 +350,7 @@ async function processFile(file, plugins, astVisitors, pluginContext, config, lo + * @param logger - Logger for warnings/errors + * @param fileErrors - Optional array to collect per-file error messages + */ +-async function preScanFile(file, astVisitors, config, logger$1 = new logger.ConsoleLogger(), fileErrors) { ++async function preScanFile(file, astVisitors, config, logger$1 = new logger.ConsoleLogger(), fileErrors, parsedFiles) { + try { + // Skip directories that happen to match file-extension globs (e.g. a directory named "Foo.tsx") + const fileStat = await promises.stat(file); +@@ -396,6 +402,8 @@ async function preScanFile(file, astVisitors, config, logger$1 = new logger.Cons + astUtils.normalizeSpansToCharIndices(ast, code); + astVisitors.setCurrentFile(file, code); + astVisitors.preScanForConstants(ast); ++ if (parsedFiles && mayContainExtractionSite(code, config)) ++ parsedFiles.set(file, { code, ast }); + } + catch (error) { + if (error instanceof pluginManager.ConflictError) +diff --git a/dist/cjs/extractor/core/key-finder.js b/dist/cjs/extractor/core/key-finder.js +index 1411f287157..200b6b974b6 100644 +--- a/dist/cjs/extractor/core/key-finder.js ++++ b/dist/cjs/extractor/core/key-finder.js +@@ -40,6 +40,8 @@ async function findKeys(config, logger$1 = new logger.ConsoleLogger(), fileError + const plugins = pluginsOrUndefined || []; + const sourceFiles = await processSourceFiles(config); + const allKeys = new Map(); ++ // Retain only translation-site trees; plugins can transform code between passes. ++ const parsedFiles = plugins.length === 0 ? new Map() : undefined; + // 1. Create the base context with config and logger. + const pluginContext = pluginManager.createPluginContext(allKeys, plugins, otherConfig, logger$1); + // 2. Create hooks for plugins to hook into AST +@@ -92,7 +94,7 @@ async function findKeys(config, logger$1 = new logger.ConsoleLogger(), fileError + // correctly even when the defining constants file is processed after the file that + // uses the identifier. + for (const file of sourceFiles) { +- await extractor.preScanFile(file, astVisitors$1, otherConfig, logger$1, fileErrors); ++ await extractor.preScanFile(file, astVisitors$1, otherConfig, logger$1, fileErrors, parsedFiles); + } + // 6b. Mirror declarations onto the local names they were imported as, so that + // `import { ResourceStatus as Status }` resolves like the unaliased import. +@@ -100,7 +102,7 @@ async function findKeys(config, logger$1 = new logger.ConsoleLogger(), fileError + // 7. Extraction pass: all shared tables are now fully populated, so every + // identifier reference can be resolved regardless of file order. + for (const file of sourceFiles) { +- await extractor.processFile(file, plugins, astVisitors$1, pluginContext, otherConfig, logger$1, fileErrors); ++ await extractor.processFile(file, plugins, astVisitors$1, pluginContext, otherConfig, logger$1, fileErrors, parsedFiles); + } + // 7. Run onEnd hooks + for (const plugin of plugins) { diff --git a/dist/esm/extractor/core/extractor.js b/dist/esm/extractor/core/extractor.js -index 910bdb6e25e26c01ea6127191f1a00c7089693c1..00da4a6a7acb6c3da8d4b6fe7be106c409542f8d 100644 +index 910bdb6e25e..a3868d29950 100644 --- a/dist/esm/extractor/core/extractor.js +++ b/dist/esm/extractor/core/extractor.js @@ -141,6 +141,8 @@ const extractionSiteRegexes = new WeakMap(); @@ -42,3 +219,180 @@ index 910bdb6e25e26c01ea6127191f1a00c7089693c1..00da4a6a7acb6c3da8d4b6fe7be106c4 extractionSiteRegexes.set(config, re); } return re.test(code); +@@ -183,13 +185,15 @@ function mayContainExtractionSite(code, config) { + * + * @internal + */ +-async function processFile(file, plugins, astVisitors, pluginContext, config, logger = new ConsoleLogger(), fileErrors) { ++async function processFile(file, plugins, astVisitors, pluginContext, config, logger = new ConsoleLogger(), fileErrors, parsedFiles) { + try { + // Skip directories that happen to match file-extension globs (e.g. a directory named "Foo.tsx") + const fileStat = await stat(file); + if (fileStat.isDirectory()) + return; +- let code = await readFile(file, 'utf-8'); ++ const cached = parsedFiles?.get(file); ++ parsedFiles?.delete(file); ++ let code = cached?.code ?? await readFile(file, 'utf-8'); + // Run onLoad hooks from plugins with error handling. + // Track whether any plugin actually transformed the code so we can make + // the right parse decision for non-native file types (e.g. .svelte, .vue). +@@ -239,59 +243,61 @@ async function processFile(file, plugins, astVisitors, pluginContext, config, lo + const isTypeScriptFile = effectiveExt === '.ts' || effectiveExt === '.tsx' || effectiveExt === '.mts' || effectiveExt === '.cts'; + const isTSX = effectiveExt === '.tsx'; + const isJSX = effectiveExt === '.jsx'; +- let ast; +- try { +- ast = await parse(code, { +- syntax: isTypeScriptFile ? 'typescript' : 'ecmascript', +- tsx: isTSX, +- jsx: isJSX, +- decorators: true, +- dynamicImport: true, +- comments: true, +- }); +- } +- catch (err) { +- // Fallback for .ts files with JSX (already present) +- if (effectiveExt === '.ts' && !isTSX) { +- try { +- ast = await parse(code, { +- syntax: 'typescript', +- tsx: true, +- decorators: true, +- dynamicImport: true, +- comments: true, +- }); +- logger.info?.(`Parsed ${file} using TSX fallback`); +- } +- catch (err2) { +- throw new ExtractorError('Failed to process file', file, err2); +- } +- // Fallback for .js files with JSX ++ let ast = cached?.ast; ++ if (!cached) { ++ try { ++ ast = await parse(code, { ++ syntax: isTypeScriptFile ? 'typescript' : 'ecmascript', ++ tsx: isTSX, ++ jsx: isJSX, ++ decorators: true, ++ dynamicImport: true, ++ comments: true, ++ }); + } +- else if (effectiveExt === '.js' && !isJSX) { +- try { +- ast = await parse(code, { +- syntax: 'ecmascript', +- jsx: true, +- decorators: true, +- dynamicImport: true, +- comments: true, +- }); +- logger.info?.(`Parsed ${file} using JSX fallback`); ++ catch (err) { ++ // Fallback for .ts files with JSX (already present) ++ if (effectiveExt === '.ts' && !isTSX) { ++ try { ++ ast = await parse(code, { ++ syntax: 'typescript', ++ tsx: true, ++ decorators: true, ++ dynamicImport: true, ++ comments: true, ++ }); ++ logger.info?.(`Parsed ${file} using TSX fallback`); ++ } ++ catch (err2) { ++ throw new ExtractorError('Failed to process file', file, err2); ++ } ++ // Fallback for .js files with JSX + } +- catch (err2) { +- throw new ExtractorError('Failed to process file', file, err2); ++ else if (effectiveExt === '.js' && !isJSX) { ++ try { ++ ast = await parse(code, { ++ syntax: 'ecmascript', ++ jsx: true, ++ decorators: true, ++ dynamicImport: true, ++ comments: true, ++ }); ++ logger.info?.(`Parsed ${file} using JSX fallback`); ++ } ++ catch (err2) { ++ throw new ExtractorError('Failed to process file', file, err2); ++ } ++ } ++ else { ++ throw new ExtractorError('Failed to process file', file, err); + } + } +- else { +- throw new ExtractorError('Failed to process file', file, err); +- } ++ // Normalize SWC span offsets so every span is a file-relative (0-based) ++ // character index. SWC accumulates byte offsets across successive parse() ++ // calls and Module.span.start points to the first token, NOT to byte 0 of ++ // the source. ++ normalizeSpansToCharIndices(ast, code); + } +- // Normalize SWC span offsets so every span is a file-relative (0-based) +- // character index. SWC accumulates byte offsets across successive parse() +- // calls and Module.span.start points to the first token, NOT to byte 0 of +- // the source. +- normalizeSpansToCharIndices(ast, code); + // "Wire up" the visitor's scope method to the context. + // This avoids a circular dependency while giving plugins access to the scope. + pluginContext.getVarFromScope = astVisitors.getVarFromScope.bind(astVisitors); +@@ -342,7 +348,7 @@ async function processFile(file, plugins, astVisitors, pluginContext, config, lo + * @param logger - Logger for warnings/errors + * @param fileErrors - Optional array to collect per-file error messages + */ +-async function preScanFile(file, astVisitors, config, logger = new ConsoleLogger(), fileErrors) { ++async function preScanFile(file, astVisitors, config, logger = new ConsoleLogger(), fileErrors, parsedFiles) { + try { + // Skip directories that happen to match file-extension globs (e.g. a directory named "Foo.tsx") + const fileStat = await stat(file); +@@ -394,6 +400,8 @@ async function preScanFile(file, astVisitors, config, logger = new ConsoleLogger + normalizeSpansToCharIndices(ast, code); + astVisitors.setCurrentFile(file, code); + astVisitors.preScanForConstants(ast); ++ if (parsedFiles && mayContainExtractionSite(code, config)) ++ parsedFiles.set(file, { code, ast }); + } + catch (error) { + if (error instanceof ConflictError) +diff --git a/dist/esm/extractor/core/key-finder.js b/dist/esm/extractor/core/key-finder.js +index 86612731b2d..4e724e84538 100644 +--- a/dist/esm/extractor/core/key-finder.js ++++ b/dist/esm/extractor/core/key-finder.js +@@ -38,6 +38,8 @@ async function findKeys(config, logger = new ConsoleLogger(), fileErrors) { + const plugins = pluginsOrUndefined || []; + const sourceFiles = await processSourceFiles(config); + const allKeys = new Map(); ++ // Retain only translation-site trees; plugins can transform code between passes. ++ const parsedFiles = plugins.length === 0 ? new Map() : undefined; + // 1. Create the base context with config and logger. + const pluginContext = createPluginContext(allKeys, plugins, otherConfig, logger); + // 2. Create hooks for plugins to hook into AST +@@ -90,7 +92,7 @@ async function findKeys(config, logger = new ConsoleLogger(), fileErrors) { + // correctly even when the defining constants file is processed after the file that + // uses the identifier. + for (const file of sourceFiles) { +- await preScanFile(file, astVisitors, otherConfig, logger, fileErrors); ++ await preScanFile(file, astVisitors, otherConfig, logger, fileErrors, parsedFiles); + } + // 6b. Mirror declarations onto the local names they were imported as, so that + // `import { ResourceStatus as Status }` resolves like the unaliased import. +@@ -98,7 +100,7 @@ async function findKeys(config, logger = new ConsoleLogger(), fileErrors) { + // 7. Extraction pass: all shared tables are now fully populated, so every + // identifier reference can be resolved regardless of file order. + for (const file of sourceFiles) { +- await processFile(file, plugins, astVisitors, pluginContext, otherConfig, logger, fileErrors); ++ await processFile(file, plugins, astVisitors, pluginContext, otherConfig, logger, fileErrors, parsedFiles); + } + // 7. Run onEnd hooks + for (const plugin of plugins) { diff --git a/config/scripts/build-wayland-clipboard.test.mjs b/config/scripts/build-wayland-clipboard.test.mjs index 9977a8353d8..64ebce4770f 100644 --- a/config/scripts/build-wayland-clipboard.test.mjs +++ b/config/scripts/build-wayland-clipboard.test.mjs @@ -12,6 +12,8 @@ import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { parse } from 'yaml' +import { runProcessSync } from '@orca/process-host' import { buildWaylandClipboard } from './build-wayland-clipboard.mjs' import glibcVerification from './verify-linux-glibc-floor.cjs' @@ -148,6 +150,50 @@ describe('Wayland clipboard build', () => { }) }) +describe('Wayland clipboard CI cache recovery', () => { + it.skipIf(process.platform === 'win32').each([ + ['healthy cache', true, 0, 0, 1], + ['invalid cache', true, 1, 0, 2], + ['failed replacement build', true, 1, 1, 2], + ['failed uncached build', false, 1, 1, 1] + ])('%s preserves build validation and failures', (_label, hit, initial, rebuild, calls) => { + const action = parse( + readFileSync('.github/actions/prepare-wayland-clipboard/action.yml', 'utf8') + ) + const build = action.runs.steps.find( + (step) => step.name === 'Build or validate Wayland clipboard helper' + ) + mkdirSync(join(root, 'native', 'wayland-clipboard', '.build'), { recursive: true }) + writeFileSync(join(root, 'native', 'wayland-clipboard', '.build', 'restored-cache'), '') + const script = ` + node() { + echo attempt >> calls + if [ -e native/wayland-clipboard/.build/restored-cache ]; then + return "$INITIAL_STATUS" + fi + return "$REBUILD_STATUS" + } + ${build.run} + ` + const result = runProcessSync({ + program: 'bash', + args: ['-e', '-c', script], + cwd: root, + env: { + ...process.env, + CACHE_HIT: String(hit), + INITIAL_STATUS: String(initial), + REBUILD_STATUS: String(rebuild) + } + }) + expect(result.code).toBe(initial === 0 ? 0 : hit ? rebuild : initial) + expect(readFileSync(join(root, 'calls'), 'utf8').trim().split('\n')).toHaveLength(calls) + expect(existsSync(join(root, 'native', 'wayland-clipboard', '.build', 'restored-cache'))).toBe( + calls === 1 + ) + }) +}) + describe('Wayland clipboard packaging', () => { it.each([ [1, 'x64'], diff --git a/config/scripts/check-node-runtime-pin.test.mjs b/config/scripts/check-node-runtime-pin.test.mjs index a805985d966..885bbc39776 100644 --- a/config/scripts/check-node-runtime-pin.test.mjs +++ b/config/scripts/check-node-runtime-pin.test.mjs @@ -189,7 +189,8 @@ describe('committed pin', () => { it('runs in the static analysis job', () => { const workflow = parse(readFileSync(path.join(projectDir, '.github/workflows/pr.yml'), 'utf8')) + const packageJson = JSON.parse(readFileSync(path.join(projectDir, 'package.json'), 'utf8')) const commands = workflow.jobs.preflight.steps.map((step) => step.run ?? '') - expect(commands).toContain('pnpm run check:node-runtime-pin') + expect(commands).toContain(packageJson.scripts['check:node-runtime-pin']) }) }) diff --git a/config/scripts/electron-builder-mac-channel-config.test.mjs b/config/scripts/electron-builder-mac-channel-config.test.mjs index 05572f3497a..693ac753978 100644 --- a/config/scripts/electron-builder-mac-channel-config.test.mjs +++ b/config/scripts/electron-builder-mac-channel-config.test.mjs @@ -133,20 +133,4 @@ describe('electron-builder mac channel config', () => { } ) }) - - // Why: the dev channels share every packaging decision except where they - // publish, so a future edit that collapses them must not also collapse the - // repos — a branch or daily build landing in orca-hourly would be offered to - // everyone riding main's hourlies. - it('keeps the dev channels on separate repos', () => { - withHourlyEnv((hourly) => { - withDailyEnv((daily) => { - withAdhocEnv((adhoc) => { - expect(new Set([hourly.publish.repo, daily.publish.repo, adhoc.publish.repo]).size).toBe( - 3 - ) - }) - }) - }) - }) }) diff --git a/config/scripts/generated-artifact-line-ending-pin.test.mjs b/config/scripts/generated-artifact-line-ending-pin.test.mjs index 7720cda1938..70e2ab41075 100644 --- a/config/scripts/generated-artifact-line-ending-pin.test.mjs +++ b/config/scripts/generated-artifact-line-ending-pin.test.mjs @@ -16,10 +16,14 @@ it('keeps byte-compared artifacts and source LF in an autocrlf checkout', () => ['src/renderer/src/example.tsx', lf], ['src/main/example.ts', lf], ['config/scripts/example.mjs', lf], + ['config/scripts/nested/deeper/example.mjs', lf], ...['cmd', 'bat', 'ps1', 'nsh'].map((extension) => [`config/example.${extension}`, crlf]), ['resources/skills-extra/example.json', crlf], ['vendor/resources/skills/example.json', crlf], ['scripts/example.mjs', crlf], + ['config/scripts-extra/example.mjs', crlf], + ['vendor/config/scripts/example.mjs', crlf], + ['config/scripts/example.mjsx', crlf], ['src/main/runtime/__fixtures__/example.txt', crlf], ['src/main/daemon/__fixtures__/pty-transcripts/example.txt', crlf] ]) diff --git a/config/scripts/happy-dom-offscreen-canvas.ts b/config/scripts/happy-dom-offscreen-canvas.ts index 1b06e472025..04e116a4192 100644 --- a/config/scripts/happy-dom-offscreen-canvas.ts +++ b/config/scripts/happy-dom-offscreen-canvas.ts @@ -49,7 +49,7 @@ export function installHappyDomOffscreenCanvasCompatibility(): boolean { return context } - const canvas = document.createElement('canvas') as PatchableCanvas + const canvas = document.createElement('canvas') return htmlCanvas.prototype.getContext!.call(canvas, contextType, contextAttributes) } prototype[OFFSCREEN_CANVAS_COMPATIBILITY_INSTALLED] = true diff --git a/config/scripts/locale-ko-key-overrides.test.mjs b/config/scripts/locale-ko-key-overrides.test.mjs deleted file mode 100644 index da98d401e54..00000000000 --- a/config/scripts/locale-ko-key-overrides.test.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import fs from 'node:fs' - -import { describe, expect, it } from 'vitest' - -describe('locale-ko-key-overrides', () => { - it('keeps Korean key override data scoped to Korean values', () => { - const overrides = JSON.parse( - fs.readFileSync(new URL('./locale-ko-key-overrides.json', import.meta.url), 'utf8') - ) - for (const value of Object.values(overrides)) { - expect(Object.keys(value)).toEqual(['ko']) - } - }) -}) diff --git a/config/scripts/localization-extraction-call-syntax.test.mjs b/config/scripts/localization-extraction-call-syntax.test.mjs index 125fe9c4fcb..6ae1f24be04 100644 --- a/config/scripts/localization-extraction-call-syntax.test.mjs +++ b/config/scripts/localization-extraction-call-syntax.test.mjs @@ -53,4 +53,47 @@ describe.each([ rmSync(directory, { recursive: true, force: true }) } }) + + it('preserves cross-file namespaces, source locations and fresh plugin transformations', async () => { + const directory = mkdtempSync(join(tmpdir(), 'orca-extraction-passes-')) + const file = join(directory, 'a-consumer.ts') + const config = { + locales: ['en'], + extract: { + input: [join(directory, '*.ts').replaceAll('\\', '/')], + functions: ['t', '*.t'], + disablePlurals: true + } + } + try { + writeFileSync(join(directory, 'z-constants.ts'), "export const NS = 'probe' as const\n") + const source = [ + "import { NS } from './z-constants'", + '/* 🙂 */ const { t } = useTranslation(NS);', + "t('first', 'First')" + ].join('\n') + writeFileSync(file, source) + const first = await extractKeys(config) + expect([...first.allKeys.values()]).toMatchObject([ + { key: 'first', ns: 'probe', locations: [{ file, line: 3, column: 2 }] } + ]) + + writeFileSync(file, source.replace("'first'", "'updated'")) + const updated = await extractKeys(config) + expect([...updated.allKeys.values()].map((entry) => entry.key)).toEqual(['updated']) + + const transformed = await extractKeys({ + ...config, + plugins: [ + { + name: 'fixture-transform', + onLoad: (code) => code.replace("'updated'", "'transformed'") + } + ] + }) + expect([...transformed.allKeys.values()].map((entry) => entry.key)).toEqual(['transformed']) + } finally { + rmSync(directory, { recursive: true, force: true }) + } + }) }) diff --git a/config/scripts/mobile-release-check-scope.test.mjs b/config/scripts/mobile-release-check-scope.test.mjs index 7a07b19af2f..5325c1626c6 100644 --- a/config/scripts/mobile-release-check-scope.test.mjs +++ b/config/scripts/mobile-release-check-scope.test.mjs @@ -102,7 +102,8 @@ it('gates Ruby independently and retains mobile static validation', () => { for (const file of [ 'config/scripts/mobile-release-check-scope.mjs', 'config/scripts/mobile-release-check-scope.test.mjs', - 'config/scripts/pr-code-change-scope.mjs' + 'config/scripts/pr-code-change-scope.mjs', + 'config/scripts/pr-package-test-paths.mjs' ]) { expect(workflow.on.pull_request.paths.some((pattern) => matchesGlob(file, pattern))).toBe(true) } @@ -137,6 +138,7 @@ function fixture(detectorStep = detector) { for (const file of [ 'package.json', 'config/scripts/pr-code-change-scope.mjs', + 'config/scripts/pr-package-test-paths.mjs', 'config/scripts/pr-layout-oracle-route.mjs', 'config/scripts/mobile-release-check-scope.mjs', 'config/scripts/mobile-test-change-scope.mjs' diff --git a/config/scripts/mobile-test-change-scope.mjs b/config/scripts/mobile-test-change-scope.mjs index f773fb8fa02..3be5e7c5ec2 100644 --- a/config/scripts/mobile-test-change-scope.mjs +++ b/config/scripts/mobile-test-change-scope.mjs @@ -8,6 +8,7 @@ const NON_TEST_FILES = new Set([ 'mobile/README.md', '.github/workflows/mobile-ios-release.yml', 'config/scripts/pr-code-change-scope.mjs', + 'config/scripts/pr-package-test-paths.mjs', 'config/scripts/pr-code-change-scope.test.mjs', 'config/scripts/pr-layout-oracle-route.mjs', 'config/scripts/mobile-release-check-scope.mjs', diff --git a/config/scripts/mobile-test-change-scope.test.mjs b/config/scripts/mobile-test-change-scope.test.mjs index 06d5273ec74..d5cdad6abbb 100644 --- a/config/scripts/mobile-test-change-scope.test.mjs +++ b/config/scripts/mobile-test-change-scope.test.mjs @@ -12,6 +12,7 @@ it.each([ 'mobile/fastlane/Fastfile', '.github/workflows/mobile-ios-release.yml', 'config/scripts/pr-code-change-scope.mjs', + 'config/scripts/pr-package-test-paths.mjs', 'config/scripts/mobile-release-check-scope.test.mjs' ])('skips only known non-test inputs: %s', (file) => { expect(shouldRunMobileTests([file])).toBe(false) diff --git a/config/scripts/mobile-web-app-browser-pane-text-inputs.test.mjs b/config/scripts/mobile-web-app-browser-pane-text-inputs.test.mjs deleted file mode 100644 index 0482a51079f..00000000000 --- a/config/scripts/mobile-web-app-browser-pane-text-inputs.test.mjs +++ /dev/null @@ -1,108 +0,0 @@ -/** - * The browser pane's two text inputs, read the way C4.2's census reads them. - * - * The pane is not in any page route today, so no route closure reaches it and the census that - * enforces the seam is not run against it: it walks the source-control hub and the review route, - * and the pane is in neither. C6 raises both inputs anyway, because the failure is not cosmetic — - * an input under 16px makes an iOS browser zoom the page on focus, and `keyboard-occlusion.web.ts` - * reads a visual viewport scale other than 1 as "no keyboard" for the rest of the typing session. - * - * This file is the census run by hand over the pane's own modules, so the raise is checked by the - * rule that will judge it once a route lists the pane. - */ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { fileURLToPath } from 'node:url' -import { describe, expect, it } from 'vitest' -import { - modulesDeclaringTextInput, - textInputFontSizeOffenders, - unresolvedTextInputStyles -} from './mobile-web-app-text-input-font-size-seam.mjs' - -const mobileDir = join(fileURLToPath(new URL('../..', import.meta.url)), 'mobile') - -/** - * The pane's text-input modules, named as the bundler resolves them: a `.web.ts` where one exists, - * which is what `mobileWebAppRouteClosure` reports for a real route. - */ -const PANE_CLOSURE = { - local: [ - 'src/browser/MobileBrowserPaneView.tsx', - 'src/browser/MobileBrowserAddressField.tsx', - 'src/browser/mobile-browser-pane-styles.ts', - 'src/browser/browser-address-field-styles.web.ts', - 'src/platform/text-input-font-size.web.ts' - ] -} - -const KEY_ROW_STYLES = 'src/browser/mobile-browser-pane-styles.ts' - -/** The modules in `PANE_CLOSURE` that render an input, which is what the rule below judges. */ -const PANE_INPUT_MODULES = [ - 'src/browser/MobileBrowserAddressField.tsx', - 'src/browser/MobileBrowserPaneView.tsx' -] - -function offendingFiles() { - return textInputFontSizeOffenders(mobileDir, PANE_CLOSURE).map((entry) => - entry.slice(0, entry.lastIndexOf(':')) - ) -} - -describe('the browser pane text inputs under the C4.2 census', () => { - /** - * The precondition the rest of this file rests on. - * - * `PANE_CLOSURE` is written by hand, because the pane is in no page route and there is no - * closure to derive it from. So "no unresolved styles" says the walk read the files listed, not - * that those files are the pane's inputs — a third input module added under `src/browser/` would - * leave every assertion below green and unchecked. This scans for it instead. - */ - it('lists every module under src/browser that renders an input', () => { - expect(modulesDeclaringTextInput(mobileDir, 'src/browser')).toEqual(PANE_INPUT_MODULES) - expect(PANE_CLOSURE.local).toEqual(expect.arrayContaining(PANE_INPUT_MODULES)) - }) - - it('reads every input the pane declares, so the verdicts below are complete', () => { - // The completeness half: an empty offender list means nothing if the walk found no input. - expect(unresolvedTextInputStyles(mobileDir, PANE_CLOSURE)).toEqual([]) - expect(PANE_CLOSURE.local).toContain('src/platform/text-input-font-size.web.ts') - }) - - // A scan that happens to find two things is not a scan that would find a third. Planted in a - // scratch tree rather than in src/browser, so no other census ever walks the plant. - it('would report a third input module, and ignores tests and non-JSX mentions', () => { - const scratch = mkdtempSync(join(tmpdir(), 'orca-browser-pane-inputs-')) - try { - mkdirSync(join(scratch, 'src/browser'), { recursive: true }) - for (const [name, source] of Object.entries({ - 'Planted.tsx': 'export const P = () => ', - 'Planted.test.tsx': 'it("x", () => )', - 'mentions-only.ts': "import { TextInput } from 'react-native' // TextInput, named twice" - })) { - writeFileSync(join(scratch, 'src/browser', name), source) - } - - expect(modulesDeclaringTextInput(scratch, 'src/browser')).toEqual(['src/browser/Planted.tsx']) - } finally { - rmSync(scratch, { recursive: true, force: true }) - } - }) - - it('takes the key row input through the seam', () => { - expect(offendingFiles()).not.toContain(KEY_ROW_STYLES) - }) - - /** - * The address field is split, and the split is what the census now reads. - * - * Native keeps the 12px the toolbar has always rendered, because no phone has a page to zoom; - * the browser gets the seam. `resolveLocal` follows the `.web.ts` the builder would have - * resolved, so the size judged here is the size the page runs rather than the one it does not. - */ - it('takes the address field through the seam, through its .web sibling', () => { - expect(offendingFiles()).toEqual([]) - }) -}) diff --git a/config/scripts/mobile-web-app-module-closure.test.mjs b/config/scripts/mobile-web-app-module-closure.test.mjs deleted file mode 100644 index 4be15640f3b..00000000000 --- a/config/scripts/mobile-web-app-module-closure.test.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import { mkdir, realpath, writeFile } from 'node:fs/promises' -import { join } from 'node:path' -import { expect, it } from 'vitest' -import { mobileWebAppModuleClosure } from './build-mobile-web-app-bundle.mjs' -import { withScratch } from './mobile-web-app-bundle-test-fixture.mjs' - -it.each(['app/_layout', 'app/_layout.tsx'])( - 'resolves %s from the supplied directory without a TypeScript baseUrl', - async (entry) => { - await withScratch(async (scratch) => { - const root = await realpath(scratch) - await mkdir(join(root, 'app')) - await writeFile(join(root, 'app/_layout.tsx'), "export default 'native'") - await writeFile(join(root, 'app/_layout.web.tsx'), "export default 'web'") - const closure = await mobileWebAppModuleClosure([entry], { absWorkingDir: root }) - expect(closure.modules).toEqual(['app/_layout.web.tsx']) - expect(closure.local).toEqual(['app/_layout.web.tsx']) - }) - } -) diff --git a/config/scripts/mobile-web-app-session-terminal-closure.test.mjs b/config/scripts/mobile-web-app-session-terminal-closure.test.mjs index 8146aa13f0c..c6ff5223013 100644 --- a/config/scripts/mobile-web-app-session-terminal-closure.test.mjs +++ b/config/scripts/mobile-web-app-session-terminal-closure.test.mjs @@ -1,6 +1,6 @@ import { join } from 'node:path' import { fileURLToPath } from 'node:url' -import { describe, expect, it } from 'vitest' +import { beforeAll, describe, expect, it } from 'vitest' import { mobileWebAppRouteClosure } from './build-mobile-web-app-bundle.mjs' import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-dependencies.mjs' import { mobileWebAppRouteChunkClosure } from './mobile-web-app-route-chunk-closure.mjs' @@ -478,8 +478,13 @@ const describeClosure = bundles ? describe : describe.skip describeClosure( "the session route's page closure with the terminal on it", () => { + let closure + beforeAll(async () => { + closure = await mobileWebAppRouteClosure(SESSION_ROUTE) + }, 300_000) + it('gains the document, xterm and the addons, and sheds the engine string', async () => { - const { local, modules } = await mobileWebAppRouteClosure(SESSION_ROUTE) + const { local, modules } = closure for (const gone of SHED) { expect(local, `${gone} is still in the closure`).not.toContain(gone) } @@ -506,7 +511,7 @@ describeClosure( }, 300_000) it('enters through the web sibling and the route body, not the switch', async () => { - const { local } = await mobileWebAppRouteClosure(SESSION_ROUTE) + const { local } = closure for (const entry of ROUTE_ENTRY) { expect(local, `${entry} is not in the closure`).toContain(entry) } @@ -518,7 +523,7 @@ describeClosure( }, 300_000) it('reaches the engine as one deferred module and never as part of the download', async () => { - const { modules } = await mobileWebAppRouteClosure(SESSION_ROUTE) + const { modules } = closure // The engine is here, as the one artifact the loader imports. expect(artifactModules(modules)).toHaveLength(1) // Package sources stay inside that artifact; unrelated module counts are not this boundary. @@ -537,16 +542,17 @@ describeClosure( }, 600_000) it('leaves the 16px seam census exactly where C7.2 left it', async () => { - const closure = await mobileWebAppRouteClosure(SESSION_ROUTE) // Two preconditions, because zero offenders is what a walk that read nothing also reports: // the seam's own web module has to be in the closure, and no style may be unresolved. expect(closure.local).toContain('src/platform/text-input-font-size.web.ts') + expect(closure.local.length).toBeGreaterThan(900) expect(unresolvedTextInputStyles(mobileDir, closure)).toEqual([]) expect(textInputFontSizeOffenders(mobileDir, closure)).toHaveLength(EXPECTED_OFFENDERS) + expect(closure.local).toContain('src/session/mobile-native-chat-input-styles.web.ts') + expect(closure.local).not.toContain('src/session/mobile-native-chat-input-styles.ts') }, 300_000) it('holds the editables the TextInput census cannot see to the same floor', async () => { - const closure = await mobileWebAppRouteClosure(SESSION_ROUTE) // The rich Markdown editor's surface is a `contenteditable` in a markup string, sized by a // rule in a stylesheet: `modulesDeclaringTextInput` matches JSX tags and never sees it, so it // shipped at 14 px and was measured at 14 px in both engines. The same floor, read by a rule diff --git a/config/scripts/mobile-web-app-session-text-inputs.test.mjs b/config/scripts/mobile-web-app-session-text-inputs.test.mjs deleted file mode 100644 index c8b1f76bb1c..00000000000 --- a/config/scripts/mobile-web-app-session-text-inputs.test.mjs +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Every text input the session screen reaches, and the size it declares. - * - * iOS zooms the page on focus of any input under 16px and does not zoom back out, so the document - * spends the rest of that typing session at a scale other than 1 — which `keyboard-occlusion.web.ts` - * reads as "not a keyboard" on purpose, because geometry cannot separate a zoom from a keyboard. - * - * This closure is the one that cannot afford it. The terminal's own input is a hidden field the - * keyboard seam's geometry is the whole basis of, and this screen reaches nine inputs that declare - * a size — a custom-key capture field, a prompt modal, the chat's ask/composer/question fields, the - * quick-command editor and its search, and the terminal command bar. One of them left off the seam - * leaves every later focus on this screen measuring a zoomed document. - * - * The rule is the closure rather than the nine sites it happens to have today: a module entering it - * later is held to it without anyone remembering to add it here. - */ -import { fileURLToPath } from 'node:url' -import { describe, expect, it } from 'vitest' -import { mobileWebAppRouteClosure } from './build-mobile-web-app-bundle.mjs' -import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-dependencies.mjs' -import { - TEXT_INPUT_FONT_SIZE_SEAM, - textInputFontSizeOffenders, - unresolvedTextInputStyles -} from './mobile-web-app-text-input-font-size-seam.mjs' - -const mobileDir = fileURLToPath(new URL('../../mobile/', import.meta.url)) -const describeClosure = mobileWebAppDependenciesPresent() ? describe : describe.skip - -const SESSION = 'app/h/[hostId]/session/[worktreeId].tsx' - -/** - * The style module this screen splits, as the page bundle resolves it. - * - * Named rather than left to the offender list because a split is the one fix that can be undone - * without reopening the offence: delete the `.web.ts` and the native sibling's size is what the - * page runs, which is a 15px chat composer and a zoomed document, and the offender list would say - * so — but only the next time someone reads it. Listed here, the closure says which file the page - * loads. - */ -const SPLIT_WEB_STYLES = ['src/session/mobile-native-chat-input-styles.web.ts'] - -describeClosure( - 'the text inputs the session screen reaches', - () => { - it('takes every input size through the seam', async () => { - const closure = await mobileWebAppRouteClosure(SESSION) - expect(textInputFontSizeOffenders(mobileDir, closure)).toEqual([]) - }) - - it('reads every input it found, so the list above is complete', async () => { - // The completeness half: an empty offender list is evidence only if every input was read. - const closure = await mobileWebAppRouteClosure(SESSION) - expect(unresolvedTextInputStyles(mobileDir, closure)).toEqual([]) - }) - - it('carries the seam, so the rule is not vacuous', async () => { - // Without this an empty offender list would also be what a closure reaching no text input at - // all produces, and the census would pass against a page that has nothing to raise. - const closure = await mobileWebAppRouteClosure(SESSION) - expect(closure.local).toContain(TEXT_INPUT_FONT_SIZE_SEAM) - expect(closure.local.length).toBeGreaterThan(900) - }) - - it('loads the web half of the split style module, not the native one', async () => { - const closure = await mobileWebAppRouteClosure(SESSION) - expect(closure.local).toEqual(expect.arrayContaining(SPLIT_WEB_STYLES)) - expect( - closure.local.filter((file) => - SPLIT_WEB_STYLES.some((web) => file === web.replace('.web.ts', '.ts')) - ) - ).toEqual([]) - }) - }, - 240_000 -) diff --git a/config/scripts/mobile-web-app-terminal-live-input-render.test.mjs b/config/scripts/mobile-web-app-terminal-live-input-render.test.mjs index b1821fd0683..20ae89f970b 100644 --- a/config/scripts/mobile-web-app-terminal-live-input-render.test.mjs +++ b/config/scripts/mobile-web-app-terminal-live-input-render.test.mjs @@ -36,7 +36,7 @@ import { LAYOUT_SOURCE } from './mobile-web-app-terminal-probe-route.mjs' * ref points at is inside that branch and never renders, `liveInputRef.current` is null, and * `liveInputRef.current?.setNativeProps(...)` is skipped by its own optional chain. The check's * error list is exactly empty for a page that never made the call. Its two siblings — - * `mobile-web-app-session-text-inputs.test.mjs` and `mobile-web-app-session-media-picker.test.mjs` + * `mobile-web-app-session-terminal-closure.test.mjs` and `mobile-web-app-session-media-picker.test.mjs` * — are source censuses over the route closure and mount nothing at all. * * So this file gives the hooks the one thing that route cannot: a mounted field. Everything else diff --git a/config/scripts/mobile-web-app-text-input-discovery.test.mjs b/config/scripts/mobile-web-app-text-input-discovery.test.mjs new file mode 100644 index 00000000000..6dc9241a898 --- /dev/null +++ b/config/scripts/mobile-web-app-text-input-discovery.test.mjs @@ -0,0 +1,22 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { expect, it } from 'vitest' +import { modulesDeclaringTextInput } from './mobile-web-app-text-input-font-size-seam.mjs' + +it('discovers input modules and ignores tests and non-JSX mentions', () => { + const scratch = mkdtempSync(join(tmpdir(), 'orca-text-input-discovery-')) + try { + mkdirSync(join(scratch, 'src/browser'), { recursive: true }) + for (const [name, source] of Object.entries({ + 'Planted.tsx': 'export const P = () => ', + 'Planted.test.tsx': 'it("x", () => )', + 'mentions-only.ts': "import { TextInput } from 'react-native' // TextInput, named twice" + })) { + writeFileSync(join(scratch, 'src/browser', name), source) + } + expect(modulesDeclaringTextInput(scratch, 'src/browser')).toEqual(['src/browser/Planted.tsx']) + } finally { + rmSync(scratch, { recursive: true, force: true }) + } +}) diff --git a/config/scripts/node-server-change-scope.mjs b/config/scripts/node-server-change-scope.mjs index 7b14c8dbbdf..9e66329e65d 100644 --- a/config/scripts/node-server-change-scope.mjs +++ b/config/scripts/node-server-change-scope.mjs @@ -18,6 +18,7 @@ const BUILD_SCRIPTS = [ 'config/scripts/build-windows-security-addon.mjs', 'config/scripts/run-node-server-tests.mjs', 'config/vitest.config.ts', + 'config/scripts/vitest-dom-setup.ts', 'config/scripts/happy-dom-offscreen-canvas.ts', 'config/scripts/happy-dom-mutation-observer-retention.ts', 'config/scripts/vitest-host-ports-setup.ts', diff --git a/config/scripts/node-server-change-scope.test.mjs b/config/scripts/node-server-change-scope.test.mjs index 156c2bcdeeb..dfaf3f2cb59 100644 --- a/config/scripts/node-server-change-scope.test.mjs +++ b/config/scripts/node-server-change-scope.test.mjs @@ -527,7 +527,7 @@ it('keeps every platform job and runs them when detection is skipped or fails', expect(workflow.on).toHaveProperty('workflow_dispatch') expect(workflow.on).toHaveProperty('workflow_call') expect(workflow.on).not.toHaveProperty('push') - expect(workflow.on).not.toHaveProperty('schedule') + expect(workflow.on.schedule).toHaveLength(1) expect(workflow.jobs.changes.if).toBe( "github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.draft != true)" ) diff --git a/config/scripts/orcad-windows-prebuild-workflow.test.mjs b/config/scripts/orcad-windows-prebuild-workflow.test.mjs index 6c39c265e50..97f54cf3bd7 100644 --- a/config/scripts/orcad-windows-prebuild-workflow.test.mjs +++ b/config/scripts/orcad-windows-prebuild-workflow.test.mjs @@ -225,6 +225,7 @@ node() { IFS= read -r verdict < out/orcad-prebuilds/manifest.json [ "$verdict" = valid ] ;; *--print-slot) echo win32-x64 ;; + *--require-slots*|*--smoke) grep -qx valid out/orcad-prebuilds/manifest.json ;; *) return 41 ;; esac } @@ -288,8 +289,8 @@ describe.skipIf(process.platform === 'win32')('Windows prebuild Bash fallback', ).toBe(validates) expect(commands.slice(-3)).toEqual([ 'node config/scripts/build-orcad-prebuilds.mjs --print-slot', - 'pnpm build:orcad-prebuilds --require-slots win32-x64', - 'pnpm build:orcad-prebuilds --smoke' + 'node config/scripts/build-orcad-prebuilds.mjs --require-slots win32-x64', + 'node config/scripts/build-orcad-prebuilds.mjs --smoke' ]) }) diff --git a/config/scripts/playwright-focused-workflow-selection.test.mjs b/config/scripts/playwright-focused-workflow-selection.test.mjs index 36ee3045e1e..12403ab4a53 100644 --- a/config/scripts/playwright-focused-workflow-selection.test.mjs +++ b/config/scripts/playwright-focused-workflow-selection.test.mjs @@ -75,29 +75,36 @@ it('focused IME and golden commands discover only their requested files with the `const { test } = require(${testPackage}); test('selected', () => {});` ) } - for (const args of commands) { - const result = await runProcess({ - program: process.execPath, - cwd: directory, - args: [ - join(dirname(require.resolve('playwright/package.json')), 'cli.js'), - 'test', - '--config', - config, - '--list', - '--reporter=json', - ...args - ], - env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }, - timeoutMs: 20_000 + const results = await Promise.allSettled( + commands.map(async (args) => { + const result = await runProcess({ + program: process.execPath, + cwd: directory, + args: [ + join(dirname(require.resolve('playwright/package.json')), 'cli.js'), + 'test', + '--config', + config, + '--list', + '--reporter=json', + ...args + ], + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }, + timeoutMs: 20_000 + }) + expect(result.code, result.stderr).toBe(0) + const report = JSON.parse(result.stdout) + const expected = args.filter((arg) => arg.endsWith('.spec.ts')).sort() + expect( + report.suites.map((suite) => suite.file.replaceAll('\\', '/')).sort(), + JSON.stringify(args) + ).toEqual(expected) }) - expect(result.code, result.stderr).toBe(0) - const report = JSON.parse(result.stdout) - const expected = args.filter((arg) => arg.endsWith('.spec.ts')).sort() - expect( - report.suites.map((suite) => suite.file.replaceAll('\\', '/')).sort(), - JSON.stringify(args) - ).toEqual(expected) + ) + for (const result of results) { + if (result.status === 'rejected') { + throw result.reason + } } } finally { rmSync(directory, { recursive: true, force: true }) diff --git a/config/scripts/pr-auxiliary-preflight-admission.test.mjs b/config/scripts/pr-auxiliary-preflight-admission.test.mjs index d0c41ffc7c7..e52617fbca4 100644 --- a/config/scripts/pr-auxiliary-preflight-admission.test.mjs +++ b/config/scripts/pr-auxiliary-preflight-admission.test.mjs @@ -266,6 +266,7 @@ describe('Mobile CI tool preflight routing', () => { [tool, 'package.json'], [tool, 'config/vitest.config.ts'], [tool, 'config/scripts/pr-code-change-scope.mjs'], + [tool, 'config/scripts/pr-package-test-paths.mjs'], [tool, 'config/scripts/new-mobile-tool.mjs'], [tool, 'README.md'], [tool, 'mobile/vitest.config.ts'] @@ -301,6 +302,7 @@ function createMobileCiScopeRepository( for (const file of [ 'package.json', 'config/scripts/pr-code-change-scope.mjs', + 'config/scripts/pr-package-test-paths.mjs', 'config/scripts/pr-layout-oracle-route.mjs', 'config/scripts/git-pull-request-diff-base.mjs', 'config/scripts/pr-e2e-source-routing.mjs', diff --git a/config/scripts/pr-code-change-scope.mjs b/config/scripts/pr-code-change-scope.mjs index d006e10a6cb..29fb7490431 100644 --- a/config/scripts/pr-code-change-scope.mjs +++ b/config/scripts/pr-code-change-scope.mjs @@ -3,6 +3,7 @@ import { join } from 'node:path' import process from 'node:process' import { pathToFileURL } from 'node:url' import { isLayoutOracleSource } from './pr-layout-oracle-route.mjs' +import { LINUX_PACKAGE_TESTS, WINDOWS_PACKAGE_TESTS } from './pr-package-test-paths.mjs' const DOCS_ONLY_FILES = new Set([ 'README.md', @@ -50,7 +51,8 @@ const GLOBAL_FORCE_PREFIXES = [ '.github/actions/install-node-dependencies/', '.github/actions/restore-pnpm-verification/', '.github/actions/prepare-native-runtime/', - 'config/scripts/pr-code-change-scope' + 'config/scripts/pr-code-change-scope', + 'config/scripts/pr-package-test-paths' ] const GLOBAL_FORCE_FILES = new Set(['package.json', 'pnpm-lock.yaml']) @@ -334,6 +336,7 @@ const LINUX_PACKAGE_PREFIXES = [ 'config/scripts/process-failure-message.mjs', 'config/scripts/space-sharing-copy.mjs', '.github/actions/prepare-linux-package-fixture/', + '.github/actions/prepare-wayland-clipboard/', 'config/docker/cli-launch-contract/', 'config/docker/headless-pairing/', 'config/docker/headless-serve-shutdown/', @@ -343,6 +346,7 @@ const LINUX_PACKAGE_PREFIXES = [ 'config/scripts/run-daemon-shutdown-descendants-docker', 'config/scripts/static-appimage-package-contract', 'native/computer-use-linux/', + 'native/wayland-clipboard/', 'resources/linux/', 'config/scripts/run-headless-serve' ] @@ -361,85 +365,6 @@ const WINDOWS_PACKAGE_PREFIXES = [ 'tests/tools/windows-pty-native-capability-smoke/' ] -const LINUX_PACKAGE_TESTS = [ - 'src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts', - 'src/main/browser/browser-route-tcp-egress.electron.test.ts', - 'src/main/browser/browser-route-webrtc-egress.electron.test.ts', - 'src/main/browser/browser-route-h3-egress.electron.test.ts', - 'src/main/browser/browser-route-dns-prefetch.electron.test.ts', - 'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts', - 'src/main/browser/browser-popup-compatibility.electron.test.ts', - 'src/main/browser/browser-popup-native-compatibility.electron.test.ts', - 'src/main/browser/browser-popup-background-compatibility.electron.test.ts' -] - -const WINDOWS_PACKAGE_TESTS = [ - 'src/main/zcode/zcode-plan-api-key-store-native-windows.test.ts', - ...LINUX_PACKAGE_TESTS, - 'config/scripts/rebuild-native-deps.test.mjs', - 'config/scripts/rebuild-native-deps-windows-process-tree.test.mjs', - 'config/scripts/rebuild-native-deps-node-pty.test.mjs', - 'config/scripts/nsis-process-check.test.mjs', - 'config/scripts/idle-cpu-process-sampling.test.mjs', - 'config/scripts/ensure-native-runtime-job-ownership.test.mjs', - 'config/scripts/verify-packaged-node-pty-job-ownership.test.mjs', - 'config/scripts/windows-pe-machine.test.mjs', - 'config/scripts/build-windows-cli-launcher.test.mjs', - 'config/scripts/verify-packaged-windows-agent-hook.test.mjs', - 'config/scripts/script-module-dependencies.test.mjs', - 'src/main/windows-registry-addon.test.ts', - 'src/shared/windows-security-addon.win32.test.ts', - 'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts', - 'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts', - 'src/shared/__tests__/process-host/windows-command-line.win32.test.ts', - 'src/shared/__tests__/process-host/windows-cmd-shim-resolution.test.ts', - 'src/shared/__tests__/process-host/windows-cmd-shim-resolution.win32.test.ts', - 'src/main/agent-hooks/windows-hook-payload-delivery.test.ts', - 'src/main/agent-hooks/managed-hook-stdin-lifecycle.test.ts', - 'src/main/jcode/hook-gate-script.test.ts', - 'src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts', - 'src/main/agent-hooks/windows-native-hook-exe-hosts.test.ts', - 'src/main/claude/windows-native-hook-registration.test.ts', - 'src/main/agent-hooks/windows-cmd-hook-command-unicode.test.ts', - 'src/main/agent-hooks/windows-batch-hook-launcher.test.ts', - 'src/main/agent-hooks/windows-powershell-hook-launcher.test.ts', - 'src/main/claude/claude-status-line-windows.test.ts', - 'src/main/codex/windows-hook-command.test.ts', - 'src/main/codex/windows-hook-upgrade.test.ts', - 'src/main/codex/hook-service-managed-install.test.ts', - 'src/main/windows/windows-pty-job.win32.test.ts', - 'src/main/windows/windows-msys-job.win32.test.ts', - 'src/main/providers/agent-foreground-process-git-bash.win32.test.ts', - 'src/main/windows/windows-host-job.win32.test.ts', - 'src/main/windows/windows-process-tree-command-line-patch.test.ts', - 'src/main/windows/windows-process-table-native-addon.win32.test.ts', - 'src/main/daemon/daemon-pid-identity-native.win32.test.ts', - 'src/main/windows/windows-administrator-status.win32.test.ts', - 'src/main/windows/windows-administrator-restart.win32.test.ts', - 'src/main/persistence/profile-state/profile-state-access-windows-native.win32.test.ts', - 'src/main/windows-live-tree-kill.win32.test.ts', - 'src/main/wsl/wsl-runner.test.ts', - 'src/main/wsl/wsl-guest-environment.test.ts', - 'src/main/wsl/wsl-executable-path.win32.test.ts', - 'src/main/wsl/wsl-w1-w3-contract.test.ts', - 'src/shared/source-scan/source-tree-scan.test.ts', - 'src/main/cli/wsl-cli-powershell-boundary.test.ts', - 'src/main/computer/desktop-script-runtime-host.win32.test.ts', - 'src/main/cursor/hook-service.test.ts', - 'src/main/orca-profiles/profile-index-store.test.ts', - 'src/main/startup/windows-install-dir-acl-repair.win32.test.ts', - 'src/main/runtime/repo-worktree-admin-fingerprint.test.ts', - 'src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts', - 'src/shared/secure-file-fsync-flags.test.ts', - 'src/shared/secure-path-windows-acl.win32.test.ts', - 'src/main/runtime/unreadable-secret-store-preservation.win32.test.ts', - 'src/main/ipc/pty-codex-account-attribution.test.ts', - 'src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts', - 'src/main/ipc/preflight-provider-command-selection.test.ts', - 'src/main/ipc/preflight-runnable-local-cli.test.ts', - 'src/main/ssh/remote-node-runtime-store-windows.test.ts' -] - const DESKTOP_IRRELEVANT_PREFIXES = [ 'mobile/', 'cloud/', diff --git a/config/scripts/pr-code-change-scope.test.mjs b/config/scripts/pr-code-change-scope.test.mjs index 9a9d30852e2..6565e12977b 100644 --- a/config/scripts/pr-code-change-scope.test.mjs +++ b/config/scripts/pr-code-change-scope.test.mjs @@ -204,10 +204,9 @@ describe('per-job path classification', () => { }) it('runs native package jobs only for the platform that ships the changed native', () => { - expectClassification(['native/windows-cli-launcher/src/main.rs'], { - package_windows: true - }) - expectClassification(['native/computer-use-linux/runtime.py'], { + expectClassification(['native/windows-cli-launcher/src/main.rs'], { package_windows: true }) + expectClassification(['native/computer-use-linux/runtime.py'], { package: true }) + expectClassification(['native/wayland-clipboard/wayland-clipboard.c'], { package: true }) expectClassification(['native/computer-use-macos/Package.swift'], {}) @@ -220,6 +219,7 @@ describe('per-job path classification', () => { 'config/scripts/process-failure-message.mjs', 'config/scripts/space-sharing-copy.mjs', '.github/actions/prepare-linux-package-fixture/action.yml', + '.github/actions/prepare-wayland-clipboard/action.yml', 'config/docker/cli-launch-contract/Dockerfile', 'config/docker/cli-launch-contract/run-cli-case.sh', 'config/docker/headless-pairing/Dockerfile', diff --git a/config/scripts/pr-e2e-gate-contract.test.mjs b/config/scripts/pr-e2e-gate-contract.test.mjs index 46c5cff3629..88d70856dd8 100644 --- a/config/scripts/pr-e2e-gate-contract.test.mjs +++ b/config/scripts/pr-e2e-gate-contract.test.mjs @@ -210,8 +210,8 @@ describe('PR E2E gate contract', () => { 'src/renderer/src/lib/pane-manager/terminal-ime-anchor.ts', 'src/shared/terminal-unicode-provider.ts', // The xterm fork owns the helper textarea the IME attaches to; no file here says "ime". - 'config/patches/@xterm__xterm@6.1.0-beta.287.patch', - 'config/patches/xterm-src/browser/Terminal.ts', + 'config/patches/@xterm__xterm@6.1.0-beta.304.patch', + 'config/patches/xterm-src/@xterm__xterm@6.1.0-beta.304.src.patch', // The harness is source too: breaking the runner or a probe is how the lane goes blind. 'config/scripts/run-terminal-ibus-hangul-e2e.mjs', 'config/scripts/terminal-ime-engagement-receipt.mjs', @@ -237,6 +237,36 @@ describe('PR E2E gate contract', () => { } }) + it('routes only core xterm patches to both IME lanes across version updates', () => { + for (const file of [ + 'config/patches/@xterm__xterm@6.1.0-beta.304.patch', + 'config/patches/xterm-src/@xterm__xterm@6.1.0-beta.304.src.patch', + 'config/patches/@xterm__xterm@7.0.0.patch', + 'config/patches/xterm-src/@xterm__xterm@7.0.0.src.patch' + ]) { + const specs = selectPrE2eSpecs([file]) + expect(hasNativeImeSourceChange([file]), file).toBe(true) + expect(specs, file).toContain('tests/e2e/terminal-ime-exact-byte.spec.ts') + expect(specs, file).toContain('tests/e2e/terminal-ibus-hangul-native.spec.ts') + } + for (const file of [ + 'config/patches/i18next-cli@1.74.2.patch', + 'config/patches/node-pty@1.1.0.patch', + 'config/patches/monaco-editor@0.55.1.patch', + 'config/patches/@xterm__addon-webgl@0.20.0-beta.300.patch', + 'config/patches/xterm-src/@xterm__addon-search@0.17.0-beta.301.src.patch', + 'config/patches/xterm-upstream.json', + 'config/patches/@xterm__xterm@7.0.0.patch.bak' + ]) { + expect(hasNativeImeSourceChange([file]), file).toBe(false) + expect(selectPrE2eSpecs([file]), file).toEqual([]) + } + const corePatch = 'config/patches/@xterm__xterm@6.1.0-beta.304.patch' + const mixedChanges = ['config/patches/i18next-cli@1.74.2.patch', corePatch] + expect(hasNativeImeSourceChange(mixedChanges)).toBe(true) + expect(selectPrE2eSpecs(mixedChanges)).toEqual(selectPrE2eSpecs([corePatch])) + }) + it('keeps the native IME spec out of the lane that would silently skip it', () => { expect(DEDICATED_E2E_SPECS).toContain('tests/e2e/terminal-ibus-hangul-native.spec.ts') // Why it still has to be routed: the dedicated lane is selected by the same route, so the diff --git a/config/scripts/pr-e2e-source-routing.mjs b/config/scripts/pr-e2e-source-routing.mjs index 530abb1da06..24fed423c7a 100644 --- a/config/scripts/pr-e2e-source-routing.mjs +++ b/config/scripts/pr-e2e-source-routing.mjs @@ -9,10 +9,12 @@ import { LAYOUT_ORACLE_E2E_SPECS } from './pr-layout-oracle-route.mjs' const isProductSource = (file) => !/\.test\.tsx?$/.test(file) && !isUnitTestSupportSource(file) -// Why config/patches: the xterm fork owns the helper textarea an input method attaches to, so a -// patch edit can break composition without touching a file named "ime". +// The core xterm patch owns the helper textarea an input method attaches to. +const XTERM_CORE_PATCH = + /^config\/patches\/(?:@xterm__xterm@[^/]+\.patch|xterm-src\/@xterm__xterm@[^/]+\.src\.patch)$/ + const NATIVE_IME_PRODUCT_SOURCE = - /^(?:config\/patches\/|src\/shared\/terminal-unicode-provider\.ts$|src\/renderer\/src\/lib\/pane-manager\/terminal-ime-|src\/renderer\/src\/components\/terminal-pane\/(?:terminal-ime-|terminal-ios-hangul-|xterm-bypass-policy))/ + /^(?:src\/shared\/terminal-unicode-provider\.ts$|src\/renderer\/src\/lib\/pane-manager\/terminal-ime-|src\/renderer\/src\/components\/terminal-pane\/(?:terminal-ime-|terminal-ios-hangul-|xterm-bypass-policy))/ /** The harness itself: the session runner, the boundary probes, and the native specs. */ const NATIVE_IME_HARNESS = @@ -255,9 +257,10 @@ export const PR_E2E_SOURCE_ROUTES = [ ], matches: (file) => isProductSource(file) && - /^(?:config\/patches\/|src\/renderer\/src\/components\/terminal-pane\/(?:terminal-ime-|use-terminal-pane-lifecycle|xterm-bypass-policy|terminal-option-shortcut-policy))/.test( - file - ) + (XTERM_CORE_PATCH.test(file) || + /^src\/renderer\/src\/components\/terminal-pane\/(?:terminal-ime-|use-terminal-pane-lifecycle|xterm-bypass-policy|terminal-option-shortcut-policy)/.test( + file + )) }, { // Why a route beside terminal-input.ime-and-synthetic-forwarding rather than more specs on @@ -268,7 +271,8 @@ export const PR_E2E_SOURCE_ROUTES = [ id: 'terminal-ime.native-input-method', specs: ['tests/e2e/terminal-ibus-hangul-native.spec.ts'], matches: (file) => - (isProductSource(file) && NATIVE_IME_PRODUCT_SOURCE.test(file)) || + (isProductSource(file) && + (XTERM_CORE_PATCH.test(file) || NATIVE_IME_PRODUCT_SOURCE.test(file))) || NATIVE_IME_HARNESS.test(file) }, { diff --git a/config/scripts/pr-package-test-paths.mjs b/config/scripts/pr-package-test-paths.mjs new file mode 100644 index 00000000000..e19e1be0d51 --- /dev/null +++ b/config/scripts/pr-package-test-paths.mjs @@ -0,0 +1,78 @@ +export const LINUX_PACKAGE_TESTS = [ + 'src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts', + 'src/main/browser/browser-route-tcp-egress.electron.test.ts', + 'src/main/browser/browser-route-webrtc-egress.electron.test.ts', + 'src/main/browser/browser-route-h3-egress.electron.test.ts', + 'src/main/browser/browser-route-dns-prefetch.electron.test.ts', + 'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts', + 'src/main/browser/browser-popup-compatibility.electron.test.ts', + 'src/main/browser/browser-popup-native-compatibility.electron.test.ts', + 'src/main/browser/browser-popup-background-compatibility.electron.test.ts' +] + +export const WINDOWS_PACKAGE_TESTS = [ + 'src/main/zcode/zcode-plan-api-key-store-native-windows.test.ts', + ...LINUX_PACKAGE_TESTS, + 'config/scripts/rebuild-native-deps.test.mjs', + 'config/scripts/rebuild-native-deps-windows-process-tree.test.mjs', + 'config/scripts/rebuild-native-deps-node-pty.test.mjs', + 'config/scripts/nsis-process-check.test.mjs', + 'config/scripts/idle-cpu-process-sampling.test.mjs', + 'config/scripts/ensure-native-runtime-job-ownership.test.mjs', + 'config/scripts/verify-packaged-node-pty-job-ownership.test.mjs', + 'config/scripts/windows-pe-machine.test.mjs', + 'config/scripts/build-windows-cli-launcher.test.mjs', + 'config/scripts/verify-packaged-windows-agent-hook.test.mjs', + 'config/scripts/script-module-dependencies.test.mjs', + 'src/main/windows-registry-addon.test.ts', + 'src/shared/windows-security-addon.win32.test.ts', + 'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts', + 'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts', + 'src/shared/__tests__/process-host/windows-command-line.win32.test.ts', + 'src/shared/__tests__/process-host/windows-cmd-shim-resolution.test.ts', + 'src/shared/__tests__/process-host/windows-cmd-shim-resolution.win32.test.ts', + 'src/main/agent-hooks/windows-hook-payload-delivery.test.ts', + 'src/main/agent-hooks/managed-hook-stdin-lifecycle.test.ts', + 'src/main/jcode/hook-gate-script.test.ts', + 'src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts', + 'src/main/agent-hooks/windows-native-hook-exe-hosts.test.ts', + 'src/main/claude/windows-native-hook-registration.test.ts', + 'src/main/agent-hooks/windows-cmd-hook-command-unicode.test.ts', + 'src/main/agent-hooks/windows-batch-hook-launcher.test.ts', + 'src/main/agent-hooks/windows-powershell-hook-launcher.test.ts', + 'src/main/claude/claude-status-line-windows.test.ts', + 'src/main/codex/windows-hook-command.test.ts', + 'src/main/codex/windows-hook-upgrade.test.ts', + 'src/main/codex/hook-service-managed-install.test.ts', + 'src/main/windows/windows-pty-job.win32.test.ts', + 'src/main/windows/windows-msys-job.win32.test.ts', + 'src/main/providers/agent-foreground-process-git-bash.win32.test.ts', + 'src/main/windows/windows-host-job.win32.test.ts', + 'src/main/windows/windows-process-tree-command-line-patch.test.ts', + 'src/main/windows/windows-process-table-native-addon.win32.test.ts', + 'src/main/daemon/daemon-pid-identity-native.win32.test.ts', + 'src/main/windows/windows-administrator-status.win32.test.ts', + 'src/main/windows/windows-administrator-restart.win32.test.ts', + 'src/main/persistence/profile-state/profile-state-access-windows-native.win32.test.ts', + 'src/main/windows-live-tree-kill.win32.test.ts', + 'src/main/wsl/wsl-runner.test.ts', + 'src/main/wsl/wsl-guest-environment.test.ts', + 'src/main/wsl/wsl-executable-path.win32.test.ts', + 'src/main/wsl/wsl-w1-w3-contract.test.ts', + 'src/shared/source-scan/source-tree-scan.test.ts', + 'src/main/cli/wsl-cli-powershell-boundary.test.ts', + 'src/main/computer/desktop-script-runtime-host.win32.test.ts', + 'src/main/cursor/hook-service.test.ts', + 'src/main/orca-profiles/profile-index-store.test.ts', + 'src/main/startup/windows-install-dir-acl-repair.win32.test.ts', + 'src/main/runtime/repo-worktree-admin-fingerprint.test.ts', + 'src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts', + 'src/shared/secure-file-fsync-flags.test.ts', + 'src/shared/secure-path-windows-acl.win32.test.ts', + 'src/main/runtime/unreadable-secret-store-preservation.win32.test.ts', + 'src/main/ipc/pty-codex-account-attribution.test.ts', + 'src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts', + 'src/main/ipc/preflight-provider-command-selection.test.ts', + 'src/main/ipc/preflight-runnable-local-cli.test.ts', + 'src/main/ssh/remote-node-runtime-store-windows.test.ts' +] diff --git a/config/scripts/process-host-build-dist.test.mjs b/config/scripts/process-host-build-dist.test.mjs index c79b912606d..b4b560b37d5 100644 --- a/config/scripts/process-host-build-dist.test.mjs +++ b/config/scripts/process-host-build-dist.test.mjs @@ -82,6 +82,26 @@ function runBuild(packageDir, { script = buildScript, cwd = realPackageDir } = { // Emits replacement output, then takes over or deletes the build lock as a competing build would. function installLockStealingCompiler(packageDir, { recreateLock }) { + const compromised = join(packageDir, 'lock-compromised') + const script = join(packageDir, 'observe-lock-loss.mjs') + writeFileSync( + script, + `import { writeFileSync } from 'node:fs' +import lockfile from ${JSON.stringify(pathToFileURL(packageRequire.resolve('proper-lockfile')).href)} +const lock = lockfile.lock +lockfile.lock = (file, options) => lock(file, { + ...options, + update: 1000, + onCompromised(error) { + options.onCompromised(error) + writeFileSync(${JSON.stringify(compromised)}, '') + } +}) +const { buildPackageDist } = await import(${JSON.stringify(pathToFileURL(buildScript).href)}) +try { await buildPackageDist(process.argv[2]) } +catch (error) { console.error(error.message); process.exitCode = 1 } +` + ) const compilerDir = join(packageDir, 'node_modules', 'typescript') rmSync(compilerDir, { recursive: true }) mkdirSync(join(compilerDir, 'bin'), { recursive: true }) @@ -91,7 +111,7 @@ function installLockStealingCompiler(packageDir, { recreateLock }) { ) writeFileSync( join(compilerDir, 'bin', 'tsc'), - `const { mkdirSync, rmSync, utimesSync, writeFileSync } = require('node:fs') + `const { existsSync, mkdirSync, rmSync, utimesSync, writeFileSync } = require('node:fs') const { join } = require('node:path') const outDir = process.argv[process.argv.indexOf('--outDir') + 1] mkdirSync(outDir, { recursive: true }) @@ -99,10 +119,14 @@ writeFileSync(join(outDir, 'entry.js'), 'module.exports = "from a build that los writeFileSync(join(outDir, 'added.js'), '') rmSync('.dist-build.lock', { recursive: true, force: true }) ${recreateLock ? "mkdirSync('.dist-build.lock')\nconst foreignMtime = new Date(Date.now() + 60_000)\nutimesSync('.dist-build.lock', foreignMtime, foreignMtime)" : ''} -// Hold compilation past proper-lockfile's 15-second refresh so compromise is observed. -setTimeout(() => {}, 17_000) +// Wait for the real lock monitor's verdict before compilation completes. +const wait = setInterval(() => { + if (existsSync(${JSON.stringify(compromised)})) clearInterval(wait) +}, 10) +setTimeout(() => process.exit(2), 20_000).unref() ` ) + return script } async function build(packageDir, expectResult = expect) { @@ -302,9 +326,9 @@ while (!existsSync(${JSON.stringify(finished)}) || reads === 0) { const packageDir = fixture(onTestFinished, seededDist) const before = distSnapshot(packageDir) const entry = readFileSync(join(packageDir, 'dist', 'entry.js'), 'utf8') - installLockStealingCompiler(packageDir, { recreateLock }) + const script = installLockStealingCompiler(packageDir, { recreateLock }) - const result = await runBuild(packageDir) + const result = await runBuild(packageDir, { script }) expectResult(result.code).toBe(1) expectResult(result.stderr).toContain('build lock was compromised') diff --git a/config/scripts/shebang-script-line-ending-pin.test.mjs b/config/scripts/shebang-script-line-ending-pin.test.mjs deleted file mode 100644 index 075eea6456d..00000000000 --- a/config/scripts/shebang-script-line-ending-pin.test.mjs +++ /dev/null @@ -1,70 +0,0 @@ -import { execFileSync } from 'node:child_process' -import { existsSync, readFileSync } from 'node:fs' -import { join, resolve } from 'node:path' -import { describe, expect, it } from 'vitest' - -/** - * Guard the `.gitattributes` pin that keeps `config/scripts` scripts on LF. - * - * `core.autocrlf=true` ships in the Git-for-Windows system config, so without a - * pin a Windows checkout gets CRLF. Vite's SSR transform locates the shebang - * with `/^#!.*\n/` — `\r` is a JS regex line terminator, so `.` never matches it - * and the pattern misses on CRLF. The hoisted import/export preamble then lands - * at offset 0, ahead of the shebang, which in turn defeats the `code[0] === '#'` - * guard that blanks it. A literal `#!` survives into the middle of the module and - * every suite importing the script dies at load with a SyntaxError. - * - * Scoped to `config/scripts` because that is where tests import scripts. Other - * shebanged `.mjs` in the tree are spawned, not imported, so they cannot hit this. - */ -const projectDir = resolve(import.meta.dirname, '../..') -const SCRIPT_DIRECTORY = 'config/scripts' - -function git(args) { - return execFileSync('git', args, { cwd: projectDir, encoding: 'utf8' }) -} - -/** `git check-attr -z` emits NUL-separated path/attr/value triples. */ -function eolAttributes(paths) { - const fields = git(['check-attr', '-z', 'eol', '--', ...paths]).split('\0') - const found = new Map() - for (let index = 0; index + 2 < fields.length; index += 3) { - found.set(fields[index], fields[index + 2]) - } - return found -} - -function shebangScripts() { - return git(['ls-files', '-z', '--', `${SCRIPT_DIRECTORY}/*.mjs`]) - .split('\0') - .filter((path) => path && existsSync(join(projectDir, path))) - .filter((path) => readFileSync(join(projectDir, path), 'utf8').startsWith('#!')) -} - -describe('config/scripts line-ending pin', () => { - it('pins every shebanged script to LF', () => { - const scripts = shebangScripts() - expect(scripts.length).toBeGreaterThan(0) - - const attributes = eolAttributes(scripts) - const unpinned = scripts.filter((path) => attributes.get(path) !== 'lf') - - expect( - unpinned, - 'A shebanged script left on the platform default gets CRLF on Windows, ' + - 'which makes every suite importing it fail to load. Pin it in .gitattributes.' - ).toEqual([]) - }) - - // Why: without these the assertion above still passes against a pattern so broad - // it says nothing, or so narrow it only covers the files that exist today. - it.each([ - ['config/scripts/example.mjs', 'lf'], - ['config/scripts/nested/deeper/example.mjs', 'lf'], - ['config/scripts-extra/example.mjs', 'unspecified'], - ['vendor/config/scripts/example.mjs', 'unspecified'], - ['config/scripts/example.mjsx', 'unspecified'] - ])('resolves %s to eol=%s', (path, expected) => { - expect(eolAttributes([path]).get(path)).toBe(expected) - }) -}) diff --git a/config/scripts/vitest-dom-setup.ts b/config/scripts/vitest-dom-setup.ts new file mode 100644 index 00000000000..738a876a21a --- /dev/null +++ b/config/scripts/vitest-dom-setup.ts @@ -0,0 +1,7 @@ +export {} + +// DOM compatibility patches are only needed after Vitest installs a DOM environment. +if (globalThis.document !== undefined) { + await import('./happy-dom-offscreen-canvas') + await import('./happy-dom-mutation-observer-retention') +} diff --git a/config/scripts/vitest-host-ports-setup.ts b/config/scripts/vitest-host-ports-setup.ts index 980775ec3f1..ab17cac58b4 100644 --- a/config/scripts/vitest-host-ports-setup.ts +++ b/config/scripts/vitest-host-ports-setup.ts @@ -15,14 +15,17 @@ const { mkdtempSync, rmSync } = process.getBuiltinModule('fs') * that asserts on host behaviour installs its own and wins, because this runs first. */ -// One directory per test environment, not per test: suites that write a file in one -// step and read it back in the next would otherwise lose it between them. Removed on -// teardown — vitest builds one environment per test file, so without this a full run -// leaves thousands of directories behind. -const userData = mkdtempSync(join(tmpdir(), 'orca-vitest-userdata-')) +// Most suites never read a host data path; create its per-file directory only when needed. +let userData: string | undefined + +function userDataPath(): string { + return (userData ??= mkdtempSync(join(tmpdir(), 'orca-vitest-userdata-'))) +} afterAll(() => { - rmSync(userData, { recursive: true, force: true }) + if (userData) { + rmSync(userData, { recursive: true, force: true }) + } }) // Deliberately not a plaintext passthrough: encryptString must return something a @@ -37,7 +40,7 @@ const SEAL_PREFIX = 'vitest-sealed:' */ export function fakeAppEnvironment(overrides: Partial = {}): AppEnvironment { return { - getPath: (name) => (name === 'home' ? homedir() : name === 'temp' ? tmpdir() : userData), + getPath: (name) => (name === 'home' ? homedir() : name === 'temp' ? tmpdir() : userDataPath()), getAppPath: () => process.cwd(), getVersion: () => '0.0.0-test', isPackaged: () => false, diff --git a/config/scripts/workflow-ref-mirror-case-safety.test.mjs b/config/scripts/workflow-ref-mirror-case-safety.test.mjs index a8333d76947..b9d9c752974 100644 --- a/config/scripts/workflow-ref-mirror-case-safety.test.mjs +++ b/config/scripts/workflow-ref-mirror-case-safety.test.mjs @@ -10,13 +10,6 @@ const projectDir = resolve(import.meta.dirname, '../..') const readWorkflow = (relativePath) => parse(readFileSync(join(projectDir, relativePath), 'utf8')) -// Every step that mirrors this repo's whole ref namespace onto a runner disk to -// prove a commit is reachable from a branch or tag before signing it. -const REF_MIRRORS = [ - ['.github/workflows/adhoc-mac-build.yml', 'build-adhoc-mac', 'Vet the requested ref'], - ['.github/workflows/dev-channel-win-build.yml', 'build-win', 'Vet the requested inputs'] -] - describe('ref-mirroring vet steps', () => { it.each(['daily', 'hourly', 'adhoc'])('%s builds only need the current commit', (channel) => { const job = readWorkflow(`.github/workflows/${channel}-mac-build.yml`).jobs[ @@ -98,30 +91,4 @@ describe('ref-mirroring vet steps', () => { rmSync(directory, { recursive: true, force: true }) } }) - - it('checks out only the vetted commit without remirroring refs', () => { - const steps = readWorkflow('.github/workflows/adhoc-mac-build.yml').jobs['build-adhoc-mac'] - .steps - const checkout = steps.find((step) => step.name === 'Checkout the requested ref') - expect(checkout.with.ref).toBe('${{ steps.vetted.outputs.sha }}') - expect(checkout.with['fetch-depth']).toBe(1) - expect(checkout.with['persist-credentials']).toBe(false) - }) - - // Why: macOS and Windows runner disks are case-insensitive, and this repo has - // branches that differ only in casing. The files backend cannot store both, and - // it fails the whole fetch rather than the one ref — so the vet step dies before - // any build runs. reftable keys refs in a table instead of file paths. - it.each(REF_MIRRORS)( - '%s creates its scratch repo with the reftable backend', - (path, job, step) => { - const run = readWorkflow(path).jobs[job].steps.find( - (candidate) => candidate.name === step - ).run - - expect(run).toContain('+refs/heads/*:refs/heads/*') - expect(run).toMatch(/git init\b[^\n]*--ref-format=reftable/) - expect(run).not.toMatch(/git init -q --bare "\$scratch"/) - } - ) }) diff --git a/config/scripts/workflow-ref-reachability.test.mjs b/config/scripts/workflow-ref-reachability.test.mjs index 81a8168f1d4..7c508e5b9f6 100644 --- a/config/scripts/workflow-ref-reachability.test.mjs +++ b/config/scripts/workflow-ref-reachability.test.mjs @@ -96,6 +96,8 @@ describe('release ref trust with case-twin names', () => { }) it('checks out the vetted SHA shallowly without mirroring case-twin refs again', async () => { + expect(macCheckout.with.ref).toBe('${{ steps.vetted.outputs.sha }}') + expect(macCheckout.with['persist-credentials']).toBe(false) const checkout = join(directory, 'checkout') const env = { ...identity, ...macCheckout.env } await git(['init', checkout], env) diff --git a/config/tsconfig.node.json b/config/tsconfig.node.json index 66d9b7fe541..fafa7267bc5 100644 --- a/config/tsconfig.node.json +++ b/config/tsconfig.node.json @@ -8,6 +8,9 @@ "./scripts/vitest-caller-identity-env-setup.ts", "./scripts/vitest-real-agent-home-write-guard.ts", "./scripts/vitest-bun-node-builtins.ts", + "./scripts/vitest-dom-setup.ts", + "./scripts/happy-dom-offscreen-canvas.ts", + "./scripts/happy-dom-mutation-observer-retention.ts", "./scripts/vitest-node-runtime-pool.ts", "../src/main/**/*", "../src/renderer/src/lib/skill-freshness-display-status.ts", diff --git a/config/vitest.config.ts b/config/vitest.config.ts index c4a1b27fbd5..7c0ac6e84fe 100644 --- a/config/vitest.config.ts +++ b/config/vitest.config.ts @@ -37,8 +37,7 @@ const testOptions = { setupFiles: [ resolve('config/scripts/vitest-real-agent-home-write-guard.ts'), resolve('config/scripts/vitest-bun-node-builtins.ts'), - resolve('config/scripts/happy-dom-offscreen-canvas.ts'), - resolve('config/scripts/happy-dom-mutation-observer-retention.ts'), + resolve('config/scripts/vitest-dom-setup.ts'), resolve('config/scripts/vitest-host-ports-setup.ts'), resolve('config/scripts/vitest-caller-identity-env-setup.ts') ], diff --git a/mobile/src/session/mobile-native-chat-merged-snapshot-parity.test.ts b/mobile/src/session/mobile-native-chat-merged-snapshot-parity.test.ts index 89b22ac3461..985fd2da711 100644 --- a/mobile/src/session/mobile-native-chat-merged-snapshot-parity.test.ts +++ b/mobile/src/session/mobile-native-chat-merged-snapshot-parity.test.ts @@ -209,7 +209,7 @@ const range = (from: number, to: number): number[] => describe.each(['after-end', 'during-turn'] as const)( 'a short chat, the send while stopping written %s: merged frames draw as a fresh snapshot does', (sendWhileStopping) => { - const journal = stopJournal(8, sendWhileStopping) + const journal = stopJournal(2, sendWhileStopping) it('draws the send the exit took back with its stop row, from the frame that took it back', () => { const fresh = drawn( @@ -221,7 +221,7 @@ describe.each(['after-end', 'during-turn'] as const)( ]) }) - // Exhaustive frame enumeration shares CI CPUs with the typechecks. + // Both history turns revise status; one completes and the other is interrupted. it.each([1, 2, 3])( 'from every subscribe point, %i row(s) per frame', (rowsPerFrame) => { diff --git a/mobile/src/test-support/rpc-recording/golden-value-pool.ts b/mobile/src/test-support/rpc-recording/golden-value-pool.ts index 03a8fa04c23..f869e3fca8a 100644 --- a/mobile/src/test-support/rpc-recording/golden-value-pool.ts +++ b/mobile/src/test-support/rpc-recording/golden-value-pool.ts @@ -89,9 +89,12 @@ export function internRecording(recording: Recording): { } { const pool: ValuePool = {} const canonical = new Map() + // Checkpoints repeat their append-only histories; hash each distinct entry once. + const hashes = new Map() const intern = (entry: RecordedValue, at: string): string => { const json = canonicalJson(entry) - const hash = canonicalJsonHash(json) + const hash = hashes.get(json) ?? canonicalJsonHash(json) + hashes.set(json, hash) const seen = canonical.get(hash) if (seen !== undefined && seen !== json) { throw new Error(`Golden value hash collision at ${hash} (${at})`) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c814a86101..7bbcec05d80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,7 +173,7 @@ patchedDependencies: '@xterm/addon-serialize@0.15.0-beta.301': b35533fe252e7e45433150170348889f4e08a6c17f7017ac34ea694d831fec7f '@xterm/addon-webgl@0.20.0-beta.300': 5349718f2c19d9319a81552580bf9618ded9b00f3707a5c6cdbbae1910849cd9 '@xterm/xterm@6.1.0-beta.304': 9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c - i18next-cli@1.74.2: 7955b89d3aa229f477408608d331f78c85148a85a85913729f89fac65ad8b207 + i18next-cli@1.74.2: 2ee32eef742615cd133e1a6940f027deb3723a8430bb7953bba3a6769b0aac47 lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673 monaco-editor@0.55.1: 53b71f7d8a7c00b76ffdb73ce83d18e31692f193f5fafa6a390e48f5b3fdd636 node-pty@1.1.0: c2fe19cde7aef223738a7503ae429dd9b03432130b064aa64a601900b33c151f @@ -484,7 +484,7 @@ importers: version: 9.1.7 i18next-cli: specifier: 1.74.2 - version: 1.74.2(patch_hash=7955b89d3aa229f477408608d331f78c85148a85a85913729f89fac65ad8b207)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2) + version: 1.74.2(patch_hash=2ee32eef742615cd133e1a6940f027deb3723a8430bb7953bba3a6769b0aac47)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2) katex: specifier: ^0.16.47 version: 0.16.47 @@ -12939,7 +12939,7 @@ snapshots: husky@9.1.7: {} - i18next-cli@1.74.2(patch_hash=7955b89d3aa229f477408608d331f78c85148a85a85913729f89fac65ad8b207)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2): + i18next-cli@1.74.2(patch_hash=2ee32eef742615cd133e1a6940f027deb3723a8430bb7953bba3a6769b0aac47)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2): dependencies: '@croct/json5-parser': 0.2.2 '@swc/core': 1.16.2 diff --git a/src/cli/browser-cookie-credentials-empty-value.test.ts b/src/cli/browser-cookie-credentials-empty-value.test.ts deleted file mode 100644 index 727348b9c06..00000000000 --- a/src/cli/browser-cookie-credentials-empty-value.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -const callMock = vi.fn() - -vi.mock('./runtime-client', () => { - class RuntimeClient { - call = callMock - getCliStatus = vi.fn() - openOrca = vi.fn() - } - - class RuntimeClientError extends Error { - readonly code: string - - constructor(code: string, message: string) { - super(message) - this.code = code - } - } - - class RuntimeRpcFailureError extends RuntimeClientError { - readonly response: unknown - - constructor(response: unknown) { - super('runtime_error', 'runtime_error') - this.response = response - } - } - - return { - RuntimeClient, - RuntimeClientError, - RuntimeRpcFailureError - } -}) - -import { main } from './index' -import { okFixture, queueFixtures } from './test-fixtures' - -// Why: CookieSet.value and SetCredentials.pass accept any string, empty included, -// while name and user require a non-empty one. -describe('orca cli cookie set and set credentials preserve an empty value', () => { - beforeEach(() => { - callMock.mockReset() - process.exitCode = undefined - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - it('passes an empty --value through to browser.cookie.set', async () => { - queueFixtures(callMock, okFixture('req_cookie_set', { success: true })) - vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main( - ['cookie', 'set', '--name', 'sid', '--value', '', '--worktree', 'all', '--json'], - '/tmp/not-an-orca-worktree' - ) - - expect(callMock).toHaveBeenCalledTimes(1) - expect(callMock).toHaveBeenCalledWith('browser.cookie.set', { - name: 'sid', - value: '', - worktree: undefined - }) - }) - - it('passes an empty --pass through to browser.setCredentials', async () => { - queueFixtures(callMock, okFixture('req_set_credentials', { success: true })) - vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main( - ['set', 'credentials', '--user', 'token', '--pass', '', '--worktree', 'all', '--json'], - '/tmp/not-an-orca-worktree' - ) - - expect(callMock).toHaveBeenCalledTimes(1) - expect(callMock).toHaveBeenCalledWith('browser.setCredentials', { - user: 'token', - pass: '', - worktree: undefined - }) - }) - - it('still rejects an empty --name before RPC dispatch', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - const priorExitCode = process.exitCode - - await main( - ['cookie', 'set', '--name', '', '--value', 'x', '--worktree', 'all'], - '/tmp/not-an-orca-worktree' - ) - - expect(callMock).not.toHaveBeenCalled() - expect(errorSpy.mock.calls.flat().join('\n')).toContain('Missing required --name') - expect(process.exitCode).toBe(1) - - process.exitCode = priorExitCode - }) - - it('still rejects a missing --pass before RPC dispatch', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - const priorExitCode = process.exitCode - - await main( - ['set', 'credentials', '--user', 'token', '--worktree', 'all'], - '/tmp/not-an-orca-worktree' - ) - - expect(callMock).not.toHaveBeenCalled() - expect(errorSpy.mock.calls.flat().join('\n')).toContain('Missing required --pass') - expect(process.exitCode).toBe(1) - - process.exitCode = priorExitCode - }) -}) diff --git a/src/cli/browser-storage-empty-value.test.ts b/src/cli/browser-empty-values.test.ts similarity index 61% rename from src/cli/browser-storage-empty-value.test.ts rename to src/cli/browser-empty-values.test.ts index b93145e93e4..23b110d46cd 100644 --- a/src/cli/browser-storage-empty-value.test.ts +++ b/src/cli/browser-empty-values.test.ts @@ -132,3 +132,82 @@ describe('orca cli storage set preserves an empty value', () => { process.exitCode = priorExitCode }) }) + +// Why: CookieSet.value and SetCredentials.pass accept any string, empty included, +// while name and user require a non-empty one. +describe('orca cli cookie set and set credentials preserve an empty value', () => { + beforeEach(() => { + callMock.mockReset() + process.exitCode = undefined + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('passes an empty --value through to browser.cookie.set', async () => { + queueFixtures(callMock, okFixture('req_cookie_set', { success: true })) + vi.spyOn(console, 'log').mockImplementation(() => {}) + + await main( + ['cookie', 'set', '--name', 'sid', '--value', '', '--worktree', 'all', '--json'], + '/tmp/not-an-orca-worktree' + ) + + expect(callMock).toHaveBeenCalledTimes(1) + expect(callMock).toHaveBeenCalledWith('browser.cookie.set', { + name: 'sid', + value: '', + worktree: undefined + }) + }) + + it('passes an empty --pass through to browser.setCredentials', async () => { + queueFixtures(callMock, okFixture('req_set_credentials', { success: true })) + vi.spyOn(console, 'log').mockImplementation(() => {}) + + await main( + ['set', 'credentials', '--user', 'token', '--pass', '', '--worktree', 'all', '--json'], + '/tmp/not-an-orca-worktree' + ) + + expect(callMock).toHaveBeenCalledTimes(1) + expect(callMock).toHaveBeenCalledWith('browser.setCredentials', { + user: 'token', + pass: '', + worktree: undefined + }) + }) + + it('still rejects an empty --name before RPC dispatch', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const priorExitCode = process.exitCode + + await main( + ['cookie', 'set', '--name', '', '--value', 'x', '--worktree', 'all'], + '/tmp/not-an-orca-worktree' + ) + + expect(callMock).not.toHaveBeenCalled() + expect(errorSpy.mock.calls.flat().join('\n')).toContain('Missing required --name') + expect(process.exitCode).toBe(1) + + process.exitCode = priorExitCode + }) + + it('still rejects a missing --pass before RPC dispatch', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const priorExitCode = process.exitCode + + await main( + ['set', 'credentials', '--user', 'token', '--worktree', 'all'], + '/tmp/not-an-orca-worktree' + ) + + expect(callMock).not.toHaveBeenCalled() + expect(errorSpy.mock.calls.flat().join('\n')).toContain('Missing required --pass') + expect(process.exitCode).toBe(1) + + process.exitCode = priorExitCode + }) +}) diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index 2d279d38c29..1687cc215ad 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -41,7 +41,6 @@ vi.mock('child_process', async () => { }) import { COMMAND_SPECS, main } from './index' -import { formatFlagHelp } from './help' import { GLOBAL_FLAGS, specPaths } from './args' import { okFixture, queueFixtures } from './test-fixtures' @@ -460,97 +459,15 @@ describe('nested command group help', () => { ) }) -describe('orca root help', () => { - it('advertises machine-readable agent discovery', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main([], '/tmp/repo') - - expect(logSpy.mock.calls.flat().join('\n')).toContain('agent-context') - logSpy.mockRestore() - }) - - it('advertises host-local account management', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main([], '/tmp/repo') - - expect(logSpy.mock.calls.flat().join('\n')).toContain( - 'account add Add a managed agent account on this Orca host' - ) - expect(logSpy.mock.calls.flat().join('\n')).toContain( - 'account list List managed agent accounts on this Orca host' - ) - logSpy.mockRestore() - }) - - it('labels retired coordinator scheduler commands at the root', async () => { +describe('orca help', () => { + it('prints root help without contacting the runtime', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) await main(['--help'], '/tmp/repo') - const output = String(logSpy.mock.calls[0]?.[0]) - expect(output).toContain( - 'orchestration coordinator-start Retired: load the current orchestration skill' - ) - expect(output).toContain( - 'orchestration coordinator-stop Retired: load the current orchestration skill' - ) - expect(output).not.toContain('Start the legacy automatic coordinator loop') - expect(output).not.toContain('Stop the legacy automatic coordinator loop') - logSpy.mockRestore() - }) - - it('advertises computer-use capabilities discovery', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main(['--help'], '/tmp/repo') - - expect(logSpy.mock.calls[0][0]).toContain( - 'computer capabilities Show computer-use provider capabilities' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'computer permissions Show or open computer-use permission setup' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'computer press-key Press a single key such as Return or Escape' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'project setup-existing-folder Make a project available on a host by importing an existing folder' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'project setup-create Create independent project host setup metadata' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'project setup-update Update project host setup metadata' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'project setup-delete Remove a project host setup' - ) - expect(logSpy.mock.calls[0][0]).toContain('Agent Sessions And Worktrees:') - expect(logSpy.mock.calls[0][0]).toContain( - 'orca agent launch --agent codex --model gpt-6-astra --effort high' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'orchestration worker-start Start a supervised worker locally or on a connected Orca server' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'orchestration ask Ask the coordinator a blocking question' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'orchestration worker-abandon Fence an uncertain worker without claiming it stopped' - ) - expect(logSpy.mock.calls[0][0]).toContain( - "orchestration worker-release Release a settled worker's terminal after archiving its output" - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'orchestration worker-retain Keep a worker terminal live for debugging' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'orchestration worker-list Report worker terminal resource accounting' - ) - expect(logSpy.mock.calls[0][0]).not.toContain('orchestration worker-cleanup') + expect(logSpy.mock.calls[0][0]).toContain('Usage: orca ') expect(callMock).not.toHaveBeenCalled() + logSpy.mockRestore() }) it('progressively discloses Linear commands', async () => { @@ -559,10 +476,8 @@ describe('orca root help', () => { await main(['--help'], '/tmp/repo') const rootHelp = String(logSpy.mock.calls[0][0]) - expect(rootHelp).toContain('Linear:') - expect(rootHelp).toContain('linear Read and write Linear issues for agents') + expect(rootHelp).toContain('linear') expect(rootHelp).not.toContain('linear issue') - expect(rootHelp).not.toContain('linear search') logSpy.mockClear() await main(['linear', '--help'], '/tmp/repo') @@ -570,172 +485,15 @@ describe('orca root help', () => { const groupHelp = String(logSpy.mock.calls[0][0]) expect(groupHelp).toContain('orca linear') expect(groupHelp).toContain('issue') - expect(groupHelp).toContain('search') expect(groupHelp).not.toContain('--comments') - expect(groupHelp).not.toContain('--attachments') logSpy.mockClear() await main(['linear', 'issue', '--help'], '/tmp/repo') const issueHelp = String(logSpy.mock.calls[0][0]) - expect(issueHelp).toContain('orca linear issue []') - expect(issueHelp).toContain('--comments Include threaded Linear comments') - expect(issueHelp).toContain('--attachments Include attachment metadata and URLs') - expect(issueHelp).toContain('--activity Include issue field-change history') - expect(issueHelp).toContain('--workspace Connected Linear workspace id') - expect(issueHelp).toContain('--id Linear issue key, id, or URL') - - logSpy.mockClear() - await main(['linear', 'search', '--help'], '/tmp/repo') - - const searchHelp = String(logSpy.mock.calls[0][0]) - expect(searchHelp).toContain('orca linear search ') - expect(searchHelp).toContain('--workspace Connected Linear workspace id, or all') - expect(searchHelp).toContain('--query Text to search across Linear issues') - - logSpy.mockClear() - await main(['linear', 'list-issues', '--help'], '/tmp/repo') - - const listIssuesHelp = String(logSpy.mock.calls[0][0]) - expect(listIssuesHelp).toContain( - '--cursor Opaque cursor from a previous list-issues page; issued cursors bind the workspace, raw Linear cursors need --workspace' - ) - expect(listIssuesHelp).toContain('--workspace Connected Linear workspace id, or all') - expect(listIssuesHelp).toContain('0=none, 1=urgent, 2=high, 3=medium, 4=low') - expect(listIssuesHelp).toContain( - '--limit Max issues to return; omit to return every match' - ) - expect(listIssuesHelp).not.toContain('Line cursor from a previous read') - expect(callMock).not.toHaveBeenCalled() - }) - - it('documents the machine-readable terminal topology opt-in', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['terminal', 'list', '--help'], '/tmp/repo') - - const help = String(logSpy.mock.calls[0][0]) - expect(help).toContain('[--include-visual-layouts] [--json]') - expect(help).toContain('--include-visual-layouts Include tab and pane topology in JSON output') - expect(help).toContain('JSON omits visualLayouts by default') - expect(callMock).not.toHaveBeenCalled() - }) - - it('describes worker-read cursors as opaque', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['orchestration', 'worker-read', '--help'], '/tmp/repo') - - const help = String(logSpy.mock.calls[0][0]) - expect(help).toContain( - '--cursor Opaque cursor returned by a previous worker-read page' - ) - expect(help).not.toContain('Line cursor from a previous read') - expect(callMock).not.toHaveBeenCalled() - }) - - it('describes worker-list cursors as opaque page cursors', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['orchestration', 'worker-list', '--help'], '/tmp/repo') - - const help = String(logSpy.mock.calls[0][0]) - expect(help).toContain('[--cursor ]') - expect(help).toContain('--cursor Opaque page cursor copied from page.nextCursor') - expect(help).toContain('Continue with the opaque page.nextCursor value unchanged.') - expect(help).not.toContain('--cursor ') - expect(help).not.toContain('Line cursor from a previous read') - expect(callMock).not.toHaveBeenCalled() - }) - - it('advertises Linear issue linking on worktree create and set help', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['worktree', 'create', '--help'], '/tmp/repo') - - expect(String(logSpy.mock.calls[0][0])).toContain('--linear-issue ') - - logSpy.mockClear() - await main(['worktree', 'set', '--help'], '/tmp/repo') - - const setHelp = String(logSpy.mock.calls[0][0]) - expect(setHelp).toContain('--linear-issue ') - expect(setHelp).toContain('--linear-issue Linked Linear issue identifier or URL') - expect(callMock).not.toHaveBeenCalled() - }) - - it('advertises explicit orchestration task display labels', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['orchestration', 'task-create', '--help'], '/tmp/repo') - - const help = String(logSpy.mock.calls[0][0]) - expect(help).toContain('[--task-title ] [--display-name ]') - expect(help).toContain('--task-title Concise title for the orchestration task') - expect(help).toContain('--display-name UI label shown for dispatched worker rows') - expect(callMock).not.toHaveBeenCalled() - }) - - it('hides removed parent-workspace help and scopes create parent selectors', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['--help'], '/tmp/repo') - - const rootHelp = String(logSpy.mock.calls[0][0]) - expect(rootHelp).not.toContain('--parent-workspace') - expect(rootHelp).toContain('[--parent-worktree ] [--no-parent]') - expect(rootHelp).toContain('identity:') - - logSpy.mockClear() - await main(['worktree', 'create', '--help'], '/tmp/repo') - - const createHelp = String(logSpy.mock.calls[0][0]) - expect(createHelp).not.toContain('--parent-workspace') - expect(createHelp).not.toContain('checkout/workspace') - expect(createHelp).not.toContain('caller workspace') - expect(createHelp).not.toContain('current workspace') - expect(createHelp).not.toContain('active Orca workspace') - expect(createHelp).not.toContain('folderWorkspaceId') - expect(createHelp).toContain('folder:') - expect(createHelp).toContain('folder:') - expect(createHelp).toContain('worktree:') - expect(createHelp).toContain( - '--no-parent only affects Orca lineage; omit --base-branch to use the repo default base' - ) - - logSpy.mockClear() - await main(['worktree', 'set', '--help'], '/tmp/repo') - - const setHelp = String(logSpy.mock.calls[0][0]) - expect(setHelp).not.toContain('--parent-workspace') - expect(setHelp).not.toContain('folder:') - expect(formatFlagHelp('parent-worktree')).toContain('identity:') - expect(setHelp).not.toContain('worktree:') - expect(callMock).not.toHaveBeenCalled() - }) - - it('points both create commands at the agent launch for starting agents', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['worktree', 'create', '--help'], '/tmp/repo') - - expect(String(logSpy.mock.calls[0][0])).toContain('This creates a new checkout.') - expect(String(logSpy.mock.calls[0][0])).toContain( - 'For a fresh agent in an existing worktree, use `orca agent launch`' - ) - - logSpy.mockClear() - await main(['terminal', 'create', '--help'], '/tmp/repo') - - const terminalHelp = String(logSpy.mock.calls[0][0]) - expect(terminalHelp).toContain('To start an agent, use `orca agent launch`') + expect(issueHelp).toContain('orca linear issue') + expect(issueHelp).toContain('--comments') expect(callMock).not.toHaveBeenCalled() + logSpy.mockRestore() }) }) diff --git a/src/cli/specs/file.test.ts b/src/cli/specs/file.test.ts deleted file mode 100644 index 9ef94f20614..00000000000 --- a/src/cli/specs/file.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { findCommandSpec } from '../args' -import { formatCommandHelp } from '../help' -import { COMMAND_SPECS } from './index' - -function optionLine(path: string[], flag: string): string | undefined { - const spec = findCommandSpec(COMMAND_SPECS, path) - if (!spec) { - throw new Error(`no spec for ${path.join(' ')}`) - } - return formatCommandHelp(spec) - .split('\n') - .find((line) => line.trimStart().startsWith(`--${flag} `)) -} - -describe('orca file command help', () => { - it.each([['open'], ['diff'], ['open-changed']])( - 'file %s describes --focus as bringing the user to the file', - (command) => { - expect(optionLine(['file', command], 'focus')).toBe( - " --focus Bring the user to the file (switches Orca's window to its worktree)" - ) - } - ) - - it('leaves terminal create --focus describing its terminal session', () => { - expect(optionLine(['terminal', 'create'], 'focus')).toBe( - ' --focus Reveal the created terminal session in Orca' - ) - }) -}) diff --git a/src/cli/specs/search.test.ts b/src/cli/specs/search.test.ts deleted file mode 100644 index 007da8c9fc3..00000000000 --- a/src/cli/specs/search.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { SEARCH_COMMAND_SPECS } from './search' -import { effectiveAllowedFlags, findCommandSpec, GLOBAL_FLAGS } from '../args' -import { buildAgentContext } from '../agent-context' -import { suggestCommands } from '../command-suggestion' -import { HANDLER_COMMAND_KEYS } from '../dispatch' -import { formatCommandHelp, printHelp } from '../help' -import { ROOT_HELP_TEXT_PRIMARY } from '../root-help-text-primary' -import { ROOT_HELP_TEXT_SECONDARY } from '../root-help-text-secondary' -import { COMMAND_SPECS } from './index' -import { CLI_COMMAND_NAMES } from '../../main/startup/cli-command-names' - -const searchSpec = SEARCH_COMMAND_SPECS[0]! -const help = formatCommandHelp(searchSpec) - -describe('orca search command spec', () => { - it('is one command, not a group, because the query is a bare positional', () => { - expect(SEARCH_COMMAND_SPECS).toHaveLength(1) - expect(searchSpec.path).toEqual(['search']) - expect(searchSpec.positionalArgs).toEqual(['query']) - }) - - it('is registered in the live spec table, the dispatcher and the launch redirect', () => { - expect(COMMAND_SPECS).toContain(searchSpec) - expect(HANDLER_COMMAND_KEYS.has('search')).toBe(true) - expect(CLI_COMMAND_NAMES).toContain('search') - }) - - it('accepts exactly the flags that map onto the search contract', () => { - expect([...searchSpec.allowedFlags].sort()).toEqual([ - 'agent', - 'cursor', - 'debug', - 'environment', - 'fresh', - 'help', - 'index-status', - 'json', - 'limit', - 'pairing-code', - 'path', - 'query', - 'scope', - 'since', - 'sort' - ]) - }) - - it('keeps --agent and --path repeatability scoped to search', () => { - expect(searchSpec.repeatableFlags).toEqual(['agent', 'path']) - for (const spec of COMMAND_SPECS) { - if (spec !== searchSpec) { - expect(spec.repeatableFlags ?? []).not.toContain('agent') - expect(spec.repeatableFlags ?? []).not.toContain('path') - } - } - }) - - it('does not accept or advertise browser page targeting', () => { - expect(effectiveAllowedFlags(searchSpec)).not.toContain('page') - expect(help).not.toContain('--page') - }) - - it('describes every search flag rather than falling back to the bare name', () => { - const searchOnly = searchSpec.allowedFlags.filter((flag) => !GLOBAL_FLAGS.includes(flag)) - expect(searchOnly).toHaveLength(11) - for (const flag of searchOnly) { - expect(help).toContain(`--${flag}`) - expect(help.split('\n')).not.toContain(` --${flag}`) - } - }) - - it('describes --agent as a search filter, not a terminal agent to launch', () => { - expect(help).toContain('Restrict to one agent; repeat for several') - expect(help).not.toContain('TUI agent') - }) - - it('tells the reader to quote a multi-word query', () => { - expect(searchSpec.notes?.join('\n')).toContain('Quote a multi-word query') - }) - - it('states that it searches one host and offers no all-computers search', () => { - expect(searchSpec.notes?.join('\n')).toContain('There is no all-computers search.') - }) - - it('shows both the query and the index report in its usage', () => { - expect(searchSpec.usage).toContain('orca search ') - expect(searchSpec.usage).toContain('orca search --index-status') - }) -}) - -describe('orca search discovery surfaces', () => { - it('is listed in the root help', () => { - expect(ROOT_HELP_TEXT_PRIMARY).toContain('Agent Sessions:') - expect(ROOT_HELP_TEXT_PRIMARY).toContain( - ' search Search the full text of agent sessions on one Orca host' - ) - expect(ROOT_HELP_TEXT_SECONDARY).toContain(' orca search --index-status [--json]') - }) - - it('prints its own help for `orca search --help`', () => { - const lines: string[] = [] - const restore = console.log - console.log = (value: unknown) => void lines.push(String(value)) - try { - printHelp(COMMAND_SPECS, ['search']) - } finally { - console.log = restore - } - expect(lines.join('\n')).toContain('Usage: orca search ') - }) - - it('resolves for dispatch', () => { - expect(findCommandSpec(COMMAND_SPECS, ['search'])).toBe(searchSpec) - }) - - it('exposes the command to agent discovery with its positional and flags', () => { - const command = buildAgentContext(COMMAND_SPECS).commands.find( - (entry) => entry.command === 'search' - ) - expect(command?.positionalArgs).toEqual(['query']) - expect(command?.flags).toContain('index-status') - expect(command?.flags).not.toContain('page') - }) - - it('is offered as a suggestion for a near-miss command', () => { - expect(suggestCommands(COMMAND_SPECS, ['serch'])).toContain('search') - }) -}) diff --git a/src/cli/terminal-format-draft.test.ts b/src/cli/terminal-format-draft.test.ts deleted file mode 100644 index 3f38e48dd21..00000000000 --- a/src/cli/terminal-format-draft.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { formatTerminalRead } from './terminal-format' - -describe('formatTerminalRead draft separation', () => { - it('labels composer drafts outside terminal output', () => { - const output = formatTerminalRead({ - terminal: { - handle: 'term_1', - status: 'running', - tail: ['Build passed', '❯'], - truncated: false, - nextCursor: null, - source: 'screen', - draft: 'proceed with the release' - } - }) - - expect(output).toContain('draft: "proceed with the release"') - expect(output).toContain('\n\nBuild passed\n❯') - }) -}) diff --git a/src/cli/terminal-format.test.ts b/src/cli/terminal-format.test.ts index ee0a985441e..58904929a55 100644 --- a/src/cli/terminal-format.test.ts +++ b/src/cli/terminal-format.test.ts @@ -7,6 +7,7 @@ import type { import { formatTerminalClose, formatTerminalFocus, + formatTerminalRead, formatTerminalSend, formatTerminalShow, formatTerminalWait @@ -268,3 +269,22 @@ describe('blocked-reason rendering against a mixed-version host', () => { ) }) }) + +describe('formatTerminalRead draft separation', () => { + it('labels composer drafts outside terminal output', () => { + const output = formatTerminalRead({ + terminal: { + handle: 'term_1', + status: 'running', + tail: ['Build passed', '❯'], + truncated: false, + nextCursor: null, + source: 'screen', + draft: 'proceed with the release' + } + }) + + expect(output).toContain('draft: "proceed with the release"') + expect(output).toContain('\n\nBuild passed\n❯') + }) +}) diff --git a/src/main/agent-hooks/server-transport-interference.test.ts b/src/main/agent-hooks/server-transport-interference.test.ts index e8d5a8e357b..dc39cebed46 100644 --- a/src/main/agent-hooks/server-transport-interference.test.ts +++ b/src/main/agent-hooks/server-transport-interference.test.ts @@ -2,11 +2,12 @@ // short of its own Content-Length. The listener fails open on every request error, so the only // way this stays diagnosable is if the truncation is classified before it is swallowed. import { connect } from 'node:net' -import { ServerResponse } from 'node:http' +import { IncomingMessage, ServerResponse } from 'node:http' import { afterEach, describe, expect, it, vi } from 'vitest' import type { HookTransportInterferenceReport } from '../../shared/agent-hook-transport-interference' import { AgentHookServer } from './server' import { OPENCODE_STARTUP_PROMPT_CLAIM_PATH } from '../../shared/opencode-startup-prompt' +import { HOOK_REQUEST_SLOWLORIS_MS } from '../../shared/agent-hook-listener/listener-limits' async function postTruncatedHook( port: number, @@ -47,15 +48,29 @@ async function postStalledHook( token: string, pathname = '/hook/claude' ): Promise { - const socket = connect({ port, host: '127.0.0.1' }) - await new Promise((resolve) => socket.on('connect', () => resolve())) - socket.write( - `POST ${pathname} HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nX-Orca-Agent-Hook-Token: ${token}\r\nContent-Length: 100000\r\n\r\n` - ) - await new Promise((resolve) => { - socket.on('close', () => resolve()) - socket.on('error', () => resolve()) + const setRequestTimeout = IncomingMessage.prototype.setTimeout + // Keep the native timeout/destroy path while checking its full production budget. + const timeout = vi.spyOn(IncomingMessage.prototype, 'setTimeout').mockImplementation(function ( + this: IncomingMessage, + milliseconds, + callback + ) { + expect(milliseconds).toBe(HOOK_REQUEST_SLOWLORIS_MS) + return setRequestTimeout.call(this, 25, callback) }) + try { + const socket = connect({ port, host: '127.0.0.1' }) + await new Promise((resolve) => socket.on('connect', () => resolve())) + socket.write( + `POST ${pathname} HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nX-Orca-Agent-Hook-Token: ${token}\r\nContent-Length: 100000\r\n\r\n` + ) + await new Promise((resolve) => { + socket.on('close', () => resolve()) + socket.on('error', () => resolve()) + }) + } finally { + timeout.mockRestore() + } await new Promise((resolve) => setTimeout(resolve, 50)) } diff --git a/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts b/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts index 367a19c96c6..7544390583d 100644 --- a/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts +++ b/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts @@ -24,17 +24,17 @@ import { } from '../ai-vault/session-transcript-consumers' import { SELF_CONTAINED_TRANSCRIPT_AGENTS } from './session-search-index-consumer' -// Spying on every fs export also mocks ReadStream internals and stalls real reads. vi.mock('node:fs', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - readFile: vi.fn(actual.readFile), - readFileSync: vi.fn(actual.readFileSync), - open: vi.fn(actual.open), - openSync: vi.fn(actual.openSync), - createReadStream: vi.fn(actual.createReadStream) + const original = await importOriginal() + // Recursive spies alter native stream prototypes and prevent readers from finishing. + const tracked = { + readFile: vi.fn(original.readFile), + readFileSync: vi.fn(original.readFileSync), + open: vi.fn(original.open), + openSync: vi.fn(original.openSync), + createReadStream: vi.fn(original.createReadStream) } + return { ...original, ...tracked, default: { ...original.default, ...tracked } } }) vi.mock('node:fs/promises', { spy: true }) vi.mock('../native-chat/wsl-transcript-fs-access', { spy: true }) diff --git a/src/main/browser/browser-popup-background-cdp-probe.ts b/src/main/browser/browser-popup-background-cdp-probe.ts index 171125823ce..4552730417e 100644 --- a/src/main/browser/browser-popup-background-cdp-probe.ts +++ b/src/main/browser/browser-popup-background-cdp-probe.ts @@ -98,14 +98,30 @@ export async function clickBackgroundPopupProbe( page: Page, probe: BackgroundPopupProbe ): Promise { - if (probe.context === 'opaque') { - // Electron does not forward opaque OOPIF input to the root route gesture gate. - await page.locator('#background-gesture').click() - } const locator = probe.context === 'subframe' || probe.context === 'opaque' ? page.frameLocator('#background-probe').locator('#background-open') : page.locator('#background-open') + if (probe.context === 'opaque') { + // A hidden OOPIF can finish loading before browser input reaches its new frame. + await locator.evaluate((element) => { + element.removeAttribute('data-background-input-ready') + element.addEventListener('pointermove', (event) => { + if (event.isTrusted) { + element.setAttribute('data-background-input-ready', 'true') + } + }) + }) + await page.locator('#background-gesture').click() + await expect + .poll(async () => { + await locator.hover() + return locator.getAttribute('data-background-input-ready') + }) + .toBe('true') + // Opaque frame input does not reach Electron's root route gesture gate. + await page.locator('#background-gesture').click() + } await locator.click( probe.input === 'middle' ? { button: 'middle' } : { modifiers: [backgroundModifier] } ) diff --git a/src/main/daemon/daemon-pty-startup-delivery.test.ts b/src/main/daemon/daemon-pty-startup-delivery.test.ts index 6691ca0a514..e005df68524 100644 --- a/src/main/daemon/daemon-pty-startup-delivery.test.ts +++ b/src/main/daemon/daemon-pty-startup-delivery.test.ts @@ -73,14 +73,19 @@ describe('DaemonPtyAdapter startup delivery', () => { const missingShell = join(dir, 'missing-fish') const fallbackName = basename(localPtyUtils.resolveUnixShellPath(missingShell)) context.skip(!['bash', 'zsh'].includes(fallbackName), 'Requires a Bash/zsh fallback') - await adapter.spawn({ - cols: 80, - rows: 24, - command: 'codex', - env: { SHELL: source === 'environment' ? missingShell : '/bin/sh' }, - ...(source === 'override' ? { shellOverride: missingShell } : {}) - }) - await new Promise((resolve) => setTimeout(resolve, 350)) + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) + try { + await adapter.spawn({ + cols: 80, + rows: 24, + command: 'codex', + env: { SHELL: source === 'environment' ? missingShell : '/bin/sh' }, + ...(source === 'override' ? { shellOverride: missingShell } : {}) + }) + await vi.advanceTimersByTimeAsync(350) + } finally { + vi.useRealTimers() + } expect(lastSubprocess.write).not.toHaveBeenCalled() expect(lastSpawnOpts).toEqual( expect.objectContaining({ startupCommandDelivery: 'shell-ready' }) @@ -96,9 +101,13 @@ describe('DaemonPtyAdapter startup delivery', () => { { command: 'codex', startupCommandDelivery: 'fast' as const }, { command: "codex 'linked issue context'", startupCommandDelivery: 'shell-ready' as const } ])('waits past 300ms and submits once after readiness: %j', async (startup) => { - await adapter.spawn({ cols: 80, rows: 24, ...startup, env: { SHELL: '/bin/zsh' } }) - - await new Promise((resolve) => setTimeout(resolve, 350)) + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) + try { + await adapter.spawn({ cols: 80, rows: 24, ...startup, env: { SHELL: '/bin/zsh' } }) + await vi.advanceTimersByTimeAsync(350) + } finally { + vi.useRealTimers() + } expect(lastSubprocess.write).not.toHaveBeenCalled() expect(lastSpawnOpts).toEqual( expect.objectContaining({ startupCommandDelivery: 'shell-ready' }) diff --git a/src/main/daemon/slow-daemon-session-verification.test.ts b/src/main/daemon/slow-daemon-session-verification.test.ts index 9668f1de26d..1781d423135 100644 --- a/src/main/daemon/slow-daemon-session-verification.test.ts +++ b/src/main/daemon/slow-daemon-session-verification.test.ts @@ -1,6 +1,7 @@ import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { connect, createServer, type Server, type Socket } from 'node:net' +import { once } from 'node:events' import { tmpdir } from 'node:os' import { join } from 'node:path' import { mkdirSync, mkdtempSync, rmSync } from 'node:fs' @@ -15,10 +16,7 @@ import type { SubprocessHandle } from './session-subprocess-handle' // time out the 3s startup health check against a daemon that was alive and // owning sessions, and the unhealthy path killed it. The fix re-verifies // with listSessions, which has far larger budgets (5s hello, 30s request). -// This test reproduces the production asymmetry against a REAL daemon by -// inserting a response delay that exceeds the health-check budget but fits -// the verification budgets, and asserts the guard's two inputs disagree the -// way the fix depends on. +// Real sockets preserve the handshake; virtual time crosses both production budgets. const RESPONSE_DELAY_MS = 3_500 function createMockSubprocess(): SubprocessHandle { @@ -52,6 +50,7 @@ function startDelayProxy(listenPath: string, upstreamPath: string): Server { clientSocket.write(chunk) } }, RESPONSE_DELAY_MS) + proxy.emit('response-queued') }) const teardown = (): void => { clientSocket.destroy() @@ -88,6 +87,7 @@ describe('slow daemon session verification', () => { }) afterEach(async () => { + vi.useRealTimers() for (const client of clients.splice(0)) { client.disconnect() } @@ -121,17 +121,33 @@ describe('slow daemon session verification', () => { }) proxy = startDelayProxy(proxySocketPath, daemonSocketPath) + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) // The exact pre-fix kill trigger: the daemon is alive but too slow for // the health-check budget. - await expect(healthCheckDaemon(proxySocketPath, tokenPath)).resolves.toBe(false) + const healthResponse = once(proxy, 'response-queued') + const health = healthCheckDaemon(proxySocketPath, tokenPath) + await healthResponse + await vi.advanceTimersByTimeAsync(3_000) + await expect(health).resolves.toBe(false) // The fix's re-verification against the SAME slow daemon: the larger // client budgets absorb the latency and prove the session is alive. const verificationClient = new DaemonClient({ socketPath: proxySocketPath, tokenPath }) clients.push(verificationClient) - await verificationClient.ensureConnected() - const result = await verificationClient.request('listSessions', undefined) + const helloResponse = once(proxy, 'response-queued') + const connected = verificationClient.ensureConnected() + await helloResponse + const streamResponse = once(proxy, 'response-queued') + await vi.advanceTimersByTimeAsync(RESPONSE_DELAY_MS) + await streamResponse + await vi.advanceTimersByTimeAsync(RESPONSE_DELAY_MS) + await connected + const sessionsResponse = once(proxy, 'response-queued') + const sessions = verificationClient.request('listSessions', undefined) + await sessionsResponse + await vi.advanceTimersByTimeAsync(RESPONSE_DELAY_MS) + const result = await sessions const liveSessionCount = result.sessions.filter((session) => session.isAlive).length expect(liveSessionCount).toBe(1) } diff --git a/src/main/git/repo-ref-maintenance-real-git.test.ts b/src/main/git/repo-ref-maintenance-real-git.test.ts index 7c3041e6dfe..fc74a1c2d18 100644 --- a/src/main/git/repo-ref-maintenance-real-git.test.ts +++ b/src/main/git/repo-ref-maintenance-real-git.test.ts @@ -1,8 +1,8 @@ import { execFileSync } from 'node:child_process' -import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises' +import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest' import { countLooseRefs } from '../../shared/loose-ref-count' import { RepoRefMaintenance } from '../../shared/repo-ref-maintenance' import { PACK_INDEX_MAINTENANCE_COOLDOWN_MS } from '../../shared/repo-pack-index-maintenance-policy' @@ -20,6 +20,8 @@ const roots: string[] = [] // three real `pack-refs` runs before the deferral budget is spent. const QUIET_MS = 25 const THRESHOLD = 20 +let packFixtureRoot = '' +let packFixtureBlobs: string[] = [] function git(cwd: string, args: string[], input?: string): string { return execFileSync('git', args, { @@ -45,11 +47,33 @@ function hasWriteOption(option: string): boolean { } async function createFragmentedPacks(repoPath: string): Promise { + await cp( + join(packFixtureRoot, '.git', 'objects', 'pack'), + join(repoPath, '.git', 'objects', 'pack'), + { + recursive: true + } + ) + return packFixtureBlobs +} + +beforeAll(async () => { + // Each test gets its own pack files; generating the same 64 packs only needs to happen once. + packFixtureRoot = await mkdtemp(join(tmpdir(), 'orca-fragmented-packs-')) + git(packFixtureRoot, ['init', '--quiet']) + packFixtureBlobs = await buildFragmentedPacks(packFixtureRoot) +}) + +afterAll(async () => { + await rm(packFixtureRoot, { recursive: true, force: true }) +}) + +async function buildFragmentedPacks(repoPath: string): Promise { const objects = join(repoPath, '.git', 'objects') - const blobs: string[] = [] - for (let index = 0; index < PACK_INDEX_THRESHOLD; index += 1) { - const blob = git(repoPath, ['hash-object', '-w', '--stdin'], `packed-${index}\n`) - blobs.push(blob) + const files = Array.from({ length: PACK_INDEX_THRESHOLD }, (_, index) => `packed-${index}`) + await Promise.all(files.map((file) => writeFile(join(repoPath, file), `${file}\n`))) + const blobs = git(repoPath, ['hash-object', '-w', ...files]).split('\n') + for (const blob of blobs) { git(repoPath, ['pack-objects', join(objects, 'pack', 'pack')], `${blob}\n`) await rm(join(objects, blob.slice(0, 2), blob.slice(2))) } diff --git a/src/main/git/worktree-rebase-update-refs-real-git.test.ts b/src/main/git/worktree-rebase-update-refs-real-git.test.ts index 8da9da2388b..c655836f415 100644 --- a/src/main/git/worktree-rebase-update-refs-real-git.test.ts +++ b/src/main/git/worktree-rebase-update-refs-real-git.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promi import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { runProcess } from '@orca/process-host' +import { GitBinaryTestFixture } from '../../shared/git-binary.test-fixture' vi.mock('./runner', () => ({ gitExecFileAsync: vi.fn(), gitExecFileSync: vi.fn() })) vi.mock('./local-repo-ref-maintenance', () => ({ @@ -11,51 +11,19 @@ vi.mock('./local-repo-ref-maintenance', () => ({ import { forceDeleteLocalBranch } from './worktree-branch-removal' -const image = process.env.ORCA_GIT_COMPAT_IMAGE -const binary = process.env.ORCA_GIT_COMPAT_BINARY ?? 'git' -const dockerUser = - typeof process.getuid === 'function' && typeof process.getgid === 'function' - ? ['--user', `${process.getuid()}:${process.getgid()}`] - : [] +let fixture: GitBinaryTestFixture let root = '' let repo = '' let checkout = '' async function git(args: string[], cwd = repo): Promise<{ stdout: string; stderr: string }> { - const result = await runProcess({ - program: image ? 'docker' : binary, - args: image - ? [ - 'run', - '--rm', - '--network=none', - ...dockerUser, - '-v', - `${root}:${root}`, - '-w', - cwd, - image, - '-c', - `safe.directory=${cwd}`, - ...args - ] - : args, - cwd, - env: { - ...process.env, - GIT_CONFIG_GLOBAL: join(root, 'empty-config'), - GIT_CONFIG_NOSYSTEM: '1' - }, - maxOutputBytes: 2 * 1024 * 1024 - }) - if (result.code !== 0) { - throw Object.assign(new Error(result.stderr), { code: result.code, stderr: result.stderr }) - } - return result + return fixture.exec(args, cwd, { GIT_CONFIG_GLOBAL: join(root, 'empty-config') }) } beforeEach(async () => { root = await realpath(await mkdtemp(join(tmpdir(), 'orca-rebase-reservation-'))) + fixture = new GitBinaryTestFixture(root) + await fixture.start() repo = join(root, 'repo') checkout = join(root, 'checkout') await mkdir(repo) @@ -77,7 +45,11 @@ beforeEach(async () => { }) afterEach(async () => { - await rm(root, { recursive: true, force: true }) + try { + await fixture.stop() + } finally { + await rm(root, { recursive: true, force: true }) + } }) describe('rebase branch reservations with real Git', () => { diff --git a/src/main/git/worktree-safety-real-git.test.ts b/src/main/git/worktree-safety-real-git.test.ts index 9a3e2fdfffa..c32f546d309 100644 --- a/src/main/git/worktree-safety-real-git.test.ts +++ b/src/main/git/worktree-safety-real-git.test.ts @@ -1,9 +1,7 @@ -import { execFile } from 'node:child_process' import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { promisify } from 'node:util' -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { runner } = vi.hoisted(() => ({ runner: vi.fn() })) vi.mock('./runner', () => ({ @@ -15,43 +13,15 @@ vi.mock('./status', () => ({ runWithGitReadCacheInvalidation: (run: () => unknow import { getBranchConflictKind, getBranchConflictKindViaExec } from './repo-branch-conflict' import { clearGitCapabilityStateForTests } from './git-capability-state' +import { GitBinaryTestFixture } from '../../shared/git-binary.test-fixture' -const execFileAsync = promisify(execFile) -const image = process.env.ORCA_GIT_COMPAT_IMAGE -const binary = process.env.ORCA_GIT_COMPAT_BINARY ?? 'git' const expectedVersion = process.env.ORCA_GIT_COMPAT_VERSION -const dockerUser = - typeof process.getuid === 'function' && typeof process.getgid === 'function' - ? ['--user', `${process.getuid()}:${process.getgid()}`] - : [] +let fixture: GitBinaryTestFixture let root = '' let repo = '' async function git(args: string[], cwd = repo): Promise<{ stdout: string; stderr: string }> { - return image - ? execFileAsync( - 'docker', - [ - 'run', - '--rm', - '--network=none', - ...dockerUser, - '-v', - `${root}:${root}`, - '-w', - cwd, - image, - '-c', - `safe.directory=${cwd}`, - ...args - ], - { maxBuffer: 2 * 1024 * 1024 } - ) - : execFileAsync(binary, args, { - cwd, - env: { ...process.env, HOME: root, XDG_CONFIG_HOME: root, GIT_CONFIG_NOSYSTEM: '1' }, - maxBuffer: 2 * 1024 * 1024 - }) + return fixture.exec(args, cwd) } async function initializeRepo(cwd: string): Promise { @@ -68,23 +38,30 @@ async function initializeRepo(cwd: string): Promise { } beforeAll(async () => { - const { stdout } = await execFileAsync( - image ? 'docker' : binary, - image ? ['run', '--rm', '--network=none', ...dockerUser, image, '--version'] : ['--version'] - ) + root = await realpath(await mkdtemp(join(tmpdir(), 'orca-worktree-safety-'))) + fixture = new GitBinaryTestFixture(root) + await fixture.start() + const { stdout } = await fixture.exec(['--version']) expect(stdout).toContain(expectedVersion ? `git version ${expectedVersion}` : 'git version ') }) +afterAll(async () => { + try { + await fixture.stop() + } finally { + await rm(root, { recursive: true, force: true }) + } +}) + beforeEach(async () => { - root = await realpath(await mkdtemp(join(tmpdir(), 'orca-worktree-safety-'))) - repo = join(root, 'repo') + repo = await mkdtemp(join(root, 'repo-')) await initializeRepo(repo) clearGitCapabilityStateForTests() runner.mockImplementation((args: string[], options: { cwd?: string }) => git(args, options.cwd)) }) afterEach(async () => { - await rm(root, { recursive: true, force: true }) + await rm(repo, { recursive: true, force: true }) }) describe('worktree safety with the real Git binary', () => { diff --git a/src/main/hook-termination-real-process.test.ts b/src/main/hook-termination-real-process.test.ts index 65d1b0910d1..a314a2d99f4 100644 --- a/src/main/hook-termination-real-process.test.ts +++ b/src/main/hook-termination-real-process.test.ts @@ -30,14 +30,14 @@ async function survivorsAfterDeadline( try { const result = await runHook('archive', dir, REPO, dir, undefined, 400) expect(result.success).toBe(false) - // SIGTERM lands at the deadline, SIGKILL two seconds later. - await new Promise((resolve) => setTimeout(resolve, 3_500)) expect(existsSync(pidFile)).toBe(true) pids = readFileSync(pidFile, 'utf8').trim().split(/\s+/).map(Number) // Without this, a script that recorded only the shell leaves `pids[1]` undefined, `alive` // throws, and the missing descendant reads as dead — a test that passes on nothing. expect(pids).toHaveLength(2) expect(pids.every((pid) => Number.isSafeInteger(pid) && pid > 0)).toBe(true) + // SIGTERM lands at the deadline; resistant descendants need the real SIGKILL grace. + await expect.poll(() => pids.some(alive), { timeout: 5_000, interval: 25 }).toBe(false) return { shell: alive(pids[0]!), child: alive(pids[1]!), output: result.output, pids } } finally { for (const pid of pids) { diff --git a/src/main/ipc/jira.ts b/src/main/ipc/jira.ts index 47727598a61..2ebd3b27988 100644 --- a/src/main/ipc/jira.ts +++ b/src/main/ipc/jira.ts @@ -72,13 +72,9 @@ export function registerJiraHandlers(): void { return selectSite(siteId) }) - ipcMain.handle('jira:status', async () => { - return getStatus() - }) + ipcMain.handle('jira:status', async () => getStatus()) - ipcMain.handle('jira:readStatus', async () => { - return getStatus() - }) + ipcMain.handle('jira:readStatus', async () => getStatus()) ipcMain.handle('jira:testConnection', async (_event, args?: { siteId?: string }) => { return testConnection(normalizeJiraSiteId(args?.siteId)) diff --git a/src/main/native-chat/transcript-opencode2-sqlite.test.ts b/src/main/native-chat/transcript-opencode2-sqlite.test.ts index 7ddd653071d..df36bbc78a1 100644 --- a/src/main/native-chat/transcript-opencode2-sqlite.test.ts +++ b/src/main/native-chat/transcript-opencode2-sqlite.test.ts @@ -26,11 +26,10 @@ function fixture() { CREATE TABLE session_message (id TEXT PRIMARY KEY, session_id TEXT, type TEXT, seq INTEGER, data TEXT, time_created INTEGER, time_updated INTEGER); INSERT INTO session_v2 VALUES ('session');`) + const insertMessage = db.prepare('INSERT INTO session_message VALUES (?, ?, ?, ?, ?, ?, ?)') let sequence = 0 const insert = (id: string, type: string, data: unknown, time = 1) => - db - .prepare('INSERT INTO session_message VALUES (?, ?, ?, ?, ?, ?, ?)') - .run(id, 'session', type, ++sequence, JSON.stringify(data), time, time) + insertMessage.run(id, 'session', type, ++sequence, JSON.stringify(data), time, time) return { db, path, insert } } @@ -159,6 +158,8 @@ describe('OpenCode 2 native transcript', () => { it('refuses excessive sparse scans instead of returning a partial transcript', () => { const { db, path, insert } = fixture() + // The regression is the scan cap; avoid quadratic fixture scans between batches. + db.exec('CREATE INDEX session_message_sequence ON session_message (session_id, seq)') db.exec('BEGIN') for (let index = 0; index < 10001; index++) { insert(String(index), 'idle', {}) diff --git a/src/main/persistence/profile-state/profile-state-backup-worker.test.ts b/src/main/persistence/profile-state/profile-state-backup-worker.test.ts index a2d40b8328c..2f684a619cd 100644 --- a/src/main/persistence/profile-state/profile-state-backup-worker.test.ts +++ b/src/main/persistence/profile-state/profile-state-backup-worker.test.ts @@ -191,7 +191,10 @@ describe('profile state backup worker', () => { const { directory, job } = fixture() const failedWorker = script(directory, source) await expect( - runProfileStateBackupWorker(job, { workerPath: failedWorker, timeoutMs: 500 }) + runProfileStateBackupWorker(job, { + workerPath: failedWorker, + timeoutMs: error === 'timed out' ? 500 : undefined + }) ).rejects.toThrow(error) expect(existsSync(job.targetPath)).toBe(false) rmSync(directory, { recursive: true }) diff --git a/src/main/provider-process/provider-process-supervisor.integration.test.ts b/src/main/provider-process/provider-process-supervisor.integration.test.ts index 98e03a679d5..141935da0f6 100644 --- a/src/main/provider-process/provider-process-supervisor.integration.test.ts +++ b/src/main/provider-process/provider-process-supervisor.integration.test.ts @@ -348,15 +348,28 @@ describe.runIf(process.platform !== 'win32')('POSIX provider supervisor processe }) it('never spawns the provider when its owner is not its parent at start', async () => { - const marker = join(tempDir(), 'provider-started') + const dir = tempDir() + const marker = join(dir, 'provider-spawned') + const preload = join(dir, 'record-spawn.js') + writeFileSync( + preload, + String.raw` + const childProcess = require('node:child_process') + const spawn = childProcess.spawn + childProcess.spawn = (...args) => { + require('node:fs').writeFileSync(process.env.ORCA_TEST_PROVIDER_PID_FILE, 'spawned') + return spawn(...args) + } + ` + ) const { exit } = launchSupervisor( { ownerPid: process.pid === 1 ? 2 : 1 }, - {}, - { command: 'touch', args: [marker] } + { ORCA_TEST_PROVIDER_PID_FILE: marker }, + { command: 'touch', args: [join(dir, 'provider-started')] }, + ['--require', preload] ) await expect(exit).resolves.toEqual({ code: 1, signal: null }) - await new Promise((resolve) => setTimeout(resolve, 200)) expect(existsSync(marker)).toBe(false) }) @@ -397,10 +410,12 @@ describe.runIf(process.platform !== 'win32')('POSIX provider supervisor processe expect(alive(provider)).toBe(false) }) - it('gives a provider 1 s after stdin end, then 3 s after SIGTERM before SIGKILL', async () => { + it('gives a provider its stdin-end grace, then its SIGTERM grace before SIGKILL', async () => { const signalFile = join(tempDir(), 'provider-sigterm-at') + const stdinEndGraceMs = 200 + const sigtermGraceMs = 300 const { supervisor, exit } = launchSupervisor( - {}, + { stdinEndGraceMs, sigtermGraceMs }, { ORCA_TEST_PROVIDER_SIGNAL_FILE: signalFile }, { command: process.execPath, args: ['-e', RECORDS_SIGTERM_PROVIDER] } ) @@ -414,9 +429,9 @@ describe.runIf(process.platform !== 'win32')('POSIX provider supervisor processe expect(exited).toEqual({ code: 137, signal: null }) // Timers may fire a tick early against another process's clock. - expect(signalledAt - endedAt).toBeGreaterThanOrEqual(1_000 - 20) - expect(exitedAt - signalledAt).toBeGreaterThanOrEqual(3_000 - 20) - expect(exitedAt - endedAt).toBeLessThan(PROVIDER_SUPERVISOR_MAX_STOP_MS + 1_000) + expect(signalledAt - endedAt).toBeGreaterThanOrEqual(stdinEndGraceMs - 20) + expect(exitedAt - signalledAt).toBeGreaterThanOrEqual(sigtermGraceMs - 20) + expect(exitedAt - endedAt).toBeLessThan(stdinEndGraceMs + sigtermGraceMs + 1_000) expect(alive(provider)).toBe(false) }) @@ -513,10 +528,10 @@ describe.runIf(process.platform !== 'win32')('POSIX provider supervisor processe it('lets a one-shot answer after its stdin ends and relays its exit code', async () => { const signalFile = join(tempDir(), 'provider-signal') const { supervisor, exit } = launchSupervisor( - { lifetime: 'one-shot' }, + { lifetime: 'one-shot', stdinEndGraceMs: 100 }, { ORCA_TEST_PROVIDER_SIGNAL_FILE: signalFile, - ORCA_TEST_PROVIDER_ANSWER_DELAY_MS: String(PROVIDER_STDIN_END_GRACE_MS * 1.5) + ORCA_TEST_PROVIDER_ANSWER_DELAY_MS: '250' }, { command: process.execPath, args: ['-e', ONE_SHOT_PROVIDER] } ) @@ -536,12 +551,13 @@ describe.runIf(process.platform !== 'win32')('POSIX provider supervisor processe ] as const)( 'reaps a one-shot that ignores its stdin end when its owner %s', async (_, signal, env) => { + const stdinEndGraceMs = 100 const { owner, pids } = await launchUnderOwner( - { lifetime: 'one-shot', sigtermGraceMs: 300 }, + { lifetime: 'one-shot', stdinEndGraceMs, sigtermGraceMs: 300 }, { ORCA_TEST_OWNER_ENDS_STDIN: '1', ...env } ) // Past the session grace: a stdin end alone must not have stopped it. - await new Promise((resolve) => setTimeout(resolve, PROVIDER_STDIN_END_GRACE_MS + 300)) + await new Promise((resolve) => setTimeout(resolve, stdinEndGraceMs + 150)) expect(alive(-pids.provider)).toBe(true) owner.kill(signal) diff --git a/src/main/runtime/orca-runtime-files-artifact-facade.test.ts b/src/main/runtime/orca-runtime-files-artifact-facade.test.ts index cf4a01ac462..c2ad9bd35cc 100644 --- a/src/main/runtime/orca-runtime-files-artifact-facade.test.ts +++ b/src/main/runtime/orca-runtime-files-artifact-facade.test.ts @@ -1,6 +1,7 @@ import { afterEach, expect, it, vi } from 'vitest' import { RuntimeFileCommands } from './orca-runtime-files' import { OrcaRuntimeService } from './orca-runtime' +import { RuntimeFileAccess } from './files/file-access' import { RuntimeFileReads } from './files/file-reads' import { RuntimeTerminalArtifacts } from './terminal/artifacts/terminal-artifacts' import type { RuntimeFileCommandHost } from './files/file-command-host' @@ -25,7 +26,7 @@ const reachedArtifactOwner = (): never => { afterEach(() => vi.restoreAllMocks()) -it('preserves artifact facade arity, optional slots, and the same artifact receiver', () => { +it('preserves artifact optional slots and the same artifact receiver', () => { const read = vi .spyOn(RuntimeTerminalArtifacts.prototype, 'readTerminalArtifactFile') .mockImplementation(reachedArtifactOwner) @@ -40,13 +41,6 @@ it('preserves artifact facade arity, optional slots, and the same artifact recei .mockImplementation(reachedArtifactOwner) const commands = new RuntimeFileCommands(host) - expect([ - commands.readTerminalArtifactFile.length, - commands.readTerminalArtifactPreview.length, - commands.writeTerminalArtifactFile.length, - commands.revokeTerminalFileGrantsForClient.length - ]).toEqual([4, 5, 5, 1]) - expect(() => commands.readTerminalArtifactFile('worktree', 'grant', '/artifact')).toThrow( 'artifact_owner_reached' ) @@ -91,81 +85,37 @@ it('preserves artifact facade arity, optional slots, and the same artifact recei } }) -const originalMethodMetadata = [ - ['openMobileFile', 4, true], - ['openMobileDiff', 4, true], - ['listRuntimeMarkdownDocuments', 1, true], - ['pathsExistRuntimeFiles', 2, true], - ['statRuntimeFile', 2, true], - ['readMobileFile', 2, true], - ['readFileExplorerPreview', 3, true], - ['readDocPreviewFile', 6, true], - ['readFileExplorerChunk', 4, true], - ['writeFileExplorerFile', 6, true], - ['writeFileExplorerFileBase64', 6, true], - ['writeFileExplorerFileBase64Chunk', 7, true], - ['createFileExplorerFile', 5, true], - ['createFileExplorerDir', 5, true], - ['createFileExplorerDirNoClobber', 5, true], - ['commitFileExplorerUpload', 6, true], - ['renameFileExplorerPath', 6, true], - ['copyFileExplorerPath', 6, true], - ['deleteFileExplorerPath', 6, true], - ['readFileExplorerDir', 2, true], - ['watchFileExplorer', 2, true], - ['closeFileExplorerWatchersForPath', 2, true], - ['restoreFileExplorerWatchersAfterFailedRemoval', 2, true], - ['forgetFileExplorerWatchersAfterRemoval', 2, false], - ['resolveTerminalPath', 7, true] -] as const +it.each(['command facade', 'installed runtime'])( + 'forwards omitted file arguments and optional positions through the %s', + (surface) => { + const returned: ReturnType = Promise.resolve({ + worktree: 'worktree', + relativePath: 'file', + kind: 'text', + opened: true + }) + const open = vi.spyOn(RuntimeFileAccess.prototype, 'openMobileFile').mockReturnValue(returned) + const commands = + surface === 'command facade' ? new RuntimeFileCommands(host) : new OrcaRuntimeService(null) + const position = { line: 12, column: 5 } -it('preserves the original public command arities and native async functions', () => { - const commands = new RuntimeFileCommands(host) - for (const [name, arity, async] of originalMethodMetadata) { - expect(commands[name].length, name).toBe(arity) - expect(Object.prototype.toString.call(commands[name]), name).toBe( - async ? '[object AsyncFunction]' : '[object Function]' - ) - } -}) - -it('installs the original public file command metadata on the actual runtime', () => { - const runtime = new OrcaRuntimeService(null) - const commands = { - openMobileFile: runtime.openMobileFile, - openMobileDiff: runtime.openMobileDiff, - listRuntimeMarkdownDocuments: runtime.listRuntimeMarkdownDocuments, - pathsExistRuntimeFiles: runtime.pathsExistRuntimeFiles, - statRuntimeFile: runtime.statRuntimeFile, - readMobileFile: runtime.readMobileFile, - readFileExplorerPreview: runtime.readFileExplorerPreview, - readDocPreviewFile: runtime.readDocPreviewFile, - readFileExplorerChunk: runtime.readFileExplorerChunk, - writeFileExplorerFile: runtime.writeFileExplorerFile, - writeFileExplorerFileBase64: runtime.writeFileExplorerFileBase64, - writeFileExplorerFileBase64Chunk: runtime.writeFileExplorerFileBase64Chunk, - createFileExplorerFile: runtime.createFileExplorerFile, - createFileExplorerDir: runtime.createFileExplorerDir, - createFileExplorerDirNoClobber: runtime.createFileExplorerDirNoClobber, - commitFileExplorerUpload: runtime.commitFileExplorerUpload, - renameFileExplorerPath: runtime.renameFileExplorerPath, - copyFileExplorerPath: runtime.copyFileExplorerPath, - deleteFileExplorerPath: runtime.deleteFileExplorerPath, - readFileExplorerDir: runtime.readFileExplorerDir, - watchFileExplorer: runtime.watchFileExplorer, - resolveTerminalPath: runtime.resolveTerminalPath - } - for (const [name, arity, async] of originalMethodMetadata) { - if (!(name in commands)) { - continue + expect(commands.openMobileFile('worktree', 'file')).toBe(returned) + expect(commands.openMobileFile('worktree', 'file', 'caller')).toBe(returned) + expect(commands.openMobileFile('worktree', 'file', undefined, position)).toBe(returned) + expect(commands.openMobileFile('worktree', 'file', undefined, undefined)).toBe(returned) + expect(open.mock.calls).toEqual([ + ['worktree', 'file'], + ['worktree', 'file', 'caller'], + ['worktree', 'file', undefined, position], + ['worktree', 'file', undefined, undefined] + ]) + const owner = open.mock.contexts[0] + expect(owner).toBeInstanceOf(RuntimeFileAccess) + for (const context of open.mock.contexts) { + expect(context).toBe(owner) } - const installed = Object.getOwnPropertyDescriptor(commands, name)?.value - expect(installed.length, name).toBe(arity) - expect(Object.prototype.toString.call(installed), name).toBe( - async ? '[object AsyncFunction]' : '[object Function]' - ) } -}) +) it('preserves omitted argument slots, owner receivers, and the returned promise', () => { const returned: ReturnType = Promise.resolve({ diff --git a/src/main/ssh/orcad-state-mutation-fence-heartbeat.test.ts b/src/main/ssh/orcad-state-mutation-fence-heartbeat.test.ts index 5896eda7ec0..fdc44d078a7 100644 --- a/src/main/ssh/orcad-state-mutation-fence-heartbeat.test.ts +++ b/src/main/ssh/orcad-state-mutation-fence-heartbeat.test.ts @@ -1,6 +1,6 @@ /** * A state mutation can outlast the activation fence's stale window, so it keeps the fence - * fresh while it runs. Run for real with a one-second beat and the real steal command. + * fresh while it runs. Run a short real heartbeat and the real steal command. */ import { existsSync, @@ -24,6 +24,8 @@ import { getRemoteHostPlatform } from './ssh-remote-platform' const posix = getRemoteHostPlatform('linux-x64') const STALE_SECONDS = 3 +const HEARTBEAT_SECONDS = 0.1 +const OBSERVE_HEARTBEATS_MS = 300 async function sh(command: string): Promise { return (await runProcess({ program: '/bin/sh', args: ['-c', command] })).stdout.trim() @@ -44,7 +46,10 @@ describe.skipIf(process.platform === 'win32')( }) // The mutation runs under the fence its run holds, generation `holder-1`. const mutation = (script: string): string => - serializedStateMutationCommand(base, script, 1, { lockDir: fence, token: 'holder-1' }) + serializedStateMutationCommand(base, script, HEARTBEAT_SECONDS, { + lockDir: fence, + token: 'holder-1' + }) afterEach(() => { rmSync(base, { recursive: true, force: true }) }) @@ -66,15 +71,15 @@ describe.skipIf(process.platform === 'win32')( .toBeTruthy() await backdate() - await pause(2_500) + await expect.poll(age, { timeout: 5_000, interval: 25 }).toBeLessThan(STALE_SECONDS) expect(age()).toBeLessThan(STALE_SECONDS) expect(await steal()).toBe('BUSY') // The host process dies (OOM, reboot of the session): nothing refreshes the fence now. process.kill(Number(readFileSync(pidFile, 'utf8').trim()), 'SIGKILL') - await pause(1_500) + await pause(OBSERVE_HEARTBEATS_MS) await backdate() - await pause(2_500) + await pause(OBSERVE_HEARTBEATS_MS) expect(age()).toBeGreaterThan(STALE_SECONDS) expect(await steal()).toMatch(/OK$/u) } finally { @@ -85,10 +90,14 @@ describe.skipIf(process.platform === 'win32')( it('stops refreshing a fence another run took over mid-mutation, and keeps its token', async () => { const run = spawnProcess({ program: '/bin/sh', args: ['-c', mutation('sleep 6')] }) try { - await pause(1_500) + await expect + .poll(() => existsSync(join(base, 'orcad-state-mutation.lock', 'pid'))) + .toBe(true) + await backdate() + await expect.poll(age, { timeout: 5_000, interval: 25 }).toBeLessThan(STALE_SECONDS) writeFileSync(join(fence, ORCAD_FENCE_OWNER_FILENAME), 'successor') await backdate() - await pause(2_500) + await pause(OBSERVE_HEARTBEATS_MS) expect(age()).toBeGreaterThan(STALE_SECONDS) expect(readFileSync(join(fence, ORCAD_FENCE_OWNER_FILENAME), 'utf8')).toBe('successor') } finally { @@ -105,13 +114,13 @@ describe.skipIf(process.platform === 'win32')( }, 20_000) it('stops refreshing once the mutation finishes, and never creates a missing fence', async () => { - await sh(mutation('sleep 2')) + await sh(mutation('sleep 0.3')) await backdate() - await pause(2_500) + await pause(OBSERVE_HEARTBEATS_MS) expect(age()).toBeGreaterThan(STALE_SECONDS) rmSync(fence, { recursive: true }) - await sh(serializedStateMutationCommand(base, 'sleep 2', 1, null)) + await sh(serializedStateMutationCommand(base, ':', HEARTBEAT_SECONDS, null)) expect(existsSync(fence)).toBe(false) }, 20_000) } diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx index d2a19807321..f4d141ea388 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx @@ -113,6 +113,16 @@ describe('jumping from the rail while following the end', () => { let lastEventScrollTop = 0 beforeEach(() => { + vi.useFakeTimers({ + toFake: [ + 'Date', + 'performance', + 'setTimeout', + 'clearTimeout', + 'requestAnimationFrame', + 'cancelAnimationFrame' + ] + }) const undoLayout = stubLayout({ scrollGeometry: true, offsetChain: true }) const stubbedHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight') restore = [ @@ -169,6 +179,7 @@ describe('jumping from the rail while following the end', () => { }) afterEach(() => { + vi.useRealTimers() vi.restoreAllMocks() Object.defineProperty(layout, 'aboveTranscriptPx', { configurable: true, @@ -202,8 +213,7 @@ describe('jumping from the rail while following the end', () => { fireEvent.scroll(element) } await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 0)) - await new Promise((resolve) => requestAnimationFrame(() => resolve())) + vi.advanceTimersByTime(16) }) } diff --git a/src/renderer/src/components/onboarding/OnboardingFlow.test.tsx b/src/renderer/src/components/onboarding/OnboardingFlow.test.tsx index 97dbc731a05..312a82c3b7b 100644 --- a/src/renderer/src/components/onboarding/OnboardingFlow.test.tsx +++ b/src/renderer/src/components/onboarding/OnboardingFlow.test.tsx @@ -218,7 +218,7 @@ describe('OnboardingFlow', () => { ) }) - it('renders onboarding inside a centered modal shell', () => { + it('exposes an accessible onboarding dialog', () => { const html = renderOnboardingFlow({ onboarding: getDefaultOnboardingState(), onOnboardingChange: vi.fn() @@ -226,11 +226,5 @@ describe('OnboardingFlow', () => { expect(html).toContain('role="dialog"') expect(html).toContain('aria-modal="true"') - expect(html).toContain('data-onboarding-modal="true"') - expect(html).toContain('h-[calc(100vh-2rem)]') - expect(html).toContain('rounded-xl') - expect(html).toContain('h-7 w-auto shrink-0 invert dark:invert-0') - expect(html).not.toContain('min-h-screen') - expect(html).not.toContain('background-color:#12181e') }) }) diff --git a/src/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.test.ts b/src/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.test.ts deleted file mode 100644 index e2abc2db64c..00000000000 --- a/src/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { - getNextTerminalReadyRetryAttempt, - READY_MAX_ATTEMPTS -} from './OnboardingInlineCommandTerminal' - -vi.mock('@/components/terminal-pane/TerminalPane', () => ({ - default: () => { - throw new Error('This suite must not render a terminal pane') - } -})) - -describe('getNextTerminalReadyRetryAttempt', () => { - it('stops scheduling readiness checks after the capped number of attempts', () => { - let attempt = 0 - let scheduledRetries = 0 - - while (true) { - const nextAttempt = getNextTerminalReadyRetryAttempt(attempt) - if (nextAttempt === null) { - break - } - scheduledRetries += 1 - attempt = nextAttempt - } - - expect(scheduledRetries).toBe(READY_MAX_ATTEMPTS) - expect(attempt).toBe(READY_MAX_ATTEMPTS) - expect(getNextTerminalReadyRetryAttempt(READY_MAX_ATTEMPTS)).toBeNull() - }) -}) diff --git a/src/renderer/src/components/settings/GeneralPane.test.ts b/src/renderer/src/components/settings/GeneralPane.test.ts index 05c963feed3..e5d5f6a8a94 100644 --- a/src/renderer/src/components/settings/GeneralPane.test.ts +++ b/src/renderer/src/components/settings/GeneralPane.test.ts @@ -9,6 +9,12 @@ import { } from './GeneralPane' import { matchesSettingsSearch } from './settings-search' +vi.mock('@/store', () => ({ + useAppStore: () => { + throw new Error('Pure settings helpers must not access the app store') + } +})) + vi.mock('@/components/terminal-pane/TerminalPane', () => ({ default: () => { throw new Error('This suite must not render a terminal pane') diff --git a/src/renderer/src/components/settings/browser-cookie-import-label.test.ts b/src/renderer/src/components/settings/browser-cookie-import-label.test.ts deleted file mode 100644 index 108a2e4c8d2..00000000000 --- a/src/renderer/src/components/settings/browser-cookie-import-label.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest' - -import en from '@/i18n/locales/en.json' -import { i18n, translate } from '@/i18n/i18n' - -// Keys for the "From " import-cookies menu entries. The label is built -// by interpolating the browser name into a single localized string; a regression -// where the name was concatenated directly onto "From" dropped the space and -// rendered e.g. "FromGoogle Chrome". -const IMPORT_FROM_KEYS = [ - 'auto.components.settings.BrowserProfileRow.c5a273a809', - 'auto.components.settings.BrowserUsePane.5301857d88' -] -const LAST_IMPORTED_FROM_KEY = 'auto.components.settings.BrowserUsePane.112f70adc4' - -function catalogEntry(key: string): unknown { - return key.split('.').reduce((node, part) => { - if (typeof node === 'object' && node !== null) { - return (node as Record)[part] - } - return undefined - }, en) -} - -describe('import-cookies "From " label', () => { - beforeEach(async () => { - await i18n.changeLanguage('en') - }) - - it('keeps a space between "From" and the browser name', () => { - for (const key of IMPORT_FROM_KEYS) { - expect(translate(key, 'From {{value0}}', { value0: 'Google Chrome' })).toBe( - 'From Google Chrome' - ) - expect(translate(key, 'From {{value0}}', { value0: 'Safari' })).toBe('From Safari') - } - }) - - it('stores the catalog entry with an interpolation placeholder', () => { - for (const key of IMPORT_FROM_KEYS) { - expect(catalogEntry(key)).toBe('From {{value0}}') - } - expect(catalogEntry(LAST_IMPORTED_FROM_KEY)).toBe('Last imported from {{value0}}') - }) - - it('keeps a space in the last-imported source label', () => { - expect( - translate(LAST_IMPORTED_FROM_KEY, 'Last imported from {{value0}}', { - value0: 'Google Chrome' - }) - ).toBe('Last imported from Google Chrome') - }) -}) diff --git a/src/renderer/src/components/settings/osc52-clipboard-copy.test.ts b/src/renderer/src/components/settings/osc52-clipboard-copy.test.ts deleted file mode 100644 index 2940148b50e..00000000000 --- a/src/renderer/src/components/settings/osc52-clipboard-copy.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, expect, it } from 'vitest' -import en from '@/i18n/locales/en.json' -import es from '@/i18n/locales/es.json' -import ja from '@/i18n/locales/ja.json' -import ko from '@/i18n/locales/ko.json' -import zh from '@/i18n/locales/zh.json' - -// Why assert the catalog rather than the component: en.json is bundled as the `en` -// resource, so a catalog value wins over translate()'s code fallback. Editing a fallback -// alone renders nothing, and nothing else in CI compares the two. -describe('OSC 52 setting copy', () => { - const locales = { en, es, ja, ko, zh } - - for (const [name, locale] of Object.entries(locales)) { - it(`names Zellij and Grok in both ${name} OSC 52 setting descriptions`, () => { - const pane = locale.auto.components.settings.TerminalPane - for (const copy of [pane['69c64a479c'], pane['6e6480a7df']]) { - expect(copy).toContain('Zellij') - expect(copy).toContain('Grok') - } - }) - } - - // The notice is a one-shot the user sees exactly once, so a stale catalog value would - // ship unnoticed. Pin en.json to the code fallbacks verbatim. - it('keeps the migration notice catalog identical to its code fallbacks', () => { - const notice = en.auto.components.terminal.pane.osc52.clipboard.default.on.notice - expect(notice.title).toBe('TUI clipboard writes are now on by default') - expect(notice.description).toBe( - 'Zellij, tmux, Neovim and other terminal programs can now copy to your clipboard. Turn it off in Terminal settings.' - ) - expect(notice.action).toBe('Open Setting') - }) - - for (const [name, locale] of Object.entries({ es, ja, ko, zh })) { - it(`translates the ${name} migration notice rather than leaving English placeholders`, () => { - const notice = locale.auto.components.terminal.pane.osc52.clipboard.default.on.notice - const english = en.auto.components.terminal.pane.osc52.clipboard.default.on.notice - expect(notice.title).not.toBe(english.title) - expect(notice.description).not.toBe(english.description) - // Product names must survive translation — they are how the user recognizes the change. - expect(notice.description).toContain('Zellij') - }) - } -}) diff --git a/src/renderer/src/components/settings/setting-ownership.test.ts b/src/renderer/src/components/settings/setting-ownership.test.ts deleted file mode 100644 index 2edad0cb3cf..00000000000 --- a/src/renderer/src/components/settings/setting-ownership.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { getSettingOwnershipSummary } from './setting-ownership' - -describe('getSettingOwnershipSummary', () => { - it('documents Source Control AI as client defaults with host-scoped model choices', () => { - const summary = getSettingOwnershipSummary('sourceControlAiDefaults') - - expect(summary.ownership).toBe('client-default') - expect(summary.description).toContain('shared by this client') - expect(summary.description).toContain('model choices and discovery stay scoped to the host') - }) - - it('documents repository Source Control AI as project-host setup scoped', () => { - const summary = getSettingOwnershipSummary('repositorySourceControlAi') - - expect(summary.ownership).toBe('project-host-setup') - expect(summary.description).toContain('this project setup') - }) - - it('documents agent launch defaults as owned by the selected execution host', () => { - const summary = getSettingOwnershipSummary('agentLaunchDefaults') - - expect(summary.ownership).toBe('host-collection') - expect(summary.description).toContain('saved on the selected host') - }) - - it('keeps workspace directories and provider accounts explicitly host-aware', () => { - expect(getSettingOwnershipSummary('workspaceDirectory').ownership).toBe('host-override') - expect(getSettingOwnershipSummary('providerAccounts').ownership).toBe('provider-host') - }) - - it('documents quick commands as host-owned collections', () => { - const summary = getSettingOwnershipSummary('terminalQuickCommands') - - expect(summary.ownership).toBe('host-collection') - expect(summary.description).toContain('selected Orca host') - expect(summary.description).toContain('remain available in remote workspaces') - }) -}) diff --git a/src/renderer/src/components/sidebar/wsl-host-copy-catalog.test.ts b/src/renderer/src/components/sidebar/wsl-host-copy-catalog.test.ts deleted file mode 100644 index 5c966e9c604..00000000000 --- a/src/renderer/src/components/sidebar/wsl-host-copy-catalog.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { readFileSync } from 'node:fs' -import { join } from 'node:path' -import { describe, expect, it } from 'vitest' -import { z } from 'zod' - -// Why read the catalog: the shipped build resolves a key from en.json, not from the inline -// default, so changing copy under an existing key leaves the old words on screen. -const HostRemoveDialogCopySchema = z.object({ - auto: z.object({ - components: z.object({ - sidebar: z.object({ HostRemoveDialog: z.record(z.string(), z.string()) }) - }) - }) -}) - -const dialog = HostRemoveDialogCopySchema.parse( - JSON.parse(readFileSync(join(__dirname, '../../i18n/locales/en.json'), 'utf8')) -).auto.components.sidebar.HostRemoveDialog - -describe('WSL host removal copy', () => { - it('ships the copy that says removal ends the host’s terminals', () => { - expect(dialog.wslHostEndsTerminals).toBe( - 'Removes {{value0}} from Orca, ends its open terminals and stops Orca’s server in that distro. Files in the distro are untouched.' - ) - expect(dialog).not.toHaveProperty('wslHost') - }) -}) diff --git a/src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts b/src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts index 57c31c14722..e9c67415fee 100644 --- a/src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts +++ b/src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts @@ -46,7 +46,10 @@ vi.mock('@/runtime/local-runtime-capabilities', async (importOriginal) => ({ vi.mock('@/runtime/host-structured-agents', async (importOriginal) => ({ ...(await importOriginal>()), readHostStructuredAgents: () => - ['claude', 'codex', 'grok', 'omp', 'opencode', 'pi'].map((agent) => ({ agent, capabilities: {} })) + ['claude', 'codex', 'grok', 'omp', 'opencode', 'pi'].map((agent) => ({ + agent, + capabilities: {} + })) })) vi.mock('@/lib/renderer-app-platform', () => ({ getRendererAppPlatform: () => mocks.platform })) diff --git a/src/renderer/src/i18n/ja-technical-literal-mistranslations.test.ts b/src/renderer/src/i18n/ja-technical-literal-mistranslations.test.ts deleted file mode 100644 index c2134ebf818..00000000000 --- a/src/renderer/src/i18n/ja-technical-literal-mistranslations.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Guards literal-sense mistranslations (Push as 押す, run as 走る, Linear as - * リニア, ...) so bootstrap re-translation cannot silently regress them. - */ -import { describe, expect, it } from 'vitest' -import ja from './locales/ja.json' - -function collectValues(node: unknown, out: string[] = []): string[] { - if (typeof node === 'string') { - out.push(node) - } else if (node && typeof node === 'object') { - for (const value of Object.values(node)) { - collectValues(value, out) - } - } - return out -} - -function findByKey(node: unknown, key: string): string | undefined { - if (!node || typeof node !== 'object') { - return undefined - } - if (Array.isArray(node)) { - for (const item of node) { - const found = findByKey(item, key) - if (found !== undefined) { - return found - } - } - return undefined - } - const record = node as Record - if (typeof record[key] === 'string') { - return record[key] as string - } - for (const value of Object.values(record)) { - const found = findByKey(value, key) - if (found !== undefined) { - return found - } - } - return undefined -} - -describe('ja technical literal / sense fixes', () => { - it('translates the source control Push action as プッシュ, not 押す', () => { - expect(findByKey(ja, '95550cff15')).toBe('プッシュ') - }) - - it('translates the source control Pull action as プル, not 引く', () => { - expect(findByKey(ja, 'd64292a938')).toBe('プル') - }) - - it.each([ - ['走る', 'run as running on foot'], - ['走っている', 'running as running on foot'], - ['走らないで', "Don't run as don't run on foot"], - ['逃走', 'missed run as escape'], - ['労働者', 'worker as a labourer'], - ['対立', 'merge conflict as a dispute'], - ['使用法', 'usage as instructions for use'], - ['リニア', 'the Linear product name as the word linear'], - ['シャチ', 'the orca-linear skill name as the animal'] - ])('never renders %s (%s)', (literal) => { - expect(collectValues(ja).filter((value) => value.includes(literal))).toEqual([]) - }) -}) diff --git a/src/renderer/src/i18n/ko-ui-semantic-mistranslations.test.ts b/src/renderer/src/i18n/ko-ui-semantic-mistranslations.test.ts deleted file mode 100644 index 7f0e40ddea4..00000000000 --- a/src/renderer/src/i18n/ko-ui-semantic-mistranslations.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import fs from 'node:fs' - -import { describe, expect, it } from 'vitest' -import ko from './locales/ko.json' - -const correctedValues = { - 'auto.components.settings.AutoRenameBranchFromWorkSetting.d9b65054ef': - ') 작업을 요약하는 짧은 이름으로 변경됩니다. Orca가 직접 이름 붙인 브랜치만 이름을 바꾸며, 푸시된 후에는 이름을 바꾸지 않습니다.', - 'auto.components.settings.source.control.action.recipe.options.commitMessage': - '스테이징된 변경 사항에서 commit 메시지를 생성합니다.', - 'auto.components.settings.DevToolsPane.orcaCloudDescription': - '자사 클라우드 로그인의 개발자 전용 미리보기. 프로덕션에서는 숨겨집니다. 개발 환경에서는 ORCA_CLOUD_API_URL과 ORCA_CLOUD_CLIENT_ID가 설정되면 사이드바 계정 전환기에도 표시됩니다.', - 'auto.components.settings.EphemeralVmsPane.whatTitle': '이 스킬로 함께 하는 작업', - 'auto.components.settings.EphemeralVmsPane.recipes': '레시피', - 'auto.components.right.sidebar.SourceControl.a5e5a11090': - '모든 변경 사항 취소 실패 — 취소 전에 파일의 스테이징을 해제하지 못했습니다.', - 'auto.components.right.sidebar.SourceControl.6d7f2a47e5': '폴더의 변경 사항 취소', - 'auto.components.right.sidebar.source.control.discard.confirmation.40e9357b2a': - '이렇게 하면 HEAD에서 파일을 복원하고 파일 삭제를 취소합니다. 이 작업은 취소할 수 없습니다.', - 'auto.components.right.sidebar.source.control.primary.action.5a477d80cb': - '모든 변경 사항 스테이징', - 'auto.components.settings.DeveloperPermissionsPane.f903bf20b5': - '터미널과 개발 도구가 로컬 네트워크의 서비스에 연결할 수 있도록 허용합니다. macOS는 이 권한의 현재 상태를 Orca에 보고하지 않습니다.', - 'auto.components.sidebar.AddRemoteHostDialog.sshImportSynced': - '{{value0}} 호스트{{value1}}을(를) Orca에 추가했습니다.', - 'auto.components.editor.CheckRunJobs.1c0a4d7e02': '성공', - 'components.native-chat.approval.allow': '허용', - 'components.native-chat.approval.deny': '거부', - 'sessionHistory.settings.computersConsent': - '각 컴퓨터는 자체 에이전트 대화와 도구 출력의 검색 가능한 사본을 보관합니다. 이 데이터는 해당 컴퓨터 밖으로 전송되지 않습니다.', - 'sessionHistory.settings.open': '열기', - 'auto.components.settings.contrast.custom': '사용자 지정', - 'components.settings.TerminalInteraction.copyTrimsGutterDescription': - '복사한 텍스트에서 에이전트 출력의 왼쪽 여백을 제거합니다. 선택한 모든 줄에 공통된 들여쓰기만 제거됩니다.', - 'auto.components.settings.TerminalAppearanceSection.db270cc9a9': '커서 모양', - 'auto.components.settings.TerminalAppearanceSection.b9f1804422': '커서 불투명도', - 'auto.components.settings.TerminalWindowSection.a2d9f095a7': '커서 텍스트' -} as const - -function getLocaleValue(path: string): unknown { - return path.split('.').reduce((node, segment) => { - if (!node || typeof node !== 'object' || Array.isArray(node)) { - return undefined - } - return (node as Record)[segment] - }, ko) -} - -describe('Korean UI semantic mistranslation fixes', () => { - it('keeps the corrected values in the Korean catalog', () => { - for (const [path, expected] of Object.entries(correctedValues)) { - expect(getLocaleValue(path), path).toBe(expected) - } - }) - - it('keeps the corrected values in the translation overrides', () => { - const overrides = JSON.parse( - fs.readFileSync( - new URL('../../../../config/scripts/locale-ko-key-overrides.json', import.meta.url), - 'utf8' - ) - ) as Record - - for (const [path, expected] of Object.entries(correctedValues)) { - expect(overrides[path]?.ko, path).toBe(expected) - } - }) -}) diff --git a/src/renderer/src/i18n/locale-english-regression.test.ts b/src/renderer/src/i18n/locale-english-regression.test.ts deleted file mode 100644 index 8f120f1d23c..00000000000 --- a/src/renderer/src/i18n/locale-english-regression.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * #10770 merged from a base predating #8549, so its stale locale copies - * overwrote ~185 already-translated strings per locale back to English. A - * present catalog value always beats the English `translate()` fallback, so - * those strings render English at runtime with nothing to signal the loss. - * - * Later translation passes (#11205 and friends) independently re-covered most of - * ja/ko/zh, so the re-applied set is only what is still English at HEAD: es 182, - * zh 55, ja 32, ko 17. The keys sampled below stayed English in all four. - * - * These are keys whose English source is UNCHANGED, so an English catalog value - * can only be a revert. Locale-wide coverage is deliberately not asserted here: - * genuinely-untranslated new keys are normal, and a blanket gate would fail on - * every future English-source addition. - */ -import { describe, expect, it } from 'vitest' -import en from './locales/en.json' -import es from './locales/es.json' -import ja from './locales/ja.json' -import ko from './locales/ko.json' -import zh from './locales/zh.json' - -const catalogs = { es, ja, ko, zh } - -const WORKSPACE_CLEANUP_BROWSE_TRANSLATIONS = { - zh: { - selectionWithheldOne: '当前筛选条件隐藏了 1 个已选工作区,已取消选择。', - selectionWithheld: '当前筛选条件隐藏了 {{value0}} 个已选工作区,已取消选择。', - selectAllCountOne: '选择 1 个通过安全检查的工作区', - selectAllCount: '选择全部 {{value0}} 个通过安全检查的工作区' - }, - ko: { - selectionWithheldOne: - '선택한 워크스페이스 1개가 현재 필터에 의해 숨겨져 선택이 해제되었습니다.', - selectionWithheld: - '선택한 워크스페이스 {{value0}}개가 현재 필터에 의해 숨겨져 선택이 해제되었습니다.', - selectAllCountOne: '안전 검사를 통과한 워크스페이스 1개 선택', - selectAllCount: '안전 검사를 통과한 워크스페이스 {{value0}}개 모두 선택' - } -} as const - -function lookup(catalog: unknown, key: string): string | undefined { - const value = key - .split('.') - .reduce( - (node, part) => - node && typeof node === 'object' ? (node as Record)[part] : undefined, - catalog - ) - return typeof value === 'string' ? value : undefined -} - -// Sampled across the reverted set: an unconditional sidebar filter row, plugin -// command failures on two surfaces, a settings description, and a status-bar -// metric — so a partial re-revert cannot pass by covering one namespace. -// (workingSetDescription was re-translated upstream for ja/ko; it still guards -// es/zh, and every other key here was English in all four.) -const REVERTED_KEYS = [ - 'auto.components.sidebar.SidebarFilter.detachedHead', - 'auto.App.pluginCommandFailed', - 'auto.components.WorktreeJumpPalette.pluginCommandFailed', - 'auto.hooks.useSettingsNavigationMetadata.pluginsDescription', - 'auto.components.status.bar.resource.memory.metric.workingSetDescription', - 'auto.components.settings.EphemeralVmsPane.recipesHelp' -] - -describe('locale catalogs reverted by a stale branch base (#10770)', () => { - it.each(Object.entries(catalogs))('%s translates the reverted keys', (_code, catalog) => { - for (const key of REVERTED_KEYS) { - const english = lookup(en, key) - const localized = lookup(catalog, key) - expect(english, `${key} missing from en.json`).toBeDefined() - expect(localized, `${key} missing from catalog`).toBeDefined() - expect(localized?.trim()).not.toBe('') - expect(localized, `${key} fell back to the English source`).not.toBe(english) - } - }) - - // The same stale base regressed English itself: the catalog kept pre-plugins - // copy while EphemeralVmsPane.tsx already shipped the newer sentence, and a - // present catalog value wins over the source fallback. - it('keeps the English catalog in step with its live source string', () => { - expect(lookup(en, 'auto.components.settings.EphemeralVmsPane.recipesHelp')).toBe( - 'Recipes from orca.yaml and enabled plugins show up here, ready to launch a workspace on.' - ) - }) - - it.each(Object.entries(WORKSPACE_CLEANUP_BROWSE_TRANSLATIONS))( - '%s retains the merged workspace cleanup browse translations', - (code, translations) => { - const catalog = code === 'zh' ? zh : ko - for (const [key, expected] of Object.entries(translations)) { - expect(lookup(catalog, `components.workspace.cleanup.browse.${key}`)).toBe(expected) - } - } - ) -}) diff --git a/src/renderer/src/i18n/native-chat-locales.test.ts b/src/renderer/src/i18n/native-chat-locales.test.ts deleted file mode 100644 index e8652e66a5f..00000000000 --- a/src/renderer/src/i18n/native-chat-locales.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { describe, expect, it } from 'vitest' -import en from './locales/en.json' -import es from './locales/es.json' -import fr from './locales/fr.json' -import ja from './locales/ja.json' -import ko from './locales/ko.json' -import zh from './locales/zh.json' - -const localizedCatalogs = { es, ja, ko, zh } -const englishSetting = en.auto.components.settings.ExperimentalPane.nativeChat -const englishSearch = en.auto.components.settings.experimental.search.nativeChat -const englishComposer = en.components['native-chat'].composer -const localizedEffortValues = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'] as const - -describe('native chat locale copy', () => { - it.each(Object.entries({ en, es, fr, ja, ko, zh }))( - '%s covers chat naming controls and ordinary labels', - (_code, catalog) => { - const names = catalog.settings.chat.names - expect(Object.keys(names).sort()).toEqual(Object.keys(en.settings.chat.names).sort()) - for (const value of Object.values(names)) { - expect(value.trim()).not.toBe('') - } - expect(names.description).toContain('Claude Chat') - expect(names.description).toContain('Codex Chat') - expect(catalog.auto.components.settings.Settings['17bdee4ff1']).not.toContain('Git') - expect(catalog.auto.components.settings.Settings['43b68e10f0']).not.toContain('Git') - } - ) - - it.each(Object.entries(localizedCatalogs))( - '%s falls back to current Chat UI copy until it is translated', - (_code, catalog) => { - const setting = catalog.auto.components.settings.ExperimentalPane.nativeChat - const search = catalog.auto.components.settings.experimental.search.nativeChat - expect(englishSetting.description).toContain('Chat UI') - expect(englishSetting.copy).toContain('structured chat') - expect(englishSearch.description).toContain('Chat UI') - expect(setting).not.toHaveProperty('description') - expect(setting).not.toHaveProperty('copy') - expect(setting).not.toHaveProperty('defaultCopy') - expect(search).not.toHaveProperty('description') - expect(search.grok).toBe('grok') - const composer = catalog.components['native-chat'].composer - for (const key of [ - 'model', - 'effort', - 'fastMode', - 'thinking', - 'sessionOptions', - 'chooseInAgentPicker', - 'toggleOption', - 'sentNotConfirmed' - ] as const) { - expect(composer[key].trim()).not.toBe('') - expect(composer[key]).not.toBe(englishComposer[key]) - } - for (const key of ['fast', ...localizedEffortValues] as const) { - expect(composer.optionValue[key].trim()).not.toBe('') - expect(composer.optionValue[key]).not.toBe(englishComposer.optionValue[key]) - } - for (const key of ['minimal', 'medium', 'xhigh'] as const) { - expect(composer.optionValueShort[key].trim()).not.toBe('') - expect(composer.optionValueShort[key]).not.toBe(englishComposer.optionValueShort[key]) - } - } - ) -}) - -describe('account-aware Send fixes', () => { - it.each(Object.entries({ en, es, fr, ja, ko, zh }))( - '%s includes the same fixes for Send and post-send failures', - (_locale, catalog) => { - const words = catalog.components['native-chat'].failureWords - expect(words.claudeSystemNotSignedIn).toContain('`{{loginCommand}}`') - expect(words.codexSystemNotSignedIn).toContain('`{{loginCommand}}`') - expect(words.claudeManagedNotSignedIn).not.toContain('/login') - expect(words.codexManagedNotSignedIn).not.toContain('`codex login`') - expect(words.claudeManagedNotSignedIn).not.toContain('{{loginCommand}}') - expect(words.codexManagedNotSignedIn).not.toContain('{{loginCommand}}') - expect(words.agentCommandNotSignedIn).toContain('{{agent}}') - expect(words.agentCommandNotSignedIn).toContain('`{{loginCommand}}`') - expect(words.interactiveAgentNotSignedIn).toContain('`{{slashCommand}}`') - expect(words.cliMissing).toContain('{{agent}}') - for (const key of ['claudeManagedNotSignedIn', 'codexManagedNotSignedIn'] as const) { - expect(words[key].trim()).not.toBe('') - } - } - ) -}) diff --git a/src/renderer/src/i18n/relative-duration-copy-localization.test.ts b/src/renderer/src/i18n/relative-duration-copy-localization.test.ts deleted file mode 100644 index 65e5175e639..00000000000 --- a/src/renderer/src/i18n/relative-duration-copy-localization.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Guards the localized relative-duration copy for the Cmd+J session-age badge and the - * native-chat turn row. The English source values ("{{value0}}m/h/d", "Working for N") are - * ambiguous, so bootstrap re-translation rendered them as a metre (米), a location particle - * (で / 에서), or a French infinitive. These are the reviewed values. - */ -import { describe, expect, it } from 'vitest' -import fr from './locales/fr.json' -import ja from './locales/ja.json' -import ko from './locales/ko.json' -import zh from './locales/zh.json' - -const catalogs = { fr, ja, ko, zh } as const - -function isStringRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value) -} - -function getByPath(node: unknown, path: string): string | undefined { - let current: unknown = node - for (const key of path.split('.')) { - if (!isStringRecord(current)) { - return undefined - } - current = current[key] - } - return typeof current === 'string' ? current : undefined -} - -type Locale = keyof typeof catalogs - -const AGE_PATHS = 'components.cmd-j.paletteSessionAge' -const STATUS_PATHS = 'components.native-chat.status' - -const cases: [Locale, string, string][] = [ - // Cmd+J session-age badge: the unit must be a real duration word, not a bare Latin "m/h/d". - ['zh', `${AGE_PATHS}.minutes`, '{{value0}}分'], - ['zh', `${AGE_PATHS}.hours`, '{{value0}}小时'], - ['zh', `${AGE_PATHS}.days`, '{{value0}}天'], - ['zh', `${AGE_PATHS}.underOneMinute`, '<1分钟'], - ['ja', `${AGE_PATHS}.hours`, '{{value0}}時間'], - ['ja', `${AGE_PATHS}.days`, '{{value0}}日'], - ['ja', `${AGE_PATHS}.underOneMinute`, '<1分'], - ['ko', `${AGE_PATHS}.minutes`, '{{value0}}분'], - ['ko', `${AGE_PATHS}.hours`, '{{value0}}시간'], - ['ko', `${AGE_PATHS}.days`, '{{value0}}일'], - // Native-chat turn row: elapsed time, not a recipient — "working/worked for ", - // never the location reading ("works at/for N"). - ['zh', `${STATUS_PATHS}.workingFor`, '已工作 {{value0}}'], - ['zh', `${STATUS_PATHS}.workedFor`, '工作了 {{value0}}'], - ['ja', `${STATUS_PATHS}.workingFor`, '{{value0}} 処理中'], - ['ja', `${STATUS_PATHS}.workedFor`, '{{value0}} で完了'], - ['ko', `${STATUS_PATHS}.workingFor`, '{{value0}} 작업 중'], - ['ko', `${STATUS_PATHS}.workedFor`, '{{value0}} 작업 완료'], - ['fr', `${STATUS_PATHS}.workingFor`, 'En cours depuis {{value0}}'], - ['fr', `${STATUS_PATHS}.workedFor`, 'A travaillé pendant {{value0}}'], - // "turn" is a conversation turn (回合 / 턴 / tour), not a road turn (转弯 / 회전 / virage). - ['zh', `${STATUS_PATHS}.toggleDetails`, '切换回合详情'], - ['ko', `${STATUS_PATHS}.toggleDetails`, '턴 세부정보 전환'], - ['fr', `${STATUS_PATHS}.toggleDetails`, 'Basculer les détails du tour'], - // Workspace-cleanup durations keep the day unit localized too. - ['ja', 'components.workspace.cleanup.browse.chip.idleDays', 'アイドル {{value0}}日以上'], - ['ko', 'components.workspace.cleanup.relativeTime.daysAgo', '{{value0}}일 전'] -] - -describe('relative-duration copy localization', () => { - it.each(cases)('%s %s', (locale, path, expected) => { - expect(getByPath(catalogs[locale], path), path).toBe(expected) - }) -}) diff --git a/src/renderer/src/i18n/settings-status-label-localization.test.ts b/src/renderer/src/i18n/settings-status-label-localization.test.ts deleted file mode 100644 index 1bb8fa312c0..00000000000 --- a/src/renderer/src/i18n/settings-status-label-localization.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Status pills in Settings and Stats used to be built from bare string literals - * inside local helper functions, so they stayed English no matter which language - * was selected. The coverage audit does not reach them: it inspects JSX - * attributes and object properties, not values returned by helpers. - * - * These assertions pin the catalog contract for those helpers. A future refactor - * that drops `translate()` and returns a literal again would leave the key - * missing from `en.json` and fail here. - */ -import { describe, expect, it } from 'vitest' -import en from './locales/en.json' -import es from './locales/es.json' -import ja from './locales/ja.json' -import ko from './locales/ko.json' -import zh from './locales/zh.json' - -const LOCALE_CATALOGS = { es, ja, ko, zh } - -function lookupIn(catalog: unknown, key: string): string | undefined { - const value = key - .split('.') - .reduce( - (node, part) => - node && typeof node === 'object' ? (node as Record)[part] : undefined, - catalog - ) - return typeof value === 'string' ? value : undefined -} - -function lookup(key: string): string | undefined { - return lookupIn(en, key) -} - -function placeholdersOf(value: string): string[] { - return [...new Set(value.match(/\{\{[^}]+\}\}/g) ?? [])].sort() -} - -const REQUIRED_KEYS: Record = { - // DeveloperPermissionsPane — macOS permission rows - 'auto.components.settings.DeveloperPermissionsPane.statusGranted': 'Granted', - 'auto.components.settings.DeveloperPermissionsPane.statusDenied': 'Denied', - 'auto.components.settings.DeveloperPermissionsPane.statusNotRequested': 'Not requested', - 'auto.components.settings.DeveloperPermissionsPane.statusRestricted': 'Restricted', - 'auto.components.settings.DeveloperPermissionsPane.statusUnsupported': 'macOS only', - 'auto.components.settings.DeveloperPermissionsPane.statusEntitled': 'Entitled', - 'auto.components.settings.DeveloperPermissionsPane.statusCheckManually': 'Check manually', - 'auto.components.settings.DeveloperPermissionsPane.actionRequest': 'Request', - 'auto.components.settings.DeveloperPermissionsPane.actionTriggerPrompt': 'Trigger Prompt', - 'auto.components.settings.DeveloperPermissionsPane.actionOpenSettings': 'Open Settings', - // ComputerUsePane — permission rows - 'auto.components.settings.ComputerUsePane.statusGranted': 'Granted', - 'auto.components.settings.ComputerUsePane.statusUnsupported': 'macOS only', - 'auto.components.settings.ComputerUsePane.statusNotEnabled': 'Not enabled', - // Linear setup checklist — unverifiable skill scan (Settings pane + Task Sources card) - 'auto.components.settings.LinearAgentSkillGuide.setupUnverified': 'Cannot verify', - 'auto.components.settings.TaskSourceProviderCard.statusUnverified': 'Cannot verify', - // Source-control CLI integration cards - 'auto.components.settings.cli.source.control.integration.cards.statusConnected': 'Connected', - 'auto.components.settings.cli.source.control.integration.cards.statusUnavailable': 'Unavailable', - 'auto.components.settings.cli.source.control.integration.cards.statusNotInstalled': - 'Not installed', - 'auto.components.settings.cli.source.control.integration.cards.statusNotAuthenticated': - 'Not authenticated', - // Stats — provider rows - 'auto.components.stats.usage.overview.sections.statusScanning': 'Scanning', - 'auto.components.stats.usage.overview.sections.statusEnabled': 'Enabled', - 'auto.components.stats.usage.overview.sections.statusOff': 'Off' -} - -const INTERPOLATED_KEYS: Record = { - 'auto.components.stats.StatsPane.trackingSince': ['{{value0}}'], - 'auto.components.settings.computerUseSummary.permissionsRequired_one': [], - 'auto.components.settings.computerUseSummary.permissionsRequired_other': ['{{value0}}'], - 'auto.components.settings.computerUseSummary.unavailableDescription': ['{{value0}}'], - 'auto.components.settings.OrchestrationSkillAgentCoverage.fullCoverage_one': [], - 'auto.components.settings.OrchestrationSkillAgentCoverage.fullCoverage_other': ['{{value0}}'], - 'auto.components.settings.OrchestrationSkillAgentCoverage.partialCoverage': [ - '{{value0}}', - '{{value1}}' - ] -} - -// Base keys i18next resolves through CLDR plural categories (`_`). -const PLURAL_KEYS = [ - 'auto.components.settings.computerUseSummary.permissionsRequired', - 'auto.components.settings.OrchestrationSkillAgentCoverage.fullCoverage' -] - -function pluralCategories(locale: string): string[] { - return [...new Intl.PluralRules(locale).resolvedOptions().pluralCategories] -} - -// A locale owes `_other` plus whichever of its categories English also spells -// out. Categories English never defines (es `many`, only reached past 1e6) fall -// back to the English source instead of rendering wrong, so they aren't gated. -function requiredPluralSuffixes(locale: string, base: string): string[] { - return pluralCategories(locale).filter( - (category) => category === 'other' || lookup(`${base}_${category}`) !== undefined - ) -} - -describe('settings and stats status labels', () => { - it.each(Object.entries(REQUIRED_KEYS))('keeps %s in the English catalog', (key, expected) => { - expect(lookup(key)).toBe(expected) - }) - - it.each(Object.entries(INTERPOLATED_KEYS))( - 'keeps the placeholders of %s intact', - (key, placeholders) => { - const value = lookup(key) - expect(value).toBeDefined() - for (const placeholder of placeholders) { - expect(value).toContain(placeholder) - } - } - ) - - it('keeps the orchestration usage summaries in the catalog', () => { - for (const id of [ - 'handoffSummary', - 'worktreeHandoffSummary', - 'childSequenceSummary', - 'childParallelSummary', - 'prSplitSummary' - ]) { - expect(lookup(`auto.lib.orchestration.usage.examples.${id}`)).toBeTruthy() - } - }) -}) - -// Translation lands incrementally and a missing key renders the English -// `translate()` fallback, so absence is fine — a *present* value that drops a -// placeholder or invents one interpolates to nothing at runtime, and a plural -// family translated only halfway silently reverts to English for some counts. -describe('shipped locale catalogs', () => { - it.each(Object.entries(LOCALE_CATALOGS))( - '%s keeps the English placeholders of every message it translates', - (_locale, catalog) => { - for (const key of Object.keys(INTERPOLATED_KEYS)) { - const localized = lookupIn(catalog, key) - if (localized === undefined) { - continue - } - const english = lookup(key) - expect(english, `${key} missing from en.json`).toBeDefined() - expect(placeholdersOf(localized), `${key} placeholders drifted`).toEqual( - placeholdersOf(english ?? '') - ) - } - } - ) - - it.each(Object.entries(LOCALE_CATALOGS))( - '%s completes every plural family it translates', - (locale, catalog) => { - for (const base of PLURAL_KEYS) { - const suffixes = [...new Set([...pluralCategories(locale), ...pluralCategories('en')])] - if (!suffixes.some((suffix) => lookupIn(catalog, `${base}_${suffix}`))) { - continue - } - for (const suffix of requiredPluralSuffixes(locale, base)) { - expect(lookupIn(catalog, `${base}_${suffix}`), `${base}_${suffix} missing`).toBeTruthy() - } - } - } - ) -}) diff --git a/src/renderer/src/i18n/technical-literal-catalog-values.test.ts b/src/renderer/src/i18n/technical-literal-catalog-values.test.ts deleted file mode 100644 index 8ce7eb7cb5b..00000000000 --- a/src/renderer/src/i18n/technical-literal-catalog-values.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import en from './locales/en.json' -import es from './locales/es.json' -import ko from './locales/ko.json' -import zh from './locales/zh.json' - -const repairedEntries = [ - ['es', 'auto.components.sidebar.AddRepoCreateStep.0ae45b8238'], - ['es', 'auto.components.settings.IntegrationsPane.e74de656ce'], - ['es', 'auto.components.settings.IntegrationsPane.51000487c4'], - ['es', 'auto.components.settings.RepositoryHooksSection.925f9e0dc4'], - ['es', 'auto.components.settings.RepositoryHooksSection.c90b858573'], - ['es', 'auto.components.settings.RepositoryHooksSection.32f417fe17'], - ['es', 'auto.components.settings.RepositoryIconPicker.03ca1a4e9b'], - ['es', 'auto.components.right.sidebar.index.6306b48afd'], - ['es', 'auto.components.feature.wall.ReviewAnimatedVisual.8df4d52b68'], - ['es', 'auto.components.editor.CombinedDiffViewer.8368d256ec'], - ['es', 'auto.components.editor.MarkdownTemplatePicker.22cd94426f'], - ['ko', 'auto.components.terminal.pane.stale.agent.row.090d607412'], - ['ko', 'auto.components.settings.IntegrationsPane.51000487c4'], - ['ko', 'auto.components.settings.git.search.upstream'], - ['ko', 'auto.components.right.sidebar.index.6306b48afd'], - ['ko', 'auto.components.feature.wall.FeatureWallSetupWorkflowActions.5c5b65044e'], - ['ko', 'auto.components.editor.CombinedDiffViewer.8368d256ec'], - ['ko', 'auto.components.editor.MarkdownTemplatePicker.22cd94426f'], - ['zh', 'auto.lib.terminal.shortcut.capture.notification.0ab0cd001a'], - ['zh', 'auto.components.github.IssueSourceSelector.643d7e9496'], - ['zh', 'auto.components.sparse.SparseCheckoutPresetSelect.ddbcaef7be'], - ['zh', 'auto.components.sidebar.AddRepoCreateStep.0ae45b8238'], - ['zh', 'auto.components.settings.AutoRenameBranchPromptEditor.ebb942a2ec'], - ['zh', 'auto.components.settings.BaseRefPicker.915ad97875'], - ['zh', 'auto.components.settings.IntegrationsPane.e74de656ce'], - ['zh', 'auto.components.settings.IntegrationsPane.51000487c4'], - ['zh', 'auto.components.settings.RepositoryHooksSection.39da2ae12f'], - ['zh', 'auto.components.settings.RepositoryHooksSection.925f9e0dc4'], - ['zh', 'auto.components.settings.RepositoryHooksSection.c90b858573'], - ['zh', 'auto.components.settings.RepositoryHooksSection.32f417fe17'], - ['zh', 'auto.components.settings.SparsePresetSettingsSection.fde7ff2cc3'], - ['zh', 'auto.components.settings.git.search.upstream'], - ['zh', 'auto.components.settings.mobile.emulator.search.84e5706975'], - ['zh', 'auto.components.settings.repository.search.603c68b68c'], - ['zh', 'auto.components.right.sidebar.index.6306b48afd'], - ['zh', 'auto.components.feature.wall.BrowserAnimatedVisual.7da6eed7bf'], - ['zh', 'auto.components.feature.wall.WorkbenchAnimatedVisual.defe550fe2'], - ['zh', 'auto.components.feature.wall.WorkbenchAnimatedVisual.4371cc9931'], - ['zh', 'auto.components.feature.wall.ReviewAnimatedVisual.8df4d52b68'], - ['zh', 'auto.components.editor.CombinedDiffViewer.8368d256ec'], - ['zh', 'auto.components.editor.MarkdownTemplatePicker.22cd94426f'] -] as const - -const catalogs = { es, ko, zh } as const - -function readValue(catalog: Record, key: string): unknown { - return key.split('.').reduce((value, part) => { - if (typeof value !== 'object' || value === null || Array.isArray(value)) { - return undefined - } - return (value as Record)[part] - }, catalog) -} - -describe('technical literal catalog repairs (#13121)', () => { - it('keeps every repaired value identical to English', () => { - expect(repairedEntries).toHaveLength(41) - - for (const [locale, key] of repairedEntries) { - expect(readValue(catalogs[locale], key), `${locale}:${key}`).toBe(readValue(en, key)) - } - }) -}) diff --git a/src/renderer/src/i18n/zh-technical-literal-mistranslations.test.ts b/src/renderer/src/i18n/zh-technical-literal-mistranslations.test.ts deleted file mode 100644 index 6273265d5b3..00000000000 --- a/src/renderer/src/i18n/zh-technical-literal-mistranslations.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Issue #9574 — high-confidence Simplified Chinese semantic fixes. - * Guards the anchor GH PR mistranslation and a sample of technical literals / - * clear sense errors so bootstrap re-translation cannot silently regress them. - */ -import { describe, expect, it } from 'vitest' -import zh from './locales/zh.json' - -function findByKey(node: unknown, key: string): string | undefined { - if (!node || typeof node !== 'object') { - return undefined - } - if (Array.isArray(node)) { - for (const item of node) { - const found = findByKey(item, key) - if (found !== undefined) { - return found - } - } - return undefined - } - const record = node as Record - if (typeof record[key] === 'string') { - return record[key] as string - } - for (const value of Object.values(record)) { - const found = findByKey(value, key) - if (found !== undefined) { - return found - } - } - return undefined -} - -describe('zh technical literal / sense fixes (#9574)', () => { - it('keeps GH PR as a technical literal (not 生长激素受体)', () => { - expect(findByKey(zh, '1b91db7e14')).toBe('GH PR') - }) - - it('keeps CLI / product technical strings un-translated', () => { - expect(findByKey(zh, 'fe119187bb')).toBe('--model sonnet') - expect(findByKey(zh, '5c5b65044e')).toBe('pnpm install') - expect(findByKey(zh, '5af8251002')).toBe('SCSS') - expect(findByKey(zh, '97e96cc027')).toBe('/goal') - expect(findByKey(zh, 'f62ce91ade')).toBe('origin') - expect(findByKey(zh, '79afc6772b')).toBe('orca.yaml') - }) - - it('uses the correct sense for short UI verbs/nouns', () => { - expect(findByKey(zh, 'ac037cfac2')).toBe('移动') // Move, not 手机 - expect(findByKey(zh, '1b24a32d3a')).toBe('内存') // Memory, not 记忆 - expect(findByKey(zh, '8cde1a2fb0')).toBe('暂存') // Stage, not 阶段 - expect(findByKey(zh, 'af2b07bda5')).toBe('状态:{{value0}}') // State, not 州 - expect(findByKey(zh, 'e070e8aeba')).toBe('竖线') // Bar cursor - expect(findByKey(zh, '52854a5608')).toBe('块状') // Block cursor - }) - - it('preserves brand names in product copy', () => { - expect(findByKey(zh, '855a76343a')).toBe('从 Ghostty 导入') - expect(findByKey(zh, 'ff450194cd')).toBe('Kagi 私密会话链接') - }) -}) - -// #12881 — the status bar provider usage surfaces settle on 使用情况, matching the Kimi/MiniMax -// entries that already used it. Scope is those surfaces, not the word "usage" everywhere: a -// measured volume ("Daily usage" 每日使用量, "Refresh Claude usage" 刷新 Claude 使用量) stays 使用量. -describe('zh provider usage wording (#12881)', () => { - it('renders every " Usage" label as " 使用情况"', () => { - for (const [key, brand] of [ - // status bar item menu - ['3885eb74d8', 'Claude'], - ['c0909c686e', 'Codex'], - ['c1df0d67ec', 'Gemini'], - ['antigravityUsage', 'Antigravity'], - ['8c86cd77b0', 'OpenCode Go'], - ['5e59007df4', 'Kimi'], - ['3bbf140864', 'MiniMax'], - ['grokUsageMenu', 'Grok'], - // Settings > Appearance search index — mirrors the menu labels - ['9dc15020d7', 'Claude'], - ['54b1acf24f', 'Codex'], - ['5bfb874d05', 'Gemini'], - ['antigravityUsageTitle', 'Antigravity'], - ['bc046e7899', 'OpenCode Go'], - ['3a6c028ea8', 'Kimi'], - ['0f08f6b483', 'MiniMax'], - ['f8e2a1c4b6', 'Grok'], - // Settings > Accounts search index - ['733f9e2a93', 'MiniMax'], - ['f4a8c2e1b7', 'Grok (xAI)'] - ]) { - expect(findByKey(zh, key), key).toBe(`${brand} 使用情况`) - } - }) - - it('leaves the Kimi and Moonshot search keywords untransliterated', () => { - expect(findByKey(zh, '40e5c3c285')).toBe('kimi') // not 基米 - expect(findByKey(zh, '35565867cb')).toBe('moonshot') // not 登月计划 ("moon landing programme") - }) - - it('keeps the status bar toggle descriptions on 使用情况', () => { - expect(findByKey(zh, 'antigravityUsageDescription')).toBe( - '在状态栏中显示 Antigravity 订阅使用情况。' - ) - expect(findByKey(zh, 'e7d1b0f3a5')).toBe('显示来自 Grok CLI OAuth 的 Grok 每周额度使用情况。') - expect(findByKey(zh, 'grokToggleDescription')).toBe( - '通过 Grok CLI 登录后显示 Grok 订阅额度使用情况。' - ) - expect(findByKey(zh, 'antigravityToggleDescription')).toBe( - '显示当前工作区的 Antigravity 订阅使用情况。' - ) - }) - - it('uses one wording for the "Open usage details" labels', () => { - for (const [key, brand] of [ - ['fda8146810', 'Kimi'], - ['629251f4b6', 'OpenCode Go'], - ['d2375976eb', 'Gemini'], - ['06741a2f3d', 'MiniMax'], - ['antigravityUsageDetails', 'Antigravity'], - ['grokUsageAria', 'Grok'] - ]) { - expect(findByKey(zh, key), key).toBe(`打开 ${brand} 使用详情`) - } - }) -}) - -// Duplicate is an action ("make a copy of"), not the adjective 重复. In the file explorer row -// menu it sat next to Copy — both read 复制, two different actions under one label. -describe('zh Duplicate vs Copy wording', () => { - it('separates the file explorer Duplicate action from Copy', () => { - expect(findByKey(zh, '0fec99bfd7')).toBe('创建副本') // Duplicate, not 复制 - expect(findByKey(zh, '98a79948b3')).toBe('复制') // Copy keeps the clipboard sense - }) - - it('reads Duplicate Tab as an action, matching the menu on 选项卡', () => { - expect(findByKey(zh, '5d6e89891f')).toBe('复制选项卡') // not 重复选项卡 - }) -}) diff --git a/src/shared/constants.test.ts b/src/shared/constants.test.ts index b69ebda0d2d..941bc6e6c13 100644 --- a/src/shared/constants.test.ts +++ b/src/shared/constants.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest' -import { DEFAULT_TERMINAL_INACTIVE_PANE_OPACITY, getDefaultSettings } from './constants' -import { getDefaultNotificationSettings } from './notification-settings-defaults' +import { getDefaultSettings } from './constants' import { getDefaultPrimarySelectionMiddleClickPaste, getDefaultTerminalRightClickToPaste @@ -14,137 +13,9 @@ describe('getDefaultSettings', () => { ) }) - it('enables gitignored file decorations by default', () => { - expect(getDefaultSettings('/tmp').showGitIgnoredFiles).toBe(true) - }) - - it('uses list view for Source Control changes by default', () => { - expect(getDefaultSettings('/tmp').sourceControlViewMode).toBe('list') - }) - - it('keeps Source Control changes first by default', () => { - expect(getDefaultSettings('/tmp').sourceControlGroupOrder).toBe('changes-first') - }) - - it('defaults mobile pairing to discovered network addresses', () => { - expect(getDefaultSettings('/tmp').mobilePairingCustomAddress).toBeNull() - expect(getDefaultSettings('/tmp').mobilePairingCustomAddresses).toEqual([]) - }) - - it('keeps first-work branch auto-renaming on by default for new settings', () => { - expect(getDefaultSettings('/tmp').autoRenameBranchFromWork).toBe(true) - expect(getDefaultSettings('/tmp').autoRenameBranchFromWorkDefaultedOn).toBe(true) - }) - - it('uses a block terminal cursor by default for new settings', () => { - expect(getDefaultSettings('/tmp').terminalCursorStyle).toBe('block') - expect(getDefaultSettings('/tmp').terminalCursorStyleDefaultedToBlock).toBe(true) - }) - - it('allows OSC 52 clipboard writes by default for new settings', () => { - expect(getDefaultSettings('/tmp').terminalAllowOsc52Clipboard).toBe(true) - expect(getDefaultSettings('/tmp').terminalAllowOsc52ClipboardDefaultedOnForAllUsers).toBe(true) - }) - - it('enables separate light terminal theme by default', () => { - expect(getDefaultSettings('/tmp').terminalUseSeparateLightTheme).toBe(true) - }) - - it('keeps inactive terminal panes readable by default', () => { - expect(getDefaultSettings('/tmp').terminalInactivePaneOpacity).toBe( - DEFAULT_TERMINAL_INACTIVE_PANE_OPACITY - ) - }) - it('asks before closing terminals with running processes by default', () => { expect(getDefaultSettings('/tmp').skipCloseTerminalWithRunningProcessConfirm).toBe(false) }) - - it('uses system language by default', () => { - expect(getDefaultSettings('/tmp').uiLanguage).toBe('system') - }) - - it('defaults the menu bar icon on so the value round-trips across platforms', () => { - expect(getDefaultSettings('/tmp').showMenuBarIcon).toBe(true) - }) - - it('shows terminal link actions by default', () => { - expect(getDefaultSettings('/tmp').terminalLinkActionPopoverEnabled).toBe(true) - }) - - it('confirms before closing pinned tabs by default', () => { - expect(getDefaultSettings('/tmp').confirmClosePinnedTab).toBe(true) - }) - - it('keeps file-editor word wrapping enabled by default', () => { - expect(getDefaultSettings('/tmp').editorWordWrap).toBe(true) - }) - - it('keeps rich Markdown spellcheck enabled by default', () => { - expect(getDefaultSettings('/tmp').richMarkdownSpellcheckEnabled).toBe(true) - }) - - it('enables Source Control AI by default without pinning a separate agent', () => { - expect(getDefaultSettings('/tmp').commitMessageAi).toMatchObject({ - enabled: true, - agentId: null, - selectedModelByAgent: {} - }) - expect(getDefaultSettings('/tmp').sourceControlAi).toMatchObject({ - enabled: true, - agentId: null, - selectedModelByAgent: {}, - instructionsByOperation: { - commitMessage: '', - pullRequest: '', - branchName: '' - } - }) - }) - - it('keeps compact worktree cards disabled by default', () => { - expect(getDefaultSettings('/tmp').compactWorktreeCards).toBe(false) - }) - - it('keeps per-workspace environments disabled by default', () => { - expect(getDefaultSettings('/tmp').experimentalEphemeralVms).toBe(false) - }) - - it('keeps the agent dashboard popout disabled by default', () => { - expect(getDefaultSettings('/tmp').experimentalAgentDashboardPopout).toBeUndefined() - expect(getDefaultSettings('/tmp').experimentalAgentDashboardShowIdle).toBeUndefined() - }) - - it('defaults local Windows projects to the host runtime', () => { - expect(getDefaultSettings('/tmp').localWindowsRuntimeDefault).toEqual({ - kind: 'windows-host' - }) - }) - - it('suppresses notifications for the focused worktree by default for new users', () => { - expect(getDefaultNotificationSettings().suppressWhenFocused).toBe(true) - expect(getDefaultSettings('/tmp').notifications.suppressWhenFocused).toBe(true) - }) - - it('defaults agent launch args to yolo mode where the CLI supports it', () => { - const settings = getDefaultSettings('/tmp') - - expect(settings.agentDefaultArgs).toMatchObject({ - claude: '--dangerously-skip-permissions', - codex: '--dangerously-bypass-approvals-and-sandbox', - gemini: '--yolo', - cursor: '--yolo', - muse: '--yolo', - copilot: '--yolo', - grok: '--permission-mode bypassPermissions' - }) - expect(settings.agentDefaultArgs).not.toHaveProperty('opencode') - expect(settings.agentDefaultArgs).not.toHaveProperty('kilo') - expect(settings.agentDefaultEnv).toMatchObject({ - goose: { GOOSE_MODE: 'auto' } - }) - expect(settings.agentYoloDefaultsMigrated).toBe(true) - }) }) describe('getDefaultPrimarySelectionMiddleClickPaste', () => { @@ -168,20 +39,3 @@ describe('getDefaultTerminalRightClickToPaste', () => { expect(getDefaultTerminalRightClickToPaste('linux')).toBe(false) }) }) - -describe('MiniMax defaults', () => { - it('starts MiniMax with empty group id and the canonical default model', () => { - const settings = getDefaultSettings('/tmp') - // Why: the fetcher reads these defaults on first launch. An empty - // group id is the signal that the fetcher must pull the value from - // the cookie itself, and "general" matches the model name the - // MiniMax usage endpoint exposes by default. - expect(settings.minimaxGroupId).toBe('') - expect(settings.minimaxUsageModels).toBe('general') - }) - - it('defaults the MiniMax endpoint to overseas', () => { - const settings = getDefaultSettings('/tmp') - expect(settings.minimaxEndpoint).toBe('overseas') - }) -}) diff --git a/src/shared/contextual-tours.test.ts b/src/shared/contextual-tours.test.ts index 9f197f32326..3bde403ae70 100644 --- a/src/shared/contextual-tours.test.ts +++ b/src/shared/contextual-tours.test.ts @@ -1,190 +1,7 @@ import { describe, expect, it } from 'vitest' -import { - CONTEXTUAL_TOURS, - normalizeContextualTourIds, - type ContextualTour, - type ContextualTourId -} from './contextual-tours' - -describe('contextual tour definitions', () => { - it('defines the required tours with concise visible steps', () => { - const expectedIds: ContextualTourId[] = [ - 'workspace-board', - 'workspace-agent-sessions', - 'browser', - 'client-hosted-browser', - 'tasks', - 'automations', - 'floating-workspace', - 'workspace-creation' - ] - - expect(CONTEXTUAL_TOURS.map((tour) => tour.id)).toEqual(expectedIds) - for (const tour of CONTEXTUAL_TOURS) { - expect(tour.steps[0]?.requiredForStart).toBe(true) - const stepCount = (tour.steps as readonly unknown[]).length - if (stepCount === 1) { - // A lone step needs a completion path beyond the default Next: either it - // self-completes on the feature interaction, or it carries an explicit - // complete CTA (the client-hosted browser tip's "Got it"). - const step = tour.steps[0] as ContextualTour['steps'][number] - expect( - Boolean(step.advanceOnFeatureInteraction) || step.primaryAction?.kind === 'complete' - ).toBe(true) - } else { - expect(stepCount).toBeGreaterThanOrEqual(2) - } - expect(stepCount).toBeLessThanOrEqual(tour.id === 'workspace-agent-sessions' ? 5 : 3) - for (const step of tour.steps) { - expect(step.title.length).toBeGreaterThan(0) - expect(step.body.length).toBeGreaterThan(0) - expect(step.body.length).toBeLessThanOrEqual(140) - expect(step.targetSelector).toContain('data-contextual-tour-target') - } - } - }) - - it('defines the workspace agent sessions value tour as split then create-worktree', () => { - const tour = CONTEXTUAL_TOURS.find((entry) => entry.id === 'workspace-agent-sessions') as - | ContextualTour - | undefined - - // Two steps only: tasks and orchestration education lives in their own - // page tours, so the in-app tour ends after the worktree CTA. - expect(tour?.steps.map((step) => step.title)).toEqual([ - 'Split a terminal pane', - 'Start another task in parallel' - ]) - // The opening step teaches the split gesture and offers the convenience button. - expect(tour?.steps[0]).toMatchObject({ - requiredForStart: true, - primaryAction: { kind: 'split-terminal-pane', label: 'Split terminal' }, - advanceOnFeatureInteraction: 'terminal-pane-split' - }) - expect(tour?.steps[0]?.body).toContain('{terminal.splitRight}') - expect(tour?.steps[0]?.targetSelector).toContain('terminal-pane-split-target') - expect(tour?.steps[0]?.targetSelector).not.toContain('terminal-split-control') - expect(tour?.steps[0]?.secondaryAction).toBeUndefined() - // The closing step anchors on the real new-worktree button; the pulse makes - // that button the CTA instead of duplicating it inside the panel. - expect(tour?.steps[1]).toMatchObject({ - targetPulse: true, - hidePrimaryAction: true - }) - expect(tour?.steps[1]?.targetSelector).toContain('workspace-create-control') - expect(tour?.steps[1]?.primaryAction).toBeUndefined() - expect(tour?.steps[1]?.secondaryAction).toBeUndefined() - }) - - it('points the workspace board tour at the board center and done lane', () => { - const tour = CONTEXTUAL_TOURS.find((entry) => entry.id === 'workspace-board') as - | ContextualTour - | undefined - - expect(tour?.steps.map((step) => step.title)).toEqual([ - 'Plan work on the board', - 'Move work through lanes' - ]) - expect(tour?.steps[0]).toMatchObject({ - targetSelector: '[data-contextual-tour-target="workspace-board-center"]', - requiredForStart: true, - preferredPlacement: 'bottom' - }) - expect(tour?.steps[1]).toMatchObject({ - body: 'Drag workspaces between lanes as their status changes.', - targetSelector: - '[data-contextual-tour-target="workspace-board-done-lane"], [data-contextual-tour-target="workspace-board-lanes"]' - }) - }) - - it('orders the browser tour as grab, annotate, then import cookies', () => { - const tour = CONTEXTUAL_TOURS.find((entry) => entry.id === 'browser') as - | ContextualTour - | undefined - - expect(tour?.steps.map((step) => step.title)).toEqual([ - 'Grab page context for agents', - 'Mark design feedback in place', - 'Stay logged in' - ]) - expect(tour?.steps[0]).toMatchObject({ - targetSelector: '[data-contextual-tour-target="browser-grab-control"]', - preferredPlacement: 'bottom' - }) - expect(tour?.steps[1]).toMatchObject({ - targetSelector: '[data-contextual-tour-target="browser-annotation-control"]', - preferredPlacement: 'bottom' - }) - expect(tour?.steps[2]).toMatchObject({ - body: 'Bring your existing logins into Orca to stay signed in immediately.', - // Prefers the always-visible Import button, falling back to the overflow - // menu's Import Cookies row once the hint button is dismissed. - targetSelector: - '[data-contextual-tour-target="browser-import-hint"], [data-contextual-tour-target="browser-import-cookies-control"]', - preferredPlacement: 'bottom' - }) - }) - - it('points the tasks tour at the row workspace action before toolbar fallbacks', () => { - const tour = CONTEXTUAL_TOURS.find((entry) => entry.id === 'tasks') as - | ContextualTour - | undefined - const step = tour?.steps[2] - - expect(step).toMatchObject({ - title: 'Start from work items', - body: 'Use Start or Open on a task, issue, review, or merge request to bring its context into a workspace.' - }) - expect(step?.targetSelector.split(', ')).toEqual([ - '[data-contextual-tour-target="tasks-start-workspace"]', - '[data-contextual-tour-target="tasks-actions"]', - '[data-contextual-tour-target="tasks-search-presets"]' - ]) - }) - - it('orders the automations tour as create, then results', () => { - const tour = CONTEXTUAL_TOURS.find((entry) => entry.id === 'automations') as - | ContextualTour - | undefined - - expect(tour?.steps.map((step) => step.title)).toEqual([ - 'What is an automation?', - 'Find the results' - ]) - expect(tour?.steps[0]).toMatchObject({ - body: 'Automations run agent work on a schedule. Add an automation by clicking this button.', - requiredForStart: true - }) - expect(tour?.steps.map((step) => step.targetSelector)).toEqual([ - '[data-contextual-tour-target="automations-create"]', - '[data-contextual-tour-target="automations-runs"]' - ]) - }) - - it('defines the floating workspace tour on the action list with a surface fallback', () => { - const tour = CONTEXTUAL_TOURS.find((entry) => entry.id === 'floating-workspace') as - | ContextualTour - | undefined - - expect(tour?.steps.map((step) => step.title)).toEqual([ - 'Run an agent across every repo', - 'Or use it as a scratchpad' - ]) - expect(tour?.steps.map((step) => step.body)).toEqual([ - 'Agents here run in any folder you choose. Point one at the directory above your services to work across all your repos at once.', - 'Open agents, scratch terminals, notes, and browser tabs without cluttering the worktree you’re focused on.' - ]) - expect(tour?.steps[0]).toMatchObject({ - requiredForStart: true, - preferredPlacement: 'left' - }) - expect(tour?.steps[1]?.preferredPlacement).toBe('left') - expect(tour?.steps.map((step) => step.targetSelector)).toEqual([ - '[data-contextual-tour-target="floating-workspace-new-terminal"], [data-contextual-tour-target="floating-workspace-surface"]', - '[data-contextual-tour-target="floating-workspace-new-markdown"], [data-contextual-tour-target="floating-workspace-surface"]' - ]) - }) +import { normalizeContextualTourIds } from './contextual-tours' +describe('contextual tour ids', () => { it('normalizes persisted ids by removing unknowns and duplicates', () => { expect( normalizeContextualTourIds([ diff --git a/src/shared/feature-tips.test.ts b/src/shared/feature-tips.test.ts index e74faa8b5e9..ed479db848e 100644 --- a/src/shared/feature-tips.test.ts +++ b/src/shared/feature-tips.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest' import { - FEATURE_TIPS, getCompletedFeatureTipIds, getOrderedUnseenFeatureTips, normalizeFeatureTipIds, @@ -109,38 +108,4 @@ describe('feature tips', () => { ]) ).toEqual(['orca-cli', 'cmd-j-palette', 'voice-dictation']) }) - - it('describes the command palette tip as a passive acknowledgement', () => { - const paletteTip = FEATURE_TIPS.find((tip) => tip.id === 'cmd-j-palette') - - expect(paletteTip).toMatchObject({ - action: 'learn-cmd-j-palette', - priority: 'new', - eyebrow: 'Tip', - ctaLabel: 'Got it' - }) - expect(paletteTip?.description).toContain('worktrees') - expect(paletteTip?.description).toContain('spin up a new worktree') - }) - - it('describes the CLI tip as an install action with concrete workflows', () => { - const cliTip = FEATURE_TIPS.find((tip) => tip.id === 'orca-cli') - - expect(cliTip).toMatchObject({ - action: 'setup-cli', - title: 'Let agents drive Orca with the Orca CLI', - ctaLabel: 'Install CLI & Skills' - }) - expect(cliTip?.description).toContain('coordinate child worktrees') - expect(cliTip?.description).toContain('communicate between worktrees') - }) - - it('does not label the voice dictation tip as new', () => { - const voiceTip = FEATURE_TIPS.find((tip) => tip.id === 'voice-dictation') - - expect(voiceTip?.eyebrow).toBe('Tip') - expect(voiceTip?.priority).toBe('unseen') - expect(voiceTip?.title).toBe('Dictate into any pane') - expect(voiceTip?.ctaLabel).toBe('Set up voice dictation') - }) }) diff --git a/src/shared/git-binary-compatibility.test.ts b/src/shared/git-binary-compatibility.test.ts index 69fa1ec3e3a..c61efa3a7ee 100644 --- a/src/shared/git-binary-compatibility.test.ts +++ b/src/shared/git-binary-compatibility.test.ts @@ -1,8 +1,6 @@ -import { execFile } from 'node:child_process' -import { mkdir, mkdtemp, readFile, rm, unlink, utimes, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, readFile, realpath, rm, unlink, utimes, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' -import { promisify } from 'node:util' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { isUnsupportedMergeTreeMergeBaseError, @@ -29,11 +27,10 @@ import { parseWorktreeList } from './git-worktree-porcelain-parser' import { fastForwardLocalBaseBranch } from './worktree/local-base-branch-fast-forward' import { gitChangeListArgs, parseGitChangeList } from './git-change-list' import { encodeGitPathspecs } from './git-pathspec-stdin' -import { endSubprocessStdin } from './subprocess-stdin-write' +import { GitBinaryTestFixture } from './git-binary.test-fixture' import { registerGitRepositoryRegistrationBinaryCompatibilityCase } from './git-repository-registration-binary-compatibility.test-cases' import { registerGitResolutionBinaryCompatibilityCases } from './git-resolution-binary-compatibility.test-cases' -const execFileAsync = promisify(execFile) const image = process.env.ORCA_GIT_COMPAT_IMAGE const binary = process.env.ORCA_GIT_COMPAT_BINARY const expectedVersion = process.env.ORCA_GIT_COMPAT_VERSION @@ -43,6 +40,7 @@ type GitResult = { stdout: string; stderr: string } describeBinaryCompatibility('real Git binary compatibility', () => { let repoPath = '' + let fixture: GitBinaryTestFixture let version = { major: 0, minor: 0 } async function runGit( @@ -50,53 +48,7 @@ describeBinaryCompatibility('real Git binary compatibility', () => { env?: NodeJS.ProcessEnv, stdin?: string ): Promise { - if (image) { - const dockerUser = - typeof process.getuid === 'function' && typeof process.getgid === 'function' - ? ['--user', `${process.getuid()}:${process.getgid()}`] - : [] - const pending = execFileAsync( - 'docker', - [ - 'run', - '--rm', - ...(stdin === undefined ? [] : ['-i']), - '--network=none', - ...dockerUser, - ...Object.entries(env ?? {}).flatMap(([key, value]) => - value === undefined ? [] : ['--env', `${key}=${value}`] - ), - '-v', - `${repoPath}:/repo`, - '-w', - '/repo', - image, - '-c', - 'safe.directory=/repo', - ...args - ], - { maxBuffer: 2 * 1024 * 1024 } - ) - if (stdin !== undefined) { - endSubprocessStdin(pending.child.stdin, stdin) - } - return pending - } - const pending = execFileAsync(binary!, args, { - cwd: repoPath, - env: { - ...process.env, - HOME: repoPath, - XDG_CONFIG_HOME: repoPath, - GIT_CONFIG_NOSYSTEM: '1', - ...env - }, - maxBuffer: 2 * 1024 * 1024 - }) - if (stdin !== undefined) { - endSubprocessStdin(pending.child.stdin, stdin) - } - return pending + return fixture.exec(args, fixture.executionRoot, env, stdin) } function supports(major: number, minor: number): boolean { @@ -118,7 +70,9 @@ describeBinaryCompatibility('real Git binary compatibility', () => { } beforeAll(async () => { - repoPath = await mkdtemp(join(tmpdir(), 'orca-git-binary-compat-')) + repoPath = await realpath(await mkdtemp(join(tmpdir(), 'orca-git-binary-compat-'))) + fixture = new GitBinaryTestFixture(repoPath, '/repo') + await fixture.start() const versionOutput = await runGit(['--version']) expect(versionOutput.stdout).toContain(`git version ${expectedVersion}`) const match = versionOutput.stdout.match(/git version (\d+)\.(\d+)/) @@ -135,7 +89,11 @@ describeBinaryCompatibility('real Git binary compatibility', () => { afterAll(async () => { if (repoPath) { - await rm(repoPath, { recursive: true, force: true }) + try { + await fixture.stop() + } finally { + await rm(repoPath, { recursive: true, force: true }) + } } }) diff --git a/src/shared/git-binary.test-fixture.ts b/src/shared/git-binary.test-fixture.ts new file mode 100644 index 00000000000..33d3975c8af --- /dev/null +++ b/src/shared/git-binary.test-fixture.ts @@ -0,0 +1,114 @@ +import { runProcess } from '@orca/process-host' + +type GitResult = { stdout: string; stderr: string } + +export class GitBinaryTestFixture { + private container = '' + private readonly image = process.env.ORCA_GIT_COMPAT_IMAGE + private readonly binary = process.env.ORCA_GIT_COMPAT_BINARY ?? 'git' + readonly executionRoot: string + + constructor( + private readonly root: string, + containerRoot = root + ) { + this.executionRoot = this.image ? containerRoot : root + } + + async start(): Promise { + if (!this.image) { + return + } + const dockerUser = + typeof process.getuid === 'function' && typeof process.getgid === 'function' + ? ['--user', `${process.getuid()}:${process.getgid()}`] + : [] + // Container creation dwarfs Git execution; keep the same isolated image alive for the suite. + const result = await this.run('docker', [ + 'run', + '--detach', + '--rm', + '--network=none', + ...dockerUser, + '-v', + `${this.root}:${this.executionRoot}`, + '--entrypoint', + '/bin/sleep', + this.image, + 'infinity' + ]) + this.container = result.stdout.trim() + } + + exec( + args: string[], + cwd = this.executionRoot, + env: NodeJS.ProcessEnv = {}, + stdin?: string + ): Promise { + const isolatedEnv = { + HOME: this.executionRoot, + XDG_CONFIG_HOME: this.executionRoot, + GIT_CONFIG_NOSYSTEM: '1', + ...env + } + if (!this.image) { + return this.run(this.binary, args, cwd, { ...process.env, ...isolatedEnv }, stdin) + } + if (!this.container) { + throw new Error('Git compatibility container has not started') + } + return this.run( + 'docker', + [ + 'exec', + ...(stdin === undefined ? [] : ['-i']), + ...Object.entries(isolatedEnv).flatMap(([key, value]) => + value === undefined ? [] : ['--env', `${key}=${value}`] + ), + '-w', + cwd, + this.container, + 'git', + '-c', + `safe.directory=${cwd}`, + ...args + ], + undefined, + undefined, + stdin + ) + } + + async stop(): Promise { + if (this.container) { + await this.run('docker', ['rm', '--force', this.container]) + this.container = '' + } + } + + private async run( + program: string, + args: string[], + cwd?: string, + env?: NodeJS.ProcessEnv, + stdin?: string + ): Promise { + const result = await runProcess({ + program, + args, + cwd, + env, + input: stdin, + maxOutputBytes: 2 * 1024 * 1024 + }) + if (result.code !== 0) { + throw Object.assign(new Error(`${program} ${args.join(' ')}: ${result.stderr}`), { + code: result.code, + stdout: result.stdout, + stderr: result.stderr + }) + } + return result + } +} diff --git a/src/shared/git-branch-line-total-soft-deadline.test.ts b/src/shared/git-branch-line-total-soft-deadline.test.ts index 9ef6c6872d9..5c426ea009a 100644 --- a/src/shared/git-branch-line-total-soft-deadline.test.ts +++ b/src/shared/git-branch-line-total-soft-deadline.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { GIT_BRANCH_LINE_TOTAL_SOFT_DEADLINE_MS } from './git-branch-line-total' import { beginGitStatusLineStatsCacheWrite, clearGitStatusLineStatsCache, @@ -33,30 +34,41 @@ async function runPass(diffDelayMs: number): Promise<{ elapsed: number; total: u } describe('branch line total never blocks the status response', () => { - it('returns fast when the ranged diff is slow, then publishes it on the next pass', async () => { + beforeEach(() => { + vi.useFakeTimers() clearGitStatusLineStatsCache() - const slow = await runPass(3000) - expect(slow.total).toBeUndefined() - expect(slow.elapsed).toBeLessThan(1200) + }) - // Why: poll the cache rather than run another pass — a second recompute - // stores, which retires the first pass's still-pending late-arrival token. - await vi.waitFor( - () => { - expect( - readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: MERGE_BASE }) - ).toEqual({ added: 5, removed: 5, mergeBase: MERGE_BASE }) - }, - { timeout: 15000, interval: 50 } - ) - const next = await runPass(3000) + afterEach(() => { + clearGitStatusLineStatsCache() + vi.useRealTimers() + }) + + it('returns fast when the ranged diff is slow, then publishes it on the next pass', async () => { + const slowPass = runPass(3000) + await vi.advanceTimersByTimeAsync(GIT_BRANCH_LINE_TOTAL_SOFT_DEADLINE_MS) + const slow = await slowPass + expect(slow.total).toBeUndefined() + expect(slow.elapsed).toBe(GIT_BRANCH_LINE_TOTAL_SOFT_DEADLINE_MS) + + // Let the first diff backfill before a second pass retires its write token. + await vi.advanceTimersByTimeAsync(3000 - GIT_BRANCH_LINE_TOTAL_SOFT_DEADLINE_MS) + expect(readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: MERGE_BASE })).toEqual({ + added: 5, + removed: 5, + mergeBase: MERGE_BASE + }) + const nextPass = runPass(3000) + await vi.advanceTimersByTimeAsync(GIT_BRANCH_LINE_TOTAL_SOFT_DEADLINE_MS) + const next = await nextPass expect(next.total).toEqual({ added: 5, removed: 5, mergeBase: MERGE_BASE }) - }, 20000) + }) it('still returns the total inline when the diff is fast', async () => { - clearGitStatusLineStatsCache() - const fast = await runPass(10) + const fastPass = runPass(10) + await vi.advanceTimersByTimeAsync(10) + const fast = await fastPass expect(fast.total).toEqual({ added: 5, removed: 5, mergeBase: MERGE_BASE }) - expect(fast.elapsed).toBeLessThan(400) + expect(fast.elapsed).toBe(10) }) }) diff --git a/src/shared/native-chat-types.test.ts b/src/shared/native-chat-types.test.ts deleted file mode 100644 index ce799378bbc..00000000000 --- a/src/shared/native-chat-types.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { - isTextBlock, - isToolCallBlock, - isToolResultBlock, - isImageRefBlock, - NATIVE_CHAT_SOURCE_PRIORITY, - type NativeChatBlock -} from './native-chat-types' - -const textBlock: NativeChatBlock = { type: 'text', text: 'hello' } -const toolCallBlock: NativeChatBlock = { type: 'tool-call', name: 'Edit', input: { path: 'a' } } -const toolResultBlock: NativeChatBlock = { type: 'tool-result', output: 'done', isError: false } -const imageRefBlock: NativeChatBlock = { type: 'image-ref', path: '/tmp/a.png', alt: 'a' } - -describe('native chat block guards', () => { - it('isTextBlock narrows only text blocks', () => { - expect(isTextBlock(textBlock)).toBe(true) - expect(isTextBlock(toolCallBlock)).toBe(false) - expect(isTextBlock(toolResultBlock)).toBe(false) - expect(isTextBlock(imageRefBlock)).toBe(false) - }) - - it('isToolCallBlock narrows only tool-call blocks', () => { - expect(isToolCallBlock(toolCallBlock)).toBe(true) - expect(isToolCallBlock(textBlock)).toBe(false) - }) - - it('isToolResultBlock narrows only tool-result blocks', () => { - expect(isToolResultBlock(toolResultBlock)).toBe(true) - expect(isToolResultBlock(toolCallBlock)).toBe(false) - }) - - it('isImageRefBlock narrows only image-ref blocks', () => { - expect(isImageRefBlock(imageRefBlock)).toBe(true) - expect(isImageRefBlock(textBlock)).toBe(false) - }) -}) - -describe('source priority', () => { - it('ranks transcript > hook > scrape', () => { - expect(NATIVE_CHAT_SOURCE_PRIORITY.transcript).toBeGreaterThan(NATIVE_CHAT_SOURCE_PRIORITY.hook) - expect(NATIVE_CHAT_SOURCE_PRIORITY.hook).toBeGreaterThan(NATIVE_CHAT_SOURCE_PRIORITY.scrape) - }) -}) diff --git a/tests/e2e/browser-tab.spec.ts b/tests/e2e/browser-tab.spec.ts index 9ddc7d7f14c..48da3235a40 100644 --- a/tests/e2e/browser-tab.spec.ts +++ b/tests/e2e/browser-tab.spec.ts @@ -14,9 +14,6 @@ import { getActiveWorktreeId, getActiveTabType, getBrowserTabs, - getAllWorktreeIds, - switchToOtherWorktree, - switchToWorktree, ensureTerminalVisible } from './helpers/store' import { startBrowserLinkServer } from './helpers/browser-link-server' @@ -54,25 +51,6 @@ async function createBrowserTab( ) } -async function switchToTerminalTab( - page: Parameters[0], - worktreeId: string -): Promise { - await page.evaluate((targetWorktreeId) => { - const store = window.__store - if (!store) { - return - } - - const state = store.getState() - const terminalTab = (state.tabsByWorktree[targetWorktreeId] ?? [])[0] - if (terminalTab) { - state.setActiveTab(terminalTab.id) - } - state.setActiveTabType('terminal', store.getState().activeWorktreeId) - }, worktreeId) -} - async function switchToBrowserTab( page: Parameters[0], worktreeId: string, @@ -370,77 +348,6 @@ test.describe('Browser Tab', () => { await ensureTerminalVisible(orcaPage) }) - /** - * User Prompt: - * - Browser works and also retains state when switching tabs etc. - */ - test('creating a browser tab adds it and activates browser view', async ({ orcaPage }) => { - const worktreeId = (await getActiveWorktreeId(orcaPage))! - const browserTabsBefore = await getBrowserTabs(orcaPage, worktreeId) - - await createBrowserTab(orcaPage, worktreeId) - - // Wait for the browser tab to appear in the store - await expect - .poll(async () => (await getBrowserTabs(orcaPage, worktreeId)).length, { timeout: 5_000 }) - .toBe(browserTabsBefore.length + 1) - - // The active tab type should switch to 'browser' - await expect.poll(async () => getActiveTabType(orcaPage), { timeout: 3_000 }).toBe('browser') - }) - - /** - * User Prompt: - * - Browser works and also retains state when switching tabs etc. - */ - test('browser tab is created and active in the store', async ({ orcaPage }) => { - const worktreeId = (await getActiveWorktreeId(orcaPage))! - - await createBrowserTab(orcaPage, worktreeId) - await expect.poll(async () => getActiveTabType(orcaPage), { timeout: 5_000 }).toBe('browser') - - // Verify the browser tab exists in the store - const browserTabs = await getBrowserTabs(orcaPage, worktreeId) - expect(browserTabs.length).toBeGreaterThan(0) - - // The active browser tab should have a URL (even if it's about:blank or the default) - const activeBrowserTabId = await orcaPage.evaluate(() => { - const store = window.__store - return store?.getState().activeBrowserTabId ?? null - }) - expect(activeBrowserTabId).not.toBeNull() - }) - - /** - * User Prompt: - * - Browser works and also retains state when switching tabs etc. - */ - test('browser tab retains state when switching to terminal and back', async ({ orcaPage }) => { - const worktreeId = (await getActiveWorktreeId(orcaPage))! - - await createBrowserTab(orcaPage, worktreeId) - await expect.poll(async () => getActiveTabType(orcaPage), { timeout: 5_000 }).toBe('browser') - - // Record the browser tab info - const browserTabsBefore = await getBrowserTabs(orcaPage, worktreeId) - expect(browserTabsBefore.length).toBeGreaterThan(0) - const browserTabId = browserTabsBefore.at(-1)?.id - expect(browserTabId).toBeTruthy() - - // Switch to the terminal view - await switchToTerminalTab(orcaPage, worktreeId) - await expect.poll(async () => getActiveTabType(orcaPage), { timeout: 3_000 }).toBe('terminal') - - // Switch back to browser tab - await switchToBrowserTab(orcaPage, worktreeId, browserTabId!) - await expect.poll(async () => getActiveTabType(orcaPage), { timeout: 3_000 }).toBe('browser') - - // The browser tab should still exist with the same ID - const browserTabsAfter = await getBrowserTabs(orcaPage, worktreeId) - const tabStillExists = browserTabsAfter.some((tab) => tab.id === browserTabId) - expect(tabStillExists).toBe(true) - }) - test('browser webview form state survives switching between browser tabs', async ({ orcaPage }) => { @@ -805,38 +712,4 @@ test.describe('Browser Tab', () => { await closeServer.close() } }) - - /** - * User Prompt: - * - Browser works and also retains state when switching tabs etc. - */ - test('browser tab retains state when switching worktrees and back', async ({ orcaPage }) => { - const allWorktreeIds = await getAllWorktreeIds(orcaPage) - if (allWorktreeIds.length < 2) { - test.skip(true, 'Need at least 2 worktrees to test worktree switching') - } - - const worktreeId = (await getActiveWorktreeId(orcaPage))! - - await createBrowserTab(orcaPage, worktreeId) - await expect.poll(async () => getActiveTabType(orcaPage), { timeout: 5_000 }).toBe('browser') - - const browserTabsBefore = await getBrowserTabs(orcaPage, worktreeId) - expect(browserTabsBefore.length).toBeGreaterThan(0) - - // Switch to a different worktree via the store - const otherId = await switchToOtherWorktree(orcaPage, worktreeId) - expect(otherId).not.toBeNull() - await expect.poll(async () => getActiveWorktreeId(orcaPage), { timeout: 5_000 }).toBe(otherId) - - // Switch back to the original worktree - await switchToWorktree(orcaPage, worktreeId) - await expect - .poll(async () => getActiveWorktreeId(orcaPage), { timeout: 5_000 }) - .toBe(worktreeId) - - // Browser tabs should still be preserved - const browserTabsAfter = await getBrowserTabs(orcaPage, worktreeId) - expect(browserTabsAfter.length).toBe(browserTabsBefore.length) - }) }) diff --git a/tests/e2e/helpers/e2e-completed-onboarding-profile.ts b/tests/e2e/helpers/e2e-completed-onboarding-profile.ts index 82f79564dd3..f4393fb33d2 100644 --- a/tests/e2e/helpers/e2e-completed-onboarding-profile.ts +++ b/tests/e2e/helpers/e2e-completed-onboarding-profile.ts @@ -47,7 +47,8 @@ export function getE2ECompletedOnboardingProfile() { browserImportHintHidden: true, // Why: E2E profiles model completed existing users and should not be // interrupted by the usage-display change toast covering the UI under test. - usagePercentageDisplayChangeNoticeDismissed: true + usagePercentageDisplayChangeNoticeDismissed: true, + statusBarCompactChangeNoticeDismissed: true } } } diff --git a/tests/e2e/onboarding.spec.ts b/tests/e2e/onboarding.spec.ts index a5a6557b4b9..9cdcf17e5d0 100644 --- a/tests/e2e/onboarding.spec.ts +++ b/tests/e2e/onboarding.spec.ts @@ -160,10 +160,13 @@ test.describe('Onboarding flow', () => { await waitForSessionReady(orcaPage) }) - test('renders on first launch with the agent step active', async ({ orcaPage }) => { + test('Continue advances steps, persists progress, and applies user-visible settings', async ({ + orcaPage + }) => { await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({ timeout: 15_000 }) + await expectOnboardingProgress(orcaPage, /^1 of [345]$/) await expect(onboardingFooterButton(orcaPage, /^Continue\b/)).toBeVisible() await expect(onboardingFooterButton(orcaPage, SKIP_TO_PROJECT_SETUP_BUTTON)).toBeVisible() @@ -180,14 +183,6 @@ test.describe('Onboarding flow', () => { .filter({ hasText: /⌘|Ctrl/ }) .first() ).toBeVisible() - }) - - test('Continue advances steps, persists progress, and applies user-visible settings', async ({ - orcaPage - }) => { - await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({ - timeout: 15_000 - }) // --- Step 1: agent --- // Force a deterministic, non-default selection so the assertion below @@ -209,6 +204,7 @@ test.describe('Onboarding flow', () => { await orcaPage.getByText(/Show \d+ more agents/).click() } await codexButton.click() + await expect(codexButton).toHaveAttribute('aria-pressed', 'true') await continueOnboarding(orcaPage) await expect(orcaPage.getByRole('heading', { name: /Make it feel like home/i })).toBeVisible() @@ -548,26 +544,6 @@ test.describe('Onboarding flow', () => { expect((await getOnboardingState(orcaPage)).closedAt).not.toBeNull() }) - test('selected agent button reports aria-pressed=true', async ({ orcaPage }) => { - await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({ - timeout: 15_000 - }) - - const codexButton = orcaPage.getByRole('button', { name: 'Codex', exact: true }) - const codexVisible = await codexButton - .first() - .waitFor({ state: 'visible', timeout: 1_000 }) - .then(() => true) - .catch(() => false) - if (!codexVisible) { - await orcaPage.getByText(/Show \d+ more agents/).click() - } - await codexButton.click() - // Why: AgentButton now sets aria-pressed so screen readers and assistive - // tech can announce the selection. Verify the attribute reflects state. - await expect(codexButton).toHaveAttribute('aria-pressed', 'true') - }) - test('notification sound choice persists on Continue', async ({ orcaPage }) => { await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({ timeout: 15_000 diff --git a/tests/e2e/tabs.spec.ts b/tests/e2e/tabs.spec.ts index 50d02e507a4..063809f32ad 100644 --- a/tests/e2e/tabs.spec.ts +++ b/tests/e2e/tabs.spec.ts @@ -236,129 +236,6 @@ test.describe('Tabs', () => { await expect.poll(() => getDomActiveTabId(orcaPage), { timeout: 3_000 }).toBe(storeActiveId) }) - /** - * User Prompt: - * - New tab works - * - * Why: we still use the store's `setActiveTab` for the switch itself (the - * hotkey path that used to be here turned out to target bracket-chord next/ - * prev tab cycling, not arbitrary tab selection), but the final assertion - * checks DOM `data-active` to prove the selection actually paints onto the - * right tab element. - */ - test('Cmd/Ctrl+Shift+] and Cmd/Ctrl+Shift+[ switch between tabs', async ({ orcaPage }) => { - const worktreeId = (await getActiveWorktreeId(orcaPage))! - - // Ensure we have at least 2 tabs — use the real "+" flow so a render - // regression would fail setup before we even start the cycle check. - if ((await countRenderedTabs(orcaPage)) < 2) { - await orcaPage.getByRole('button', { name: 'New tab' }).click() - await orcaPage - .getByRole('menuitem', { name: /New Terminal/i }) - .first() - .click() - await expect - .poll(() => countRenderedTabs(orcaPage), { timeout: 5_000 }) - .toBeGreaterThanOrEqual(2) - } - - const firstTabId = await getActiveTabId(orcaPage) - const orderedTabs = await getWorktreeTabs(orcaPage, worktreeId) - const secondTabId = orderedTabs.find((tab) => tab.id !== firstTabId)?.id - expect(secondTabId).toBeTruthy() - - await orcaPage.evaluate((tabId) => { - window.__store?.getState().setActiveTab(tabId) - }, secondTabId) - - // DOM assertion — the second tab must actually show the active indicator. - await expect.poll(() => getDomActiveTabId(orcaPage), { timeout: 3_000 }).toBe(secondTabId) - - // Switch back. - await orcaPage.evaluate((tabId) => { - window.__store?.getState().setActiveTab(tabId) - }, firstTabId) - await expect.poll(() => getDomActiveTabId(orcaPage), { timeout: 3_000 }).toBe(firstTabId) - }) - - /** - * User Prompt: - * - dragging tabs around to reorder them - * - * Why the reorder is still store-driven: real dnd-kit pointer events are - * unreliable in the hidden-window Electron mode we run E2E in (pointer - * capture + collision detection interact poorly with `window.show()` being - * suppressed). We seed the post-drag state via `reorderUnifiedTabs` — the - * same action dnd-kit calls on drop — and then assert the tab bar's DOM - * order matches the new sequence. That final DOM check is what makes this - * a real test: a pure store round-trip would not catch a regression where - * the tab strip stopped re-rendering in the store's new order. - */ - test('dragging a tab to a new position reorders it', async ({ orcaPage }) => { - const worktreeId = (await getActiveWorktreeId(orcaPage))! - - if ((await countRenderedTabs(orcaPage)) < 2) { - await orcaPage.getByRole('button', { name: 'New tab' }).click() - await orcaPage - .getByRole('menuitem', { name: /New Terminal/i }) - .first() - .click() - await expect - .poll(() => countRenderedTabs(orcaPage), { timeout: 5_000 }) - .toBeGreaterThanOrEqual(2) - } - - const domOrderBefore = await orcaPage.$$eval(SORTABLE_TAB, (nodes) => - nodes.map((n) => (n as HTMLElement).dataset.tabId ?? '') - ) - expect(domOrderBefore.length).toBeGreaterThanOrEqual(2) - - await orcaPage.evaluate((targetWorktreeId) => { - const store = window.__store - if (!store) { - return - } - - const state = store.getState() - const groups = state.groupsByWorktree[targetWorktreeId] ?? [] - const activeGroupId = state.activeGroupIdByWorktree[targetWorktreeId] - const activeGroup = activeGroupId - ? groups.find((group) => group.id === activeGroupId) - : groups[0] - - if (activeGroup?.tabOrder?.length >= 2) { - const nextOrder = [ - activeGroup.tabOrder[1], - activeGroup.tabOrder[0], - ...activeGroup.tabOrder.slice(2) - ] - state.reorderUnifiedTabs(activeGroup.id, nextOrder) - return - } - - const terminalOrder = (state.tabsByWorktree[targetWorktreeId] ?? []).map((tab) => tab.id) - if (terminalOrder.length >= 2) { - state.setTabBarOrder(targetWorktreeId, [ - terminalOrder[1], - terminalOrder[0], - ...terminalOrder.slice(2) - ]) - } - }, worktreeId) - - // Final assertion: the tab strip must re-render with the swapped order. - // Keying off `data-tab-id` makes this independent of title formatting. - await expect - .poll( - async () => - orcaPage.$$eval(SORTABLE_TAB, (nodes) => - nodes.map((n) => (n as HTMLElement).dataset.tabId ?? '') - ), - { timeout: 3_000, message: 'Tab bar DOM order did not reflect the reorder' } - ) - .toEqual([domOrderBefore[1], domOrderBefore[0], ...domOrderBefore.slice(2)]) - }) - test('clicking tabs still switches after dragging a terminal tab to reorder', async ({ orcaPage }) => { @@ -516,7 +393,7 @@ test.describe('Tabs', () => { * so the test fails if the store cleared the tab but the DOM didn't * re-render. */ - test('closing a tab removes it from the tab bar', async ({ orcaPage }) => { + test('closing the active tab removes it and activates a neighbor', async ({ orcaPage }) => { const worktreeId = (await getActiveWorktreeId(orcaPage))! // Need a second tab so we can close one without deactivating the worktree. @@ -545,45 +422,7 @@ test.describe('Tabs', () => { message: 'Clicking close did not remove the tab element from the DOM' }) .toBe(tabsBefore - 1) - }) - - /** - * User Prompt: - * - closing tabs works - * - * The DOM check (`data-active="true"` lands on a different element) proves - * the tab bar re-paints the active indicator after a close — a store-only - * check would pass even if the indicator failed to shift. - */ - test('closing the active tab activates a neighbor tab', async ({ orcaPage }) => { - const worktreeId = (await getActiveWorktreeId(orcaPage))! - - await orcaPage.evaluate((targetWorktreeId) => { - const store = window.__store - if (!store) { - return - } - const state = store.getState() - if ((state.tabsByWorktree[targetWorktreeId] ?? []).length < 2) { - state.createTab(targetWorktreeId) - } - }, worktreeId) - await expect - .poll(() => countRenderedTabs(orcaPage), { timeout: 5_000 }) - .toBeGreaterThanOrEqual(2) - - const activeTabBefore = await getActiveTabId(orcaPage) - expect(activeTabBefore).not.toBeNull() - - await closeTabFromTabBar(orcaPage, activeTabBefore!) - - // Final DOM assertion: some *other* tab element now carries data-active. - await expect - .poll(() => getDomActiveTabId(orcaPage), { - timeout: 5_000, - message: 'After closing the active tab, no neighbor tab took over the active indicator' - }) - .not.toBe(activeTabBefore) + await expect.poll(() => getDomActiveTabId(orcaPage), { timeout: 5_000 }).not.toBe(activeId) await expect.poll(() => getDomActiveTabId(orcaPage), { timeout: 5_000 }).not.toBeNull() }) }) diff --git a/tests/e2e/terminal-tui-wheel-drain.spec.ts b/tests/e2e/terminal-tui-wheel-drain.spec.ts index b2e2cd9167c..cee81a02ec1 100644 --- a/tests/e2e/terminal-tui-wheel-drain.spec.ts +++ b/tests/e2e/terminal-tui-wheel-drain.spec.ts @@ -3,6 +3,7 @@ import os from 'node:os' import path from 'node:path' import type { Page } from '@stablyai/playwright-test' import { test, expect } from './helpers/orca-app' +import { startHiddenRendererFrameCapture } from './helpers/hidden-renderer-frame-capture' import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store' import { execInTerminal, @@ -159,25 +160,32 @@ test.describe('terminal TUI wheel report drain', () => { // Why: the dense CDP wheel stream is throughput-bound on loaded CI runners. test.slow() const logPath = path.join(os.tmpdir(), `tui-wheel-drain-${Date.now()}.log`) - await startHeavyTuiFixture(orcaPage, logPath) + const stopFrameCapture = await startHiddenRendererFrameCapture(orcaPage) + try { + await startHeavyTuiFixture(orcaPage, logPath) - const target = await terminalWheelTarget(orcaPage) - const input = await dispatchTrackpadWheelStream(orcaPage, { - alternate: false, - events: WHEEL_EVENTS, - deltaY: Math.min(49, target.cellHeight) - }) - // Give a laggy drain ample time to expose itself before reading the log. - await orcaPage.waitForTimeout(8000) + const target = await terminalWheelTarget(orcaPage) + const input = await dispatchTrackpadWheelStream(orcaPage, { + alternate: false, + events: WHEEL_EVENTS, + deltaY: Math.min(49, target.cellHeight) + }) + // Give a laggy drain ample time to expose itself before reading the log. + await orcaPage.waitForTimeout(8000) - const summary = summarizeArrivals(readReportArrivalLog(logPath), input) - fs.rmSync(logPath, { force: true }) - console.log(`[tui-wheel-drain] dense: ${JSON.stringify(summary)}`) + const summary = summarizeArrivals(readReportArrivalLog(logPath), input) + console.log(`[tui-wheel-drain] dense: ${JSON.stringify(summary)}`) - // The full gesture distance must reach the TUI (no dead/eaten scrolls)... - expect(summary.totalReports, JSON.stringify(summary)).toBeGreaterThanOrEqual(WHEEL_EVENTS - 10) - // ...while the gesture happens, not replayed 1-by-1 afterwards. - expect(summary.arrivalLagMs, JSON.stringify(summary)).toBeLessThanOrEqual(MAX_ARRIVAL_LAG_MS) + // The full gesture distance must reach the TUI (no dead/eaten scrolls)... + expect(summary.totalReports, JSON.stringify(summary)).toBeGreaterThanOrEqual( + WHEEL_EVENTS - 10 + ) + // ...while the gesture happens, not replayed 1-by-1 afterwards. + expect(summary.arrivalLagMs, JSON.stringify(summary)).toBeLessThanOrEqual(MAX_ARRIVAL_LAG_MS) + } finally { + await stopFrameCapture() + fs.rmSync(logPath, { force: true }) + } }) test('aggressive alternating trackpad-like gesture does not replay after input ends', async ({ @@ -186,20 +194,25 @@ test.describe('terminal TUI wheel report drain', () => { // Why: the dense CDP wheel stream is throughput-bound on loaded CI runners. test.slow() const logPath = path.join(os.tmpdir(), `tui-wheel-drain-alt-${Date.now()}.log`) - await startHeavyTuiFixture(orcaPage, logPath) + const stopFrameCapture = await startHiddenRendererFrameCapture(orcaPage) + try { + await startHeavyTuiFixture(orcaPage, logPath) - const target = await terminalWheelTarget(orcaPage) - const input = await dispatchTrackpadWheelStream(orcaPage, { - alternate: true, - events: WHEEL_EVENTS, - deltaY: Math.min(49, target.cellHeight) - }) - await orcaPage.waitForTimeout(8000) + const target = await terminalWheelTarget(orcaPage) + const input = await dispatchTrackpadWheelStream(orcaPage, { + alternate: true, + events: WHEEL_EVENTS, + deltaY: Math.min(49, target.cellHeight) + }) + await orcaPage.waitForTimeout(8000) - const summary = summarizeArrivals(readReportArrivalLog(logPath), input) - fs.rmSync(logPath, { force: true }) - console.log(`[tui-wheel-drain] alternate: ${JSON.stringify(summary)}`) + const summary = summarizeArrivals(readReportArrivalLog(logPath), input) + console.log(`[tui-wheel-drain] alternate: ${JSON.stringify(summary)}`) - expect(summary.arrivalLagMs, JSON.stringify(summary)).toBeLessThanOrEqual(MAX_ARRIVAL_LAG_MS) + expect(summary.arrivalLagMs, JSON.stringify(summary)).toBeLessThanOrEqual(MAX_ARRIVAL_LAG_MS) + } finally { + await stopFrameCapture() + fs.rmSync(logPath, { force: true }) + } }) }) diff --git a/tests/e2e/worktree-reveal-host-sections.spec.ts b/tests/e2e/worktree-reveal-host-sections.spec.ts index 1ad5bc0e6be..f3396af7426 100644 --- a/tests/e2e/worktree-reveal-host-sections.spec.ts +++ b/tests/e2e/worktree-reveal-host-sections.spec.ts @@ -18,143 +18,162 @@ const sections = [ ] as const for (const section of sections) { - for (const hostId of ['ssh:reveal-builder', 'runtime:reveal-builder'] as const) { - for (const collapsedHost of [false, true]) { - test(`reveals ${hostId} ${section.key} workspace without expanding local section (host collapsed: ${collapsedHost})`, async ({ - orcaPage - }, testInfo) => { - await orcaPage.emulateMedia({ reducedMotion: 'reduce' }) - await waitForSessionReady(orcaPage) - await waitForActiveWorktree(orcaPage) - await waitForStartupWorktreeRefresh(orcaPage) - const ids = await orcaPage.evaluate( - async ({ hostId, collapsedHost, section }) => { - const store = window.__store! - const state = store.getState() - const sourceRepo = state.repos[0] - const sourceWorktree = state.worktreesByRepo[sourceRepo.id][0] - await state.updateWorktreeMeta(sourceWorktree.id, { - isPinned: section.pinned, - displayName: 'Local workspace' - }) - const local = { - ...sourceWorktree, - hostId: 'local' as const, - isPinned: section.pinned, - displayName: 'Local workspace' - } - const remoteRepo = { - ...sourceRepo, - id: 'reveal-remote-repo', - executionHostId: hostId, - connectionId: hostId.startsWith('ssh:') ? 'reveal-builder' : null, - displayName: 'Remote project' - } - const remote = { - ...sourceWorktree, - id: 'reveal-remote-workspace', - repoId: remoteRepo.id, - hostId, - isPinned: section.pinned, - isMainWorktree: false, - displayName: 'Remote workspace' - } - state.setActiveView('terminal') - state.setSidebarOpen(true) - await window.api.ui.set({ groupBy: section.groupBy }) - state.setShowSleepingWorkspaces(true) - state.setHideDefaultBranchWorkspace(false) - state.setFilterRepoIds([]) - const collapsedGroups = [ - section.key, - `${section.key}:host:${hostId}`, - ...(collapsedHost ? [`host:${hostId}`] : []) - ] - await window.api.ui.set({ collapsedGroups }) - store.setState({ - groupBy: section.groupBy, - repos: [sourceRepo, remoteRepo], - worktreesByRepo: { [sourceRepo.id]: [local], [remoteRepo.id]: [remote] }, - sshTargetLabels: new Map([['reveal-builder', 'Remote builder']]), - sshConnectionStates: new Map([ - [ - 'reveal-builder', - { - targetId: 'reveal-builder', - status: 'connected', - error: null, - reconnectAttempt: 0 - } - ] - ]), - visibleWorkspaceHostIds: ['local', hostId], - workspaceHostScope: 'all', - activeRepoId: remoteRepo.id, - activeWorktreeId: remote.id, - activeWorkspaceKey: `worktree:${remote.id}`, - activeWorkspaceExecutionHostId: hostId, - pendingRevealWorktree: null, - collapsedGroups: new Set(collapsedGroups) - }) - return { local: local.id, remote: remote.id } - }, - { hostId, collapsedHost, section } - ) - const headers = orcaPage.getByRole('button', { name: new RegExp(`^${section.label}`) }) - const localRow = orcaPage.locator( - `[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.local)}]` - ) - const remoteRow = orcaPage.locator( - `[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.remote)}]` - ) - if (section.pinned || section.groupBy === 'workspace-status') { - await expect(headers.first()).toHaveAttribute('aria-expanded', 'false') - } - await expect(localRow).toHaveCount(0) - await expect(remoteRow).toHaveCount(0) - await orcaPage.screenshot({ - path: testInfo.outputPath('before-reveal.png'), - clip: { x: 0, y: 170, width: 280, height: 500 } - }) - await orcaPage.getByRole('button', { name: 'Reveal active workspace' }).click() - await expect(remoteRow).toBeVisible() - await expect(remoteRow).toHaveAttribute('data-scroll-reveal-highlight', 'true') - await orcaPage.screenshot({ - path: testInfo.outputPath('after-reveal.png'), - clip: { x: 0, y: 170, width: 280, height: 500 } - }) - await expect(headers).toHaveCount(2) - if (section.pinned || section.groupBy === 'workspace-status') { - await expect(headers.first()).toHaveAttribute('aria-expanded', 'false') - } - if (section.pinned || section.groupBy === 'workspace-status') { - await expect(headers.last()).toHaveAttribute('aria-expanded', 'true') - } - await expect(localRow).toHaveCount(0) - - await headers.last().click() - await expect(remoteRow).toHaveCount(0) - await orcaPage.evaluate((localId) => { - const store = window.__store! - const local = Object.values(store.getState().worktreesByRepo) - .flat() - .find((worktree) => worktree.id === localId)! - store.setState({ - activeRepoId: local.repoId, - activeWorktreeId: localId, - activeWorkspaceKey: `worktree:${localId}`, - activeWorkspaceExecutionHostId: 'local', - pendingRevealWorktree: null + test(`reveals ${section.key} workspaces without expanding another host's section`, async ({ + orcaPage + }, testInfo) => { + await orcaPage.emulateMedia({ reducedMotion: 'reduce' }) + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + await waitForStartupWorktreeRefresh(orcaPage) + for (const hostId of ['ssh:reveal-builder', 'runtime:reveal-builder'] as const) { + for (const collapsedHost of [false, true]) { + const scenario = `${hostId.split(':')[0]}-collapsed-${collapsedHost}` + await test.step(scenario, async () => { + const ids = await orcaPage.evaluate( + async ({ hostId, collapsedHost, section }) => { + const store = window.__store! + const state = store.getState() + const sourceRepo = state.repos[0] + const sourceWorktree = state.worktreesByRepo[sourceRepo.id][0] + await state.updateWorktreeMeta(sourceWorktree.id, { + isPinned: section.pinned, + displayName: 'Local workspace' + }) + const local = { + ...sourceWorktree, + hostId: 'local' as const, + isPinned: section.pinned, + displayName: 'Local workspace' + } + const remoteRepo = { + ...sourceRepo, + id: 'reveal-remote-repo', + executionHostId: hostId, + connectionId: hostId.startsWith('ssh:') ? 'reveal-builder' : null, + displayName: 'Remote project' + } + const remote = { + ...sourceWorktree, + id: 'reveal-remote-workspace', + repoId: remoteRepo.id, + hostId, + isPinned: section.pinned, + isMainWorktree: false, + displayName: 'Remote workspace' + } + state.setActiveView('terminal') + state.setSidebarOpen(true) + await window.api.ui.set({ groupBy: section.groupBy }) + state.setShowSleepingWorkspaces(true) + state.setHideDefaultBranchWorkspace(false) + state.setFilterRepoIds([]) + const collapsedGroups = [ + section.key, + `${section.key}:host:${hostId}`, + ...(collapsedHost ? [`host:${hostId}`] : []) + ] + await window.api.ui.set({ collapsedGroups }) + store.setState({ + groupBy: section.groupBy, + repos: [sourceRepo, remoteRepo], + worktreesByRepo: { [sourceRepo.id]: [local], [remoteRepo.id]: [remote] }, + sshTargetLabels: new Map([['reveal-builder', 'Remote builder']]), + sshConnectionStates: new Map([ + [ + 'reveal-builder', + { + targetId: 'reveal-builder', + status: 'connected', + error: null, + reconnectAttempt: 0 + } + ] + ]), + visibleWorkspaceHostIds: ['local', hostId], + workspaceHostScope: 'all', + activeRepoId: remoteRepo.id, + activeWorktreeId: remote.id, + activeWorkspaceKey: `worktree:${remote.id}`, + activeWorkspaceExecutionHostId: hostId, + pendingRevealWorktree: null, + collapsedGroups: new Set(collapsedGroups) + }) + return { local: local.id, remote: remote.id } + }, + { hostId, collapsedHost, section } + ) + const headers = orcaPage.getByRole('button', { name: new RegExp(`^${section.label}`) }) + const localRow = orcaPage.locator( + `[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.local)}]` + ) + const remoteRow = orcaPage.locator( + `[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.remote)}]` + ) + if (section.pinned || section.groupBy === 'workspace-status') { + await expect(headers.first()).toHaveAttribute('aria-expanded', 'false') + } + await expect(localRow).toHaveCount(0) + await expect(remoteRow).toHaveCount(0) + await orcaPage.screenshot({ + path: testInfo.outputPath(`${scenario}-before-reveal.png`), + clip: { x: 0, y: 170, width: 280, height: 500 } }) - }, ids.local) - await orcaPage.getByRole('button', { name: 'Reveal active workspace' }).click() - await expect(localRow).toBeVisible() - if (section.pinned || section.groupBy === 'workspace-status') { - await expect(headers.first()).toHaveAttribute('aria-expanded', 'true') - await expect(headers.last()).toHaveAttribute('aria-expanded', 'false') - } - await expect(remoteRow).toHaveCount(0) - }) + await orcaPage.getByRole('button', { name: 'Reveal active workspace' }).click() + await expect(remoteRow).toBeVisible() + await expect(remoteRow).toHaveAttribute('data-scroll-reveal-highlight', 'true') + await orcaPage.screenshot({ + path: testInfo.outputPath(`${scenario}-after-reveal.png`), + clip: { x: 0, y: 170, width: 280, height: 500 } + }) + await expect(headers).toHaveCount(2) + if (section.pinned || section.groupBy === 'workspace-status') { + await expect(headers.first()).toHaveAttribute('aria-expanded', 'false') + } + if (section.pinned || section.groupBy === 'workspace-status') { + await expect(headers.last()).toHaveAttribute('aria-expanded', 'true') + } + await expect(localRow).toHaveCount(0) + + await headers.last().click() + await expect(remoteRow).toHaveCount(0) + await orcaPage.evaluate((localId) => { + const store = window.__store! + const local = Object.values(store.getState().worktreesByRepo) + .flat() + .find((worktree) => worktree.id === localId)! + store.setState({ + activeRepoId: local.repoId, + activeWorktreeId: localId, + activeWorkspaceKey: `worktree:${localId}`, + activeWorkspaceExecutionHostId: 'local', + pendingRevealWorktree: null + }) + }, ids.local) + await orcaPage.getByRole('button', { name: 'Reveal active workspace' }).click() + await expect(localRow).toBeVisible() + if (section.pinned || section.groupBy === 'workspace-status') { + await expect(headers.first()).toHaveAttribute('aria-expanded', 'true') + await expect(headers.last()).toHaveAttribute('aria-expanded', 'false') + } + await expect(remoteRow).toHaveCount(0) + // Finish the reveal and its persistence before resetting the next scenario. + await expect + .poll(() => + orcaPage.evaluate(async () => ({ + pendingRevealWorktree: window.__store!.getState().pendingRevealWorktree, + collapsedGroups: [...window.__store!.getState().collapsedGroups].sort(), + persistedCollapsedGroups: (await window.api.ui.get()).collapsedGroups?.sort() + })) + ) + .toEqual({ + pendingRevealWorktree: null, + collapsedGroups: [`${section.key}:host:${hostId}`], + persistedCollapsedGroups: [`${section.key}:host:${hostId}`] + }) + await expect(localRow).toBeVisible() + await expect(remoteRow).toHaveCount(0) + }) + } } - } + }) } From 818e7829f6d7a563875ec4523793245eaae73d27 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:15:04 -0400 Subject: [PATCH 19/53] feat(relay): split step-5 shadow compare and dry-run logs per cell (#28693) * feat(relay): split step-5 shadow compare and dry-run logs per cell The 24 h per-cell shadow gate needs per-cell sticky-compare counts, per-cell unexplained counts, and dry-run `full` refusals while the map showed room. Both summary lines gain additive per-cell fields; existing fields are unchanged. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): count a cell-mismatch on both cells and bound per-cell maps by the fleet - countByCell (was cells) counts a compare on every cell it involves: the database's cell, every seat's cell when there is no row, and both cells for a cell-mismatch. Drop the 64-cell cap and cellsOverflow. - Add ReservePlacer.free and use it for the dry-run free share. - Rename the room bound to RESERVE_DRY_RUN_ROOM_BOUND. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): count a duplicate-seat compare on every seat cell An unexplained duplicate seat counted only on the database's cell, so a cell whose stale feed kept a second seat could pass its own step-5 check. Count it once on every seat cell, the same as a cell-mismatch. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): count explained cell-mismatch and duplicate-seat on the database cell only Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): key per-cell counts on prototype-free records Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): explain a duplicate seat left by a just-superseded half-open control When the database names the newest seat exactly, every other seat has a strictly lower epoch, and the newest seat joined within the cell's silence close (timeout + one ping interval + the map lag bound), the older seat is a half-open control awaiting its 75 s pong-silence close, not a fault. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): bound the superseded duplicate-seat rule on the move's age A same-epoch rejoin resets joinedAt, so a stale older seat could stay explained forever; the director-clock sighting of the epoch moves only when the epoch rises. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): leave a superseded duplicate seat unexplained without an epoch sighting Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): explain a superseded duplicate seat only from a prior sighting compare() recorded the epoch before classifying, so a first-ever sighting (restart or eviction) read as a fresh move. Classify first and take the move's age from the record that existed before this compare. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): also bound a superseded duplicate seat on its join age A long-ago lower-epoch sighting dates the move to this compare, so the current seat's join age bounds it too. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): count an ageless superseded duplicate seat as its own class A superseded-shaped duplicate-seat with no prior sighting of the host (the first compare since a restart) has no move age, so it is now `superseded-unknown-age` (explained: false) rather than a plain duplicate. The count line also carries `uptimeMs`, so the gate can discount that class in an instance's first 100 s. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): limit superseded-unknown-age to an instance's first 100 s Flushes are lazy, so a window has no upper bound and an uptime field on the count line cannot date it. The compare now records when it started and emits superseded-unknown-age only within the superseded close bound of that; later, a missing sighting means another instance served the host or the sighting was evicted, so it is a plain unexplained duplicate-seat. Drops the uptimeMs field. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * docs(relay): say where a missing sighting comes from after the first 100 s Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f --- .../apps/relay/src/reserve-assignment.test.ts | 37 ++- cloud/apps/relay/src/reserve-assignment.ts | 36 ++- .../apps/relay/src/reserve-placement.test.ts | 7 + cloud/apps/relay/src/reserve-placement.ts | 7 +- .../src/shadow-directory-compare.test.ts | 229 +++++++++++++++++- .../relay/src/shadow-directory-compare.ts | 104 +++++++- cloud/apps/relay/src/shadow-seat-directory.ts | 9 + 7 files changed, 406 insertions(+), 23 deletions(-) diff --git a/cloud/apps/relay/src/reserve-assignment.test.ts b/cloud/apps/relay/src/reserve-assignment.test.ts index b385c3ab1a7..38a6aa27e08 100644 --- a/cloud/apps/relay/src/reserve-assignment.test.ts +++ b/cloud/apps/relay/src/reserve-assignment.test.ts @@ -52,6 +52,7 @@ function setup( databaseBusy?: () => boolean mode?: 'off' | 'dry-run' | 'on' startedAt?: number + log?: (line: string) => void } = {} ) { const now = { value: 100_000 } @@ -88,7 +89,7 @@ function setup( ...(options.databaseBusy ? { databaseBusy: options.databaseBusy } : {}), now: () => now.value, random: () => 0.25, - log: () => undefined + log: options.log ?? (() => undefined) }) // Starts each cell's refill clock, as a director's first look does. for (const cell of assignment.placementCells()) placer.hasBudget(cell) @@ -381,4 +382,38 @@ describe('reserve assignment on a director', () => { expect(plan.kind === 'database' && (await plan.placeFresh?.())).toBeNull() expect(reserved).toMatchObject([{ dryRun: true }]) }) + + it('in dry run counts outcomes per picked cell, and a `full` refusal while the map showed room', async () => { + const summaries: Array<{ dryRun: Record }> = [] + const dryRunOn = async (units: number, outcome: string) => { + const { assignment, now } = setup({ + mode: 'dry-run', + cells: cellList(['c1']), + feeds: [feed('c1', { counts: { seats: units, controls: 0, bookings: 0, units, ceiling: 500 } })], + reserve: () => ({ outcome }), + log: (line) => summaries.push(JSON.parse(line)) + }) + for (let index = 0; index < 2; index += 1) { + const plan = await assignment.plan(HOST, { reconnect: false, region: US }) + if (plan.kind === 'database') await plan.placeFresh?.() + } + // Lets the fire-and-forget dry-run reserves settle before the summary flushes. + await Promise.resolve() + now.value += 60_000 + await assignment.plan(HOST, { reconnect: false, region: US }) + } + // 1 free seat of 500 is under the 1% bound; 250 free is far over it. + await dryRunOn(499, 'full') + await dryRunOn(250, 'full') + await dryRunOn(250, 'ok') + expect(summaries.map((summary) => summary.dryRun)).toMatchObject([ + { + outcomes: { full: 2 }, + count: 2, + cells: { c1: { outcomes: { full: 2 }, fullWithRoom: 0, minFullFreeShare: 0.002 } } + }, + { outcomes: { full: 2 }, cells: { c1: { outcomes: { full: 2 }, fullWithRoom: 2, minFullFreeShare: 0.5 } } }, + { outcomes: { ok: 2 }, cells: { c1: { outcomes: { ok: 2 }, fullWithRoom: 0, minFullFreeShare: null } } } + ]) + }) }) diff --git a/cloud/apps/relay/src/reserve-assignment.ts b/cloud/apps/relay/src/reserve-assignment.ts index ac5da96678f..a6417457110 100644 --- a/cloud/apps/relay/src/reserve-assignment.ts +++ b/cloud/apps/relay/src/reserve-assignment.ts @@ -24,6 +24,8 @@ export const RESERVE_STICKY_RECENT_MS = 10 * 60_000 // A starting director waits this long for every live cell before it answers. export const RESERVE_STARTUP_GATE_MS = 30_000 const SUMMARY_INTERVAL_MS = 60_000 +// A dry-run `full` while the map showed more free room than this share of the ceiling is `fullWithRoom`. +export const RESERVE_DRY_RUN_ROOM_BOUND = 0.01 // A leaver for these reasons was moved off its cell; it is not sent back there. const NO_REJOIN_CLOSE_CODES: ReadonlySet = new Set([ RELAY_CLOSE_CODE.DRAINING, @@ -342,13 +344,23 @@ export class ReserveAssignment { const cell = this.input.placer.pick(placements, region, new Set()) const target = cell && this.input.cells().find((entry) => entry.cellId === cell.cellId) if (!target) return null + // The map's free room at pick time; pick only returns a cell with a ceiling. + const freeShare = cell.ceiling ? this.input.placer.free(cell) / cell.ceiling : 0 const startedAt = performance.now() void this.input.client .reserve(target, { ...identity, epoch: 1, ttlMs: CELL_RESERVE_TTL_MS }, true) .then((answer) => { const elapsedMs = performance.now() - startedAt - this.summary.dryRun.outcomes[answer.outcome] = (this.summary.dryRun.outcomes[answer.outcome] ?? 0) + 1 - this.summary.dryRun.latenciesMs.push(elapsedMs) + const { dryRun } = this.summary + dryRun.outcomes[answer.outcome] = (dryRun.outcomes[answer.outcome] ?? 0) + 1 + dryRun.latenciesMs.push(elapsedMs) + const perCell = dryRun.cells[cell.cellId] ?? { outcomes: {}, fullWithRoom: 0, minFullFreeShare: null } + dryRun.cells[cell.cellId] = perCell + perCell.outcomes[answer.outcome] = (perCell.outcomes[answer.outcome] ?? 0) + 1 + if (answer.outcome === 'full') { + if (freeShare > RESERVE_DRY_RUN_ROOM_BOUND) perCell.fullWithRoom += 1 + perCell.minFullFreeShare = Math.min(perCell.minFullFreeShare ?? 1, freeShare) + } }) .catch(() => undefined) return null @@ -429,7 +441,7 @@ export class ReserveAssignment { const now = this.now() if (now - this.summaryAt < SUMMARY_INTERVAL_MS) return this.summaryAt = now - const { latenciesMs, outcomes } = this.summary.dryRun + const { latenciesMs, outcomes, cells } = this.summary.dryRun latenciesMs.sort((left, right) => left - right) const percentile = (share: number) => latenciesMs.length === 0 @@ -440,7 +452,7 @@ export class ReserveAssignment { event: 'orca_relay_reserve_placement_summary', mode: this.input.mode, ...this.summary, - dryRun: { outcomes, count: latenciesMs.length, p50Ms: percentile(0.5), p99Ms: percentile(0.99) } + dryRun: { outcomes, count: latenciesMs.length, p50Ms: percentile(0.5), p99Ms: percentile(0.99), cells } }) ) this.summary = emptySummary() @@ -456,7 +468,19 @@ type PlacementSummary = { supersedes: number outcomes: Record retries: { mapIncomplete: number; database: number; paced: number } - dryRun: { outcomes: Record; latenciesMs: number[] } + dryRun: { + outcomes: Record + latenciesMs: number[] + // Keyed by the picked cell, so bounded by the fleet. + cells: Record + } +} + +type DryRunCellSummary = { + outcomes: Record + fullWithRoom: number + // Lowest map free share (of the ceiling) among this cell's `full` refusals. + minFullFreeShare: number | null } function emptySummary(): PlacementSummary { @@ -469,6 +493,6 @@ function emptySummary(): PlacementSummary { supersedes: 0, outcomes: {}, retries: { mapIncomplete: 0, database: 0, paced: 0 }, - dryRun: { outcomes: {}, latenciesMs: [] } + dryRun: { outcomes: {}, latenciesMs: [], cells: Object.create(null) } } } diff --git a/cloud/apps/relay/src/reserve-placement.test.ts b/cloud/apps/relay/src/reserve-placement.test.ts index a79f3b642ea..38f92e5f898 100644 --- a/cloud/apps/relay/src/reserve-placement.test.ts +++ b/cloud/apps/relay/src/reserve-placement.test.ts @@ -29,6 +29,13 @@ function warmPlacer(now: { value: number }, random = () => 0.3) { } describe('reserve placer', () => { + it('reports free seats under the ceiling, never below zero or without a ceiling', () => { + const placer = warmPlacer({ value: 0 }) + expect(placer.free(cell('c1', { seats: 60, bookings: 10 }))).toBe(30) + expect(placer.free(cell('c1', { seats: 120 }))).toBe(0) + expect(placer.free(cell('c1', { ceiling: undefined }))).toBe(0) + }) + it('places on the first cell that answers ok and counts one call', async () => { const now = { value: 0 } const placer = warmPlacer(now) diff --git a/cloud/apps/relay/src/reserve-placement.ts b/cloud/apps/relay/src/reserve-placement.ts index b3436f148c0..3e677c054f5 100644 --- a/cloud/apps/relay/src/reserve-placement.ts +++ b/cloud/apps/relay/src/reserve-placement.ts @@ -68,6 +68,11 @@ export class ReservePlacer { return cell.seats + cell.bookings + this.ownBookingsSince(cell) } + // Estimated free seats under the ceiling; 0 when the cell reports none. + free(cell: PlacementCell): number { + return cell.ceiling === undefined ? 0 : Math.max(0, cell.ceiling - this.load(cell)) + } + hasSeat(cell: PlacementCell): boolean { return cell.ceiling !== undefined && this.load(cell) < cell.ceiling } @@ -158,7 +163,7 @@ export class ReservePlacer { if (cell.region !== region || !cell.general || cell.draining) continue if (cell.reserve) { if (this.isCandidate(cell, region, now)) { - reserveFree += Math.max(0, cell.ceiling! - this.load(cell)) + reserveFree += this.free(cell) } continue } diff --git a/cloud/apps/relay/src/shadow-directory-compare.test.ts b/cloud/apps/relay/src/shadow-directory-compare.test.ts index 896ad9297c4..c2a36d64ede 100644 --- a/cloud/apps/relay/src/shadow-directory-compare.test.ts +++ b/cloud/apps/relay/src/shadow-directory-compare.test.ts @@ -5,6 +5,7 @@ import { classifyShadowSeat, SHADOW_COMPARE_FLUSH_MS, SHADOW_COMPARE_SAMPLES_PER_CLASS, + SHADOW_COMPARE_SUPERSEDED_CLOSE_MS, ShadowDirectoryCompare } from './shadow-directory-compare.js' import { ShadowSeatDirectory, type SeatFeedResponse } from './shadow-seat-directory.js' @@ -27,8 +28,8 @@ function feed(cellId: string, overrides: Partial = {}): SeatFe return { v: 1, cellId, incarnation: `inc-${cellId}`, seq: 1, at: 1, full: [], ...overrides } } -function seat(epoch: number, state = 'active', relayHostId = HOST) { - return { userId: 'user-a', relayHostId, epoch, generation: 1, state, joinedAt: 1 } +function seat(epoch: number, state = 'active', relayHostId = HOST, joinedAt = 1) { + return { userId: 'user-a', relayHostId, epoch, generation: 1, state, joinedAt } } function directoryWith( @@ -84,7 +85,7 @@ describe('classifyShadowSeat', () => { true ], [ - directoryWith({ 'cell-a': [seat(3)], 'cell-b': [seat(2)] }), + directoryWith({ 'cell-a': [seat(3)], 'cell-b': [seat(3)] }), at('cell-a', 3), 'duplicate-seat', false @@ -191,6 +192,178 @@ describe('classifyShadowSeat', () => { }) }) + describe('a duplicate seat left by a superseded half-open control', () => { + // Seated on cell-b at epoch 2 long ago; the director first saw epoch 3 on cell-a at firstSeen. + const firstSeen = 1_000_000 + const directoryAt = (seats: Record[]>) => { + const directory = directoryWith(seats) + directory.observeDatabaseEpoch('user-a', HOST, 3, firstSeen) + return directory + } + const superseded = (joinedAt = firstSeen) => + directoryAt({ + 'cell-a': [seat(3, 'active', HOST, joinedAt)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + + it('is explained while the old control may still await its silence close', () => { + for (const now of [firstSeen + 1_700, firstSeen + SHADOW_COMPARE_SUPERSEDED_CLOSE_MS]) { + expect(classifyShadowSeat(superseded(), IDENTITY, at('cell-a', 3), now)).toEqual({ + class: 'duplicate-seat', + explained: true + }) + } + }) + + it('is unexplained just past the close bound', () => { + const now = firstSeen + SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + 1 + expect(classifyShadowSeat(superseded(), IDENTITY, at('cell-a', 3), now)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + + it('stays unexplained past the bound after a same-epoch rejoin resets joinedAt', () => { + const now = firstSeen + SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + 30_000 + expect(classifyShadowSeat(superseded(now - 1_000), IDENTITY, at('cell-a', 3), now)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + + const unseen = () => + directoryWith({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + + it('is its own unexplained class with no sighting in the instance\'s first 100 s', () => { + const now = firstSeen + 1_700 + const startedAt = now - SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + expect(classifyShadowSeat(unseen(), IDENTITY, at('cell-a', 3), now, startedAt)).toEqual({ + class: 'superseded-unknown-age', + explained: false + }) + }) + + it('is a plain unexplained duplicate with no sighting after the first 100 s', () => { + const now = firstSeen + 1_700 + const startedAt = now - SHADOW_COMPARE_SUPERSEDED_CLOSE_MS - 1 + for (const classified of [ + classifyShadowSeat(unseen(), IDENTITY, at('cell-a', 3), now, startedAt), + classifyShadowSeat(unseen(), IDENTITY, at('cell-a', 3), now) + ]) { + expect(classified).toEqual({ class: 'duplicate-seat', explained: false }) + } + }) + + it('stays a plain duplicate with no sighting when the current seat joined long ago', () => { + const now = firstSeen + SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + 1 + const directory = directoryWith({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + expect(classifyShadowSeat(directory, IDENTITY, at('cell-a', 3), now)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + + it('classifies before recording, so a later epoch bump is explained', () => { + // Seated on cell-a at epoch 2 and on cell-b at epoch 1; the database names cell-a at 2. + const directory = directoryWith({ + 'cell-a': [seat(2, 'active', HOST, firstSeen)], + 'cell-b': [seat(1, 'active', HOST, 1)] + }) + let now = firstSeen + const compare = new ShadowDirectoryCompare(directory, () => now, () => {}) + expect(compare.compare('sticky-verify', IDENTITY, at('cell-a', 2))).toEqual({ + class: 'superseded-unknown-age', + explained: false + }) + // A re-assign to cell-b at epoch 3 supersedes cell-a's epoch-2 seat. + now = firstSeen + 1_700 + directory.apply( + 'cell-b', + feed('cell-b', { seq: 2, full: [seat(3, 'active', HOST, now)] }), + now + ) + expect(compare.compare('sticky-verify', IDENTITY, at('cell-b', 3))).toEqual({ + class: 'duplicate-seat', + explained: true + }) + }) + + it('is explained when the director last saw the host at a lower epoch', () => { + const directory = directoryWith({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + directory.observeDatabaseEpoch('user-a', HOST, 2, 1) + expect(classifyShadowSeat(directory, IDENTITY, at('cell-a', 3), firstSeen + 1_700)).toEqual({ + class: 'duplicate-seat', + explained: true + }) + }) + + it('is unexplained after a lower-epoch sighting when the current seat joined long ago', () => { + const now = firstSeen + SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + 1 + const directory = directoryWith({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + directory.observeDatabaseEpoch('user-a', HOST, 2, 1) + expect(classifyShadowSeat(directory, IDENTITY, at('cell-a', 3), now)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + + it('is unexplained when the database answer regresses below the recorded epoch', () => { + const directory = directoryWith({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + directory.observeDatabaseEpoch('user-a', HOST, 4, firstSeen) + expect(classifyShadowSeat(directory, IDENTITY, at('cell-a', 3), firstSeen + 1_700)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + + it('is unexplained when the database names a newer epoch than the seat on its cell', () => { + const directory = directoryWith({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + directory.observeDatabaseEpoch('user-a', HOST, 4, firstSeen) + expect(classifyShadowSeat(directory, IDENTITY, at('cell-a', 4), firstSeen + 1_700)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + + it('is unexplained when the database names the lower epoch, or matches no seat', () => { + for (const answer of [at('cell-b', 2), at('cell-a', 4), at('cell-b', 3)]) { + expect(classifyShadowSeat(superseded(), IDENTITY, answer, firstSeen + 1_700)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + } + }) + + it('is unexplained on a level epoch, which only the row can break', () => { + const directory = directoryAt({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(3, 'active', HOST, 1)] + }) + expect(classifyShadowSeat(directory, IDENTITY, at('cell-a', 3), firstSeen + 1_700)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + }) + it('answers map-incomplete until every listed cell has answered', () => { const directory = new ShadowSeatDirectory() directory.setCells(['cell-a', 'cell-b']) @@ -225,7 +398,8 @@ describe('ShadowDirectoryCompare', () => { class: 'map-only', explained: false, count: SHADOW_COMPARE_SAMPLES_PER_CLASS + 5, - windowMs: SHADOW_COMPARE_FLUSH_MS + windowMs: SHADOW_COMPARE_FLUSH_MS, + countByCell: { 'cell-a': SHADOW_COMPARE_SAMPLES_PER_CLASS + 5 } }, { event: 'orca_relay_shadow_compare', @@ -233,10 +407,55 @@ describe('ShadowDirectoryCompare', () => { class: 'agree', explained: true, count: 2, - windowMs: SHADOW_COMPARE_FLUSH_MS + windowMs: SHADOW_COMPARE_FLUSH_MS, + countByCell: { 'cell-a': 2 } } ]) }) + + it('counts a compare once on every cell it involves, and on none for a host neither side has', () => { + const lines: Array> = [] + const compareOn = (seats: Record[]>) => + new ShadowDirectoryCompare(directoryWith(seats), () => 0, (line) => lines.push(JSON.parse(line))) + const unseen = compareOn({ 'cell-a': [], 'cell-b': [] }) + unseen.compare('sticky-verify', IDENTITY, assignment('cell-a')) + unseen.compare('sticky-verify', IDENTITY, assignment('cell-a')) + unseen.compare('sticky-verify', IDENTITY, assignment('cell-b')) + unseen.compare('resolve', IDENTITY, null) + unseen.flush(0) + // The database says cell-a at epoch 3; the map seats the host on cell-b at a newer epoch. + const mismatch = compareOn({ 'cell-a': [], 'cell-b': [seat(4)] }) + mismatch.compare('sticky-verify', IDENTITY, assignment('cell-a')) + mismatch.flush(0) + // A level-epoch duplicate counts on the database's own cell once, and on the other seat cell. + const duplicate = compareOn({ 'cell-a': [seat(3)], 'cell-b': [seat(3)] }) + duplicate.compare('sticky-verify', IDENTITY, assignment('cell-a')) + duplicate.flush(0) + // A drain-only second seat is explained, so it counts only on the database's cell. + const draining = compareOn({ 'cell-a': [seat(3)], 'cell-b': [seat(2, 'drain-only')] }) + draining.compare('sticky-verify', IDENTITY, assignment('cell-a')) + draining.flush(0) + // A superseded older seat: the first-ever sighting has no age, so it is its own unexplained + // class on both cells; seen before, it is explained and counts only on the database's cell. + const superseded = compareOn({ 'cell-a': [seat(3)], 'cell-b': [seat(2)] }) + superseded.compare('sticky-verify', IDENTITY, assignment('cell-a')) + superseded.compare('sticky-verify', IDENTITY, assignment('cell-a')) + superseded.flush(0) + const mapOnly = compareOn({ 'cell-a': [seat(3)], 'cell-b': [seat(3)] }) + mapOnly.compare('resolve', IDENTITY, null) + mapOnly.flush(0) + const counts = lines.filter((line) => line.event === 'orca_relay_shadow_compare') + expect(counts.map((line) => [line.class, line.explained, line.count, line.countByCell])).toEqual([ + ['db-only-unseen', true, 3, { 'cell-a': 2, 'cell-b': 1 }], + ['agree-absent', true, 1, {}], + ['cell-mismatch', false, 1, { 'cell-a': 1, 'cell-b': 1 }], + ['duplicate-seat', false, 1, { 'cell-a': 1, 'cell-b': 1 }], + ['duplicate-seat', true, 1, { 'cell-a': 1 }], + ['superseded-unknown-age', false, 1, { 'cell-a': 1, 'cell-b': 1 }], + ['duplicate-seat', true, 1, { 'cell-a': 1 }], + ['map-only', false, 1, { 'cell-a': 1, 'cell-b': 1 }] + ]) + }) }) describe('unexplained samples', () => { diff --git a/cloud/apps/relay/src/shadow-directory-compare.ts b/cloud/apps/relay/src/shadow-directory-compare.ts index 328b5fa3bb4..0fbf24f90cb 100644 --- a/cloud/apps/relay/src/shadow-directory-compare.ts +++ b/cloud/apps/relay/src/shadow-directory-compare.ts @@ -1,3 +1,4 @@ +import { RELAY_PROTOCOL_LIMITS } from '@orca-cloud/relay-contract' import { relayHostLogDigest } from './relay-host-log-digest.js' import type { ShadowSeatDirectory } from './shadow-seat-directory.js' @@ -20,6 +21,8 @@ export type ShadowCompareClass = | 'cell-mismatch' | 'epoch-mismatch' | 'duplicate-seat' + // A superseded-shaped duplicate with no prior sighting, in the instance's first 100 s. + | 'superseded-unknown-age' | 'map-only' // The map seats the host on a cell the database does not call live, so resolve answered null. | 'map-only-cell-unlive' @@ -34,16 +37,28 @@ export const SHADOW_COMPARE_FLUSH_MS = 60_000 // One poll, its timeout, and a reconnect (p95 ~6 s on 10-02). A move older than // this is not "the map catching up": a lost leave must not read as explained forever. export const SHADOW_COMPARE_LAG_BOUND_MS = 10_000 +// A superseded half-open control lingers until the cell's silence close (the timeout plus up +// to one ping interval), then the map trails that close by a poll. +export const SHADOW_COMPARE_SUPERSEDED_CLOSE_MS = + RELAY_PROTOCOL_LIMITS.controlSilenceTimeoutMs + + RELAY_PROTOCOL_LIMITS.controlPingIntervalMs + + SHADOW_COMPARE_LAG_BOUND_MS // Per class per flush window, so a systemic fault cannot flood the log. export const SHADOW_COMPARE_SAMPLES_PER_CLASS = 10 +// `countByCell` is keyed by cell, so bounded by the fleet; prototype-free, so any cell id is a key. +type ShadowCompareCount = { count: number; countByCell: Record } + export function classifyShadowSeat( directory: ShadowSeatDirectory, identity: { userId: string; relayHostId: string }, answer: ShadowCompareDatabaseAnswer, - now: number + now: number, + // When this instance began comparing; omitted, a missing sighting is never excused. + startedAt = -Infinity ): ShadowCompareVerdict { - // Pure: only ShadowDirectoryCompare records sightings. An epoch never seen is new. + // Pure: only ShadowDirectoryCompare records sightings. An epoch never seen is new; unlike the + // superseded rule, a wrong guess here only explains a mismatch for one lag bound. const movedAt = answer ? (directory.databaseEpochFirstSeen( identity.userId, @@ -69,7 +84,31 @@ export function classifyShadowSeat( } if (seats.length > 1) { // Inside a drain or rehome grace the old seat is drain-only until it closes. - return { class: 'duplicate-seat', explained: seats.some((seat) => seat.state === 'drain-only') } + if (seats.some((seat) => seat.state === 'drain-only')) { + return { class: 'duplicate-seat', explained: true } + } + // A newer DB seat supersedes older-epoch seats until their silence close; + // a level epoch is a real tie. + const current = seats.find( + (seat) => seat.cellId === answer.cellId && seat.epoch === answer.assignmentEpoch + ) + const supersededShape = + current !== undefined && + seats.every((seat) => seat === current || seat.epoch < current.epoch) && + // A long-ago lower-epoch sighting dates the move to now; the join bounds the seat itself. + now - current.joinedAt <= SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + if (!supersededShape) return { class: 'duplicate-seat', explained: false } + const known = directory.databaseEpochOf(identity.userId, identity.relayHostId) + if (!known) { + // Later, no sighting means another instance served the earlier compares, or the map evicted + // the host, so a harmless supersede still reads as an unexplained duplicate-seat. + const starting = now - startedAt <= SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + return { class: starting ? 'superseded-unknown-age' : 'duplicate-seat', explained: false } + } + const supersededAt = movedSince(known, answer, now) + const superseded = + supersededAt !== undefined && now - supersededAt <= SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + return { class: 'duplicate-seat', explained: superseded } } const seat = seats[0] if (!seat) { @@ -99,17 +138,54 @@ export function classifyShadowSeat( return { class: 'agree', explained: true } } +// When the move to the answer's epoch happened, from the sighting recorded before this compare. +// A regressed epoch means unknown, not new. +function movedSince( + known: { epoch: number; firstSeenAt: number }, + answer: { assignmentEpoch: number }, + now: number +): number | undefined { + if (known.epoch > answer.assignmentEpoch) return undefined + // A lower recorded epoch: the move is this sighting. + return known.epoch < answer.assignmentEpoch ? now : known.firstSeenAt +} + +// The database's cell, or with no row every seat's cell; an unexplained cell-mismatch or +// duplicate-seat also counts on every seat cell, so a stale seat feed shows on its own cell. +// A compare with neither (agree-absent, or map-incomplete with no row and no seat) has none. +function comparedCells( + directory: ShadowSeatDirectory, + identity: { userId: string; relayHostId: string }, + answer: ShadowCompareDatabaseAnswer, + verdict: ShadowCompareVerdict +): Set { + const seats = directory.seatsOf(identity.userId, identity.relayHostId) + if (!answer) return new Set(seats.map((seat) => seat.cellId)) + const cells = new Set([answer.cellId]) + if ( + !verdict.explained && + (verdict.class === 'cell-mismatch' || + verdict.class === 'duplicate-seat' || + verdict.class === 'superseded-unknown-age') + ) { + for (const seat of seats) cells.add(seat.cellId) + } + return cells +} + export class ShadowDirectoryCompare { - private counts = new Map() + private counts = new Map() private samples = new Map() private windowStartedAt: number + private readonly startedAt: number constructor( private readonly directory: ShadowSeatDirectory, private readonly now: () => number = Date.now, private readonly log: (line: string) => void = (line) => console.warn(line) ) { - this.windowStartedAt = now() + this.startedAt = now() + this.windowStartedAt = this.startedAt } compare( @@ -118,6 +194,8 @@ export class ShadowDirectoryCompare { answer: ShadowCompareDatabaseAnswer ): ShadowCompareVerdict { const now = this.now() + // Classify first, so it can tell a first-ever sighting from one seen before. + const verdict = classifyShadowSeat(this.directory, identity, answer, now, this.startedAt) // Every answer, so a move's age counts from its first sighting on either route. if (answer) { this.directory.observeDatabaseEpoch( @@ -127,17 +205,22 @@ export class ShadowDirectoryCompare { now ) } - const verdict = classifyShadowSeat(this.directory, identity, answer, now) const key = `${route}\u0000${verdict.class}\u0000${verdict.explained}` - this.counts.set(key, (this.counts.get(key) ?? 0) + 1) + const entry = this.counts.get(key) ?? { count: 0, countByCell: Object.create(null) } + this.counts.set(key, entry) + entry.count += 1 + for (const cellId of comparedCells(this.directory, identity, answer, verdict)) { + entry.countByCell[cellId] = (entry.countByCell[cellId] ?? 0) + 1 + } if (!verdict.explained) this.sample(route, identity, answer, verdict, now) if (now - this.windowStartedAt >= SHADOW_COMPARE_FLUSH_MS) this.flush(now) return verdict } - // One line per route and class per window; the log metric sums `count`. + // One line per route and class per window; the log metric sums `count`. `countByCell` + // counts each compare once on every cell it involves (see comparedCells). flush(now: number = this.now()): void { - for (const [key, count] of this.counts) { + for (const [key, { count, countByCell }] of this.counts) { const [route, verdictClass, explained] = key.split('\u0000') this.log( JSON.stringify({ @@ -146,7 +229,8 @@ export class ShadowDirectoryCompare { class: verdictClass, explained: explained === 'true', count, - windowMs: now - this.windowStartedAt + windowMs: now - this.windowStartedAt, + countByCell }) ) } diff --git a/cloud/apps/relay/src/shadow-seat-directory.ts b/cloud/apps/relay/src/shadow-seat-directory.ts index 7dacf6a21df..f57db08dea7 100644 --- a/cloud/apps/relay/src/shadow-seat-directory.ts +++ b/cloud/apps/relay/src/shadow-seat-directory.ts @@ -404,6 +404,15 @@ export class ShadowSeatDirectory { return known && known.epoch >= epoch ? known.firstSeenAt : undefined } + // The newest epoch this director has seen the database name for the host, if still kept. + databaseEpochOf( + userId: string, + relayHostId: string + ): { epoch: number; firstSeenAt: number } | undefined { + const known = this.databaseEpochs.get(hostKey(userId, relayHostId)) + return known ? { ...known } : undefined + } + // Records a sighting; returns when this director first saw the host at this epoch. observeDatabaseEpoch(userId: string, relayHostId: string, epoch: number, now: number): number { const key = hostKey(userId, relayHostId) From 66451a2c3a52276fabf580673f14afc72ca43717 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:28:23 -0700 Subject: [PATCH 20/53] feat(orcad-migration): a capability-gated blob channel beside scrollback (D1b-6a) (#28866) * feat(orcad-migration): a capability-gated blob channel beside scrollback, for the WSL mover's new data (D1b-6a) * fix(orcad-migration): the commit names its blob set, prune clears leaked blobs, reserved names refused (D1b-6a) --- .../persistence-orcad-migration-blobs.test.ts | 115 +++++++ .../loading-store/store-runtime-state.ts | 4 + .../orcad-catalog-import.ts | 103 ++++-- .../orcad-migration-blob-staging.test.ts | 264 +++++++++++++++ .../orcad-migration-blob-staging.ts | 305 ++++++++++++++++++ .../orcad-migration-staged-file.ts | 214 ++++++++++++ ...orcad-scrollback-snapshot-transfer.test.ts | 34 +- .../orcad-scrollback-snapshot-transfer.ts | 228 +++---------- src/main/runtime/orca-runtime-get-status.ts | 4 + .../runtime/orca-runtime-migration-catalog.ts | 18 +- .../runtime/orcad-migration-catalog-import.ts | 7 +- .../rpc/methods/orcad-migration.test.ts | 77 ++++- .../runtime/rpc/methods/orcad-migration.ts | 22 +- src/main/runtime/runtime-store-contract.ts | 1 + .../orcad-migration-blob-coordinator.test.ts | 182 +++++++++++ .../ssh/orcad-migration-blob-coordinator.ts | 154 +++++++++ ...migration-catalog-client-blob-kind.test.ts | 174 ++++++++++ .../ssh/orcad-migration-catalog-client.ts | 67 +++- .../orcad-migration-blob-transfer.test.ts | 192 +++++++++++ src/shared/orcad-migration-blob-transfer.ts | 269 +++++++++++++++ src/shared/orcad-migration-catalog-state.ts | 5 +- src/shared/orcad-migration-manifest.ts | 3 + src/shared/orcad-migration-scrollback.ts | 24 +- src/shared/orcad-runtime-capabilities.ts | 4 + .../rpc-contract/orcad-migration-params.ts | 12 + .../rpc-params-catalog.generated.ts | 9 +- 26 files changed, 2258 insertions(+), 233 deletions(-) create mode 100644 src/main/persistence-orcad-migration-blobs.test.ts create mode 100644 src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.test.ts create mode 100644 src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.ts create mode 100644 src/main/persistence/migrating-orcad-catalog/orcad-migration-staged-file.ts create mode 100644 src/main/ssh/orcad-migration-blob-coordinator.test.ts create mode 100644 src/main/ssh/orcad-migration-blob-coordinator.ts create mode 100644 src/main/ssh/orcad-migration-catalog-client-blob-kind.test.ts create mode 100644 src/shared/orcad-migration-blob-transfer.test.ts create mode 100644 src/shared/orcad-migration-blob-transfer.ts diff --git a/src/main/persistence-orcad-migration-blobs.test.ts b/src/main/persistence-orcad-migration-blobs.test.ts new file mode 100644 index 00000000000..0d95320d2bd --- /dev/null +++ b/src/main/persistence-orcad-migration-blobs.test.ts @@ -0,0 +1,115 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createHash } from 'node:crypto' +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import type { OrcadMigrationBlobDescriptor } from '../shared/orcad-migration-blob-transfer' +import { getOrcadMigrationReceivedBlobPath } from './persistence/migrating-orcad-catalog/orcad-migration-blob-staging' +import { manifest } from './persistence-orcad-migration-catalog-fixture' +import { createStore, testState } from './persistence-test-harness' + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { + isEncryptionAvailable: () => true, + encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'), + decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length) + } +})) + +vi.mock('./telemetry/client', () => ({ track: vi.fn() })) + +const bytes = Buffer.from('history line one\nhistory line two\n') +const blob: OrcadMigrationBlobDescriptor = { + blobKind: 'shell-history', + key: `${'d'.repeat(16)}-zsh`, + sha256: createHash('sha256').update(bytes).digest('hex'), + byteLength: bytes.length +} + +function chunk(input: ReturnType, offset: number, end: number) { + return { + migrationId: input.migrationId, + manifestSha256: input.manifestSha256, + blob, + offset, + bytesBase64: bytes.subarray(offset, end).toString('base64') + } +} + +function blobRoot(): string { + return join(testState.dir, 'orcad-migration-blobs') +} + +describe('orcad migration blobs through the store', () => { + beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'orca-migration-blobs-')) + }) + + afterEach(() => { + rmSync(testState.dir, { recursive: true, force: true }) + }) + + it('reports staged progress across a restart and refuses commit until complete', async () => { + const input = manifest() + const store = createStore() + store.stageOrcadMigrationCatalog(input) + await store.flushPendingOrThrowAsync() + expect(store.getOrcadMigrationCatalogState(input)).not.toHaveProperty('blobUploads') + store.stageOrcadMigrationBlobChunk(chunk(input, 0, 8)) + + const restarted = createStore() + expect(restarted.getOrcadMigrationCatalogState(input)).toMatchObject({ + state: 'staged', + blobUploads: [{ ...blob, receivedBytes: 8 }] + }) + expect(() => restarted.commitStagedOrcadMigrationCatalog(input)).toThrow( + `orcad_migration_blob_incomplete:shell-history:${blob.key}` + ) + expect(restarted.getOrcadMigrationCatalogState(input).state).toBe('staged') + + restarted.stageOrcadMigrationBlobChunk(chunk(input, 8, bytes.length)) + expect(restarted.commitStagedOrcadMigrationCatalog(input).state).toBe('committed') + expect(readFileSync(getOrcadMigrationReceivedBlobPath(blobRoot(), input, blob))).toEqual(bytes) + }) + + it('commits only the blob set the sender names, and checks nothing when it names none', () => { + const input = manifest() + const store = createStore() + store.stageOrcadMigrationCatalog(input) + store.stageOrcadMigrationBlobChunk(chunk(input, 0, bytes.length)) + const missing: OrcadMigrationBlobDescriptor = { + blobKind: 'agent-session-records', + key: 'session-lost', + sha256: 'e'.repeat(64), + byteLength: 4 + } + + expect(() => + store.commitStagedOrcadMigrationCatalog(input, { + expectedBlobs: [blob, missing] + }) + ).toThrow('orcad_migration_blob_set_mismatch:agent-session-records:session-lost') + expect(() => store.commitStagedOrcadMigrationCatalog(input, { expectedBlobs: [] })).toThrow( + `orcad_migration_blob_set_mismatch:shell-history:${blob.key}` + ) + expect(store.getOrcadMigrationCatalogState(input).state).toBe('staged') + expect(store.commitStagedOrcadMigrationCatalog(input, { expectedBlobs: [blob] }).state).toBe( + 'committed' + ) + }) + + it('removes staged blobs when the catalog is aborted', () => { + const input = manifest() + const store = createStore() + store.stageOrcadMigrationCatalog(input) + store.stageOrcadMigrationBlobChunk(chunk(input, 0, 8)) + + expect(store.abortStagedOrcadMigrationCatalog(input)).toMatchObject({ aborted: true }) + expect(() => store.stageOrcadMigrationBlobChunk(chunk(input, 8, bytes.length))).toThrow( + 'orcad_migration_blob_catalog_not_staged' + ) + store.stageOrcadMigrationCatalog(input) + expect(store.getOrcadMigrationCatalogState(input)).not.toHaveProperty('blobUploads') + }) +}) diff --git a/src/main/persistence/loading-store/store-runtime-state.ts b/src/main/persistence/loading-store/store-runtime-state.ts index ce557228c29..c8e8dc8e42c 100644 --- a/src/main/persistence/loading-store/store-runtime-state.ts +++ b/src/main/persistence/loading-store/store-runtime-state.ts @@ -9,6 +9,7 @@ import { } from '../../terminal-scrollback-snapshots' import { ProtectedSecretPersistence } from '../../protected-secret-persistence' import { getDataFile, getGithubCacheFile } from './user-data-path' +import { dirname, join } from 'node:path' import { STALE_DURABLE_WRITE_TEMP_AGE_MS } from '../tracking-repos/worktree-metadata-normalization' import type { ProjectGroupPersistenceOperations } from '../tracking-repos/project-group-operations' import type { FolderWorkspacePersistenceOperations } from '../restoring-sessions/folder-workspace-operations' @@ -45,6 +46,8 @@ export class StoreRuntimeState { automationListProjectionCache: AutomationListProjectionCache | null = null activeViewPreference!: ActiveViewPreference readonly terminalScrollbackSnapshotStorage: TerminalScrollbackSnapshotStorage + /** Staged and received migration blobs (see orcad-migration-blob-staging.ts). */ + readonly orcadMigrationBlobRoot: string /** Scrollback refs each in-flight migration export still reads, keyed by migration id. */ readonly retainedScrollbackRefsByMigrationId = new Map>() writeTimer: ReturnType | null = null @@ -112,5 +115,6 @@ export class StoreRuntimeState { snapshotRoot: profileSnapshotRoot, fallbackSnapshotRoot: legacySnapshotRoot === profileSnapshotRoot ? null : legacySnapshotRoot } + this.orcadMigrationBlobRoot = join(dirname(this.dataFile), 'orcad-migration-blobs') } } diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-catalog-import.ts b/src/main/persistence/migrating-orcad-catalog/orcad-catalog-import.ts index 817e061e755..a76ce6f1cba 100644 --- a/src/main/persistence/migrating-orcad-catalog/orcad-catalog-import.ts +++ b/src/main/persistence/migrating-orcad-catalog/orcad-catalog-import.ts @@ -10,6 +10,11 @@ import type { OrcadMigrationSnapshotChunkRequest, OrcadMigrationSnapshotChunkResult } from '../../../shared/orcad-migration-scrollback' +import type { + OrcadMigrationBlobChunkRequest, + OrcadMigrationBlobChunkResult, + OrcadMigrationBlobDescriptor +} from '../../../shared/orcad-migration-blob-transfer' import type { RepoLifecycleOperations } from '../loading-store/repo-lifecycle-operations' import type { StoreRuntimeState } from '../loading-store/store-runtime-state' import type { WriteSchedulingOperations } from '../loading-store/write-scheduling' @@ -35,10 +40,19 @@ import { pruneOrcadMigrationSnapshotStaging, stageOrcadMigrationSnapshotChunk } from './orcad-scrollback-snapshot-transfer' +import { + abortOrcadMigrationBlobs, + assertOrcadMigrationBlobSetMatches, + assertOrcadMigrationBlobsReady, + commitOrcadMigrationBlobs, + inspectOrcadMigrationBlobUploads, + pruneOrcadMigrationBlobStaging, + stageOrcadMigrationBlobChunk +} from './orcad-migration-blob-staging' type OrcadCatalogImportRuntime = Pick< StoreRuntimeState, - 'state' | 'terminalScrollbackSnapshotStorage' + 'state' | 'terminalScrollbackSnapshotStorage' | 'orcadMigrationBlobRoot' > const orcadCatalogImportContext = Symbol('OrcadCatalogImportPersistence') @@ -73,11 +87,12 @@ export class OrcadCatalogImportPersistence { stagedManifests(context.runtime.state), context.runtime.terminalScrollbackSnapshotStorage ) - const current = migrationCatalogState( - context.runtime.state, - manifest, - context.runtime.terminalScrollbackSnapshotStorage + pruneOrcadMigrationBlobStaging( + stagedManifests(context.runtime.state), + committedMigrations(context.runtime.state), + context.runtime.orcadMigrationBlobRoot ) + const current = migrationCatalogState(context.runtime, manifest) if (current.state === 'committed') { return current } @@ -99,7 +114,7 @@ export class OrcadCatalogImportPersistence { } ] scheduleSave(context.scheduling) - return stagedCatalogState(manifest, stagedAt, context.runtime.terminalScrollbackSnapshotStorage) + return stagedCatalogState(manifest, stagedAt, context.runtime) } stageOrcadMigrationSnapshotChunk( @@ -116,16 +131,27 @@ export class OrcadCatalogImportPersistence { }) } + stageOrcadMigrationBlobChunk( + request: OrcadMigrationBlobChunkRequest + ): OrcadMigrationBlobChunkResult { + const context = this[orcadCatalogImportContext] + return stageOrcadMigrationBlobChunk({ + stagedManifest: + stagedManifests(context.runtime.state).find( + (manifest) => manifest.migrationId === request.migrationId + ) ?? null, + root: context.runtime.orcadMigrationBlobRoot, + request + }) + } + + /** `expectedBlobs` absent (SSH, older senders) keeps the pre-blob-set commit unchanged. */ commitStagedOrcadMigrationCatalog( manifest: OrcadMigrationManifest, - options: { now?: () => Date } = {} + options: { now?: () => Date; expectedBlobs?: readonly OrcadMigrationBlobDescriptor[] } = {} ): OrcadMigrationCatalogState { const context = this[orcadCatalogImportContext] - const current = migrationCatalogState( - context.runtime.state, - manifest, - context.runtime.terminalScrollbackSnapshotStorage - ) + const current = migrationCatalogState(context.runtime, manifest) if (current.state === 'committed') { return current } @@ -134,7 +160,16 @@ export class OrcadCatalogImportPersistence { } const prepared = prepareOrcadMigrationCatalog(manifest, context.runtime.state) assertOrcadMigrationSnapshotsReady(manifest, context.runtime.terminalScrollbackSnapshotStorage) + if (options.expectedBlobs) { + assertOrcadMigrationBlobSetMatches( + manifest, + context.runtime.orcadMigrationBlobRoot, + options.expectedBlobs + ) + } + assertOrcadMigrationBlobsReady(manifest, context.runtime.orcadMigrationBlobRoot) commitOrcadMigrationSnapshots(manifest, context.runtime.terminalScrollbackSnapshotStorage) + commitOrcadMigrationBlobs(manifest, context.runtime.orcadMigrationBlobRoot) const receipt = commitPreparedCatalog(context, manifest, prepared, options.now) scheduleSave(context.scheduling) return committedCatalogState(receipt) @@ -144,11 +179,7 @@ export class OrcadCatalogImportPersistence { manifest: OrcadMigrationManifest ): OrcadMigrationCatalogAbortResult { const context = this[orcadCatalogImportContext] - const current = migrationCatalogState( - context.runtime.state, - manifest, - context.runtime.terminalScrollbackSnapshotStorage - ) + const current = migrationCatalogState(context.runtime, manifest) if (current.state === 'committed' || current.state === 'absent') { return { ...current, aborted: false } } @@ -157,17 +188,14 @@ export class OrcadCatalogImportPersistence { context.runtime.state.orcadMigrationStagedCatalogs ?? [] ).filter((entry) => entry.manifest.migrationId !== manifest.migrationId) abortOrcadMigrationSnapshots(manifest, context.runtime.terminalScrollbackSnapshotStorage) + abortOrcadMigrationBlobs(manifest, context.runtime.orcadMigrationBlobRoot) scheduleSave(context.scheduling) return { ...absentCatalogState(manifest), aborted: true } } getOrcadMigrationCatalogState(manifest: OrcadMigrationManifest): OrcadMigrationCatalogState { const context = this[orcadCatalogImportContext] - return migrationCatalogState( - context.runtime.state, - manifest, - context.runtime.terminalScrollbackSnapshotStorage - ) + return migrationCatalogState(context.runtime, manifest) } /** A migration into this server staged recently and neither committed nor aborted. */ @@ -203,10 +231,10 @@ function commitPreparedCatalog( } function migrationCatalogState( - state: StoreRuntimeState['state'], - manifest: OrcadMigrationManifest, - storage?: StoreRuntimeState['terminalScrollbackSnapshotStorage'] + runtime: OrcadCatalogImportRuntime, + manifest: OrcadMigrationManifest ): OrcadMigrationCatalogState { + const { state } = runtime const receipt = findOrcadMigrationImportReceipt(state, manifest) if (receipt) { assertCommittedReceipt(receipt, manifest) @@ -219,13 +247,22 @@ function migrationCatalogState( return absentCatalogState(manifest) } assertSameOrcadMigrationManifest(staged.manifest, manifest) - return stagedCatalogState(manifest, staged.stagedAt, storage) + return stagedCatalogState(manifest, staged.stagedAt, runtime) } function stagedManifests(state: StoreRuntimeState['state']): OrcadMigrationManifest[] { return (state.orcadMigrationStagedCatalogs ?? []).map((entry) => entry.manifest) } +function committedMigrations( + state: StoreRuntimeState['state'] +): { migrationId: string; manifestSha256: string }[] { + return [ + ...(state.orcadMigrationImportReceipts ?? []), + ...(state.orcadMigrationEvictedReceipts ?? []) + ] +} + /** * The receipt alone proves the commit: the server is live afterwards, so its rows and snapshot * files may legitimately change, and a later read must still say "committed". @@ -251,17 +288,21 @@ function absentCatalogState(manifest: OrcadMigrationManifest): OrcadMigrationCat function stagedCatalogState( manifest: OrcadMigrationManifest, stagedAt: string, - storage?: StoreRuntimeState['terminalScrollbackSnapshotStorage'] + runtime: OrcadCatalogImportRuntime ): OrcadMigrationCatalogState { - const snapshotUploads = storage - ? inspectOrcadMigrationSnapshotUploads(manifest, storage) - : undefined + const snapshotUploads = inspectOrcadMigrationSnapshotUploads( + manifest, + runtime.terminalScrollbackSnapshotStorage + ) + // Absent, not empty, when nothing is staged: the shape an older host already answers with. + const blobUploads = inspectOrcadMigrationBlobUploads(manifest, runtime.orcadMigrationBlobRoot) return { state: 'staged', migrationId: manifest.migrationId, manifestSha256: manifest.manifestSha256, stagedAt, - ...(snapshotUploads ? { snapshotUploads } : {}) + ...(snapshotUploads ? { snapshotUploads } : {}), + ...(blobUploads ? { blobUploads } : {}) } } diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.test.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.test.ts new file mode 100644 index 00000000000..f0cdb938c12 --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.test.ts @@ -0,0 +1,264 @@ +import { createHash } from 'node:crypto' +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import type { OrcadMigrationBlobDescriptor } from '../../../shared/orcad-migration-blob-transfer' +import { + ORCAD_MIGRATION_MANIFEST_VERSION, + type OrcadMigrationManifest +} from '../../../shared/orcad-migration-manifest' +import { + abortOrcadMigrationBlobs, + assertOrcadMigrationBlobSetMatches, + assertOrcadMigrationBlobsReady, + commitOrcadMigrationBlobs, + getOrcadMigrationReceivedBlobPath, + inspectOrcadMigrationBlobUploads, + pruneOrcadMigrationBlobStaging, + stageOrcadMigrationBlobChunk +} from './orcad-migration-blob-staging' + +let root: string + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'orcad-migration-blobs-')) +}) + +afterEach(() => rmSync(root, { recursive: true, force: true })) + +describe('orcad migration blob staging', () => { + it('stages, resumes from disk after a restart, and accepts only exact retries', () => { + const bytes = Buffer.from('echo one\necho two 🐋\n', 'utf8') + const descriptor = blob('shell-history', `${'a'.repeat(16)}-zsh`, bytes) + const manifest = migrationManifest('resume') + const first = bytes.subarray(0, 9) + + expect(stage(manifest, descriptor, 0, first).receivedBytes).toBe(first.length) + // A fresh read of the directory is all a restarted host has. + expect(inspectOrcadMigrationBlobUploads(manifest, root)).toEqual([ + { ...descriptor, receivedBytes: first.length } + ]) + expect(stage(manifest, descriptor, 0, first).receivedBytes).toBe(first.length) + expect(() => stage(manifest, descriptor, 0, Buffer.from('different'))).toThrow( + 'orcad_migration_blob_retry_mismatch' + ) + expect(() => stage(manifest, descriptor, first.length + 1, Buffer.from('x'))).toThrow( + 'orcad_migration_blob_offset_invalid' + ) + expect(() => stage(manifest, descriptor, first.length, bytes)).toThrow( + 'orcad_migration_blob_offset_invalid' + ) + stage(manifest, descriptor, first.length, bytes.subarray(first.length)) + + assertOrcadMigrationBlobsReady(manifest, root) + commitOrcadMigrationBlobs(manifest, root) + expect(readFileSync(getOrcadMigrationReceivedBlobPath(root, manifest, descriptor))).toEqual( + bytes + ) + expect(inspectOrcadMigrationBlobUploads(manifest, root)).toBeUndefined() + }) + + it('refuses a chunk for a migration the importer does not hold staged', () => { + const descriptor = blob('terminal-restore-hints', 'migration', Buffer.from('{}')) + const manifest = migrationManifest('unstaged') + expect(() => + stageOrcadMigrationBlobChunk({ + stagedManifest: null, + root, + request: request(manifest, descriptor, 0, Buffer.from('{}')) + }) + ).toThrow('orcad_migration_blob_catalog_not_staged') + expect(() => + stageOrcadMigrationBlobChunk({ + stagedManifest: { ...manifest, manifestSha256: 'e'.repeat(64) }, + root, + request: request(manifest, descriptor, 0, Buffer.from('{}')) + }) + ).toThrow('orcad_migration_blob_catalog_not_staged') + }) + + it('never replaces a staged key with different bytes', () => { + const manifest = migrationManifest('conflict') + const original = blob('agent-session-records', 'session-1', Buffer.from('original')) + stage(manifest, original, 0, Buffer.from('orig')) + const recaptured = blob('agent-session-records', 'session-1', Buffer.from('recaptured')) + + expect(() => stage(manifest, recaptured, 0, Buffer.from('reca'))).toThrow( + 'orcad_migration_blob_descriptor_conflict:agent-session-records' + ) + expect(inspectOrcadMigrationBlobUploads(manifest, root)).toEqual([ + { ...original, receivedBytes: 4 } + ]) + }) + + it('refuses commit while a blob is incomplete or its bytes do not match the digest', () => { + const bytes = Buffer.from('expected bytes') + const descriptor = blob('shell-history', `${'b'.repeat(16)}-bash`, bytes) + const manifest = migrationManifest('refuse') + + stage(manifest, descriptor, 0, bytes.subarray(0, 4)) + expect(() => assertOrcadMigrationBlobsReady(manifest, root)).toThrow( + `orcad_migration_blob_incomplete:shell-history:${descriptor.key}` + ) + stage(manifest, descriptor, 4, Buffer.alloc(bytes.length - 4, 0x78)) + expect(() => assertOrcadMigrationBlobsReady(manifest, root)).toThrow( + 'orcad_migration_blob_digest_mismatch' + ) + expect(existsSync(getOrcadMigrationReceivedBlobPath(root, manifest, descriptor))).toBe(false) + }) + + it('treats a matching received file as done and a different one as a conflict', () => { + const bytes = Buffer.from('hints') + const descriptor = blob('terminal-restore-hints', 'migration', bytes) + const manifest = migrationManifest('received') + stage(manifest, descriptor, 0, bytes) + const finalPath = getOrcadMigrationReceivedBlobPath(root, manifest, descriptor) + mkdirSync(dirname(finalPath), { recursive: true }) + writeFileSync(finalPath, 'other') + + expect(() => commitOrcadMigrationBlobs(manifest, root)).toThrow( + 'orcad_migration_blob_destination_conflict:terminal-restore-hints:migration' + ) + writeFileSync(finalPath, bytes) + assertOrcadMigrationBlobsReady(manifest, root) + commitOrcadMigrationBlobs(manifest, root) + expect(readFileSync(finalPath)).toEqual(bytes) + }) + + it('aborts one migration, keeps another, and prunes migrations no longer staged', () => { + const bytes = Buffer.from('staged') + const descriptor = blob('agent-session-records', 'session-2', bytes) + const first = migrationManifest('abort-first') + const second = migrationManifest('retain-second') + stage(first, descriptor, 0, bytes) + stage(second, descriptor, 0, bytes) + + abortOrcadMigrationBlobs(first, root) + expect(inspectOrcadMigrationBlobUploads(first, root)).toBeUndefined() + expect(inspectOrcadMigrationBlobUploads(second, root)?.[0]?.receivedBytes).toBe(bytes.length) + pruneOrcadMigrationBlobStaging([second], [], root) + expect(inspectOrcadMigrationBlobUploads(second, root)?.[0]?.receivedBytes).toBe(bytes.length) + pruneOrcadMigrationBlobStaging([], [], root) + expect(inspectOrcadMigrationBlobUploads(second, root)).toBeUndefined() + }) + + it('prunes received blobs whose migration is neither staged nor committed', () => { + const bytes = Buffer.from('moved') + const descriptor = blob('shell-history', `${'e'.repeat(16)}-fish`, bytes) + const leaked = migrationManifest('failed-commit') + const staged = migrationManifest('still-staged') + const committed = migrationManifest('committed') + for (const manifest of [leaked, staged, committed]) { + stage(manifest, descriptor, 0, bytes) + commitOrcadMigrationBlobs(manifest, root) + } + const receivedPath = (manifest: OrcadMigrationManifest) => + getOrcadMigrationReceivedBlobPath(root, manifest, descriptor) + + pruneOrcadMigrationBlobStaging( + [staged], + [ + { + migrationId: committed.migrationId, + manifestSha256: committed.manifestSha256 + } + ], + root + ) + expect(existsSync(receivedPath(leaked))).toBe(false) + expect(existsSync(receivedPath(staged))).toBe(true) + expect(existsSync(receivedPath(committed))).toBe(true) + }) + + it('commits only when staged plus received blobs are exactly the expected set', () => { + const historyBytes = Buffer.from('history') + const recordBytes = Buffer.from('record') + const history = blob('shell-history', `${'f'.repeat(16)}-zsh`, historyBytes) + const record = blob('agent-session-records', 'session-3', recordBytes) + const manifest = migrationManifest('expected-set') + stage(manifest, history, 0, historyBytes) + + expect(() => assertOrcadMigrationBlobSetMatches(manifest, root, [history, record])).toThrow( + 'orcad_migration_blob_set_mismatch:agent-session-records:session-3' + ) + expect(() => assertOrcadMigrationBlobSetMatches(manifest, root, [])).toThrow( + `orcad_migration_blob_set_mismatch:shell-history:${history.key}` + ) + expect(() => + assertOrcadMigrationBlobSetMatches(manifest, root, [{ ...history, sha256: 'f'.repeat(64) }]) + ).toThrow('orcad_migration_blob_set_mismatch') + stage(manifest, record, 0, recordBytes) + assertOrcadMigrationBlobSetMatches(manifest, root, [history, record]) + + // A retry after a commit that moved the blobs still sees the whole set, from received. + commitOrcadMigrationBlobs(manifest, root) + assertOrcadMigrationBlobSetMatches(manifest, root, [history, record]) + expect(() => assertOrcadMigrationBlobSetMatches(manifest, root, [history])).toThrow( + 'orcad_migration_blob_set_mismatch' + ) + }) + + it('drops blobs an earlier commit attempt moved when the catalog is aborted', () => { + const bytes = Buffer.from('history') + const descriptor = blob('shell-history', `${'c'.repeat(16)}-zsh`, bytes) + const manifest = migrationManifest('abort-after-move') + stage(manifest, descriptor, 0, bytes) + commitOrcadMigrationBlobs(manifest, root) + + abortOrcadMigrationBlobs(manifest, root) + expect(existsSync(getOrcadMigrationReceivedBlobPath(root, manifest, descriptor))).toBe(false) + }) +}) + +function blob( + blobKind: OrcadMigrationBlobDescriptor['blobKind'], + key: string, + bytes: Buffer +): OrcadMigrationBlobDescriptor { + return { + blobKind, + key, + sha256: createHash('sha256').update(bytes).digest('hex'), + byteLength: bytes.length + } +} + +function migrationManifest(migrationId: string): OrcadMigrationManifest { + return { + version: ORCAD_MIGRATION_MANIFEST_VERSION, + migrationId, + createdAt: '2026-08-30T12:00:00.000Z', + source: { sshTargetId: 'source', sshTargetGeneration: 1, targetLabel: 'Source' }, + payload: { repositories: [], projectGroups: [], folderWorkspaces: [] }, + manifestSha256: 'a'.repeat(64) + } +} + +function request( + manifest: OrcadMigrationManifest, + descriptor: OrcadMigrationBlobDescriptor, + offset: number, + bytes: Buffer +) { + return { + migrationId: manifest.migrationId, + manifestSha256: manifest.manifestSha256, + blob: descriptor, + offset, + bytesBase64: bytes.toString('base64') + } +} + +function stage( + manifest: OrcadMigrationManifest, + descriptor: OrcadMigrationBlobDescriptor, + offset: number, + bytes: Buffer +) { + return stageOrcadMigrationBlobChunk({ + stagedManifest: manifest, + root, + request: request(manifest, descriptor, offset, bytes) + }) +} diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.ts new file mode 100644 index 00000000000..90d5e081fb9 --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.ts @@ -0,0 +1,305 @@ +/** + * Destination side of the migration blob channel. Blobs are not named by the signed manifest, so + * a staged file's name carries its whole descriptor (`...part` under a + * per-kind directory) and the staged set is read back from disk after a restart. At catalog commit + * every staged blob must be complete and digest-exact, then moves to the migration's received + * directory; installing received blobs into live state is each kind's installer (D1b-6b/6c). + */ +import { readdirSync, rmSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { + ORCAD_MIGRATION_BLOB_KINDS, + assertOrcadMigrationBlobSet, + decodeOrcadMigrationBlobChunk, + orcadMigrationBlobIdentity, + parseOrcadMigrationBlobDescriptor, + sameOrcadMigrationBlob, + type OrcadMigrationBlobChunkRequest, + type OrcadMigrationBlobChunkResult, + type OrcadMigrationBlobDescriptor, + type OrcadMigrationBlobKind, + type OrcadMigrationBlobUploadState +} from '../../../shared/orcad-migration-blob-transfer' +import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest' +import { syncDirectoryDurablySync } from '../../durable-file-write' +import { + appendOrcadMigrationStagedChunk, + ensureOrcadMigrationDirectory, + inspectOrcadMigrationFinalFile, + linkOrcadMigrationStagedFile, + orcadMigrationFileMatches, + orcadMigrationStagedFileSize, + orcadMigrationStagingDirectory, + orcadMigrationStagingKey, + pruneOrcadMigrationKeyedDirectories, + pruneOrcadMigrationStagingDirectories, + removeOrcadMigrationStagingDirectory, + requireStagedOrcadMigrationManifest, + type OrcadMigrationRequestIdentity +} from './orcad-migration-staged-file' + +const RECEIVED_DIRECTORY = 'received' +const STAGED_BLOB_NAME = /^([^.]+)\.([a-f0-9]{64})\.([1-9][0-9]*)\.part$/ + +type ReceivedBlobPathResolver = ( + root: string, + manifest: OrcadMigrationManifest, + descriptor: OrcadMigrationBlobDescriptor +) => string + +const receivedDirectoryPath: ReceivedBlobPathResolver = (root, manifest, descriptor) => + join(receivedBlobsDirectory(root, manifest), descriptor.blobKind, descriptor.key) + +/** Where each kind lands at commit; every kind waits in the received directory for its installer. */ +const RECEIVED_BLOB_DESTINATIONS: Record = { + 'terminal-restore-hints': receivedDirectoryPath, + 'shell-history': receivedDirectoryPath, + 'agent-session-records': receivedDirectoryPath +} + +export function getOrcadMigrationReceivedBlobPath( + root: string, + manifest: OrcadMigrationManifest, + descriptor: OrcadMigrationBlobDescriptor +): string { + return RECEIVED_BLOB_DESTINATIONS[descriptor.blobKind](root, manifest, descriptor) +} + +export type OrcadMigrationBlobChunkStaging = { + /** The manifest the importer holds staged for this migration, if any. */ + stagedManifest: OrcadMigrationManifest | null + root: string + request: OrcadMigrationBlobChunkRequest +} + +export function stageOrcadMigrationBlobChunk( + args: OrcadMigrationBlobChunkStaging +): OrcadMigrationBlobChunkResult { + const manifest = requireStagedOrcadMigrationManifest( + args.stagedManifest, + args.request, + 'orcad_migration_blob_catalog_not_staged' + ) + const descriptor = parseOrcadMigrationBlobDescriptor(args.request.blob) + const bytes = decodeOrcadMigrationBlobChunk(args.request.bytesBase64) + const staged = stagedBlobDescriptors(args.root, manifest) + const existing = staged.find( + (entry) => orcadMigrationBlobIdentity(entry) === orcadMigrationBlobIdentity(descriptor) + ) + if (existing && !sameOrcadMigrationBlob(existing, descriptor)) { + // Never replace bytes already staged under this key; the sender aborts and restages instead. + throw new Error(`orcad_migration_blob_descriptor_conflict:${descriptor.blobKind}`) + } + if (!existing) { + assertOrcadMigrationBlobSet([...staged, descriptor]) + } + const path = stagedBlobPath(args.root, manifest, descriptor) + ensureOrcadMigrationDirectory(dirname(path)) + const receivedBytes = appendOrcadMigrationStagedChunk({ + channel: 'blob', + path, + offset: args.request.offset, + bytes, + byteLength: descriptor.byteLength + }) + return { + migrationId: args.request.migrationId, + manifestSha256: args.request.manifestSha256, + blobKind: descriptor.blobKind, + key: descriptor.key, + receivedBytes + } +} + +export function inspectOrcadMigrationBlobUploads( + manifest: OrcadMigrationManifest, + root: string +): OrcadMigrationBlobUploadState[] | undefined { + const uploads = stagedBlobDescriptors(root, manifest).map((descriptor) => ({ + ...descriptor, + receivedBytes: + inspectReceivedBlob(root, manifest, descriptor) === 'matching' + ? descriptor.byteLength + : orcadMigrationStagedFileSize( + stagedBlobPath(root, manifest, descriptor), + descriptor.byteLength + ) + })) + return uploads.length > 0 ? uploads : undefined +} + +export function assertOrcadMigrationBlobsReady( + manifest: OrcadMigrationManifest, + root: string +): void { + for (const upload of inspectOrcadMigrationBlobUploads(manifest, root) ?? []) { + const label = `${upload.blobKind}:${upload.key}` + const finalStatus = inspectReceivedBlob(root, manifest, upload) + if (finalStatus === 'conflict') { + throw new Error(`orcad_migration_blob_destination_conflict:${label}`) + } + if (upload.receivedBytes !== upload.byteLength) { + throw new Error(`orcad_migration_blob_incomplete:${label}`) + } + if ( + finalStatus !== 'matching' && + !orcadMigrationFileMatches(stagedBlobPath(root, manifest, upload), upload) + ) { + throw new Error(`orcad_migration_blob_digest_mismatch:${label}`) + } + } +} + +/** + * The caller must run `assertOrcadMigrationBlobsReady` first, in the same synchronous turn: that + * pass hashes every staged file, and no chunk can land between it and these links. + */ +/** + * Refuses commit unless the staged and already-received blobs are exactly the set the sender + * moved, so a blob lost on the way (or left from another capture) fails instead of vanishing. + */ +export function assertOrcadMigrationBlobSetMatches( + manifest: OrcadMigrationManifest, + root: string, + expected: readonly OrcadMigrationBlobDescriptor[] +): void { + assertOrcadMigrationBlobSet(expected) + const wanted = new Map(expected.map((entry) => [orcadMigrationBlobIdentity(entry), entry])) + const staged = stagedBlobDescriptors(root, manifest) + const stagedIds = new Set(staged.map(orcadMigrationBlobIdentity)) + for (const descriptor of staged) { + const match = wanted.get(orcadMigrationBlobIdentity(descriptor)) + if (!match || !sameOrcadMigrationBlob(match, descriptor)) { + throw blobSetMismatch(descriptor) + } + } + for (const identity of receivedBlobIdentities(root, manifest)) { + if (!wanted.has(identity)) { + throw new Error('orcad_migration_blob_set_mismatch') + } + } + for (const descriptor of expected) { + // An earlier commit attempt may already have moved it; the received bytes must still match. + if ( + !stagedIds.has(orcadMigrationBlobIdentity(descriptor)) && + inspectReceivedBlob(root, manifest, descriptor) !== 'matching' + ) { + throw blobSetMismatch(descriptor) + } + } +} + +export function commitOrcadMigrationBlobs(manifest: OrcadMigrationManifest, root: string): void { + const directories = new Set() + for (const descriptor of stagedBlobDescriptors(root, manifest)) { + const finalPath = getOrcadMigrationReceivedBlobPath(root, manifest, descriptor) + ensureOrcadMigrationDirectory(dirname(finalPath)) + linkOrcadMigrationStagedFile({ + stagedPath: stagedBlobPath(root, manifest, descriptor), + finalPath, + expected: descriptor, + conflictError: `orcad_migration_blob_destination_conflict:${descriptor.blobKind}:${descriptor.key}` + }) + directories.add(dirname(finalPath)) + } + for (const directory of directories) { + syncDirectoryDurablySync(directory) + } + removeOrcadMigrationStagingDirectory(root, manifest) +} + +/** Also drops blobs a failed commit already moved: the catalog they belonged to never landed. */ +export function abortOrcadMigrationBlobs(manifest: OrcadMigrationManifest, root: string): void { + removeOrcadMigrationStagingDirectory(root, manifest) + rmSync(receivedBlobsDirectory(root, manifest), { recursive: true, force: true }) +} + +/** Received blobs stay only for staged or committed migrations; a failed commit's are dropped. */ +export function pruneOrcadMigrationBlobStaging( + stagedManifests: readonly OrcadMigrationManifest[], + committedMigrations: readonly OrcadMigrationRequestIdentity[], + root: string +): void { + pruneOrcadMigrationStagingDirectories(root, stagedManifests) + pruneOrcadMigrationKeyedDirectories(join(root, RECEIVED_DIRECTORY), [ + ...stagedManifests, + ...committedMigrations + ]) +} + +function blobSetMismatch(descriptor: OrcadMigrationBlobDescriptor): Error { + return new Error(`orcad_migration_blob_set_mismatch:${descriptor.blobKind}:${descriptor.key}`) +} + +function receivedBlobIdentities(root: string, manifest: OrcadMigrationManifest): string[] { + const received = receivedBlobsDirectory(root, manifest) + return ORCAD_MIGRATION_BLOB_KINDS.flatMap((blobKind) => + readDirectory(join(received, blobKind)).map((key) => + orcadMigrationBlobIdentity({ blobKind, key }) + ) + ) +} + +function stagedBlobDescriptors( + root: string, + manifest: OrcadMigrationManifest +): OrcadMigrationBlobDescriptor[] { + const staging = orcadMigrationStagingDirectory(root, manifest) + const descriptors: OrcadMigrationBlobDescriptor[] = [] + for (const blobKind of ORCAD_MIGRATION_BLOB_KINDS) { + for (const name of readDirectory(join(staging, blobKind))) { + const match = STAGED_BLOB_NAME.exec(name) + if (!match) { + continue + } + try { + descriptors.push( + parseOrcadMigrationBlobDescriptor({ + blobKind, + key: match[1], + sha256: match[2], + byteLength: Number(match[3]) + }) + ) + } catch { + // Not a name this channel writes; it cannot belong to the staged set. + } + } + } + return descriptors +} + +function readDirectory(path: string): string[] { + try { + return readdirSync(path).sort() + } catch { + return [] + } +} + +function inspectReceivedBlob( + root: string, + manifest: OrcadMigrationManifest, + descriptor: OrcadMigrationBlobDescriptor +): 'absent' | 'matching' | 'conflict' { + return inspectOrcadMigrationFinalFile( + getOrcadMigrationReceivedBlobPath(root, manifest, descriptor), + descriptor + ) +} + +function receivedBlobsDirectory(root: string, manifest: OrcadMigrationManifest): string { + return join(root, RECEIVED_DIRECTORY, orcadMigrationStagingKey(manifest)) +} + +function stagedBlobPath( + root: string, + manifest: OrcadMigrationManifest, + descriptor: OrcadMigrationBlobDescriptor +): string { + return join( + orcadMigrationStagingDirectory(root, manifest), + descriptor.blobKind, + `${descriptor.key}.${descriptor.sha256}.${descriptor.byteLength}.part` + ) +} diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-staged-file.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-staged-file.ts new file mode 100644 index 00000000000..10bddc41058 --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-staged-file.ts @@ -0,0 +1,214 @@ +/** + * The destination's resumable staging files, shared by scrollback snapshots and migration blobs: + * fsync'd appends whose retried ranges must match, size-as-progress, digest-checked hard-link + * commits, and per-migration staging directories that abort and prune remove whole. + */ +import { createHash } from 'node:crypto' +import { + closeSync, + existsSync, + fsyncSync, + linkSync, + mkdirSync, + openSync, + readFileSync, + readSync, + readdirSync, + rmSync, + statSync, + writeSync +} from 'node:fs' +import { join } from 'node:path' +import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest' +import { syncDirectoryDurablySync } from '../../durable-file-write' + +export const ORCAD_MIGRATION_STAGING_DIRECTORY = '.orcad-migration-staging' + +/** Names the error codes, e.g. `orcad_migration__offset_invalid`. */ +export type OrcadMigrationStagingChannel = 'snapshot' | 'blob' + +export type OrcadMigrationRequestIdentity = { migrationId: string; manifestSha256: string } + +export type OrcadMigrationStagedContent = { sha256: string; byteLength: number } + +export type OrcadMigrationStagedChunk = { + channel: OrcadMigrationStagingChannel + path: string + offset: number + bytes: Buffer + byteLength: number +} + +/** Appends one chunk, or confirms an identical retry; returns the staged size afterwards. */ +export function appendOrcadMigrationStagedChunk(chunk: OrcadMigrationStagedChunk): number { + const { channel, path, offset, bytes } = chunk + const descriptorFd = openStagedFile(path) + try { + const size = statSync(path).size + if (offset > size || offset + bytes.length > chunk.byteLength) { + throw new Error(`orcad_migration_${channel}_offset_invalid`) + } + if (offset < size) { + if (offset + bytes.length > size) { + throw new Error(`orcad_migration_${channel}_offset_invalid`) + } + const existing = Buffer.alloc(bytes.length) + const read = readSync(descriptorFd, existing, 0, existing.length, offset) + if (read !== bytes.length || !existing.equals(bytes)) { + throw new Error(`orcad_migration_${channel}_retry_mismatch`) + } + return size + } + const written = writeSync(descriptorFd, bytes, 0, bytes.length, offset) + if (written !== bytes.length) { + throw new Error(`orcad_migration_${channel}_write_incomplete`) + } + fsyncSync(descriptorFd) + return size + bytes.length + } finally { + closeSync(descriptorFd) + } +} + +export function orcadMigrationStagedFileSize(path: string, byteLength: number): number { + try { + return Math.min(statSync(path).size, byteLength) + } catch { + return 0 + } +} + +export function orcadMigrationFileMatches( + path: string, + expected: OrcadMigrationStagedContent +): boolean { + try { + const bytes = readFileSync(path) + return ( + bytes.length === expected.byteLength && + createHash('sha256').update(bytes).digest('hex') === expected.sha256 + ) + } catch { + return false + } +} + +export function inspectOrcadMigrationFinalFile( + path: string | null, + expected: OrcadMigrationStagedContent +): 'absent' | 'matching' | 'conflict' { + if (!path || !existsSync(path)) { + return 'absent' + } + return orcadMigrationFileMatches(path, expected) ? 'matching' : 'conflict' +} + +/** + * Hard-links a verified staged file into place, then drops the staged name. A final file that + * already holds the same bytes (an earlier commit attempt) counts as done; different bytes refuse. + */ +export function linkOrcadMigrationStagedFile(args: { + stagedPath: string + finalPath: string + expected: OrcadMigrationStagedContent + conflictError: string +}): void { + if (inspectOrcadMigrationFinalFile(args.finalPath, args.expected) !== 'matching') { + try { + linkSync(args.stagedPath, args.finalPath) + } catch (error) { + const finalStatus = inspectOrcadMigrationFinalFile(args.finalPath, args.expected) + if (finalStatus === 'conflict') { + throw new Error(args.conflictError) + } + if (finalStatus !== 'matching') { + throw error + } + } + } + rmSync(args.stagedPath, { force: true }) +} + +export function orcadMigrationStagingDirectory( + root: string, + manifest: OrcadMigrationManifest +): string { + return join(root, ORCAD_MIGRATION_STAGING_DIRECTORY, orcadMigrationStagingKey(manifest)) +} + +export function ensureOrcadMigrationDirectory(path: string): void { + mkdirSync(path, { recursive: true, mode: 0o700 }) +} + +export function removeOrcadMigrationStagingDirectory( + root: string, + manifest: OrcadMigrationManifest +): void { + rmSync(orcadMigrationStagingDirectory(root, manifest), { recursive: true, force: true }) + syncDirectoryDurablySync(join(root, ORCAD_MIGRATION_STAGING_DIRECTORY)) +} + +/** Removes staging for every migration the importer no longer holds staged. */ +export function pruneOrcadMigrationStagingDirectories( + root: string, + stagedManifests: readonly OrcadMigrationManifest[] +): void { + pruneOrcadMigrationKeyedDirectories( + join(root, ORCAD_MIGRATION_STAGING_DIRECTORY), + stagedManifests + ) +} + +/** Removes every per-migration directory under `parent` whose migration is not retained. */ +export function pruneOrcadMigrationKeyedDirectories( + parent: string, + retainedMigrations: readonly OrcadMigrationRequestIdentity[] +): void { + const retained = new Set(retainedMigrations.map(orcadMigrationStagingKey)) + let entries: string[] + try { + entries = readdirSync(parent) + } catch { + return + } + for (const entry of entries) { + if (/^[a-f0-9]{32}$/.test(entry) && !retained.has(entry)) { + rmSync(join(parent, entry), { recursive: true, force: true }) + } + } +} + +/** The importer's staged manifest for this request, or the channel's own refusal. */ +export function requireStagedOrcadMigrationManifest( + staged: OrcadMigrationManifest | null, + request: OrcadMigrationRequestIdentity, + notStagedError: string +): OrcadMigrationManifest { + if ( + !staged || + staged.migrationId !== request.migrationId || + staged.manifestSha256 !== request.manifestSha256 + ) { + throw new Error(notStagedError) + } + return staged +} + +export function orcadMigrationStagingKey(manifest: OrcadMigrationRequestIdentity): string { + return createHash('sha256') + .update(`${manifest.migrationId}\0${manifest.manifestSha256}`) + .digest('hex') + .slice(0, 32) +} + +function openStagedFile(path: string): number { + try { + return openSync(path, 'r+') + } catch { + try { + return openSync(path, 'wx+', 0o600) + } catch { + return openSync(path, 'r+') + } + } +} diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts b/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts index 8e3efdeefe6..7f1a709acb8 100644 --- a/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts +++ b/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts @@ -1,5 +1,13 @@ import { createHash } from 'node:crypto' -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync +} from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' @@ -61,6 +69,30 @@ describe('orcad scrollback snapshot transfer', () => { expect(readFinal(descriptor)).toEqual(bytes) }) + it('stages through the shared helper at the pre-blob path and with the snapshot codes', () => { + const bytes = Buffer.from('pinned layout', 'utf8') + const descriptor = snapshot('8', 'tab-8', 'leaf-8', bytes) + const manifest = migrationManifest('pinned-path', [descriptor]) + const state = stagedState(manifest) + stage(state, manifest, descriptor, 0, bytes.subarray(0, 6)) + + // A host upgraded mid-move must find the staging it wrote before the shared helper existed. + const key = createHash('sha256') + .update(`${manifest.migrationId}\0${manifest.manifestSha256}`) + .digest('hex') + .slice(0, 32) + const stagingDirectory = join(storage.snapshotRoot ?? '', '.orcad-migration-staging', key) + const stagedName = `${descriptor.ref}.${descriptor.sha256}.part` + expect(readdirSync(stagingDirectory)).toEqual([stagedName]) + expect(readFileSync(join(stagingDirectory, stagedName))).toEqual(bytes.subarray(0, 6)) + expect(() => stage(state, manifest, descriptor, 0, Buffer.from('other!'))).toThrow( + /^orcad_migration_snapshot_retry_mismatch$/ + ) + expect(() => stage(state, manifest, descriptor, 7, Buffer.from('x'))).toThrow( + /^orcad_migration_snapshot_offset_invalid$/ + ) + }) + it('refuses incomplete and digest-mismatched staged bytes', () => { const bytes = Buffer.from('expected bytes', 'utf8') const descriptor = snapshot('2', 'tab-2', 'leaf-2', bytes) diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.ts b/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.ts index 80a7e566b71..cd55576a43c 100644 --- a/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.ts +++ b/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.ts @@ -1,18 +1,4 @@ -import { createHash } from 'node:crypto' -import { - closeSync, - existsSync, - fsyncSync, - linkSync, - mkdirSync, - openSync, - readFileSync, - readSync, - readdirSync, - rmSync, - statSync, - writeSync -} from 'node:fs' +import { mkdirSync } from 'node:fs' import { join } from 'node:path' import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest' import { @@ -28,8 +14,18 @@ import { getTerminalScrollbackSnapshotRoot, type TerminalScrollbackSnapshotStorage } from '../../terminal-scrollback-snapshots' - -const STAGING_DIRECTORY = '.orcad-migration-staging' +import { + appendOrcadMigrationStagedChunk, + ensureOrcadMigrationDirectory, + inspectOrcadMigrationFinalFile, + linkOrcadMigrationStagedFile, + orcadMigrationFileMatches, + orcadMigrationStagedFileSize, + orcadMigrationStagingDirectory, + pruneOrcadMigrationStagingDirectories, + removeOrcadMigrationStagingDirectory, + requireStagedOrcadMigrationManifest +} from './orcad-migration-staged-file' /** * Destination side: append one verified chunk to the staging file for a snapshot the staged @@ -41,36 +37,26 @@ export function stageOrcadMigrationSnapshotChunk(args: { storage: TerminalScrollbackSnapshotStorage request: OrcadMigrationSnapshotChunkRequest }): OrcadMigrationSnapshotChunkResult { - const manifest = requireStagedManifest(args.stagedManifest, args.request) + const manifest = requireStagedOrcadMigrationManifest( + args.stagedManifest, + args.request, + 'orcad_migration_snapshot_catalog_not_staged' + ) const descriptor = requireDescriptor(manifest, args.request.ref) const bytes = decodeOrcadMigrationSnapshotChunk(args.request.bytesBase64) - const path = stagedSnapshotPath(args.storage, manifest, descriptor) - mkdirSync(stagingDirectory(args.storage, manifest), { recursive: true, mode: 0o700 }) - const descriptorFd = openStagedFile(path) - try { - const size = statSync(path).size - if (args.request.offset > size || args.request.offset + bytes.length > descriptor.byteLength) { - throw new Error('orcad_migration_snapshot_offset_invalid') - } - if (args.request.offset < size) { - if (args.request.offset + bytes.length > size) { - throw new Error('orcad_migration_snapshot_offset_invalid') - } - const existing = Buffer.alloc(bytes.length) - const read = readSync(descriptorFd, existing, 0, existing.length, args.request.offset) - if (read !== bytes.length || !existing.equals(bytes)) { - throw new Error('orcad_migration_snapshot_retry_mismatch') - } - return chunkResult(args.request, size) - } - const written = writeSync(descriptorFd, bytes, 0, bytes.length, args.request.offset) - if (written !== bytes.length) { - throw new Error('orcad_migration_snapshot_write_incomplete') - } - fsyncSync(descriptorFd) - return chunkResult(args.request, size + bytes.length) - } finally { - closeSync(descriptorFd) + ensureOrcadMigrationDirectory(stagingDirectory(args.storage, manifest)) + const acknowledgedOffset = appendOrcadMigrationStagedChunk({ + channel: 'snapshot', + path: stagedSnapshotPath(args.storage, manifest, descriptor), + offset: args.request.offset, + bytes, + byteLength: descriptor.byteLength + }) + return { + migrationId: args.request.migrationId, + manifestSha256: args.request.manifestSha256, + ref: args.request.ref, + acknowledgedOffset } } @@ -84,9 +70,13 @@ export function inspectOrcadMigrationSnapshotUploads( } return snapshots.map((descriptor) => ({ ...descriptor, - receivedBytes: matchingFinalSnapshot(storage, descriptor) - ? descriptor.byteLength - : stagedSnapshotSize(storage, manifest, descriptor) + receivedBytes: + inspectFinalSnapshot(storage, descriptor) === 'matching' + ? descriptor.byteLength + : orcadMigrationStagedFileSize( + stagedSnapshotPath(storage, manifest, descriptor), + descriptor.byteLength + ) })) } @@ -106,7 +96,7 @@ export function assertOrcadMigrationSnapshotsReady( finalStatus === 'matching' ? getTerminalScrollbackSnapshotPath(upload.ref, storage) : stagedSnapshotPath(storage, manifest, upload) - if (!path || !fileMatches(path, upload)) { + if (!path || !orcadMigrationFileMatches(path, upload)) { throw new Error(`orcad_migration_snapshot_digest_mismatch:${upload.ref}`) } } @@ -121,37 +111,26 @@ export function commitOrcadMigrationSnapshots( const root = getTerminalScrollbackSnapshotRoot(storage) mkdirSync(root, { recursive: true, mode: 0o700 }) for (const descriptor of snapshots) { - if (matchingFinalSnapshot(storage, descriptor)) { - rmSync(stagedSnapshotPath(storage, manifest, descriptor), { force: true }) - continue - } - const stagedPath = stagedSnapshotPath(storage, manifest, descriptor) const finalPath = getTerminalScrollbackSnapshotPath(descriptor.ref, storage) if (!finalPath) { throw new Error('orcad_migration_snapshot_ref_invalid') } - try { - linkSync(stagedPath, finalPath) - } catch (error) { - const finalStatus = inspectFinalSnapshot(storage, descriptor) - if (finalStatus === 'conflict') { - throw new Error(`orcad_migration_snapshot_destination_conflict:${descriptor.ref}`) - } - if (finalStatus !== 'matching') { - throw error - } - } - rmSync(stagedPath, { force: true }) + linkOrcadMigrationStagedFile({ + stagedPath: stagedSnapshotPath(storage, manifest, descriptor), + finalPath, + expected: descriptor, + conflictError: `orcad_migration_snapshot_destination_conflict:${descriptor.ref}` + }) } syncDirectoryDurablySync(root) - removeStagingDirectory(storage, manifest) + removeOrcadMigrationStagingDirectory(root, manifest) } export function abortOrcadMigrationSnapshots( manifest: OrcadMigrationManifest, storage: TerminalScrollbackSnapshotStorage ): void { - removeStagingDirectory(storage, manifest) + removeOrcadMigrationStagingDirectory(getTerminalScrollbackSnapshotRoot(storage), manifest) } /** Removes staging for every migration the importer no longer holds staged. */ @@ -159,33 +138,7 @@ export function pruneOrcadMigrationSnapshotStaging( stagedManifests: readonly OrcadMigrationManifest[], storage: TerminalScrollbackSnapshotStorage ): void { - const root = join(getTerminalScrollbackSnapshotRoot(storage), STAGING_DIRECTORY) - const retained = new Set(stagedManifests.map(stagingKey)) - let entries: string[] - try { - entries = readdirSync(root) - } catch { - return - } - for (const entry of entries) { - if (/^[a-f0-9]{32}$/.test(entry) && !retained.has(entry)) { - rmSync(join(root, entry), { recursive: true, force: true }) - } - } -} - -function requireStagedManifest( - staged: OrcadMigrationManifest | null, - request: Pick -): OrcadMigrationManifest { - if ( - !staged || - staged.migrationId !== request.migrationId || - staged.manifestSha256 !== request.manifestSha256 - ) { - throw new Error('orcad_migration_snapshot_catalog_not_staged') - } - return staged + pruneOrcadMigrationStagingDirectories(getTerminalScrollbackSnapshotRoot(storage), stagedManifests) } function requireDescriptor( @@ -201,68 +154,21 @@ function requireDescriptor( return descriptor } -function stagedSnapshotSize( - storage: TerminalScrollbackSnapshotStorage, - manifest: OrcadMigrationManifest, - descriptor: OrcadMigrationTerminalScrollbackSnapshot -): number { - try { - return Math.min( - statSync(stagedSnapshotPath(storage, manifest, descriptor)).size, - descriptor.byteLength - ) - } catch { - return 0 - } -} - -function matchingFinalSnapshot( - storage: TerminalScrollbackSnapshotStorage, - descriptor: OrcadMigrationTerminalScrollbackSnapshot -): boolean { - return inspectFinalSnapshot(storage, descriptor) === 'matching' -} - function inspectFinalSnapshot( storage: TerminalScrollbackSnapshotStorage, descriptor: OrcadMigrationTerminalScrollbackSnapshot ): 'absent' | 'matching' | 'conflict' { - const path = getTerminalScrollbackSnapshotPath(descriptor.ref, storage) - if (!path || !existsSync(path)) { - return 'absent' - } - return fileMatches(path, descriptor) ? 'matching' : 'conflict' -} - -function fileMatches(path: string, descriptor: OrcadMigrationTerminalScrollbackSnapshot): boolean { - try { - const bytes = readFileSync(path) - return ( - bytes.length === descriptor.byteLength && - createHash('sha256').update(bytes).digest('hex') === descriptor.sha256 - ) - } catch { - return false - } -} - -function openStagedFile(path: string): number { - try { - return openSync(path, 'r+') - } catch { - try { - return openSync(path, 'wx+', 0o600) - } catch { - return openSync(path, 'r+') - } - } + return inspectOrcadMigrationFinalFile( + getTerminalScrollbackSnapshotPath(descriptor.ref, storage), + descriptor + ) } function stagingDirectory( storage: TerminalScrollbackSnapshotStorage, manifest: OrcadMigrationManifest ): string { - return join(getTerminalScrollbackSnapshotRoot(storage), STAGING_DIRECTORY, stagingKey(manifest)) + return orcadMigrationStagingDirectory(getTerminalScrollbackSnapshotRoot(storage), manifest) } function stagedSnapshotPath( @@ -272,31 +178,3 @@ function stagedSnapshotPath( ): string { return join(stagingDirectory(storage, manifest), `${descriptor.ref}.${descriptor.sha256}.part`) } - -function removeStagingDirectory( - storage: TerminalScrollbackSnapshotStorage, - manifest: OrcadMigrationManifest -): void { - const stagingRoot = join(getTerminalScrollbackSnapshotRoot(storage), STAGING_DIRECTORY) - rmSync(stagingDirectory(storage, manifest), { recursive: true, force: true }) - syncDirectoryDurablySync(stagingRoot) -} - -function stagingKey(manifest: OrcadMigrationManifest): string { - return createHash('sha256') - .update(`${manifest.migrationId}\0${manifest.manifestSha256}`) - .digest('hex') - .slice(0, 32) -} - -function chunkResult( - request: OrcadMigrationSnapshotChunkRequest, - acknowledgedOffset: number -): OrcadMigrationSnapshotChunkResult { - return { - migrationId: request.migrationId, - manifestSha256: request.manifestSha256, - ref: request.ref, - acknowledgedOffset - } -} diff --git a/src/main/runtime/orca-runtime-get-status.ts b/src/main/runtime/orca-runtime-get-status.ts index 307b5e86dd4..a88eac44678 100644 --- a/src/main/runtime/orca-runtime-get-status.ts +++ b/src/main/runtime/orca-runtime-get-status.ts @@ -8,6 +8,7 @@ import { import { isBrowserIdentityModeStoreInitialized } from '../browser/browser-identity-mode-store' import { servesLocalWslMigrationSource } from '../orcad/orcad-migration-wsl-destination' import { ORCAD_MIGRATION_LOCAL_WSL_SOURCE_RUNTIME_CAPABILITY } from '../../shared/orcad-runtime-capabilities' +import { advertisedOrcadMigrationBlobCapabilities } from '../../shared/orcad-migration-blob-transfer' import type { RuntimeCapability } from '../../shared/protocol-version' import { BROWSER_CERTIFICATE_TRUST_RUNTIME_CAPABILITY, @@ -123,6 +124,9 @@ export abstract class OrcaRuntimeWithGetStatus extends OrcaRuntimeWithGetRuntime if (servesLocalWslMigrationSource()) { capabilities.push(ORCAD_MIGRATION_LOCAL_WSL_SOURCE_RUNTIME_CAPABILITY) } + // Why not static: a blob kind is advertised only once this build installs it, since an + // unadvertised kind is refused by the sender rather than staged and silently never applied. + capabilities.push(...advertisedOrcadMigrationBlobCapabilities()) // Why: certificate proceed is owned by the browser-hosting process for both // desktop webviews and offscreen pages. Advertise whenever either backend // can host a page so remote clients can surface Proceed Anyway (Unsafe). diff --git a/src/main/runtime/orca-runtime-migration-catalog.ts b/src/main/runtime/orca-runtime-migration-catalog.ts index 988c4464d34..2ad021f8ef4 100644 --- a/src/main/runtime/orca-runtime-migration-catalog.ts +++ b/src/main/runtime/orca-runtime-migration-catalog.ts @@ -7,6 +7,11 @@ import type { OrcadMigrationSnapshotChunkRequest, OrcadMigrationSnapshotChunkResult } from '../../shared/orcad-migration-scrollback' +import type { + OrcadMigrationBlobChunkRequest, + OrcadMigrationBlobChunkResult, + OrcadMigrationBlobDescriptor +} from '../../shared/orcad-migration-blob-transfer' import { abortStagedOrcadMigrationCatalogDurably, commitStagedOrcadMigrationCatalogDurably, @@ -20,6 +25,7 @@ type MigrationStoreKey = | 'stageOrcadMigrationCatalog' | 'commitStagedOrcadMigrationCatalog' | 'stageOrcadMigrationSnapshotChunk' + | 'stageOrcadMigrationBlobChunk' | 'abortStagedOrcadMigrationCatalog' | 'getOrcadMigrationCatalogState' | 'flushPendingOrThrowAsync' @@ -39,7 +45,7 @@ export class OrcaRuntimeWithMigrationCatalog extends OrcaRuntimeWithResolveWaite async commitStagedOrcadMigrationCatalog( manifest: OrcadMigrationManifest, - options: { signal?: AbortSignal } = {} + options: { signal?: AbortSignal; expectedBlobs?: readonly OrcadMigrationBlobDescriptor[] } = {} ): Promise { return commitStagedOrcadMigrationCatalogDurably({ store: this.requireMigrationStore( @@ -48,6 +54,7 @@ export class OrcaRuntimeWithMigrationCatalog extends OrcaRuntimeWithResolveWaite ), manifest, signal: options.signal, + expectedBlobs: options.expectedBlobs, onDurableCommit: () => { this.invalidateResolvedWorktreeCache() this.notifyReposChanged() @@ -63,6 +70,15 @@ export class OrcaRuntimeWithMigrationCatalog extends OrcaRuntimeWithResolveWaite .stageOrcadMigrationSnapshotChunk!(request) } + stageOrcadMigrationBlobChunk( + request: OrcadMigrationBlobChunkRequest + ): OrcadMigrationBlobChunkResult { + // SAFETY: The virtual Store may differ from the checked Store; preserve its native failure. + return this.requireMigrationStore('stageOrcadMigrationBlobChunk').stageOrcadMigrationBlobChunk!( + request + ) + } + async abortStagedOrcadMigrationCatalog( manifest: OrcadMigrationManifest, options: { signal?: AbortSignal } = {} diff --git a/src/main/runtime/orcad-migration-catalog-import.ts b/src/main/runtime/orcad-migration-catalog-import.ts index dbccf92adae..5fd101be700 100644 --- a/src/main/runtime/orcad-migration-catalog-import.ts +++ b/src/main/runtime/orcad-migration-catalog-import.ts @@ -3,6 +3,7 @@ import type { OrcadMigrationCatalogState, OrcadMigrationManifest } from '../../shared/orcad-migration-manifest' +import type { OrcadMigrationBlobDescriptor } from '../../shared/orcad-migration-blob-transfer' import type { Store } from '../persistence' type OrcadMigrationCatalogFlushStore = Partial> @@ -28,10 +29,14 @@ export async function commitStagedOrcadMigrationCatalogDurably(args: { store: OrcadMigrationCatalogCommitStore manifest: OrcadMigrationManifest signal?: AbortSignal + expectedBlobs?: readonly OrcadMigrationBlobDescriptor[] onDurableCommit: () => void }): Promise { // SAFETY: Returned or mutable Store slots retain their original native missing-method failure. - const result = args.store.commitStagedOrcadMigrationCatalog!(args.manifest) + const result = args.store.commitStagedOrcadMigrationCatalog!( + args.manifest, + args.expectedBlobs ? { expectedBlobs: args.expectedBlobs } : {} + ) await flushMigrationState(args.store, args.signal) args.onDurableCommit() return result diff --git a/src/main/runtime/rpc/methods/orcad-migration.test.ts b/src/main/runtime/rpc/methods/orcad-migration.test.ts index 3c40126336c..a517ecb5ca4 100644 --- a/src/main/runtime/rpc/methods/orcad-migration.test.ts +++ b/src/main/runtime/rpc/methods/orcad-migration.test.ts @@ -31,6 +31,7 @@ const MIGRATION_METHOD_NAMES = [ 'orcad.migration.stageCatalog', 'orcad.migration.commitCatalog', 'orcad.migration.stageSnapshotChunk', + 'orcad.migration.stageBlobChunk', 'orcad.migration.abortCatalog', 'orcad.migration.catalogState' ] as const @@ -123,9 +124,11 @@ describe('orcad migration RPC', () => { ? 'commitStagedOrcadMigrationCatalog' : name === 'orcad.migration.stageSnapshotChunk' ? 'stageOrcadMigrationSnapshotChunk' - : name === 'orcad.migration.abortCatalog' - ? 'abortStagedOrcadMigrationCatalog' - : 'getOrcadMigrationCatalogState' + : name === 'orcad.migration.stageBlobChunk' + ? 'stageOrcadMigrationBlobChunk' + : name === 'orcad.migration.abortCatalog' + ? 'abortStagedOrcadMigrationCatalog' + : 'getOrcadMigrationCatalogState' await expect( method.handler( @@ -137,6 +140,61 @@ describe('orcad migration RPC', () => { } }) + it('passes a named blob set to commit only when the sender sent one', async () => { + const input = manifest() + const commit = vi.fn().mockResolvedValue({ state: 'committed' }) + const caller = context(vi.fn(), {}, { commitStagedOrcadMigrationCatalog: commit }) + const method = migrationMethod('orcad.migration.commitCatalog') + const expectedBlobs = [ + { + blobKind: 'shell-history', + key: `${'a'.repeat(16)}-zsh`, + sha256: 'b'.repeat(64), + byteLength: 1 + } + ] + + await method.handler(method.params?.parse({ manifest: input, expectedBlobs }), caller) + expect(commit).toHaveBeenLastCalledWith(input, { + signal: undefined, + expectedBlobs + }) + await method.handler(method.params?.parse({ manifest: input }), caller) + expect(commit).toHaveBeenLastCalledWith(input, { signal: undefined }) + expect( + method.params?.safeParse({ + manifest: input, + expectedBlobs: [ + { + ...expectedBlobs[0], + blobKind: 'agent-session-records', + key: 'con' + } + ] + }).success + ).toBe(false) + }) + + it('stages a blob chunk through its own method and refuses malformed descriptors', async () => { + const input = manifest() + const blob = vi.fn().mockReturnValue({ receivedBytes: 1 }) + const caller = context(vi.fn(), {}, { stageOrcadMigrationBlobChunk: blob }) + const method = migrationMethod('orcad.migration.stageBlobChunk') + const request = migrationBlobRequest(input) + + await expect(method.handler(method.params?.parse(request), caller)).resolves.toEqual({ + receivedBytes: 1 + }) + expect(blob).toHaveBeenCalledWith(request) + for (const key of ['../escape', `${'A'.repeat(16)}-zsh`, `${'a'.repeat(16)}-pwsh`]) { + expect(method.params?.safeParse(migrationBlobRequest(input, key)).success, key).toBe(false) + } + expect( + method.params?.safeParse({ ...request, blob: { ...request.blob, blobKind: 'future-kind' } }) + .success + ).toBe(false) + }) + it('rejects malformed and tampered manifests before staging', async () => { const stageCatalog = vi.fn() const method = migrationMethod() @@ -205,7 +263,20 @@ function migrationSnapshotRequest(input: OrcadMigrationManifest) { function migrationMethodInput(name: (typeof MIGRATION_METHOD_NAMES)[number]) { const input = manifest() + if (name === 'orcad.migration.stageBlobChunk') { + return migrationBlobRequest(input) + } return name === 'orcad.migration.stageSnapshotChunk' ? migrationSnapshotRequest(input) : { manifest: input } } + +function migrationBlobRequest(input: OrcadMigrationManifest, key = `${'a'.repeat(16)}-zsh`) { + return { + migrationId: input.migrationId, + manifestSha256: input.manifestSha256, + blob: { blobKind: 'shell-history', key, sha256: 'b'.repeat(64), byteLength: 1 }, + offset: 0, + bytesBase64: 'YQ==' + } +} diff --git a/src/main/runtime/rpc/methods/orcad-migration.ts b/src/main/runtime/rpc/methods/orcad-migration.ts index 4aee8bcb4a5..90a79cbb7eb 100644 --- a/src/main/runtime/rpc/methods/orcad-migration.ts +++ b/src/main/runtime/rpc/methods/orcad-migration.ts @@ -1,7 +1,11 @@ import type { z } from 'zod' -import { OrcadMigrationCatalogParams } from '../../../../shared/rpc-contract/orcad-migration-params' +import { + OrcadMigrationCatalogParams, + OrcadMigrationCommitCatalogParams +} from '../../../../shared/rpc-contract/orcad-migration-params' import { parseOrcadMigrationManifest } from '../../../../shared/orcad-migration-manifest' import { OrcadMigrationSnapshotChunkRequestSchema } from '../../../../shared/orcad-migration-scrollback' +import { OrcadMigrationBlobChunkRequestSchema } from '../../../../shared/orcad-migration-blob-transfer' import { assertOrcadMigrationManifestDigest } from '../../../orcad/orcad-migration-manifest-digest' import { assertOrcadMigrationSourceServedHere } from '../../../orcad/orcad-migration-wsl-destination' import { defineMethod, type RpcContext } from '../core' @@ -21,11 +25,12 @@ export const ORCAD_MIGRATION_METHODS = [ defineMethod({ name: 'orcad.migration.commitCatalog', permission: 'host-admin', - params: OrcadMigrationCatalogParams, + params: OrcadMigrationCommitCatalogParams, handler: async (params, context) => { requireMigrationRuntimeClient(context) return context.runtime.commitStagedOrcadMigrationCatalog(migrationManifest(params), { - signal: context.signal + signal: context.signal, + ...(params.expectedBlobs ? { expectedBlobs: params.expectedBlobs } : {}) }) } }), @@ -38,6 +43,17 @@ export const ORCAD_MIGRATION_METHODS = [ return context.runtime.stageOrcadMigrationSnapshotChunk(params) } }), + // A separate method, not a field on stageSnapshotChunk: an older host would strip the field and + // stage the bytes as a snapshot. + defineMethod({ + name: 'orcad.migration.stageBlobChunk', + permission: 'host-admin', + params: OrcadMigrationBlobChunkRequestSchema, + handler: async (params, context) => { + requireMigrationRuntimeClient(context) + return context.runtime.stageOrcadMigrationBlobChunk(params) + } + }), defineMethod({ name: 'orcad.migration.abortCatalog', permission: 'host-admin', diff --git a/src/main/runtime/runtime-store-contract.ts b/src/main/runtime/runtime-store-contract.ts index c6ed7797ae4..801ca76f36d 100644 --- a/src/main/runtime/runtime-store-contract.ts +++ b/src/main/runtime/runtime-store-contract.ts @@ -58,6 +58,7 @@ export type RuntimeStore = { stageOrcadMigrationCatalog?: Store['stageOrcadMigrationCatalog'] commitStagedOrcadMigrationCatalog?: Store['commitStagedOrcadMigrationCatalog'] stageOrcadMigrationSnapshotChunk?: Store['stageOrcadMigrationSnapshotChunk'] + stageOrcadMigrationBlobChunk?: Store['stageOrcadMigrationBlobChunk'] abortStagedOrcadMigrationCatalog?: Store['abortStagedOrcadMigrationCatalog'] getOrcadMigrationCatalogState?: Store['getOrcadMigrationCatalogState'] persistPtyBinding?: Store['persistPtyBinding'] diff --git a/src/main/ssh/orcad-migration-blob-coordinator.test.ts b/src/main/ssh/orcad-migration-blob-coordinator.test.ts new file mode 100644 index 00000000000..528c08fb506 --- /dev/null +++ b/src/main/ssh/orcad-migration-blob-coordinator.test.ts @@ -0,0 +1,182 @@ +import { createHash } from 'node:crypto' +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + ORCAD_MIGRATION_BLOB_CHUNK_BYTES, + requiredMigrationBlobCapabilities, + type OrcadMigrationBlobDescriptor +} from '../../shared/orcad-migration-blob-transfer' +import { + ORCAD_MIGRATION_MANIFEST_VERSION, + type OrcadMigrationCatalogState, + type OrcadMigrationManifest +} from '../../shared/orcad-migration-manifest' +import { ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY } from '../../shared/orcad-runtime-capabilities' +import { + commitOrcadMigrationBlobs, + getOrcadMigrationReceivedBlobPath, + inspectOrcadMigrationBlobUploads, + stageOrcadMigrationBlobChunk +} from '../persistence/migrating-orcad-catalog/orcad-migration-blob-staging' +import { + transferOrcadMigrationBlobs, + type OrcadMigrationBlobDestination +} from './orcad-migration-blob-coordinator' + +let root: string + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'orcad-migration-blob-sender-')) +}) + +afterEach(() => rmSync(root, { recursive: true, force: true })) + +const manifest: OrcadMigrationManifest = { + version: ORCAD_MIGRATION_MANIFEST_VERSION, + migrationId: 'migration-1', + createdAt: '2026-08-30T12:00:00.000Z', + source: { sshTargetId: 'source', sshTargetGeneration: 1, targetLabel: 'Source' }, + payload: { repositories: [], projectGroups: [], folderWorkspaces: [] }, + manifestSha256: 'a'.repeat(64) +} + +const history = Buffer.alloc(ORCAD_MIGRATION_BLOB_CHUNK_BYTES * 2 + 17, 0x61) +const hints = Buffer.from('{"panes":[]}') +const blobs = [ + descriptor('shell-history', `${'a'.repeat(16)}-zsh`, history), + descriptor('terminal-restore-hints', 'migration', hints) +] +const contents = new Map([ + [blobs[0].key, history], + [blobs[1].key, hints] +]) + +describe('orcad migration blob sender', () => { + it('uploads every blob in bounded chunks, resuming from the staged offset', async () => { + stageOrcadMigrationBlobChunk({ + stagedManifest: manifest, + root, + request: chunkRequest(blobs[0], 0, history.subarray(0, ORCAD_MIGRATION_BLOB_CHUNK_BYTES)) + }) + const destination = realDestination(requiredMigrationBlobCapabilities(blobs)) + + await transferOrcadMigrationBlobs({ source: source(), manifest, blobs, destination }) + + expect(destination.stageChunk.mock.calls.map(([request]) => request.offset)).toEqual([ + ORCAD_MIGRATION_BLOB_CHUNK_BYTES, + ORCAD_MIGRATION_BLOB_CHUNK_BYTES * 2, + 0 + ]) + commitOrcadMigrationBlobs(manifest, root) + for (const blob of blobs) { + expect(readFileSync(getOrcadMigrationReceivedBlobPath(root, manifest, blob))).toEqual( + contents.get(blob.key) + ) + } + }) + + it.each([ + ['the blob channel', [blobs[1]], ['orcad.migration-blob.terminal-restore-hints.v1']], + ["the blob's kind", [blobs[1]], [ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY]] + ])('refuses before staging anything when the destination lacks %s', async (_, sent, caps) => { + const destination = realDestination(caps) + + await expect( + transferOrcadMigrationBlobs({ source: source(), manifest, blobs: sent, destination }) + ).rejects.toThrow(/^orcad_migration_destination_blob_kind_unsupported:terminal-restore-hints:/) + expect(destination.readState).not.toHaveBeenCalled() + expect(destination.stageChunk).not.toHaveBeenCalled() + expect(inspectOrcadMigrationBlobUploads(manifest, root)).toBeUndefined() + }) + + it('confirms a chunk whose acknowledgement was lost from the staged state', async () => { + const destination = realDestination(requiredMigrationBlobCapabilities(blobs)) + const stage = destination.stageChunk.getMockImplementation() + destination.stageChunk.mockImplementationOnce(async (request) => { + await stage?.(request) + throw new Error('connection_lost') + }) + + await transferOrcadMigrationBlobs({ source: source(), manifest, blobs, destination }) + expect(inspectOrcadMigrationBlobUploads(manifest, root)?.map((u) => u.receivedBytes)).toEqual( + expect.arrayContaining([history.length, hints.length]) + ) + }) + + it('refuses when the destination holds the same key with other bytes', async () => { + const stale = descriptor('terminal-restore-hints', 'migration', Buffer.from('stale')) + stageOrcadMigrationBlobChunk({ + stagedManifest: manifest, + root, + request: chunkRequest(stale, 0, Buffer.from('st')) + }) + const destination = realDestination(requiredMigrationBlobCapabilities(blobs)) + + await expect( + transferOrcadMigrationBlobs({ source: source(), manifest, blobs, destination }) + ).rejects.toThrow('orcad_migration_blob_descriptor_conflict:terminal-restore-hints') + expect(destination.stageChunk).not.toHaveBeenCalled() + }) + + it('sends nothing and asks nothing when there are no blobs', async () => { + const destination = realDestination([]) + await transferOrcadMigrationBlobs({ source: source(), manifest, blobs: [], destination }) + expect(destination.readCapabilities).not.toHaveBeenCalled() + }) +}) + +function descriptor( + blobKind: OrcadMigrationBlobDescriptor['blobKind'], + key: string, + bytes: Buffer +): OrcadMigrationBlobDescriptor { + return { + blobKind, + key, + sha256: createHash('sha256').update(bytes).digest('hex'), + byteLength: bytes.length + } +} + +function chunkRequest(blob: OrcadMigrationBlobDescriptor, offset: number, bytes: Buffer) { + return { + migrationId: manifest.migrationId, + manifestSha256: manifest.manifestSha256, + blob, + offset, + bytesBase64: bytes.toString('base64') + } +} + +function source() { + return { + readChunk: async (blob: OrcadMigrationBlobDescriptor, offset: number, maxBytes: number) => + (contents.get(blob.key) ?? Buffer.alloc(0)).subarray(offset, offset + maxBytes) + } +} + +/** The real staging module behind the RPC shapes, so resume reads what a host would report. */ +function realDestination(capabilities: readonly string[]) { + const readState = async (): Promise => { + const blobUploads = inspectOrcadMigrationBlobUploads(manifest, root) + return { + state: 'staged', + migrationId: manifest.migrationId, + manifestSha256: manifest.manifestSha256, + stagedAt: '2026-08-30T12:01:00.000Z', + ...(blobUploads ? { blobUploads } : {}) + } + } + const destination = { + readCapabilities: vi.fn( + async () => capabilities + ), + readState: vi.fn(readState), + stageChunk: vi.fn(async (request) => + stageOrcadMigrationBlobChunk({ stagedManifest: manifest, root, request }) + ) + } + return destination +} diff --git a/src/main/ssh/orcad-migration-blob-coordinator.ts b/src/main/ssh/orcad-migration-blob-coordinator.ts new file mode 100644 index 00000000000..c358f5e7b38 --- /dev/null +++ b/src/main/ssh/orcad-migration-blob-coordinator.ts @@ -0,0 +1,154 @@ +/** + * Sending a migration's blobs to the destination in bounded, resumable chunks, beside the + * manifest's scrollback snapshots. Every kind is checked against the destination's capabilities + * before the first byte: a kind it cannot install is refused by name, never dropped. Offsets + * resume from the destination's staged state, and a lost acknowledgement is confirmed by + * re-reading that state, never assumed. + */ +import { + ORCAD_MIGRATION_BLOB_CHUNK_BYTES, + type OrcadMigrationBlobChunkRequest, + type OrcadMigrationBlobChunkResult, + type OrcadMigrationBlobDescriptor, + assertOrcadMigrationBlobSet, + findUnsupportedMigrationBlobKind, + orcadMigrationBlobIdentity, + orcadMigrationBlobKindUnsupportedBlocker, + sameOrcadMigrationBlob +} from '../../shared/orcad-migration-blob-transfer' +import type { + OrcadMigrationCatalogState, + OrcadMigrationManifest +} from '../../shared/orcad-migration-manifest' + +export type OrcadMigrationBlobSource = { + /** Up to `maxBytes` bytes of the blob starting at `offset`, exactly as the descriptor hashed. */ + readChunk: ( + descriptor: OrcadMigrationBlobDescriptor, + offset: number, + maxBytes: number + ) => Promise +} + +export type OrcadMigrationBlobDestination = { + readCapabilities: () => Promise + readState: (manifest: OrcadMigrationManifest) => Promise + stageChunk: (request: OrcadMigrationBlobChunkRequest) => Promise +} + +export type OrcadMigrationBlobTransfer = { + source: OrcadMigrationBlobSource + manifest: OrcadMigrationManifest + blobs: readonly OrcadMigrationBlobDescriptor[] + destination: OrcadMigrationBlobDestination +} + +type StagedCatalogState = Extract + +export async function transferOrcadMigrationBlobs( + transfer: OrcadMigrationBlobTransfer +): Promise { + if (transfer.blobs.length === 0) { + return + } + assertOrcadMigrationBlobSet(transfer.blobs) + const unsupported = findUnsupportedMigrationBlobKind( + transfer.blobs, + await transfer.destination.readCapabilities() + ) + if (unsupported) { + throw new Error(orcadMigrationBlobKindUnsupportedBlocker(unsupported)) + } + const offsets = receivedOffsets(transfer, await readStagedState(transfer)) + for (const blob of transfer.blobs) { + let offset = offsets.get(orcadMigrationBlobIdentity(blob)) ?? 0 + while (offset < blob.byteLength) { + const bytes = await transfer.source.readChunk( + blob, + offset, + Math.min(ORCAD_MIGRATION_BLOB_CHUNK_BYTES, blob.byteLength - offset) + ) + if ( + bytes.length === 0 || + bytes.length > ORCAD_MIGRATION_BLOB_CHUNK_BYTES || + offset + bytes.length > blob.byteLength + ) { + throw new Error(`orcad_migration_source_blob_changed:${blob.blobKind}`) + } + const request: OrcadMigrationBlobChunkRequest = { + migrationId: transfer.manifest.migrationId, + manifestSha256: transfer.manifest.manifestSha256, + blob: { ...blob }, + offset, + bytesBase64: bytes.toString('base64') + } + offset = await stageChunkWithRecovery(transfer, request, offset + bytes.length) + } + } + assertAllReceived(transfer, await readStagedState(transfer)) +} + +async function readStagedState(transfer: OrcadMigrationBlobTransfer): Promise { + const state = await transfer.destination.readState(transfer.manifest) + if (state.state !== 'staged') { + throw new Error('orcad_migration_blob_catalog_not_staged') + } + return state +} + +function receivedOffsets( + transfer: OrcadMigrationBlobTransfer, + state: StagedCatalogState +): Map { + const wanted = new Map(transfer.blobs.map((blob) => [orcadMigrationBlobIdentity(blob), blob])) + const offsets = new Map() + for (const upload of state.blobUploads ?? []) { + const blob = wanted.get(orcadMigrationBlobIdentity(upload)) + // A staged key with other bytes is a different capture; restaging it would mix the two. + if (!blob || !sameOrcadMigrationBlob(blob, upload)) { + throw new Error(`orcad_migration_blob_descriptor_conflict:${upload.blobKind}`) + } + offsets.set(orcadMigrationBlobIdentity(upload), upload.receivedBytes) + } + return offsets +} + +function assertAllReceived(transfer: OrcadMigrationBlobTransfer, state: StagedCatalogState): void { + const received = receivedOffsets(transfer, state) + for (const blob of transfer.blobs) { + if (received.get(orcadMigrationBlobIdentity(blob)) !== blob.byteLength) { + throw new Error(`orcad_migration_blob_transfer_incomplete:${blob.blobKind}`) + } + } +} + +async function stageChunkWithRecovery( + transfer: OrcadMigrationBlobTransfer, + request: OrcadMigrationBlobChunkRequest, + expectedOffset: number +): Promise { + try { + const result = await transfer.destination.stageChunk(request) + if (result.receivedBytes !== expectedOffset) { + throw new Error('orcad_migration_blob_ack_invalid') + } + return result.receivedBytes + } catch (error) { + try { + const observed = await transfer.destination.readState(transfer.manifest) + const received = + observed.state === 'staged' + ? observed.blobUploads?.find( + (entry) => + orcadMigrationBlobIdentity(entry) === orcadMigrationBlobIdentity(request.blob) + )?.receivedBytes + : undefined + if (received === expectedOffset) { + return received + } + } catch { + // The chunk stays unverifiable; report the first failure. + } + throw error + } +} diff --git a/src/main/ssh/orcad-migration-catalog-client-blob-kind.test.ts b/src/main/ssh/orcad-migration-catalog-client-blob-kind.test.ts new file mode 100644 index 00000000000..a80a55240bc --- /dev/null +++ b/src/main/ssh/orcad-migration-catalog-client-blob-kind.test.ts @@ -0,0 +1,174 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { + OrcadMigrationBlobChunkRequest, + OrcadMigrationBlobDescriptor +} from '../../shared/orcad-migration-blob-transfer' +import { + ORCAD_MIGRATION_MANIFEST_VERSION, + type OrcadMigrationManifest +} from '../../shared/orcad-migration-manifest' +import { + ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY, + ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY +} from '../../shared/orcad-runtime-capabilities' +import { encodePairingOffer, PAIRING_OFFER_VERSION } from '../../shared/pairing' + +const sendRequest = vi.hoisted(() => vi.fn()) +const status = vi.hoisted((): { capabilities: string[] } => ({ capabilities: [] })) +vi.mock('../../shared/remote-runtime-client', () => ({ + sendRemoteRuntimeRequestWithStatusPreflight: async ( + pairing: unknown, + method: string, + params: unknown, + _timeoutMs: number, + validate: (response: unknown) => void + ) => { + validate({ + ok: true, + _meta: { runtimeId: 'runtime' }, + result: { capabilities: status.capabilities } + }) + return sendRequest(pairing, method, params) + } +})) + +const { commitRemoteOrcadMigrationCatalog, stageRemoteOrcadMigrationBlobChunk } = + await import('./orcad-migration-catalog-client') + +const pairingCode = encodePairingOffer({ + v: PAIRING_OFFER_VERSION, + endpoint: 'ws://127.0.0.1:46768/runtime', + deviceToken: 'device-token', + publicKeyB64: 'public-key', + pairedDeviceId: 'paired-desktop' +}) + +const request: OrcadMigrationBlobChunkRequest = { + migrationId: 'migration-blob', + manifestSha256: 'a'.repeat(64), + blob: { + blobKind: 'shell-history', + key: `${'b'.repeat(16)}-zsh`, + sha256: 'c'.repeat(64), + byteLength: 3 + }, + offset: 0, + bytesBase64: 'YWJj' +} + +beforeEach(() => { + vi.clearAllMocks() + sendRequest.mockResolvedValue({ + ok: true, + _meta: { runtimeId: 'runtime' }, + result: { + migrationId: request.migrationId, + manifestSha256: request.manifestSha256, + blobKind: 'shell-history', + key: request.blob.key, + receivedBytes: 3 + } + }) +}) + +describe('orcad migration blob chunk capability gate', () => { + it.each([ + ['an older destination', [ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY]], + [ + 'a destination with only the channel', + [ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY, ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY] + ] + ])('never sends a blob to %s, and names the kind', async (_, capabilities) => { + status.capabilities = capabilities + await expect(stageRemoteOrcadMigrationBlobChunk(pairingCode, request)).rejects.toThrow( + /^orcad_migration_destination_blob_kind_unsupported:shell-history:/ + ) + expect(sendRequest).not.toHaveBeenCalled() + }) + + it('sends through its own method once the destination installs the kind', async () => { + status.capabilities = [ + ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY, + ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY, + 'orcad.migration-blob.shell-history.v1' + ] + await expect(stageRemoteOrcadMigrationBlobChunk(pairingCode, request)).resolves.toMatchObject({ + receivedBytes: 3 + }) + expect(sendRequest).toHaveBeenCalledWith( + expect.anything(), + 'orcad.migration.stageBlobChunk', + request + ) + }) +}) + +describe('orcad migration commit names its blob set', () => { + const manifest: OrcadMigrationManifest = { + version: ORCAD_MIGRATION_MANIFEST_VERSION, + migrationId: request.migrationId, + createdAt: '2026-08-30T12:00:00.000Z', + source: { + sshTargetId: 'source', + sshTargetGeneration: 1, + targetLabel: 'Source' + }, + payload: { repositories: [], projectGroups: [], folderWorkspaces: [] }, + manifestSha256: request.manifestSha256 + } + const expectedBlobs: OrcadMigrationBlobDescriptor[] = [request.blob] + const installing = [ + ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY, + ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY, + 'orcad.migration-blob.shell-history.v1' + ] + + beforeEach(() => { + sendRequest.mockResolvedValue({ + ok: true, + _meta: { runtimeId: 'runtime' }, + result: { + state: 'staged', + migrationId: manifest.migrationId, + manifestSha256: manifest.manifestSha256, + stagedAt: '2026-08-30T12:00:00.000Z' + } + }) + }) + + it('sends exactly `{ manifest }` when the move carried no blobs, even to a blob host', async () => { + for (const capabilities of [[ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY], installing]) { + status.capabilities = capabilities + await commitRemoteOrcadMigrationCatalog(pairingCode, manifest) + await commitRemoteOrcadMigrationCatalog(pairingCode, manifest, { + expectedBlobs: [] + }) + expect(sendRequest).toHaveBeenLastCalledWith( + expect.anything(), + 'orcad.migration.commitCatalog', + { manifest } + ) + } + }) + + it('names the blobs to a destination with the channel, and never to one without', async () => { + status.capabilities = installing + await commitRemoteOrcadMigrationCatalog(pairingCode, manifest, { + expectedBlobs + }) + expect(sendRequest).toHaveBeenLastCalledWith( + expect.anything(), + 'orcad.migration.commitCatalog', + { manifest, expectedBlobs } + ) + + sendRequest.mockClear() + status.capabilities = [ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY] + await expect( + commitRemoteOrcadMigrationCatalog(pairingCode, manifest, { + expectedBlobs + }) + ).rejects.toThrow(/^orcad_migration_destination_blob_kind_unsupported:shell-history:/) + expect(sendRequest).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/ssh/orcad-migration-catalog-client.ts b/src/main/ssh/orcad-migration-catalog-client.ts index d23a320d4c1..03e4fbad444 100644 --- a/src/main/ssh/orcad-migration-catalog-client.ts +++ b/src/main/ssh/orcad-migration-catalog-client.ts @@ -13,6 +13,14 @@ import { type OrcadMigrationSnapshotChunkRequest, type OrcadMigrationSnapshotChunkResult } from '../../shared/orcad-migration-scrollback' +import { + findUnsupportedMigrationBlobKind, + orcadMigrationBlobKindUnsupportedBlocker, + parseOrcadMigrationBlobChunkResult, + type OrcadMigrationBlobChunkRequest, + type OrcadMigrationBlobChunkResult, + type OrcadMigrationBlobDescriptor +} from '../../shared/orcad-migration-blob-transfer' import { parsePairingCode } from '../../shared/pairing' import { ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES } from '../../shared/electron-remote-runtime-client-capabilities' import { ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY } from '../../shared/orcad-runtime-capabilities' @@ -58,12 +66,16 @@ export function stageRemoteOrcadMigrationCatalog( return requestCatalogState(pairingCode, 'stage', manifest, options) } -export function commitRemoteOrcadMigrationCatalog( +/** `expectedBlobs` names what was moved so the destination refuses a commit missing any. */ +export async function commitRemoteOrcadMigrationCatalog( pairingCode: string, manifest: OrcadMigrationManifest, - options: CatalogRequestOptions = {} + options: CatalogRequestOptions & { + expectedBlobs?: readonly OrcadMigrationBlobDescriptor[] + } = {} ): Promise { - return requestCatalogState(pairingCode, 'commit', manifest, options) + const result = await request(pairingCode, 'commit', manifest, options, options.expectedBlobs) + return parseOrcadMigrationCatalogState(result, manifest) } export function readRemoteOrcadMigrationCatalogState( @@ -109,6 +121,36 @@ export async function stageRemoteOrcadMigrationSnapshotChunk( return parseOrcadMigrationSnapshotChunkResult(response.result, request) } +export async function stageRemoteOrcadMigrationBlobChunk( + pairingCode: string, + request: OrcadMigrationBlobChunkRequest, + options: CatalogRequestOptions = {} +): Promise { + const pairing = parsePairingCode(pairingCode) + if (!pairing) { + throw new Error('orcad_migration_pairing_code_invalid') + } + const response = await sendSupportedMigrationRequest( + pairing, + 'orcad.migration.stageBlobChunk', + request, + options, + false, + null, + // Per request: a downgraded destination must refuse the kind, never stage what it can't install. + (capabilities) => { + const unsupported = findUnsupportedMigrationBlobKind([request.blob], capabilities) + if (unsupported) { + throw new Error(orcadMigrationBlobKindUnsupportedBlocker(unsupported)) + } + } + ) + if (!response.ok) { + throw new Error(`orcad_migration_blob_failed:${response.error.message}`) + } + return parseOrcadMigrationBlobChunkResult(response.result, request) +} + async function requestCatalogState( pairingCode: string, operation: Exclude, @@ -123,7 +165,8 @@ async function request( pairingCode: string, operation: OrcadCatalogMigrationOperation, manifest: OrcadMigrationManifest, - options: CatalogRequestOptions + options: CatalogRequestOptions, + expectedBlobs: readonly OrcadMigrationBlobDescriptor[] = [] ): Promise { const pairing = parsePairingCode(pairingCode) if (!pairing) { @@ -137,11 +180,19 @@ async function request( const response = await sendSupportedMigrationRequest( pairing, METHOD_BY_OPERATION[operation], - { manifest }, + // Only with blobs: SSH and blob-free moves keep sending exactly `{ manifest }`. + expectedBlobs.length > 0 ? { manifest, expectedBlobs } : { manifest }, options, carriesExtraAgentArgs, // Every operation: an older destination would parse a non-SSH source as malformed or as SSH. - requiredOrcadMigrationSourceCapability(manifest.source) + requiredOrcadMigrationSourceCapability(manifest.source), + // A destination without the blob channel would strip the field and commit unchecked. + (capabilities) => { + const unsupported = findUnsupportedMigrationBlobKind(expectedBlobs, capabilities) + if (unsupported) { + throw new Error(orcadMigrationBlobKindUnsupportedBlocker(unsupported)) + } + } ) if (!response.ok) { throw new Error(`orcad_migration_${operation}_failed:${response.error.message}`) @@ -155,7 +206,8 @@ async function sendSupportedMigrationRequest( params: unknown, options: CatalogRequestOptions, requiresExtraAgentArgs = false, - requiredSourceCapability: string | null = null + requiredSourceCapability: string | null = null, + assertCapabilities: (capabilities: readonly string[]) => void = () => {} ): Promise> { const response = await sendRemoteRuntimeRequestWithStatusPreflight( pairedRuntimeConnectionFromOffer(pairing), @@ -187,6 +239,7 @@ async function sendSupportedMigrationRequest( ) { throw new Error(ORCAD_MIGRATION_DESTINATION_SOURCE_KIND_UNSUPPORTED) } + assertCapabilities(status.result.capabilities ?? []) }, undefined, ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES, diff --git a/src/shared/orcad-migration-blob-transfer.test.ts b/src/shared/orcad-migration-blob-transfer.test.ts new file mode 100644 index 00000000000..8ca5281f74b --- /dev/null +++ b/src/shared/orcad-migration-blob-transfer.test.ts @@ -0,0 +1,192 @@ +import { describe, expect, it } from 'vitest' +import { + MAX_ORCAD_MIGRATION_BLOB_TOTAL_BYTES, + ORCAD_MIGRATION_BLOB_KIND_CONTRACTS, + OrcadMigrationBlobChunkRequestSchema, + advertisedOrcadMigrationBlobCapabilities, + assertOrcadMigrationBlobSet, + findUnsupportedMigrationBlobKind, + orcadMigrationBlobKindUnsupportedBlocker, + parseOrcadMigrationBlobChunkResult, + parseOrcadMigrationBlobDescriptor, + parseOrcadMigrationBlobUploadStates, + requiredMigrationBlobCapabilities, + type OrcadMigrationBlobDescriptor +} from './orcad-migration-blob-transfer' +import { parseOrcadMigrationCatalogState } from './orcad-migration-catalog-state' +import { + ORCAD_MIGRATION_MANIFEST_VERSION, + type OrcadMigrationManifest +} from './orcad-migration-manifest' +import { ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY } from './orcad-runtime-capabilities' + +const HASH = 'a'.repeat(16) +const DIGEST = 'b'.repeat(64) + +function blob(overrides: Partial = {}): OrcadMigrationBlobDescriptor { + return { + blobKind: 'shell-history', + key: `${HASH}-zsh`, + sha256: DIGEST, + byteLength: 10, + ...overrides + } +} + +const manifest: OrcadMigrationManifest = { + version: ORCAD_MIGRATION_MANIFEST_VERSION, + migrationId: 'migration-1', + manifestSha256: 'c'.repeat(64), + createdAt: '2026-08-30T12:00:00.000Z', + source: { sshTargetId: 'ssh-prod', sshTargetGeneration: 7, targetLabel: 'Production' }, + payload: { repositories: [], projectGroups: [], folderWorkspaces: [] } +} + +describe('orcad migration blob contract', () => { + it.each([ + ['terminal-restore-hints', 'migration', true], + ['terminal-restore-hints', HASH, false], + ['shell-history', `${HASH}-bash`, true], + ['shell-history', `${HASH}-fish`, true], + ['shell-history', `${HASH}-pwsh`, false], + ['shell-history', `${'A'.repeat(16)}-zsh`, false], + ['shell-history', `${HASH}.zsh`, false], + ['agent-session-records', '0f8e2c1a-7b3d-4e5f-9a0b-1c2d3e4f5a6b', true], + ['agent-session-records', 'Session-Mixed-Case', false], + ['agent-session-records', '../escape', false], + ['agent-session-records', 'a.b', false], + ['agent-session-records', 'x'.repeat(129), false], + // Windows device names: one with any extension is the device too. + ['agent-session-records', 'con', false], + ['agent-session-records', 'prn', false], + ['agent-session-records', 'aux', false], + ['agent-session-records', 'nul', false], + ['agent-session-records', 'com0', false], + ['agent-session-records', 'com9', false], + ['agent-session-records', 'lpt1', false], + ['agent-session-records', 'console', true], + ['agent-session-records', 'com10', true], + ['agent-session-records', 'nul-1', true] + ] as const)('%s key %s valid=%s', (blobKind, key, valid) => { + const parse = () => parseOrcadMigrationBlobDescriptor(blob({ blobKind, key })) + if (valid) { + expect(parse().key).toBe(key) + } else { + expect(parse).toThrow('orcad_migration_blob_key_invalid') + } + }) + + it('enforces per-kind byte caps, a non-empty body, and the set caps', () => { + const cap = ORCAD_MIGRATION_BLOB_KIND_CONTRACTS['terminal-restore-hints'].maxBytes + const hints = blob({ blobKind: 'terminal-restore-hints', key: 'migration' }) + expect(parseOrcadMigrationBlobDescriptor({ ...hints, byteLength: cap }).byteLength).toBe(cap) + expect(() => parseOrcadMigrationBlobDescriptor({ ...hints, byteLength: cap + 1 })).toThrow( + 'orcad_migration_blob_too_large' + ) + expect(() => parseOrcadMigrationBlobDescriptor(blob({ byteLength: 0 }))).toThrow() + expect(() => assertOrcadMigrationBlobSet([blob(), blob({ sha256: 'd'.repeat(64) })])).toThrow( + 'orcad_migration_blob_duplicate' + ) + const big = ORCAD_MIGRATION_BLOB_KIND_CONTRACTS['shell-history'].maxBytes + const many = Array.from({ length: MAX_ORCAD_MIGRATION_BLOB_TOTAL_BYTES / big + 1 }, (_, i) => + blob({ key: `${i.toString(16).padStart(16, '0')}-zsh`, byteLength: big }) + ) + expect(() => assertOrcadMigrationBlobSet(many)).toThrow('orcad_migration_blobs_too_large') + }) + + it('requires the channel plus each sent kind, and names the first missing kind', () => { + const hints = blob({ blobKind: 'terminal-restore-hints', key: 'migration' }) + expect(requiredMigrationBlobCapabilities([])).toEqual([]) + expect(requiredMigrationBlobCapabilities([blob(), hints, blob()])).toEqual([ + ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY, + 'orcad.migration-blob.terminal-restore-hints.v1', + 'orcad.migration-blob.shell-history.v1' + ]) + const all = requiredMigrationBlobCapabilities([blob(), hints]) + expect(findUnsupportedMigrationBlobKind([blob(), hints], all)).toBeNull() + expect( + findUnsupportedMigrationBlobKind( + [blob(), hints], + all.filter((entry) => entry !== ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY) + ) + ).toBe('shell-history') + expect( + findUnsupportedMigrationBlobKind( + [blob(), hints], + [ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY, 'orcad.migration-blob.shell-history.v1'] + ) + ).toBe('terminal-restore-hints') + expect(orcadMigrationBlobKindUnsupportedBlocker('shell-history')).toMatch( + /^orcad_migration_destination_blob_kind_unsupported:shell-history:/ + ) + }) + + it('advertises only the channel until a kind has an installer', () => { + expect(advertisedOrcadMigrationBlobCapabilities()).toEqual([ + ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY + ]) + }) + + it('keeps unknown request fields out and checks the acknowledged length', () => { + const request = OrcadMigrationBlobChunkRequestSchema.parse({ + migrationId: manifest.migrationId, + manifestSha256: manifest.manifestSha256, + blob: blob(), + offset: 4, + bytesBase64: 'YWJj', + futureField: true + }) + expect(request).not.toHaveProperty('futureField') + const result = { + migrationId: manifest.migrationId, + manifestSha256: manifest.manifestSha256, + blobKind: 'shell-history', + key: `${HASH}-zsh`, + receivedBytes: 7 + } + expect(parseOrcadMigrationBlobChunkResult(result, request).receivedBytes).toBe(7) + expect(() => + parseOrcadMigrationBlobChunkResult({ ...result, receivedBytes: 6 }, request) + ).toThrow('orcad_migration_blob_chunk_result_invalid') + }) + + it('parses upload states strictly and treats an absent list as nothing staged', () => { + expect(parseOrcadMigrationBlobUploadStates(undefined)).toBeUndefined() + expect(parseOrcadMigrationBlobUploadStates([{ ...blob(), receivedBytes: 3 }])).toEqual([ + { ...blob(), receivedBytes: 3 } + ]) + expect(() => parseOrcadMigrationBlobUploadStates([{ ...blob(), receivedBytes: 11 }])).toThrow( + 'orcad_migration_blob_state_invalid' + ) + expect(() => parseOrcadMigrationBlobUploadStates({})).toThrow( + 'orcad_migration_blob_state_invalid' + ) + }) +}) + +describe('orcad migration catalog state with blob uploads', () => { + const staged = { + state: 'staged', + migrationId: manifest.migrationId, + manifestSha256: manifest.manifestSha256, + stagedAt: '2026-08-30T12:01:00.000Z' + } + + it('still accepts a staged state from a host that predates blobs', () => { + expect(parseOrcadMigrationCatalogState(staged, manifest)).toEqual(staged) + }) + + it('carries blob uploads from a host that has them', () => { + const uploads = [{ ...blob(), receivedBytes: 10 }] + expect(parseOrcadMigrationCatalogState({ ...staged, blobUploads: uploads }, manifest)).toEqual({ + ...staged, + blobUploads: uploads + }) + expect(() => + parseOrcadMigrationCatalogState( + { ...staged, blobUploads: [{ ...blob({ key: 'bad' }), receivedBytes: 0 }] }, + manifest + ) + ).toThrow('orcad_migration_blob_key_invalid') + }) +}) diff --git a/src/shared/orcad-migration-blob-transfer.ts b/src/shared/orcad-migration-blob-transfer.ts new file mode 100644 index 00000000000..677b8c7c787 --- /dev/null +++ b/src/shared/orcad-migration-blob-transfer.ts @@ -0,0 +1,269 @@ +/** + * Opaque blobs a migration carries beside the signed manifest: data the catalog does not model + * (pane restore hints, shell history files, native-chat records) that the destination installs + * itself. Each kind is gated by its own destination capability, because an older destination + * would drop a kind it does not know — the desktop refuses before staging instead. + */ +import { z } from 'zod' +import { + ORCAD_MIGRATION_SCROLLBACK_CHUNK_BYTES, + OrcadMigrationChunkBase64Schema, + OrcadMigrationDigestSchema, + OrcadMigrationIdentitySchema, + decodeOrcadMigrationChunk +} from './orcad-migration-scrollback' +import { requiredRecord } from './orcad-migration-dormant-value-validation' +import { assertUniqueKeys } from './orcad-migration-manifest-fields' +import { ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY } from './orcad-runtime-capabilities' + +export const ORCAD_MIGRATION_BLOB_KINDS = [ + 'terminal-restore-hints', + 'shell-history', + 'agent-session-records' +] as const + +export type OrcadMigrationBlobKind = (typeof ORCAD_MIGRATION_BLOB_KINDS)[number] + +type OrcadMigrationBlobKindContract = { + /** Filename-safe, dot-free and lowercase, so a key is one path segment on any filesystem. */ + key: RegExp + maxBytes: number + capability: string +} + +const MIB = 1024 * 1024 + +export const ORCAD_MIGRATION_BLOB_KIND_CONTRACTS: Record< + OrcadMigrationBlobKind, + OrcadMigrationBlobKindContract +> = { + // One JSON document per migration; the received directory already scopes it to the migration. + 'terminal-restore-hints': { + key: /^migration$/, + maxBytes: 4 * MIB, + capability: 'orcad.migration-blob.terminal-restore-hints.v1' + }, + // `hashWorktreeId` (16 hex) plus the shell whose history file it is. + 'shell-history': { + key: /^[a-f0-9]{16}-(?:zsh|bash|fish)$/, + maxBytes: 64 * MIB, + capability: 'orcad.migration-blob.shell-history.v1' + }, + // The agent session id; agent CLIs mint lowercase UUID-like ids. + 'agent-session-records': { + key: /^[a-z0-9][a-z0-9_-]{0,127}$/, + maxBytes: 64 * MIB, + capability: 'orcad.migration-blob.agent-session-records.v1' + } +} + +// The same bound the shared chunk decoder enforces for both channels. +export const ORCAD_MIGRATION_BLOB_CHUNK_BYTES = ORCAD_MIGRATION_SCROLLBACK_CHUNK_BYTES +export const MAX_ORCAD_MIGRATION_BLOBS = 1024 +export const MAX_ORCAD_MIGRATION_BLOB_TOTAL_BYTES = 256 * MIB + +/** Byte-for-byte what the destination must hold; empty content is sent as no blob at all. */ +export type OrcadMigrationBlobDescriptor = { + blobKind: OrcadMigrationBlobKind + key: string + sha256: string + byteLength: number +} + +export type OrcadMigrationBlobUploadState = OrcadMigrationBlobDescriptor & { + receivedBytes: number +} + +export const ORCAD_MIGRATION_DESTINATION_BLOB_KIND_UNSUPPORTED = + 'orcad_migration_destination_blob_kind_unsupported' + +// Windows refuses these as file names whatever the extension, so a key must never be one. +const WINDOWS_RESERVED_FILE_NAME = /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\..*)?$/i + +export const OrcadMigrationBlobDescriptorSchema = z + .object({ + blobKind: z.enum(ORCAD_MIGRATION_BLOB_KINDS), + key: z.string().max(256), + sha256: OrcadMigrationDigestSchema, + byteLength: z.number().int().min(1) + }) + .superRefine((value, context) => { + const contract = ORCAD_MIGRATION_BLOB_KIND_CONTRACTS[value.blobKind] + if (!contract.key.test(value.key) || WINDOWS_RESERVED_FILE_NAME.test(value.key)) { + context.addIssue({ + code: 'custom', + message: 'orcad_migration_blob_key_invalid' + }) + } + if (value.byteLength > contract.maxBytes) { + context.addIssue({ code: 'custom', message: 'orcad_migration_blob_too_large' }) + } + }) + +// Not strict: an older host must ignore optional fields a newer client adds. +export const OrcadMigrationBlobChunkRequestSchema = z.object({ + migrationId: OrcadMigrationIdentitySchema, + manifestSha256: OrcadMigrationDigestSchema, + blob: OrcadMigrationBlobDescriptorSchema, + offset: z.number().int().nonnegative().max(MAX_ORCAD_MIGRATION_BLOB_TOTAL_BYTES), + bytesBase64: OrcadMigrationChunkBase64Schema +}) + +export type OrcadMigrationBlobChunkRequest = z.infer + +export type OrcadMigrationBlobChunkResult = { + migrationId: string + manifestSha256: string + blobKind: OrcadMigrationBlobKind + key: string + receivedBytes: number +} + +export function parseOrcadMigrationBlobDescriptor(value: unknown): OrcadMigrationBlobDescriptor { + const parsed = OrcadMigrationBlobDescriptorSchema.safeParse(value) + if (!parsed.success) { + throw new Error(parsed.error.issues[0]?.message ?? 'orcad_migration_blob_descriptor_invalid') + } + return { + blobKind: parsed.data.blobKind, + key: parsed.data.key, + sha256: parsed.data.sha256, + byteLength: parsed.data.byteLength + } +} + +/** Duplicate-free and within the count and total caps; the shape both peers hold a set to. */ +export function assertOrcadMigrationBlobSet( + descriptors: readonly OrcadMigrationBlobDescriptor[] +): void { + if (descriptors.length > MAX_ORCAD_MIGRATION_BLOBS) { + throw new Error('orcad_migration_blobs_too_many') + } + assertUniqueKeys([...descriptors], orcadMigrationBlobIdentity, 'orcad_migration_blob_duplicate') + const total = descriptors.reduce((sum, entry) => sum + entry.byteLength, 0) + if (total > MAX_ORCAD_MIGRATION_BLOB_TOTAL_BYTES) { + throw new Error('orcad_migration_blobs_too_large') + } +} + +export function orcadMigrationBlobIdentity( + descriptor: Pick +): string { + return `${descriptor.blobKind}\0${descriptor.key}` +} + +export function sameOrcadMigrationBlob( + left: OrcadMigrationBlobDescriptor, + right: OrcadMigrationBlobDescriptor +): boolean { + return ( + left.blobKind === right.blobKind && + left.key === right.key && + left.sha256 === right.sha256 && + left.byteLength === right.byteLength + ) +} + +/** The channel plus each kind's capability; empty when nothing is sent. */ +export function requiredMigrationBlobCapabilities( + descriptors: readonly Pick[] +): string[] { + if (descriptors.length === 0) { + return [] + } + const kinds = new Set(descriptors.map((entry) => entry.blobKind)) + return [ + ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY, + ...ORCAD_MIGRATION_BLOB_KINDS.filter((kind) => kinds.has(kind)).map( + (kind) => ORCAD_MIGRATION_BLOB_KIND_CONTRACTS[kind].capability + ) + ] +} + +/** The first kind the destination cannot install, so the refusal names what would be lost. */ +export function findUnsupportedMigrationBlobKind( + descriptors: readonly Pick[], + capabilities: readonly string[] +): OrcadMigrationBlobKind | null { + const advertised = new Set(capabilities) + for (const descriptor of descriptors) { + if ( + !advertised.has(ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY) || + !advertised.has(ORCAD_MIGRATION_BLOB_KIND_CONTRACTS[descriptor.blobKind].capability) + ) { + return descriptor.blobKind + } + } + return null +} + +export function orcadMigrationBlobKindUnsupportedBlocker(kind: OrcadMigrationBlobKind): string { + return `${ORCAD_MIGRATION_DESTINATION_BLOB_KIND_UNSUPPORTED}:${kind}:Update Orca on this host to move this project's ${kind} to it.` +} + +export function decodeOrcadMigrationBlobChunk(bytesBase64: string): Buffer { + return decodeOrcadMigrationChunk(bytesBase64, 'orcad_migration_blob_chunk_invalid') +} + +/** Absent on hosts that predate blobs or hold none staged; both mean every blob starts at 0. */ +export function parseOrcadMigrationBlobUploadStates( + value: unknown +): OrcadMigrationBlobUploadState[] | undefined { + if (value === undefined) { + return undefined + } + if (!Array.isArray(value) || value.length > MAX_ORCAD_MIGRATION_BLOBS) { + throw new Error('orcad_migration_blob_state_invalid') + } + const uploads = value.map((entry) => { + // Fails closed on an unknown kind by design: only blobs this desktop staged can appear here. + const record = requiredRecord(entry, 'orcad_migration_blob_state_invalid') + const descriptor = parseOrcadMigrationBlobDescriptor(record) + if ( + !Number.isSafeInteger(record.receivedBytes) || + Number(record.receivedBytes) < 0 || + Number(record.receivedBytes) > descriptor.byteLength + ) { + throw new Error('orcad_migration_blob_state_invalid') + } + return { ...descriptor, receivedBytes: Number(record.receivedBytes) } + }) + assertOrcadMigrationBlobSet(uploads) + return uploads +} + +export function parseOrcadMigrationBlobChunkResult( + value: unknown, + request: OrcadMigrationBlobChunkRequest +): OrcadMigrationBlobChunkResult { + const record = requiredRecord(value, 'orcad_migration_blob_chunk_result_invalid') + const receivedBytes = request.offset + decodeOrcadMigrationBlobChunk(request.bytesBase64).length + if ( + record.migrationId !== request.migrationId || + record.manifestSha256 !== request.manifestSha256 || + record.blobKind !== request.blob.blobKind || + record.key !== request.blob.key || + record.receivedBytes !== receivedBytes + ) { + throw new Error('orcad_migration_blob_chunk_result_invalid') + } + return { + migrationId: request.migrationId, + manifestSha256: request.manifestSha256, + blobKind: request.blob.blobKind, + key: request.blob.key, + receivedBytes + } +} + +/** Kinds this build's destination installs after commit; 6b/6c add theirs with their installers. */ +export const ORCAD_MIGRATION_INSTALLABLE_BLOB_KINDS: readonly OrcadMigrationBlobKind[] = [] + +export function advertisedOrcadMigrationBlobCapabilities(): string[] { + return [ + ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY, + ...ORCAD_MIGRATION_INSTALLABLE_BLOB_KINDS.map( + (kind) => ORCAD_MIGRATION_BLOB_KIND_CONTRACTS[kind].capability + ) + ] +} diff --git a/src/shared/orcad-migration-catalog-state.ts b/src/shared/orcad-migration-catalog-state.ts index 0141935acad..e678e8ed6e7 100644 --- a/src/shared/orcad-migration-catalog-state.ts +++ b/src/shared/orcad-migration-catalog-state.ts @@ -5,6 +5,7 @@ import { type OrcadMigrationManifest } from './orcad-migration-manifest' import { parseOrcadMigrationSnapshotUploadStates } from './orcad-migration-scrollback' +import { parseOrcadMigrationBlobUploadStates } from './orcad-migration-blob-transfer' import { isRecord } from './orcad-migration-manifest-fields' export function parseOrcadMigrationCatalogState( @@ -27,11 +28,13 @@ export function parseOrcadMigrationCatalogState( record.snapshotUploads, manifest.payload.dormantState?.terminalScrollbackSnapshots ?? [] ) + const blobUploads = parseOrcadMigrationBlobUploadStates(record.blobUploads) return { state: 'staged', ...migrationIdentity(manifest), stagedAt, - ...(snapshotUploads ? { snapshotUploads } : {}) + ...(snapshotUploads ? { snapshotUploads } : {}), + ...(blobUploads ? { blobUploads } : {}) } } if (record.state === 'committed') { diff --git a/src/shared/orcad-migration-manifest.ts b/src/shared/orcad-migration-manifest.ts index 3613b17200e..429f62ddec7 100644 --- a/src/shared/orcad-migration-manifest.ts +++ b/src/shared/orcad-migration-manifest.ts @@ -13,6 +13,7 @@ import type { OrcadMigrationSnapshotUploadState, OrcadMigrationTerminalScrollbackSnapshot } from './orcad-migration-scrollback' +import type { OrcadMigrationBlobUploadState } from './orcad-migration-blob-transfer' export { MAX_ORCAD_MIGRATION_FOLDER_WORKSPACES, @@ -152,6 +153,8 @@ export type OrcadMigrationCatalogState = manifestSha256: string stagedAt: string snapshotUploads?: OrcadMigrationSnapshotUploadState[] + /** Blobs staged so far; absent from hosts without the blob channel. */ + blobUploads?: OrcadMigrationBlobUploadState[] } | { state: 'committed' diff --git a/src/shared/orcad-migration-scrollback.ts b/src/shared/orcad-migration-scrollback.ts index b78d027aefa..3b799966ae4 100644 --- a/src/shared/orcad-migration-scrollback.ts +++ b/src/shared/orcad-migration-scrollback.ts @@ -12,10 +12,10 @@ export const MAX_ORCAD_MIGRATION_SCROLLBACK_SNAPSHOTS = 512 export const MAX_ORCAD_MIGRATION_SCROLLBACK_TOTAL_BYTES = 256 * 1024 * 1024 export const ORCAD_MIGRATION_SCROLLBACK_CHUNK_BYTES = 128 * 1024 -const Identity = z.string().min(1).max(256) -const Digest = z.string().regex(/^[a-f0-9]{64}$/) +export const OrcadMigrationIdentitySchema = z.string().min(1).max(256) +export const OrcadMigrationDigestSchema = z.string().regex(/^[a-f0-9]{64}$/) const SnapshotRef = z.string().regex(/^v1-[a-f0-9]{32}$/) -const CanonicalBase64Chunk = z +export const OrcadMigrationChunkBase64Schema = z .string() .max(Math.ceil(ORCAD_MIGRATION_SCROLLBACK_CHUNK_BYTES / 3) * 4) .refine((value) => { @@ -36,11 +36,11 @@ export type OrcadMigrationTerminalScrollbackSnapshot = { // Not strict: an older host must ignore optional fields a newer client adds. export const OrcadMigrationSnapshotChunkRequestSchema = z.object({ - migrationId: Identity, - manifestSha256: Digest, + migrationId: OrcadMigrationIdentitySchema, + manifestSha256: OrcadMigrationDigestSchema, ref: SnapshotRef, offset: z.number().int().nonnegative().max(MAX_ORCAD_MIGRATION_SCROLLBACK_TOTAL_BYTES), - bytesBase64: CanonicalBase64Chunk + bytesBase64: OrcadMigrationChunkBase64Schema }) export type OrcadMigrationSnapshotChunkRequest = z.infer< @@ -151,13 +151,18 @@ export function assertOrcadMigrationScrollbackReferences( } export function decodeOrcadMigrationSnapshotChunk(bytesBase64: string): Buffer { + return decodeOrcadMigrationChunk(bytesBase64, 'orcad_migration_snapshot_chunk_invalid') +} + +/** One canonical, non-empty chunk of at most the shared transfer size; `error` names the channel. */ +export function decodeOrcadMigrationChunk(bytesBase64: string, error: string): Buffer { const bytes = Buffer.from(bytesBase64, 'base64') if ( bytes.length === 0 || bytes.length > ORCAD_MIGRATION_SCROLLBACK_CHUNK_BYTES || bytes.toString('base64') !== bytesBase64 ) { - throw new Error('orcad_migration_snapshot_chunk_invalid') + throw new Error(error) } return bytes } @@ -166,7 +171,10 @@ function parseSnapshot(value: unknown): OrcadMigrationTerminalScrollbackSnapshot const record = requiredRecord(value, 'orcad_migration_dormant_scrollback_snapshot_invalid') const ref = requiredString(record.ref, 'orcad_migration_dormant_scrollback_ref_invalid') const sha256 = requiredString(record.sha256, 'orcad_migration_dormant_scrollback_digest_invalid') - if (!SnapshotRef.safeParse(ref).success || !Digest.safeParse(sha256).success) { + if ( + !SnapshotRef.safeParse(ref).success || + !OrcadMigrationDigestSchema.safeParse(sha256).success + ) { throw new Error('orcad_migration_dormant_scrollback_identity_invalid') } if ( diff --git a/src/shared/orcad-runtime-capabilities.ts b/src/shared/orcad-runtime-capabilities.ts index b10c88751a3..888c1bfb60d 100644 --- a/src/shared/orcad-runtime-capabilities.ts +++ b/src/shared/orcad-runtime-capabilities.ts @@ -12,6 +12,10 @@ export const ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY = 'orcad.migration-catal export const ORCAD_MIGRATION_LOCAL_WSL_SOURCE_RUNTIME_CAPABILITY = 'orcad.migration-source.local-wsl.v1' as const +// Why: older hosts have no orcad.migration.stageBlobChunk. The channel alone carries nothing: each +// blob kind also needs its own `orcad.migration-blob..v1`, advertised once installed. +export const ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY = 'orcad.migration-blob.v1' as const + // Why: older hosts have no `accounts.setHostLaunchEnv`; a desktop that owns a host's accounts (a // WSL host's) pushes them only to a host advertising this. export const HOST_ACCOUNT_LAUNCH_ENV_RUNTIME_CAPABILITY = 'accounts.host-launch-env.v1' as const diff --git a/src/shared/rpc-contract/orcad-migration-params.ts b/src/shared/rpc-contract/orcad-migration-params.ts index 4a9b6087aba..81daa1b184a 100644 --- a/src/shared/rpc-contract/orcad-migration-params.ts +++ b/src/shared/rpc-contract/orcad-migration-params.ts @@ -1,5 +1,17 @@ import { z } from 'zod' +import { + MAX_ORCAD_MIGRATION_BLOBS, + OrcadMigrationBlobDescriptorSchema +} from '../orcad-migration-blob-transfer' // Why unknown: the handler parses and digest-checks the manifest itself, so a malformed one is // refused with a migration error rather than a generic params failure. export const OrcadMigrationCatalogParams = z.object({ manifest: z.unknown() }) + +// Optional: sent only to a blob-channel destination, so SSH and older peers see `{ manifest }`. +export const OrcadMigrationCommitCatalogParams = OrcadMigrationCatalogParams.extend({ + expectedBlobs: z + .array(OrcadMigrationBlobDescriptorSchema) + .max(MAX_ORCAD_MIGRATION_BLOBS) + .optional() +}) diff --git a/src/shared/rpc-contract/rpc-params-catalog.generated.ts b/src/shared/rpc-contract/rpc-params-catalog.generated.ts index 1e417f291ab..f19fb603545 100644 --- a/src/shared/rpc-contract/rpc-params-catalog.generated.ts +++ b/src/shared/rpc-contract/rpc-params-catalog.generated.ts @@ -27,6 +27,7 @@ import { } from '../mobile-relay-credential-contract' import { MobileRelayHostWorktreesParamsSchema } from '../mobile-relay-hosts-contract' import { MobileWebBundleChunkParamsSchema } from '../mobile-web-bundle/bundle-rpc-contract' +import { OrcadMigrationBlobChunkRequestSchema } from '../orcad-migration-blob-transfer' import { OrcadMigrationSnapshotChunkRequestSchema } from '../orcad-migration-scrollback' import { OrcadTerminalCensusParamsSchema } from '../orcad-terminal-census' import { pluginConsentRequestSchema } from '../plugins/plugin-consent-request' @@ -386,7 +387,10 @@ import { NotificationUnsubscribeParams, NotificationsSubscribeParams } from './notifications-params' -import { OrcadMigrationCatalogParams } from './orcad-migration-params' +import { + OrcadMigrationCatalogParams, + OrcadMigrationCommitCatalogParams +} from './orcad-migration-params' import { FederationDispatchParams, FederationFleetSnapshotParams, @@ -1081,7 +1085,8 @@ export const RPC_PARAMS_BY_METHOD = { 'notifications.unsubscribe': NotificationUnsubscribeParams, 'orcad.migration.abortCatalog': OrcadMigrationCatalogParams, 'orcad.migration.catalogState': OrcadMigrationCatalogParams, - 'orcad.migration.commitCatalog': OrcadMigrationCatalogParams, + 'orcad.migration.commitCatalog': OrcadMigrationCommitCatalogParams, + 'orcad.migration.stageBlobChunk': OrcadMigrationBlobChunkRequestSchema, 'orcad.migration.stageCatalog': OrcadMigrationCatalogParams, 'orcad.migration.stageSnapshotChunk': OrcadMigrationSnapshotChunkRequestSchema, 'orcad.terminalCensus': OrcadTerminalCensusParamsSchema, From 232f6048e77bcf0d2955ba77ce80f481283724dc Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:29:28 -0700 Subject: [PATCH 21/53] Remove obsolete Hermes cron documentation and translations (#28877) --- config/scripts/locale-ja-value-overrides.mjs | 2 -- config/scripts/locale-ko-value-overrides.mjs | 3 +-- docs/site/content/docs/cli/automations.mdx | 2 +- src/renderer/src/i18n/en-runtime-required.json | 8 +------- src/renderer/src/i18n/locales/en.json | 6 ------ src/renderer/src/i18n/locales/es.json | 6 ------ src/renderer/src/i18n/locales/fr.json | 6 ------ src/renderer/src/i18n/locales/ja.json | 6 ------ src/renderer/src/i18n/locales/ko.json | 6 ------ src/renderer/src/i18n/locales/zh.json | 6 ------ 10 files changed, 3 insertions(+), 48 deletions(-) diff --git a/config/scripts/locale-ja-value-overrides.mjs b/config/scripts/locale-ja-value-overrides.mjs index 1d220ba7c0f..57612fb249b 100644 --- a/config/scripts/locale-ja-value-overrides.mjs +++ b/config/scripts/locale-ja-value-overrides.mjs @@ -433,8 +433,6 @@ const JA_AUDIT_OVERRIDES = { 'ステージ済みの変更からコミットメッセージを生成します。', lan: 'LAN', deploy: 'deploy', - 'Connect this source to check for Hermes automations in the remote profile.': - 'このソースに接続すると、リモートプロファイル内の Hermes オートメーションを確認できます。', 'Notes sent to active agent.': 'メモをアクティブな Agent に送信しました。', 'Review before attaching. Captured page context may include visible site content.': '添付する前に確認してください。取得したページのコンテキストには、表示中のサイトの内容が含まれる場合があります。', diff --git a/config/scripts/locale-ko-value-overrides.mjs b/config/scripts/locale-ko-value-overrides.mjs index bcc5b6e9e8e..5a819b90e23 100644 --- a/config/scripts/locale-ko-value-overrides.mjs +++ b/config/scripts/locale-ko-value-overrides.mjs @@ -293,6 +293,5 @@ export const KO_VALUE_OVERRIDES = { Server: '서버', Web: '웹', Code: '코드', - Folder: '폴더', - 'Hermes automation created.': 'Hermes 자동화가 생성되었습니다.' + Folder: '폴더' } diff --git a/docs/site/content/docs/cli/automations.mdx b/docs/site/content/docs/cli/automations.mdx index 34f14fff180..5d4351cb020 100644 --- a/docs/site/content/docs/cli/automations.mdx +++ b/docs/site/content/docs/cli/automations.mdx @@ -131,7 +131,7 @@ orca automations show --json orca automations edit --enabled --json ``` -In the desktop automations list, use the search field to filter by **name**, **project**, or **prompt** text when you have many schedules. External automations that live on an SSH host still appear as jobs you can manage (including delete) when that host is disconnected — you do not need an active SSH session just to remove a schedule. +In the desktop automations list, use the search field to filter by **name**, **project**, or **prompt** text when you have many schedules. Use **Filters** to narrow the list by **Host**, **Enabled** or **Paused** state, last-run outcome (**Failed**, **Succeeded**, or **Never ran**), or one or more **Agents**. Search inside the agent submenu when the list is long; active filters appear as removable pills above the table. The table shows the host, last-run result, and relative time for each automation. Click the **Name** or **Last run** column to sort; name sorts alphabetically, while last run defaults to newest first. diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 66c44e67ace..742c92d1d2a 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -252,7 +252,6 @@ "AutomationsPage": { "2695883141": "delete", "0329f9bef1": "Close · Esc", - "0ae52dd760": "hermes", "3e42a5cc1b": "SSH connection failed.", "53f06f0ad5": "Retry source", "587a4b205c": "Next", @@ -264,7 +263,6 @@ "82eb6cb933": "source", "8705757e27": "job", "8d1afa8269": "Add automation", - "97ff587ee3": "Connect this source to check for Hermes automations in the remote profile.", "9f2855677c": "SSH connected.", "a21f6c33ad": "Automation source refreshed.", "aaa007846f": "source unavailable", @@ -288,11 +286,7 @@ "tableProject": "Project" }, "ExternalAutomationManagers": { - "330b3c32e8": "available", - "bf5f67b590": "hermes" - }, - "HermesCronOutputView": { - "88d48157fc": "default" + "330b3c32e8": "available" }, "createDestination": { "stale": "{host} changed while this form was open. Choose it again before saving.", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 4e0a6abf340..2543dade923 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -16869,7 +16869,6 @@ "295698292f": "Rerun", "0e110a3469": "Runs", "bb1b2cd31e": "Overview", - "97ff587ee3": "Connect this source to check for Hermes automations in the remote profile.", "aaa007846f": "source unavailable", "25060635c6": "Add new", "d207ab4c25": "Start from a template", @@ -16921,7 +16920,6 @@ "8705757e27": "job", "376631ef2b": "Resume", "b457436d6a": "Pause", - "0ae52dd760": "hermes", "e059042585": "Read-only", "aecdc3681f": "Manageable", "8500baacb4": "external source", @@ -16984,7 +16982,6 @@ "20fd7a3a15": "next", "c6695e6fbd": "External automations", "5524365227": "OpenClaw", - "bf5f67b590": "hermes", "2b0adbce21": "Paused", "b3feba84c7": "Active", "92405f1431": "Unavailable", @@ -16994,9 +16991,6 @@ "e2532150ed": "automations", "701515f010": "automation" }, - "HermesCronOutputView": { - "88d48157fc": "default" - }, "WorkspaceCombobox": { "ee5b280eba": "No workspaces found.", "8e9c8cc6b5": "Search workspaces...", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index d2c21019775..74e4cd79330 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -16637,7 +16637,6 @@ "295698292f": "Repetición", "0e110a3469": "Corre", "bb1b2cd31e": "Descripción general", - "97ff587ee3": "Conecte esta fuente para comprobar si hay automatizaciones Hermes en el perfil remoto.", "aaa007846f": "fuente no disponible", "25060635c6": "Agregar nuevo", "d207ab4c25": "Empezar desde una plantilla", @@ -16689,7 +16688,6 @@ "8705757e27": "trabajo", "376631ef2b": "Reanudar", "b457436d6a": "Pausa", - "0ae52dd760": "Hermes", "e059042585": "Sólo lectura", "aecdc3681f": "Manejable", "8500baacb4": "fuente externa", @@ -16749,7 +16747,6 @@ "20fd7a3a15": "próximo", "c6695e6fbd": "Automatizaciones externas", "5524365227": "OpenClaw", - "bf5f67b590": "Hermes", "2b0adbce21": "En pausa", "b3feba84c7": "Activo", "92405f1431": "Indisponible", @@ -16759,9 +16756,6 @@ "e2532150ed": "automatizaciones", "701515f010": "automatización" }, - "HermesCronOutputView": { - "88d48157fc": "por defecto" - }, "WorkspaceCombobox": { "ee5b280eba": "No se encontraron espacios de trabajo.", "8e9c8cc6b5": "Buscar espacios de trabajo...", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 136ab58d224..97e5b6b5048 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -16719,7 +16719,6 @@ "295698292f": "Relancer", "0e110a3469": "Exécutions", "bb1b2cd31e": "Vue d'ensemble", - "97ff587ee3": "Connectez cette source pour détecter les automatisations Hermes dans le profil distant.", "aaa007846f": "source indisponible", "25060635c6": "Ajouter", "d207ab4c25": "Partir d'un modèle", @@ -16768,7 +16767,6 @@ "8705757e27": "tâche", "376631ef2b": "Reprendre", "b457436d6a": "Pause", - "0ae52dd760": "hermes", "e059042585": "Lecture seule", "aecdc3681f": "Gérable", "8500baacb4": "source externe", @@ -16834,7 +16832,6 @@ "20fd7a3a15": "suivant", "c6695e6fbd": "Automatisations externes", "5524365227": "OpenClaw", - "bf5f67b590": "hermes", "2b0adbce21": "En pause", "b3feba84c7": "Actif", "92405f1431": "Indisponible", @@ -16844,9 +16841,6 @@ "e2532150ed": "automatisations", "701515f010": "automatisation" }, - "HermesCronOutputView": { - "88d48157fc": "default" - }, "WorkspaceCombobox": { "ee5b280eba": "Aucun espace de travail trouvé.", "8e9c8cc6b5": "Rechercher des espaces de travail...", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 547aa7b3f8b..388df378b4c 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -16637,7 +16637,6 @@ "295698292f": "再実行", "0e110a3469": "実行履歴", "bb1b2cd31e": "概要", - "97ff587ee3": "このソースに接続すると、リモートプロファイル内の Hermes オートメーションを確認できます。", "aaa007846f": "ソースが利用できない", "25060635c6": "新規追加", "d207ab4c25": "テンプレートから始める", @@ -16689,7 +16688,6 @@ "8705757e27": "仕事", "376631ef2b": "再開", "b457436d6a": "一時停止", - "0ae52dd760": "Hermes", "e059042585": "読み取り専用", "aecdc3681f": "管理可能", "8500baacb4": "外部ソース", @@ -16749,7 +16747,6 @@ "20fd7a3a15": "次", "c6695e6fbd": "外部自動化", "5524365227": "OpenClaw", - "bf5f67b590": "Hermes", "2b0adbce21": "一時停止中", "b3feba84c7": "アクティブ", "92405f1431": "利用不可", @@ -16759,9 +16756,6 @@ "e2532150ed": "自動化", "701515f010": "オートメーション" }, - "HermesCronOutputView": { - "88d48157fc": "デフォルト" - }, "WorkspaceCombobox": { "ee5b280eba": "ワークスペースが見つかりません。", "8e9c8cc6b5": "ワークスペースを検索…", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 512e8cc5ae7..bc3184663f5 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -16637,7 +16637,6 @@ "295698292f": "재실행", "0e110a3469": "실행", "bb1b2cd31e": "개요", - "97ff587ee3": "원격 프로필에서 Hermes 자동화를 확인하려면 이 소스를 연결하세요.", "aaa007846f": "소스를 사용할 수 없음", "25060635c6": "새로 추가", "d207ab4c25": "템플릿에서 시작", @@ -16689,7 +16688,6 @@ "8705757e27": "작업", "376631ef2b": "재개", "b457436d6a": "일시 중지", - "0ae52dd760": "Hermes", "e059042585": "읽기 전용", "aecdc3681f": "관리 가능", "8500baacb4": "외부 소스", @@ -16749,7 +16747,6 @@ "20fd7a3a15": "다음", "c6695e6fbd": "외부 자동화", "5524365227": "OpenClaw", - "bf5f67b590": "Hermes", "2b0adbce21": "일시중지됨", "b3feba84c7": "활성", "92405f1431": "사용 불가", @@ -16759,9 +16756,6 @@ "e2532150ed": "자동화", "701515f010": "자동화" }, - "HermesCronOutputView": { - "88d48157fc": "기본" - }, "WorkspaceCombobox": { "ee5b280eba": "워크스페이스를 찾을 수 없습니다.", "8e9c8cc6b5": "워크스페이스 검색...", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 566ad89480e..2822f090949 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -16637,7 +16637,6 @@ "295698292f": "重新运行", "0e110a3469": "执行", "bb1b2cd31e": "概述", - "97ff587ee3": "连接此源以检查远程配置文件中的 Hermes 自动化。", "aaa007846f": "来源不可用", "25060635c6": "添加新的", "d207ab4c25": "从模板开始", @@ -16689,7 +16688,6 @@ "8705757e27": "工作", "376631ef2b": "恢复", "b457436d6a": "暂停", - "0ae52dd760": "Hermes", "e059042585": "只读", "aecdc3681f": "易于管理", "8500baacb4": "外部来源", @@ -16749,7 +16747,6 @@ "20fd7a3a15": "下一步", "c6695e6fbd": "外部自动化", "5524365227": "OpenClaw", - "bf5f67b590": "Hermes", "2b0adbce21": "已暂停", "b3feba84c7": "当前", "92405f1431": "不可用", @@ -16759,9 +16756,6 @@ "e2532150ed": "自动化", "701515f010": "自动化" }, - "HermesCronOutputView": { - "88d48157fc": "默认" - }, "WorkspaceCombobox": { "ee5b280eba": "未找到工作区。", "8e9c8cc6b5": "搜索工作区...", From 0b1a6007e08cc393b09ca29b791873b09290d31e Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:53:05 -0700 Subject: [PATCH 22/53] feat(wsl-mover): a moved pane reopens in its last folder on the WSL host (D1b-6b) (#28885) * feat(wsl-mover): a moved pane reopens in its last folder on the WSL host (D1b-6b) * fix(wsl-mover): consume a restore hint only when its pane spawns, scope it to its worktree, retry failed installs (D1b-6b) --- ...d-migration-terminal-restore-hints.test.ts | 282 ++++++++++++++++++ .../project-collection-operations.ts | 20 +- .../repo-lifecycle-operations.ts | 12 + .../loading-store/store-domain-composition.ts | 7 +- .../loading-store/store-runtime-state.ts | 4 + src/main/persistence/loading-store/store.ts | 2 + .../orcad-catalog-import.ts | 2 + ...rcad-migration-blob-install-persistence.ts | 120 ++++++++ .../orcad-migration-blob-installers.ts | 137 +++++++++ .../orcad-migration-blob-staging.ts | 30 +- .../orcad-terminal-restore-hint-store.ts | 203 +++++++++++++ .../wsl-move-restore-hints-blob.test.ts | 53 ++++ .../wsl-move-restore-hints-blob.ts | 57 ++++ src/main/runtime/migrated-pane-restore-cwd.ts | 90 ++++++ .../runtime/orca-runtime-create-terminal.ts | 37 ++- ...-runtime-migrated-pane-restore-cwd.test.ts | 255 ++++++++++++++++ .../orca-runtime-report-pty-spawn-commit.ts | 4 +- .../runtime/orcad-migration-catalog-import.ts | 31 +- src/main/runtime/runtime-store-contract.ts | 3 + .../orcad-migration-blob-transfer.test.ts | 5 +- src/shared/orcad-migration-blob-transfer.ts | 6 +- ...d-migration-terminal-restore-hints.test.ts | 74 +++++ .../orcad-migration-terminal-restore-hints.ts | 101 +++++++ 23 files changed, 1500 insertions(+), 35 deletions(-) create mode 100644 src/main/persistence-orcad-migration-terminal-restore-hints.test.ts create mode 100644 src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-install-persistence.ts create mode 100644 src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-installers.ts create mode 100644 src/main/persistence/migrating-orcad-catalog/orcad-terminal-restore-hint-store.ts create mode 100644 src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.test.ts create mode 100644 src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.ts create mode 100644 src/main/runtime/migrated-pane-restore-cwd.ts create mode 100644 src/main/runtime/orca-runtime-migrated-pane-restore-cwd.test.ts create mode 100644 src/shared/orcad-migration-terminal-restore-hints.test.ts create mode 100644 src/shared/orcad-migration-terminal-restore-hints.ts diff --git a/src/main/persistence-orcad-migration-terminal-restore-hints.test.ts b/src/main/persistence-orcad-migration-terminal-restore-hints.test.ts new file mode 100644 index 00000000000..a4e6a8795b9 --- /dev/null +++ b/src/main/persistence-orcad-migration-terminal-restore-hints.test.ts @@ -0,0 +1,282 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createHash } from 'node:crypto' +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import type { OrcadMigrationBlobDescriptor } from '../shared/orcad-migration-blob-transfer' +import type { OrcadMigrationManifest } from '../shared/orcad-migration-manifest' +import { getOrcadMigrationReceivedBlobPath } from './persistence/migrating-orcad-catalog/orcad-migration-blob-staging' +import { buildWslMoveRestoreHintsBlob } from './persistence/moving-wsl-projects/wsl-move-restore-hints-blob' +import { + commitStagedOrcadMigrationCatalogDurably, + getOrcadMigrationCatalogState +} from './runtime/orcad-migration-catalog-import' +import { folderWorkspaceKey } from '../shared/workspace-scope' +import { + DORMANT_LEAF_ID, + DORMANT_WORKTREE_ID, + FOLDER_WORKSPACE, + REPOSITORY, + dormantState, + manifest as baseManifest +} from './persistence-orcad-migration-catalog-fixture' +import { createStore, testState } from './persistence-test-harness' + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { + isEncryptionAvailable: () => true, + encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'), + decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length) + } +})) + +vi.mock('./telemetry/client', () => ({ track: vi.fn() })) + +const FOLDER_LEAF_ID = '22222222-2222-4222-8222-222222222222' +const PANE = `tab-dormant:${DORMANT_LEAF_ID}` +const OTHER_PANE = `tab-folder:${FOLDER_LEAF_ID}` +const ABSENT_PANE = 'tab-absent:33333333-3333-4333-8333-333333333333' +const FOLDER_KEY = folderWorkspaceKey(FOLDER_WORKSPACE.id) + +type Store = ReturnType + +// The moved session owns PANE in the dormant worktree and OTHER_PANE in the folder workspace. +function manifest(): OrcadMigrationManifest { + const dormant = dormantState() + const session = dormant.workspaceSession! + dormant.workspaceSession = { + ...session, + tabsByWorktree: { + ...session.tabsByWorktree, + [FOLDER_KEY]: [ + { + id: 'tab-folder', + ptyId: null, + worktreeId: FOLDER_KEY, + title: 'Folder terminal', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 8 + } + ] + }, + terminalLayoutsByTabId: { + ...session.terminalLayoutsByTabId, + 'tab-folder': { + root: { type: 'leaf', leafId: FOLDER_LEAF_ID }, + activeLeafId: FOLDER_LEAF_ID, + expandedLeafId: null + } + } + } + const base = baseManifest() + return baseManifest({ payload: { ...base.payload, dormantState: dormant } }) +} + +function peek(store: Store, paneKey: string, worktreeId: string): string | null { + return store.peekOrcadMigrationTerminalRestoreHint(paneKey, worktreeId) +} + +function encodedHints(): { + descriptor: OrcadMigrationBlobDescriptor + bytes: Buffer +} { + const blob = buildWslMoveRestoreHintsBlob([ + { paneKey: PANE, cwd: '/home/me/src' }, + { paneKey: OTHER_PANE, cwd: '/srv/app' }, + { paneKey: ABSENT_PANE, cwd: '/srv/gone' } + ]) + if (!blob) { + throw new Error('expected a hints blob') + } + return blob +} + +function rawBlob(bytes: Buffer): { + descriptor: OrcadMigrationBlobDescriptor + bytes: Buffer +} { + return { + bytes, + descriptor: { + blobKind: 'terminal-restore-hints', + key: 'migration', + sha256: createHash('sha256').update(bytes).digest('hex'), + byteLength: bytes.length + } + } +} + +function stageBlob( + store: Store, + input: OrcadMigrationManifest, + blob: { descriptor: OrcadMigrationBlobDescriptor; bytes: Buffer } +): void { + store.stageOrcadMigrationCatalog(input) + store.stageOrcadMigrationBlobChunk({ + migrationId: input.migrationId, + manifestSha256: input.manifestSha256, + blob: blob.descriptor, + offset: 0, + bytesBase64: blob.bytes.toString('base64') + }) +} + +function receivedPath(input: OrcadMigrationManifest): string { + return getOrcadMigrationReceivedBlobPath(join(testState.dir, 'orcad-migration-blobs'), input, { + blobKind: 'terminal-restore-hints', + key: 'migration' + }) +} + +async function commitDurably(store: Store, input: OrcadMigrationManifest): Promise { + const result = await commitStagedOrcadMigrationCatalogDurably({ + store, + manifest: input, + onDurableCommit: () => {} + }) + return result.state +} + +describe('orcad migration terminal restore hints through the store', () => { + beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'orca-migration-restore-hints-')) + }) + + afterEach(() => { + rmSync(testState.dir, { recursive: true, force: true }) + }) + + it('installs hints scoped to their worktree after the durable commit, and survives a restart', async () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, encodedHints()) + + expect(await commitDurably(store, input)).toBe('committed') + expect(existsSync(receivedPath(input))).toBe(false) + + const restarted = createStore() + expect(peek(restarted, PANE, DORMANT_WORKTREE_ID)).toBe('/home/me/src') + expect(peek(restarted, PANE, FOLDER_KEY)).toBeNull() + expect(peek(restarted, OTHER_PANE, FOLDER_KEY)).toBe('/srv/app') + // A pane the moved session does not hold has no worktree to scope it to. + expect(peek(restarted, ABSENT_PANE, DORMANT_WORKTREE_ID)).toBeNull() + restarted.consumeOrcadMigrationTerminalRestoreHint(PANE) + expect(peek(createStore(), PANE, DORMANT_WORKTREE_ID)).toBeNull() + expect(peek(createStore(), OTHER_PANE, FOLDER_KEY)).toBe('/srv/app') + }) + + it('is idempotent across commit retries and never revives a consumed hint', async () => { + const input = manifest() + const store = createStore() + const blob = encodedHints() + stageBlob(store, input, blob) + store.commitStagedOrcadMigrationCatalog(input) + await store.flushPendingOrThrowAsync() + // A crash after install but before the blob was deleted leaves it in place. + store.installOrcadMigrationReceivedBlobs(input) + expect(peek(store, PANE, DORMANT_WORKTREE_ID)).toBe('/home/me/src') + store.consumeOrcadMigrationTerminalRestoreHint(PANE) + writeFileSync(receivedPath(input), blob.bytes) + + expect(await commitDurably(createStore(), input)).toBe('committed') + expect(existsSync(receivedPath(input))).toBe(false) + const restarted = createStore() + expect(peek(restarted, PANE, DORMANT_WORKTREE_ID)).toBeNull() + expect(peek(restarted, OTHER_PANE, FOLDER_KEY)).toBe('/srv/app') + }) + + it('retries a failed install on the next store load', async () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, encodedHints()) + const installed = join(testState.dir, 'orcad-migration-blobs', 'installed') + // A file where the hint store's directory belongs makes the install's write fail. + writeFileSync(installed, '') + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + + expect(await commitDurably(store, input)).toBe('committed') + expect(error).toHaveBeenCalledWith( + '[orcad-migration] installing received blobs failed:', + expect.any(Error) + ) + expect(existsSync(receivedPath(input))).toBe(true) + error.mockRestore() + + rmSync(installed) + mkdirSync(installed) + const restarted = createStore() + expect(existsSync(receivedPath(input))).toBe(false) + expect(peek(restarted, PANE, DORMANT_WORKTREE_ID)).toBe('/home/me/src') + }) + + it('retries a failed install when the committed catalog state is queried', async () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, encodedHints()) + const installed = join(testState.dir, 'orcad-migration-blobs', 'installed') + writeFileSync(installed, '') + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + expect(await commitDurably(store, input)).toBe('committed') + error.mockRestore() + + rmSync(installed) + expect(getOrcadMigrationCatalogState({ store, manifest: input }).state).toBe('committed') + expect(existsSync(receivedPath(input))).toBe(false) + expect(peek(store, PANE, DORMANT_WORKTREE_ID)).toBe('/home/me/src') + }) + + it("drops a removed project's hints and keeps the others", async () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, encodedHints()) + expect(await commitDurably(store, input)).toBe('committed') + + store.removeProjectForHost(REPOSITORY.id, 'local') + expect(peek(store, PANE, DORMANT_WORKTREE_ID)).toBeNull() + expect(peek(store, OTHER_PANE, FOLDER_KEY)).toBe('/srv/app') + + expect(store.removeFolderWorkspace(FOLDER_WORKSPACE.id)).toBe(true) + expect(peek(createStore(), OTHER_PANE, FOLDER_KEY)).toBeNull() + }) + + it('installs nothing for a catalog that is only staged', () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, encodedHints()) + store.installOrcadMigrationReceivedBlobs(input) + expect(peek(store, PANE, DORMANT_WORKTREE_ID)).toBeNull() + }) + + it('refuses to commit a digest-exact blob that is not a hints document', () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, rawBlob(Buffer.from('{"version":1,"hints":[{"paneKey":"x"}]}'))) + + expect(() => store.commitStagedOrcadMigrationCatalog(input)).toThrow( + /^orcad_migration_blob_content_invalid:terminal-restore-hints:/ + ) + expect(store.getOrcadMigrationCatalogState(input).state).toBe('staged') + }) + + it('drops a received file damaged after commit without failing the committed catalog', async () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, encodedHints()) + store.commitStagedOrcadMigrationCatalog(input) + await store.flushPendingOrThrowAsync() + writeFileSync(receivedPath(input), 'not json') + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + + expect(await commitDurably(createStore(), input)).toBe('committed') + expect(error).toHaveBeenCalledWith( + expect.stringContaining('dropped unreadable terminal restore hints'), + expect.any(Error) + ) + expect(existsSync(receivedPath(input))).toBe(false) + expect(peek(createStore(), PANE, DORMANT_WORKTREE_ID)).toBeNull() + error.mockRestore() + }) +}) diff --git a/src/main/persistence/loading-store/project-collection-operations.ts b/src/main/persistence/loading-store/project-collection-operations.ts index 08d6498a9c4..1ae4ec2d15e 100644 --- a/src/main/persistence/loading-store/project-collection-operations.ts +++ b/src/main/persistence/loading-store/project-collection-operations.ts @@ -29,6 +29,8 @@ import { import { scheduleSave } from './write-scheduling' import { removeWorkspaceLineageForFolderParent } from './metadata-lineage-operations' import { bumpLocalWorktreeScanGeneration } from '../../local-worktree-scan-generation' +import { dropOrcadTerminalRestoreHintsForWorktrees } from '../migrating-orcad-catalog/orcad-terminal-restore-hint-store' +import { folderWorkspaceKey } from '../../../shared/workspace-scope' type ProjectCollectionOperationsRuntime = Pick< StoreRuntimeState, @@ -37,6 +39,7 @@ type ProjectCollectionOperationsRuntime = Pick< | 'projectGroupOperations' | 'projectHostOperations' | 'state' + | 'orcadTerminalRestoreHints' > const projectCollectionOperationsContext = Symbol('ProjectCollectionOperations') @@ -56,7 +59,12 @@ export class ProjectCollectionOperations { scheduling: WriteSchedulingOperations, metadata: MetadataLineageOperations ) { - this[projectCollectionOperationsContext] = { runtime, repos, scheduling, metadata } + this[projectCollectionOperationsContext] = { + runtime, + repos, + scheduling, + metadata + } } getRepos(): Repo[] { @@ -145,7 +153,15 @@ export class ProjectCollectionOperations { } removeFolderWorkspace(id: string): boolean { - return getFolderWorkspaceOperations(this).removeFolderWorkspace(id) + const removed = getFolderWorkspaceOperations(this).removeFolderWorkspace(id) + if (removed) { + const workspaceKey = folderWorkspaceKey(id) + dropOrcadTerminalRestoreHintsForWorktrees( + this[projectCollectionOperationsContext].runtime.orcadTerminalRestoreHints, + (worktreeId) => worktreeId === workspaceKey + ) + } + return removed } moveProjectToGroup(repoId: string, groupId: string | null, order?: number): Repo | null { diff --git a/src/main/persistence/loading-store/repo-lifecycle-operations.ts b/src/main/persistence/loading-store/repo-lifecycle-operations.ts index 47a18973e47..67f28b36b13 100644 --- a/src/main/persistence/loading-store/repo-lifecycle-operations.ts +++ b/src/main/persistence/loading-store/repo-lifecycle-operations.ts @@ -29,6 +29,8 @@ import type { WriteSchedulingOperations } from './write-scheduling' import { scheduleSave } from './write-scheduling' import { pruneDeregisteredRepoUiResidue } from './repo-lifecycle-ui-residue' import { isWslMoveProjectSessionFrozen } from '../moving-wsl-projects/wsl-move-scope-guard' +import { dropOrcadTerminalRestoreHintsForWorktrees } from '../migrating-orcad-catalog/orcad-terminal-restore-hint-store' +import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' // A paused WSL move keeps its local rows for a downgrade. function removeLocalRepoSession( @@ -46,6 +48,7 @@ type RepoLifecycleOperationsRuntime = Pick< | 'repoOrderOperations' | 'repoUpdateOperations' | 'state' + | 'orcadTerminalRestoreHints' > const repoLifecycleOperationsContext = Symbol('RepoLifecycleOperations') @@ -89,6 +92,7 @@ export class RepoLifecycleOperations { ) if (!idStillPresent) { retireLocalWorktreeScanGeneration(id) + dropRepoRestoreHints(this, id) delete this[repoLifecycleOperationsContext].runtime.state.sparsePresetsByRepo[id] delete this[repoLifecycleOperationsContext].runtime.state.retiredWorktreeNamesByRepo?.[id] } @@ -131,6 +135,7 @@ export class RepoLifecycleOperations { } for (const repoId of orphanRepoIds) { retireLocalWorktreeScanGeneration(repoId) + dropRepoRestoreHints(this, repoId) pruneWorktreeStateForRepo(this, repoId, null) state.workspaceSession = removeLocalRepoSession(state.workspaceSession, repoId) state.workspaceSessionsByHostId = removeRepoFromHostWorkspaceSessions( @@ -184,6 +189,13 @@ export class RepoLifecycleOperations { } } +function dropRepoRestoreHints(owner: RepoLifecycleOperations, repoId: string): void { + dropOrcadTerminalRestoreHintsForWorktrees( + owner[repoLifecycleOperationsContext].runtime.orcadTerminalRestoreHints, + (worktreeId) => getRepoIdFromWorktreeId(worktreeId) === repoId + ) +} + export function getRepoOrderOperations( owner: RepoLifecycleOperations ): RepoOrderPersistenceOperations { diff --git a/src/main/persistence/loading-store/store-domain-composition.ts b/src/main/persistence/loading-store/store-domain-composition.ts index 5dac5df23c7..fbb1bc36dce 100644 --- a/src/main/persistence/loading-store/store-domain-composition.ts +++ b/src/main/persistence/loading-store/store-domain-composition.ts @@ -24,6 +24,7 @@ import { RetiredWorktreeNamePersistence } from './retired-worktree-name-persiste import { SshLeaseRecoveryOperations } from './ssh-lease-recovery-operations' import { OrcadSourceExportPersistence } from '../migrating-orcad-catalog/orcad-source-export' import { OrcadCatalogImportPersistence } from '../migrating-orcad-catalog/orcad-catalog-import' +import { OrcadMigrationBlobInstallPersistence } from '../migrating-orcad-catalog/orcad-migration-blob-install-persistence' export type StoreDomainOperations = WriteSchedulingOperations & PrimaryStateWriteOperations & @@ -43,6 +44,7 @@ export type StoreDomainOperations = WriteSchedulingOperations & SshLeaseRecoveryOperations & OrcadSourceExportPersistence & OrcadCatalogImportPersistence & + OrcadMigrationBlobInstallPersistence & WriteFlushBarrierOperations export type StoreDomains = { @@ -70,6 +72,7 @@ export type StoreDomains = { sshLeases: SshLeaseRecoveryOperations orcadSourceExport: OrcadSourceExportPersistence orcadCatalogImports: OrcadCatalogImportPersistence + orcadMigrationBlobInstalls: OrcadMigrationBlobInstallPersistence } export const STORE_DOMAIN_OPERATION_CLASSES = [ @@ -91,6 +94,7 @@ export const STORE_DOMAIN_OPERATION_CLASSES = [ SshLeaseRecoveryOperations, OrcadSourceExportPersistence, OrcadCatalogImportPersistence, + OrcadMigrationBlobInstallPersistence, WriteFlushBarrierOperations ] as const @@ -163,6 +167,7 @@ export function createStoreDomains(runtime: StoreRuntimeState): StoreDomains { sshLeases, // Read-only: holds the runtime state and nothing that writes. orcadSourceExport: new OrcadSourceExportPersistence(runtime), - orcadCatalogImports + orcadCatalogImports, + orcadMigrationBlobInstalls: new OrcadMigrationBlobInstallPersistence(runtime) } } diff --git a/src/main/persistence/loading-store/store-runtime-state.ts b/src/main/persistence/loading-store/store-runtime-state.ts index c8e8dc8e42c..e70f3c24da3 100644 --- a/src/main/persistence/loading-store/store-runtime-state.ts +++ b/src/main/persistence/loading-store/store-runtime-state.ts @@ -23,6 +23,7 @@ import type { } from '../scheduling-automations/automation-owner-projection' import type { ProfileStatePersistenceAuthority } from './profile-state-authority' import type { AutomationRun } from '../../../shared/automations-types' +import { OrcadTerminalRestoreHintStore } from '../migrating-orcad-catalog/orcad-terminal-restore-hint-store' export type DurableProfileStateMutation = { value: T @@ -48,6 +49,8 @@ export class StoreRuntimeState { readonly terminalScrollbackSnapshotStorage: TerminalScrollbackSnapshotStorage /** Staged and received migration blobs (see orcad-migration-blob-staging.ts). */ readonly orcadMigrationBlobRoot: string + /** Shared so project removal can drop hints for panes this host no longer owns. */ + readonly orcadTerminalRestoreHints: OrcadTerminalRestoreHintStore /** Scrollback refs each in-flight migration export still reads, keyed by migration id. */ readonly retainedScrollbackRefsByMigrationId = new Map>() writeTimer: ReturnType | null = null @@ -116,5 +119,6 @@ export class StoreRuntimeState { fallbackSnapshotRoot: legacySnapshotRoot === profileSnapshotRoot ? null : legacySnapshotRoot } this.orcadMigrationBlobRoot = join(dirname(this.dataFile), 'orcad-migration-blobs') + this.orcadTerminalRestoreHints = new OrcadTerminalRestoreHintStore(this.orcadMigrationBlobRoot) } } diff --git a/src/main/persistence/loading-store/store.ts b/src/main/persistence/loading-store/store.ts index 980bcfd0c46..a228f6305b9 100644 --- a/src/main/persistence/loading-store/store.ts +++ b/src/main/persistence/loading-store/store.ts @@ -126,6 +126,8 @@ export class Store { const sweptRepoIds = this.domains.repos.sweepDeregisteredRepoResidue() // Imported snapshots cannot own the live hook server or write their source file. if (!imported) { + // A committed migration whose blob install failed or was cut short is finished here. + this.domains.orcadMigrationBlobInstalls.installPendingOrcadMigrationReceivedBlobs() for (const entry of initial?.unboundPaneAliases ?? []) { agentHookServer.registerPaneKeyAlias( entry.legacyPaneKey, diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-catalog-import.ts b/src/main/persistence/migrating-orcad-catalog/orcad-catalog-import.ts index a76ce6f1cba..53a77cf8dfe 100644 --- a/src/main/persistence/migrating-orcad-catalog/orcad-catalog-import.ts +++ b/src/main/persistence/migrating-orcad-catalog/orcad-catalog-import.ts @@ -49,6 +49,7 @@ import { pruneOrcadMigrationBlobStaging, stageOrcadMigrationBlobChunk } from './orcad-migration-blob-staging' +import { assertOrcadMigrationBlobContentsValid } from './orcad-migration-blob-installers' type OrcadCatalogImportRuntime = Pick< StoreRuntimeState, @@ -168,6 +169,7 @@ export class OrcadCatalogImportPersistence { ) } assertOrcadMigrationBlobsReady(manifest, context.runtime.orcadMigrationBlobRoot) + assertOrcadMigrationBlobContentsValid(manifest, context.runtime.orcadMigrationBlobRoot) commitOrcadMigrationSnapshots(manifest, context.runtime.terminalScrollbackSnapshotStorage) commitOrcadMigrationBlobs(manifest, context.runtime.orcadMigrationBlobRoot) const receipt = commitPreparedCatalog(context, manifest, prepared, options.now) diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-install-persistence.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-install-persistence.ts new file mode 100644 index 00000000000..3f3ca260650 --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-install-persistence.ts @@ -0,0 +1,120 @@ +import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest' +import type { PersistedState } from '../../../shared/persisted-state-types' +import { parsePaneKey } from '../../../shared/stable-pane-id' +import type { StoreRuntimeState } from '../loading-store/store-runtime-state' +import { findOrcadMigrationImportReceipt } from './orcad-catalog-receipt-ledger' +import { installOrcadMigrationReceivedBlobs } from './orcad-migration-blob-installers' +import type { OrcadMigrationRequestIdentity } from './orcad-migration-staged-file' + +type OrcadMigrationBlobInstallRuntime = Pick< + StoreRuntimeState, + 'state' | 'orcadMigrationBlobRoot' | 'orcadTerminalRestoreHints' +> + +type ClockOptions = { now?: () => Date } + +const orcadMigrationBlobInstallContext = Symbol('OrcadMigrationBlobInstallPersistence') +type OrcadMigrationBlobInstallContext = { + runtime: OrcadMigrationBlobInstallRuntime +} + +/** The destination's committed migration blobs, installed into host-local state and consumed. */ +export class OrcadMigrationBlobInstallPersistence { + readonly [orcadMigrationBlobInstallContext]: OrcadMigrationBlobInstallContext + + constructor(runtime: OrcadMigrationBlobInstallRuntime) { + this[orcadMigrationBlobInstallContext] = { runtime } + } + + /** + * Run only once the commit is durable: installing first and then losing the commit would leave + * live hints for a catalog this host never received. A no-op before commit and on repeats. + */ + installOrcadMigrationReceivedBlobs( + manifest: OrcadMigrationManifest, + options: ClockOptions = {} + ): void { + const receipt = findOrcadMigrationImportReceipt( + this[orcadMigrationBlobInstallContext].runtime.state, + manifest + ) + if (receipt?.manifestSha256 !== manifest.manifestSha256) { + return + } + installCommitted(this[orcadMigrationBlobInstallContext].runtime, manifest, options) + } + + /** Finishes installs a crash or failed write left behind; every receipt on disk is durable. */ + installPendingOrcadMigrationReceivedBlobs(options: ClockOptions = {}): void { + const { runtime } = this[orcadMigrationBlobInstallContext] + for (const receipt of [ + ...(runtime.state.orcadMigrationImportReceipts ?? []), + ...(runtime.state.orcadMigrationEvictedReceipts ?? []) + ]) { + try { + installCommitted(runtime, receipt, options) + } catch (error) { + console.error( + `[orcad-migration] installing received blobs for ${receipt.migrationId} failed:`, + error + ) + } + } + } + + /** The moved pane's last folder while its hint is installed for this worktree; never removes it. */ + peekOrcadMigrationTerminalRestoreHint( + paneKey: string, + worktreeId: string, + options: ClockOptions = {} + ): string | null { + return this[orcadMigrationBlobInstallContext].runtime.orcadTerminalRestoreHints.peek( + paneKey, + worktreeId, + clock(options) + ) + } + + /** Called once the pane spawned: whatever cwd that create used, it was the restore. */ + consumeOrcadMigrationTerminalRestoreHint(paneKey: string, options: ClockOptions = {}): void { + this[orcadMigrationBlobInstallContext].runtime.orcadTerminalRestoreHints.consume( + paneKey, + clock(options) + ) + } +} + +function installCommitted( + runtime: OrcadMigrationBlobInstallRuntime, + migration: OrcadMigrationRequestIdentity, + options: ClockOptions +): void { + installOrcadMigrationReceivedBlobs({ + migration, + root: runtime.orcadMigrationBlobRoot, + terminalRestoreHints: runtime.orcadTerminalRestoreHints, + paneWorktreeId: (paneKey) => findPaneWorktreeId(runtime.state, paneKey), + now: clock(options) + }) +} + +function clock(options: ClockOptions): number { + return (options.now ?? (() => new Date()))().getTime() +} + +// The commit merged the moved session in with its worktree ids unchanged. +function findPaneWorktreeId(state: PersistedState, paneKey: string): string | null { + const tabId = parsePaneKey(paneKey)?.tabId + if (!tabId) { + return null + } + const sessions = [state.workspaceSession, ...Object.values(state.workspaceSessionsByHostId ?? {})] + for (const session of sessions) { + for (const [worktreeId, tabs] of Object.entries(session?.tabsByWorktree ?? {})) { + if (tabs.some((tab) => tab.id === tabId)) { + return worktreeId + } + } + } + return null +} diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-installers.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-installers.ts new file mode 100644 index 00000000000..7613ced0e72 --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-installers.ts @@ -0,0 +1,137 @@ +/** + * Per-kind handling of migration blobs on the destination: a content check before the catalog + * commits, and an installer after the commit is durable that moves the received blob into live + * state and deletes it. Kinds without an installer are not advertised, so none arrive. + */ +import { readFileSync, rmSync } from 'node:fs' +import { dirname } from 'node:path' +import type { OrcadMigrationBlobKind } from '../../../shared/orcad-migration-blob-transfer' +import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest' +import { + ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_KEY, + parseOrcadMigrationTerminalRestoreHintsText, + type OrcadMigrationTerminalRestoreHint +} from '../../../shared/orcad-migration-terminal-restore-hints' +import { syncDirectoryDurablySync } from '../../durable-file-write' +import { + getOrcadMigrationReceivedBlobPath, + stagedOrcadMigrationBlobFiles +} from './orcad-migration-blob-staging' +import { + orcadMigrationStagingKey, + type OrcadMigrationRequestIdentity +} from './orcad-migration-staged-file' +import type { + OrcadTerminalRestoreHintPlacement, + OrcadTerminalRestoreHintStore +} from './orcad-terminal-restore-hint-store' + +type OrcadMigrationBlobContentCheck = (bytes: Buffer) => void + +const UTF8 = new TextDecoder('utf-8', { fatal: true }) + +const BLOB_CONTENT_CHECKS: Partial> = + { + 'terminal-restore-hints': (bytes) => { + parseOrcadMigrationTerminalRestoreHintsText(decodeUtf8(bytes)) + } + } + +/** + * Bytes that matched their digest but not their format mean a buggy sender. Refusing here, before + * anything lands, fails the move while the source still owns everything — nothing is lost. + */ +export function assertOrcadMigrationBlobContentsValid( + manifest: OrcadMigrationManifest, + root: string +): void { + for (const { descriptor, path } of stagedOrcadMigrationBlobFiles(manifest, root)) { + const check = BLOB_CONTENT_CHECKS[descriptor.blobKind] + if (!check) { + continue + } + try { + check(readFileSync(path)) + } catch (error) { + const reason = error instanceof Error ? error.message : String(error) + throw new Error(`orcad_migration_blob_content_invalid:${descriptor.blobKind}:${reason}`) + } + } +} + +export type OrcadMigrationBlobInstall = { + migration: OrcadMigrationRequestIdentity + root: string + terminalRestoreHints: OrcadTerminalRestoreHintStore + /** The worktree that owns the pane on this host, or null when the pane is not here. */ + paneWorktreeId: (paneKey: string) => string | null + now: number +} + +/** + * Idempotent: a blob already installed is only deleted, and the hint store remembers installed + * migrations so a retry never revives a hint a pane already took. A write failure keeps the blob + * for the next retry: a commit retry, a catalog-state query, or the next store load. + */ +export function installOrcadMigrationReceivedBlobs(install: OrcadMigrationBlobInstall): void { + installTerminalRestoreHints(install) +} + +function installTerminalRestoreHints(install: OrcadMigrationBlobInstall): void { + const path = getOrcadMigrationReceivedBlobPath(install.root, install.migration, { + blobKind: 'terminal-restore-hints', + key: ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_KEY + }) + let bytes: Buffer + try { + bytes = readFileSync(path) + } catch { + return + } + const migrationKey = orcadMigrationStagingKey(install.migration) + if (!install.terminalRestoreHints.hasInstalledMigration(migrationKey)) { + try { + const document = parseOrcadMigrationTerminalRestoreHintsText(decodeUtf8(bytes)) + install.terminalRestoreHints.install( + { migrationKey, migrationId: install.migration.migrationId }, + placeHints(document.hints, install.paneWorktreeId), + install.now + ) + } catch (error) { + if (!isHintFormatError(error)) { + throw error + } + // The pre-commit check passed these bytes, so only a damaged file lands here; the catalog + // is already durable, and its panes reopen at their default folder. + console.error( + `[orcad-migration] dropped unreadable terminal restore hints for ${install.migration.migrationId}:`, + error + ) + } + } + rmSync(path, { force: true }) + syncDirectoryDurablySync(dirname(path)) +} + +// A hint whose pane is not in this host's session has no worktree to scope it to, so it is dropped. +function placeHints( + hints: readonly OrcadMigrationTerminalRestoreHint[], + paneWorktreeId: (paneKey: string) => string | null +): OrcadTerminalRestoreHintPlacement[] { + return hints.flatMap((hint) => { + const worktreeId = paneWorktreeId(hint.paneKey) + return worktreeId ? [{ ...hint, worktreeId }] : [] + }) +} + +function decodeUtf8(bytes: Buffer): string { + try { + return UTF8.decode(bytes) + } catch { + throw new Error('orcad_migration_terminal_restore_hints_not_utf8') + } +} + +function isHintFormatError(error: unknown): boolean { + return error instanceof Error && error.message.startsWith('orcad_migration_terminal_restore_') +} diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.ts index 90d5e081fb9..06e5be94783 100644 --- a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.ts +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.ts @@ -41,10 +41,12 @@ import { const RECEIVED_DIRECTORY = 'received' const STAGED_BLOB_NAME = /^([^.]+)\.([a-f0-9]{64})\.([1-9][0-9]*)\.part$/ +type OrcadMigrationBlobName = Pick + type ReceivedBlobPathResolver = ( root: string, - manifest: OrcadMigrationManifest, - descriptor: OrcadMigrationBlobDescriptor + manifest: OrcadMigrationRequestIdentity, + descriptor: OrcadMigrationBlobName ) => string const receivedDirectoryPath: ReceivedBlobPathResolver = (root, manifest, descriptor) => @@ -59,8 +61,8 @@ const RECEIVED_BLOB_DESTINATIONS: Record ({ + descriptor, + path: stagedBlobPath(root, manifest, descriptor) + })) +} + /** * Refuses commit unless the staged and already-received blobs are exactly the set the sender * moved, so a blob lost on the way (or left from another capture) fails instead of vanishing. @@ -189,6 +201,10 @@ export function assertOrcadMigrationBlobSetMatches( } } +/** + * The caller must run `assertOrcadMigrationBlobsReady` first, in the same synchronous turn: that + * pass hashes every staged file, and no chunk can land between it and these links. + */ export function commitOrcadMigrationBlobs(manifest: OrcadMigrationManifest, root: string): void { const directories = new Set() for (const descriptor of stagedBlobDescriptors(root, manifest)) { @@ -288,7 +304,7 @@ function inspectReceivedBlob( ) } -function receivedBlobsDirectory(root: string, manifest: OrcadMigrationManifest): string { +function receivedBlobsDirectory(root: string, manifest: OrcadMigrationRequestIdentity): string { return join(root, RECEIVED_DIRECTORY, orcadMigrationStagingKey(manifest)) } diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-terminal-restore-hint-store.ts b/src/main/persistence/migrating-orcad-catalog/orcad-terminal-restore-hint-store.ts new file mode 100644 index 00000000000..11296ea2c4f --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-terminal-restore-hint-store.ts @@ -0,0 +1,203 @@ +/** + * Installed restore hints on the destination, keyed by paneKey, until the pane's first spawn + * consumes its hint. A small fsync'd JSON file beside the received blobs rather than profile state: + * it is host-local, never synced or sent anywhere, and every change must be durable on its own. + */ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { z } from 'zod' +import { durableWriteTempPath, writeFileDurableSync } from '../../durable-file-write' +import { + MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS, + isOrcadMigrationTerminalRestoreHintCwd, + isOrcadMigrationTerminalRestoreHintPaneKey, + type OrcadMigrationTerminalRestoreHint +} from '../../../shared/orcad-migration-terminal-restore-hints' +import { ensureOrcadMigrationDirectory } from './orcad-migration-staged-file' + +const STORE_VERSION = 2 +const INSTALLED_DIRECTORY = 'installed' +const STORE_FILE = 'terminal-restore-hints.json' +// A pane not reopened within a month is not coming back through this migration. +export const ORCAD_TERMINAL_RESTORE_HINT_TTL_MS = 30 * 24 * 60 * 60 * 1000 +const MAX_INSTALLED_MIGRATIONS = 256 + +/** A received hint placed in the worktree that owns its pane on this host. */ +export type OrcadTerminalRestoreHintPlacement = OrcadMigrationTerminalRestoreHint & { + worktreeId: string +} + +export type OrcadTerminalRestoreHintMigration = { + migrationKey: string + migrationId: string +} + +type InstalledTerminalRestoreHint = OrcadTerminalRestoreHintPlacement & + OrcadTerminalRestoreHintMigration & { installedAt: number } + +type TerminalRestoreHintStoreDocument = { + version: typeof STORE_VERSION + hints: InstalledTerminalRestoreHint[] + /** Migrations already installed, so a commit retry never revives a hint a pane consumed. */ + installedMigrationKeys: string[] +} + +const MIGRATION_KEY = /^[a-f0-9]{32}$/ + +const StoreDocumentSchema = z.object({ + version: z.literal(STORE_VERSION), + hints: z.array( + z.object({ + paneKey: z.string().refine(isOrcadMigrationTerminalRestoreHintPaneKey), + cwd: z.string().refine(isOrcadMigrationTerminalRestoreHintCwd), + worktreeId: z.string().min(1), + migrationKey: z.string().regex(MIGRATION_KEY), + migrationId: z.string().min(1), + installedAt: z.number().int().nonnegative() + }) + ), + installedMigrationKeys: z.array(z.string().regex(MIGRATION_KEY)) +}) + +export class OrcadTerminalRestoreHintStore { + private readonly directory: string + private readonly path: string + private document: TerminalRestoreHintStoreDocument | null = null + + constructor(blobRoot: string) { + this.directory = join(blobRoot, INSTALLED_DIRECTORY) + this.path = join(this.directory, STORE_FILE) + } + + hasInstalledMigration(migrationKey: string): boolean { + return this.load().installedMigrationKeys.includes(migrationKey) + } + + /** Newer hints replace older ones for the same pane; the oldest drop past the count cap. */ + install( + migration: OrcadTerminalRestoreHintMigration, + hints: readonly OrcadTerminalRestoreHintPlacement[], + now: number + ): void { + const current = this.load() + if (current.installedMigrationKeys.includes(migration.migrationKey)) { + return + } + const incoming = new Set(hints.map((hint) => hint.paneKey)) + const merged = [ + ...current.hints.filter((hint) => !incoming.has(hint.paneKey)), + ...hints.map((hint) => ({ + paneKey: hint.paneKey, + cwd: hint.cwd, + worktreeId: hint.worktreeId, + migrationKey: migration.migrationKey, + migrationId: migration.migrationId, + installedAt: now + })) + ] + this.write( + { + version: STORE_VERSION, + hints: merged.slice(-MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS), + installedMigrationKeys: [...current.installedMigrationKeys, migration.migrationKey].slice( + -MAX_INSTALLED_MIGRATIONS + ) + }, + now + ) + } + + /** The pane's hinted cwd if it was moved into `worktreeId`; never removes it. */ + peek(paneKey: string, worktreeId: string, now: number): string | null { + const hint = this.load().hints.find((entry) => entry.paneKey === paneKey) + if (!hint || hint.worktreeId !== worktreeId) { + return null + } + return now - hint.installedAt > ORCAD_TERMINAL_RESTORE_HINT_TTL_MS ? null : hint.cwd + } + + /** Durably removes the pane's hint, whichever worktree it named; a no-op when it has none. */ + consume(paneKey: string, now: number): void { + this.dropWhere((hint) => hint.paneKey === paneKey, now) + } + + /** Drops hints for panes of worktrees this host no longer owns. */ + dropForWorktrees(matchesWorktreeId: (worktreeId: string) => boolean, now: number): void { + this.dropWhere((hint) => matchesWorktreeId(hint.worktreeId), now) + } + + private dropWhere(matches: (hint: InstalledTerminalRestoreHint) => boolean, now: number): void { + const current = this.load() + if (!current.hints.some(matches)) { + return + } + this.write({ ...current, hints: current.hints.filter((hint) => !matches(hint)) }, now) + } + + private write(next: TerminalRestoreHintStoreDocument, now: number): void { + const document = { + ...next, + hints: next.hints.filter( + (hint) => now - hint.installedAt <= ORCAD_TERMINAL_RESTORE_HINT_TTL_MS + ) + } + ensureOrcadMigrationDirectory(this.directory) + writeFileDurableSync( + durableWriteTempPath(this.path), + this.path, + JSON.stringify(document), + 0o600 + ) + this.document = document + } + + private load(): TerminalRestoreHintStoreDocument { + if (this.document) { + return this.document + } + this.document = readStoreDocument(this.path) + return this.document + } +} + +/** + * For project removal: a removed project's panes never come back by these keys unless a later + * migration brings them, and that migration carries its own hints. Never blocks the removal. + */ +export function dropOrcadTerminalRestoreHintsForWorktrees( + store: OrcadTerminalRestoreHintStore, + matchesWorktreeId: (worktreeId: string) => boolean +): void { + try { + store.dropForWorktrees(matchesWorktreeId, Date.now()) + } catch (error) { + console.warn('[orcad-migration] dropping terminal restore hints failed:', error) + } +} + +function emptyDocument(): TerminalRestoreHintStoreDocument { + return { version: STORE_VERSION, hints: [], installedMigrationKeys: [] } +} + +function readStoreDocument(path: string): TerminalRestoreHintStoreDocument { + let text: string + try { + text = readFileSync(path, 'utf8') + } catch { + return emptyDocument() + } + try { + const parsed = StoreDocumentSchema.safeParse(JSON.parse(text)) + if (parsed.success) { + return { + version: parsed.data.version, + hints: parsed.data.hints, + installedMigrationKeys: parsed.data.installedMigrationKeys + } + } + } catch { + // Falls through: a torn or foreign file only costs panes their old folder. + } + console.warn('[orcad-migration] ignoring an unreadable terminal restore hint store') + return emptyDocument() +} diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.test.ts b/src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.test.ts new file mode 100644 index 00000000000..0deb18ecc19 --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.test.ts @@ -0,0 +1,53 @@ +import { createHash } from 'node:crypto' +import { describe, expect, it } from 'vitest' +import { parseOrcadMigrationBlobDescriptor } from '../../../shared/orcad-migration-blob-transfer' +import { parseOrcadMigrationTerminalRestoreHintsText } from '../../../shared/orcad-migration-terminal-restore-hints' +import { buildWslMoveRestoreHintsBlob } from './wsl-move-restore-hints-blob' + +const hints = [ + { paneKey: 'tab-1:11111111-1111-4111-8111-111111111111', cwd: '/home/me/src' }, + { paneKey: 'tab-2:22222222-2222-4222-8222-222222222222', cwd: '/srv/app' } +] + +describe('WSL move restore hints blob', () => { + it('sends no blob when quiesce found no hints', () => { + expect(buildWslMoveRestoreHintsBlob([])).toBeNull() + }) + + it('encodes a descriptor the channel accepts over bytes the destination parses back', async () => { + const blob = buildWslMoveRestoreHintsBlob(hints) + expect(blob).not.toBeNull() + if (!blob) { + return + } + expect(parseOrcadMigrationBlobDescriptor(blob.descriptor)).toEqual({ + blobKind: 'terminal-restore-hints', + key: 'migration', + sha256: createHash('sha256').update(blob.bytes).digest('hex'), + byteLength: blob.bytes.length + }) + expect(parseOrcadMigrationTerminalRestoreHintsText(blob.bytes.toString('utf8'))).toEqual({ + version: 1, + hints + }) + const head = await blob.source.readChunk(blob.descriptor, 0, 5) + const rest = await blob.source.readChunk(blob.descriptor, 5, blob.bytes.length) + expect(Buffer.concat([head, rest])).toEqual(blob.bytes) + await expect( + blob.source.readChunk({ ...blob.descriptor, byteLength: 1 }, 0, 1) + ).rejects.toThrow('orcad_migration_source_blob_changed:terminal-restore-hints') + }) + + it('refuses a hint the destination would refuse, and an oversize document', () => { + expect(() => buildWslMoveRestoreHintsBlob([{ paneKey: 'tab-1:1', cwd: '/home' }])).toThrow( + 'orcad_migration_terminal_restore_hint_pane_key_invalid' + ) + const many = Array.from({ length: 1100 }, (_, index) => ({ + paneKey: `tab-${index}:11111111-1111-4111-8111-111111111111`, + cwd: `/${'a'.repeat(4000)}` + })) + expect(() => buildWslMoveRestoreHintsBlob(many)).toThrow( + 'orcad_migration_terminal_restore_hints_too_large' + ) + }) +}) diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.ts b/src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.ts new file mode 100644 index 00000000000..5ab0bed0d0f --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.ts @@ -0,0 +1,57 @@ +/** + * The quiesce step's restore hints as the `terminal-restore-hints` migration blob: the bytes, the + * descriptor the destination checks them against, and the source the blob sender reads from. + */ +import { createHash } from 'node:crypto' +import type { OrcadMigrationBlobDescriptor } from '../../../shared/orcad-migration-blob-transfer' +import { sameOrcadMigrationBlob } from '../../../shared/orcad-migration-blob-transfer' +import { + MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_BYTES, + ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_KEY, + ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_VERSION, + parseOrcadMigrationTerminalRestoreHintsDocument, + type OrcadMigrationTerminalRestoreHint +} from '../../../shared/orcad-migration-terminal-restore-hints' +import type { OrcadMigrationBlobSource } from '../../ssh/orcad-migration-blob-coordinator' + +export type WslMoveRestoreHintsBlob = { + descriptor: OrcadMigrationBlobDescriptor + bytes: Buffer + source: OrcadMigrationBlobSource +} + +/** Null when there is nothing to carry: the channel sends empty content as no blob at all. */ +export function buildWslMoveRestoreHintsBlob( + hints: readonly OrcadMigrationTerminalRestoreHint[] +): WslMoveRestoreHintsBlob | null { + if (hints.length === 0) { + return null + } + // Validated here so a bad hint fails the move on this side, before anything is staged. + const document = parseOrcadMigrationTerminalRestoreHintsDocument({ + version: ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_VERSION, + hints: hints.map((hint) => ({ paneKey: hint.paneKey, cwd: hint.cwd })) + }) + const bytes = Buffer.from(JSON.stringify(document), 'utf8') + if (bytes.length > MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_BYTES) { + throw new Error('orcad_migration_terminal_restore_hints_too_large') + } + const descriptor: OrcadMigrationBlobDescriptor = { + blobKind: 'terminal-restore-hints', + key: ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_KEY, + sha256: createHash('sha256').update(bytes).digest('hex'), + byteLength: bytes.length + } + return { + descriptor, + bytes, + source: { + readChunk: async (requested, offset, maxBytes) => { + if (!sameOrcadMigrationBlob(requested, descriptor)) { + throw new Error(`orcad_migration_source_blob_changed:${requested.blobKind}`) + } + return bytes.subarray(offset, offset + maxBytes) + } + } + } +} diff --git a/src/main/runtime/migrated-pane-restore-cwd.ts b/src/main/runtime/migrated-pane-restore-cwd.ts new file mode 100644 index 00000000000..502f6fee3b3 --- /dev/null +++ b/src/main/runtime/migrated-pane-restore-cwd.ts @@ -0,0 +1,90 @@ +import { constants } from 'node:fs' +import { access, stat } from 'node:fs/promises' +import type { RuntimeStore } from './runtime-store-contract' + +type RestoreHintSource = Pick< + RuntimeStore, + 'peekOrcadMigrationTerminalRestoreHint' | 'consumeOrcadMigrationTerminalRestoreHint' +> + +/** Non-null `connectionId` is an SSH workspace, whose folders this host's filesystem cannot see. */ +type RestoreHintWorkspace = { id: string; connectionId?: string | null } + +export type MigratedPaneRestoreHint = { + peekCwd: (requestedCwd: string | null | undefined) => Promise + consume: () => void + /** A spawn-commit callback that spends the hint first. */ + consumeThen: (onCommitted?: () => void) => () => void +} + +/** One create's view of its pane's hint: peeked to pick the cwd, consumed once the pane spawns. */ +export function migratedPaneRestoreHint( + store: RestoreHintSource | null | undefined, + workspace: RestoreHintWorkspace, + paneKey: string +): MigratedPaneRestoreHint { + const consume = (): void => consumeMigratedPaneRestoreHint(store, paneKey) + return { + peekCwd: (requestedCwd) => peekMigratedPaneRestoreCwd(store, workspace, paneKey, requestedCwd), + consume, + consumeThen: (onCommitted) => () => { + consume() + onCommitted?.() + } + } +} + +/** + * The folder a moved pane last used, for a default-cwd create. Only peeks: the hint is consumed + * when the pane spawns (see `consumeMigratedPaneRestoreHint`), so a failed create keeps it. + */ +export async function peekMigratedPaneRestoreCwd( + store: RestoreHintSource | null | undefined, + workspace: RestoreHintWorkspace, + paneKey: string, + requestedCwd: string | null | undefined, + directoryUsable: (path: string) => Promise = isSearchableDirectory +): Promise { + // Hints are Linux paths for a WSL host; a Windows process never opens one. + if (requestedCwd?.trim() || workspace.connectionId || process.platform === 'win32') { + return null + } + let cwd: string | null + try { + cwd = store?.peekOrcadMigrationTerminalRestoreHint?.(paneKey, workspace.id) ?? null + } catch (error) { + // An unreadable hint store must not block opening the terminal. + console.warn('[terminal-create] restore hint unavailable:', error) + return null + } + if (!cwd) { + return null + } + return (await directoryUsable(cwd)) ? cwd : null +} + +/** Once the pane spawned, whatever folder it opened in was its restore; the hint is spent. */ +export function consumeMigratedPaneRestoreHint( + store: RestoreHintSource | null | undefined, + paneKey: string +): void { + try { + store?.consumeOrcadMigrationTerminalRestoreHint?.(paneKey) + } catch (error) { + // The terminal already opened; a hint left behind is retried on the next spawn. + console.warn('[terminal-create] restore hint not consumed:', error) + } +} + +// A shell cannot start in a directory it cannot search, so that counts as gone. +async function isSearchableDirectory(path: string): Promise { + try { + if (!(await stat(path)).isDirectory()) { + return false + } + await access(path, constants.X_OK) + return true + } catch { + return false + } +} diff --git a/src/main/runtime/orca-runtime-create-terminal.ts b/src/main/runtime/orca-runtime-create-terminal.ts index 3143e3e4866..434762e4149 100644 --- a/src/main/runtime/orca-runtime-create-terminal.ts +++ b/src/main/runtime/orca-runtime-create-terminal.ts @@ -3,9 +3,10 @@ import { OrcaRuntimeWithTerminalCreateDeduplication } from './orca-runtime-termi import * as dependencies from './orca-runtime-create-terminal-dependencies' import { createDesktopTerminal } from './orca-runtime-create-terminal-desktop' import { buildRuntimeAgentTeamsLaunchPlan } from './terminal/launch/agent-teams-launch-plan' -import { createPtySpawnCommitReporter } from './orca-runtime-report-pty-spawn-commit' +import { createIdempotentCallback } from './orca-runtime-report-pty-spawn-commit' import { recordPtySurface, spawnSurfaceClaimSequence } from './pty-recorded-surface-topology' import { agentLaunchTokenFor } from '../agent-launch/agent-launch-token' +import { migratedPaneRestoreHint } from './migrated-pane-restore-cwd' export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreateDeduplication { async createTerminal( @@ -31,9 +32,7 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate } const workspace = await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector, created) const launchOpts = await this.resolveAgentTerminalCreateOptions(workspace, opts) - const reportPtySpawnCommitted = createPtySpawnCommitReporter(launchOpts.onPtySpawnCommitted) - const cwd = - this.resolveWorkspaceTerminalStartupCwd(workspace, launchOpts.cwd) ?? workspace.path + let cwd = this.resolveWorkspaceTerminalStartupCwd(workspace, launchOpts.cwd) ?? workspace.path const session = this.getWorkspaceSessionForWorktree(workspace.id) // The window's latest published active group is where its reveal places the tab. const groupId = @@ -54,21 +53,21 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate let preAllocatedHandle = launchOpts.preAllocatedHandle ?? this.createPreAllocatedTerminalHandle() let { tabId, leafId, paneKey } = dependencies.allocateTerminalPaneIdentity(launchOpts) + const restoreHint = migratedPaneRestoreHint(this.store, workspace, paneKey) + const reportPtySpawnCommitted = createIdempotentCallback( + restoreHint.consumeThen(launchOpts.onPtySpawnCommitted) + ) + // Claimed before any await, so a concurrent create of this pane cannot slip in between. const claimedStablePaneCreate = this.ptyController.claimStablePaneCreate?.({ worktreeId: workspace.id, connectionId: workspace.connectionId, tabId, leafId }) - let stablePaneCreateReleased = false - const releaseStablePaneCreate = (): void => { - if (stablePaneCreateReleased) { - return - } - stablePaneCreateReleased = true - claimedStablePaneCreate?.() - } + const releaseStablePaneCreate = createIdempotentCallback(claimedStablePaneCreate) try { + // A pane moved from another host reopens where it last was, until a spawn commits. + cwd = (await restoreHint.peekCwd(launchOpts.cwd)) ?? cwd if (launchOpts.signal?.aborted) { throw new Error('client_disconnected') } @@ -117,7 +116,9 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate { ...baseEnv, ...(sequencedStartupCommand - ? { [dependencies.SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: sequencedStartupCommand } + ? { + [dependencies.SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: sequencedStartupCommand + } : {}) }, paneKey, @@ -131,7 +132,10 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate let result: Awaited>> try { const { launchAgent } = launchOpts - launchOpts.onPtySpawnDispatched?.({ launchConfig: effectiveLaunchConfig, launchAgent }) + launchOpts.onPtySpawnDispatched?.({ + launchConfig: effectiveLaunchConfig, + launchAgent + }) result = await dependencies.spawnInAdmittedPane(this.store, this.ptyController, { ...initialSize, cwd, @@ -170,7 +174,9 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate } : {}), ...(launchOpts.agentSessionCreateOperationId - ? { agentSessionCreateOperationId: launchOpts.agentSessionCreateOperationId } + ? { + agentSessionCreateOperationId: launchOpts.agentSessionCreateOperationId + } : {}), ...dependencies.callerSpawnScope(launchOpts), ...(launchOpts.onPtySpawnCommitted @@ -188,6 +194,7 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate if (!result.stablePaneOwner) { reportPtySpawnCommitted() } + restoreHint.consume() // Why here: refused before a handle, snapshot or reveal could pose the live PTY as new. const adoptedStablePane = dependencies.admitStablePaneAdoption(result, launchOpts) if (result.agentSessionEnsure) { diff --git a/src/main/runtime/orca-runtime-migrated-pane-restore-cwd.test.ts b/src/main/runtime/orca-runtime-migrated-pane-restore-cwd.test.ts new file mode 100644 index 00000000000..8ab0f8b55b6 --- /dev/null +++ b/src/main/runtime/orca-runtime-migrated-pane-restore-cwd.test.ts @@ -0,0 +1,255 @@ +// A pane moved to this host reopens in the folder it last used on the source, until it spawns. +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { chmodSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { OrcaRuntimeService } from './orca-runtime' +import type { RuntimeStore } from './runtime-store-contract' +import type { RuntimePtyController } from './runtime-pty-controller-contract' +import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types' +import { OrcadTerminalRestoreHintStore } from '../persistence/migrating-orcad-catalog/orcad-terminal-restore-hint-store' + +vi.mock('electron', () => ({ + BrowserWindow: { fromId: vi.fn(() => null) }, + webContents: { fromId: vi.fn(() => null) }, + ipcMain: { on: vi.fn(), removeListener: vi.fn() }, + app: { getPath: vi.fn(() => '/tmp') } +})) + +const TAB_ID = 'tab-1' +const LEAF_ID = '11111111-1111-4111-8111-111111111111' +const OTHER_LEAF_ID = '22222222-2222-4222-8222-222222222222' +const PANE = `${TAB_ID}:${LEAF_ID}` +const WORKTREE_ID = 'wt-1' +const MIGRATION = { migrationKey: 'a'.repeat(32), migrationId: 'migration-1' } + +let dir: string +let worktreePath: string +let hinted: string + +class MigratedPaneRuntime extends OrcaRuntimeService { + constructor( + store: RuntimeStore, + private readonly connectionId: string | null + ) { + super(store) + } + + protected override async resolveTerminalWorkspaceLaunchScope(): Promise { + return { + id: WORKTREE_ID, + path: worktreePath, + connectionId: this.connectionId, + repo: null, + folderWorkspace: null + } + } +} + +type Hint = { paneKey: string; cwd: string; worktreeId?: string } + +function runtimeWithHints( + hints: Hint[], + options: { + connectionId?: string | null + spawn?: RuntimePtyController['spawn'] + claimStablePaneCreate?: RuntimePtyController['claimStablePaneCreate'] + onPeek?: () => void + } = {} +): { + runtime: MigratedPaneRuntime + spawn: ReturnType + hintStore: OrcadTerminalRestoreHintStore +} { + const hintStore = new OrcadTerminalRestoreHintStore(join(dir, 'blobs')) + hintStore.install( + MIGRATION, + hints.map((hint) => ({ + ...hint, + worktreeId: hint.worktreeId ?? WORKTREE_ID + })), + Date.now() + ) + const store: RuntimeStore = { + getRepos: () => [], + getRepo: () => undefined, + addRepo: vi.fn(), + updateRepo: vi.fn(), + getAllWorktreeMeta: () => ({}), + getWorktreeMeta: () => undefined, + setWorktreeMeta: vi.fn(), + removeWorktreeMeta: vi.fn(), + getGitHubCache: vi.fn(), + getSettings: () => ({ + workspaceDir: '/workspaces', + nestWorkspaces: false, + refreshLocalBaseRefOnWorktreeCreate: false, + branchPrefix: 'none', + branchPrefixCustom: '' + }), + peekOrcadMigrationTerminalRestoreHint: (paneKey, worktreeId) => { + options.onPeek?.() + return hintStore.peek(paneKey, worktreeId, Date.now()) + }, + consumeOrcadMigrationTerminalRestoreHint: (paneKey) => hintStore.consume(paneKey, Date.now()) + } + const runtime = new MigratedPaneRuntime(store, options.connectionId ?? null) + const spawn = vi.fn(options.spawn ?? (async () => ({ id: `pty-${spawn.mock.calls.length}` }))) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null, + ...(options.claimStablePaneCreate + ? { claimStablePaneCreate: options.claimStablePaneCreate } + : {}) + }) + return { runtime, spawn, hintStore } +} + +function spawnedCwd(spawn: ReturnType, call: number): unknown { + const args: unknown = spawn.mock.calls[call]?.[0] + return typeof args === 'object' && args !== null && 'cwd' in args ? args.cwd : undefined +} + +function remainingHint(hintStore: OrcadTerminalRestoreHintStore, paneKey = PANE): string | null { + return hintStore.peek(paneKey, WORKTREE_ID, Date.now()) +} + +describe('migrated pane restore cwd at terminal create', () => { + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'orca-migrated-pane-cwd-')) + worktreePath = join(dir, 'repo') + hinted = join(worktreePath, 'packages', 'web') + mkdirSync(hinted, { recursive: true }) + }) + + afterEach(() => { + rmSync(dir, { recursive: true, force: true }) + }) + + it('opens a default-cwd create in the hinted folder once, then at the worktree root', async () => { + const { runtime, spawn, hintStore } = runtimeWithHints([{ paneKey: PANE, cwd: hinted }]) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + expect(remainingHint(hintStore)).toBeNull() + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + + expect(spawnedCwd(spawn, 0)).toBe(hinted) + expect(spawnedCwd(spawn, 1)).toBe(worktreePath) + }) + + it('keeps the hint when the create fails, so the retry still opens in the hinted folder', async () => { + let attempts = 0 + const { runtime, spawn, hintStore } = runtimeWithHints([{ paneKey: PANE, cwd: hinted }], { + spawn: async () => { + attempts += 1 + if (attempts === 1) { + throw new Error('spawn_failed') + } + return { id: 'pty-retry' } + } + }) + + await expect( + runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + ).rejects.toThrow('spawn_failed') + expect(remainingHint(hintStore)).toBe(hinted) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + expect(spawnedCwd(spawn, 1)).toBe(hinted) + expect(remainingHint(hintStore)).toBeNull() + }) + + it('opens an explicit cwd and spends the hint: that create was the restore', async () => { + const { runtime, spawn, hintStore } = runtimeWithHints([{ paneKey: PANE, cwd: hinted }]) + + await runtime.createTerminal('id:wt-1', { + tabId: TAB_ID, + leafId: LEAF_ID, + cwd: dir + }) + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + + expect(spawnedCwd(spawn, 0)).toBe(dir) + expect(spawnedCwd(spawn, 1)).toBe(worktreePath) + expect(remainingHint(hintStore)).toBeNull() + }) + + it('never applies a hint installed for another worktree', async () => { + const { runtime, spawn } = runtimeWithHints([ + { paneKey: PANE, cwd: hinted, worktreeId: 'wt-other' } + ]) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + + expect(spawnedCwd(spawn, 0)).toBe(worktreePath) + }) + + it('falls back to the worktree root when the folder is gone, and spends the hint', async () => { + const { runtime, spawn, hintStore } = runtimeWithHints([ + { paneKey: PANE, cwd: join(worktreePath, 'deleted') } + ]) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + + expect(spawnedCwd(spawn, 0)).toBe(worktreePath) + expect(hintStore.peek(PANE, WORKTREE_ID, Date.now())).toBeNull() + }) + + it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)( + 'falls back to the worktree root for a folder the shell cannot search', + async () => { + chmodSync(hinted, 0o600) + try { + const { runtime, spawn } = runtimeWithHints([{ paneKey: PANE, cwd: hinted }]) + + await runtime.createTerminal('id:wt-1', { + tabId: TAB_ID, + leafId: LEAF_ID + }) + + expect(spawnedCwd(spawn, 0)).toBe(worktreePath) + } finally { + chmodSync(hinted, 0o700) + } + } + ) + + it('claims the pane before reading its hint', async () => { + const order: string[] = [] + const { runtime } = runtimeWithHints([{ paneKey: PANE, cwd: hinted }], { + claimStablePaneCreate: () => { + order.push('claim') + return () => order.push('release') + }, + onPeek: () => order.push('peek') + }) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + + expect(order.slice(0, 2)).toEqual(['claim', 'peek']) + }) + + it("never applies another pane's hint", async () => { + const otherPane = `${TAB_ID}:${OTHER_LEAF_ID}` + const { runtime, spawn, hintStore } = runtimeWithHints([{ paneKey: otherPane, cwd: hinted }]) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + await runtime.createTerminal('id:wt-1', {}) + + expect(spawnedCwd(spawn, 0)).toBe(worktreePath) + expect(spawnedCwd(spawn, 1)).toBe(worktreePath) + expect(remainingHint(hintStore, otherPane)).toBe(hinted) + }) + + it('ignores hints for an SSH workspace, whose folders this host cannot check', async () => { + const { runtime, spawn } = runtimeWithHints([{ paneKey: PANE, cwd: hinted }], { + connectionId: 'ssh-1' + }) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + + expect(spawnedCwd(spawn, 0)).toBe(worktreePath) + }) +}) diff --git a/src/main/runtime/orca-runtime-report-pty-spawn-commit.ts b/src/main/runtime/orca-runtime-report-pty-spawn-commit.ts index e9e2963144f..6f5741047b1 100644 --- a/src/main/runtime/orca-runtime-report-pty-spawn-commit.ts +++ b/src/main/runtime/orca-runtime-report-pty-spawn-commit.ts @@ -1,5 +1,5 @@ -/** Create an idempotent callback for the provider's spawn-commit boundary. */ -export function createPtySpawnCommitReporter(callback?: () => void): () => void { +/** An idempotent callback: the spawn-commit report and the stable-pane claim release run once. */ +export function createIdempotentCallback(callback?: () => void): () => void { let reported = false return () => { if (reported) { diff --git a/src/main/runtime/orcad-migration-catalog-import.ts b/src/main/runtime/orcad-migration-catalog-import.ts index 5fd101be700..e8bce17d8e9 100644 --- a/src/main/runtime/orcad-migration-catalog-import.ts +++ b/src/main/runtime/orcad-migration-catalog-import.ts @@ -10,7 +10,7 @@ type OrcadMigrationCatalogFlushStore = Partial> type OrcadMigrationCatalogCommitStore = OrcadMigrationCatalogFlushStore & - Partial> + Partial> type OrcadMigrationCatalogAbortStore = OrcadMigrationCatalogFlushStore & Partial> @@ -39,9 +39,23 @@ export async function commitStagedOrcadMigrationCatalogDurably(args: { ) await flushMigrationState(args.store, args.signal) args.onDurableCommit() + installReceivedBlobs(args.store, args.manifest) return result } +/** After the durable commit, on its retries and state queries; the store load retries too. */ +function installReceivedBlobs( + store: Partial>, + manifest: OrcadMigrationManifest +): void { + try { + store.installOrcadMigrationReceivedBlobs?.(manifest) + } catch (error) { + // The catalog is committed; a failed install keeps its blob for the next retry. + console.error('[orcad-migration] installing received blobs failed:', error) + } +} + export async function abortStagedOrcadMigrationCatalogDurably(args: { store: OrcadMigrationCatalogAbortStore manifest: OrcadMigrationManifest @@ -58,11 +72,17 @@ export async function abortStagedOrcadMigrationCatalogDurably(args: { } export function getOrcadMigrationCatalogState(args: { - store: Partial> + store: Partial< + Pick + > manifest: OrcadMigrationManifest }): OrcadMigrationCatalogState { // SAFETY: Returned or mutable Store slots retain their original native missing-method failure. - return args.store.getOrcadMigrationCatalogState!(args.manifest) + const result = args.store.getOrcadMigrationCatalogState!(args.manifest) + if (result.state === 'committed') { + installReceivedBlobs(args.store, args.manifest) + } + return result } function flushMigrationState( @@ -70,5 +90,8 @@ function flushMigrationState( signal?: AbortSignal ): Promise { // SAFETY: Returned or mutable Store slots retain their original native missing-method failure. - return store.flushPendingOrThrowAsync!({ signal, drainToStableGeneration: false }) + return store.flushPendingOrThrowAsync!({ + signal, + drainToStableGeneration: false + }) } diff --git a/src/main/runtime/runtime-store-contract.ts b/src/main/runtime/runtime-store-contract.ts index 801ca76f36d..c3294c3f308 100644 --- a/src/main/runtime/runtime-store-contract.ts +++ b/src/main/runtime/runtime-store-contract.ts @@ -61,6 +61,9 @@ export type RuntimeStore = { stageOrcadMigrationBlobChunk?: Store['stageOrcadMigrationBlobChunk'] abortStagedOrcadMigrationCatalog?: Store['abortStagedOrcadMigrationCatalog'] getOrcadMigrationCatalogState?: Store['getOrcadMigrationCatalogState'] + installOrcadMigrationReceivedBlobs?: Store['installOrcadMigrationReceivedBlobs'] + peekOrcadMigrationTerminalRestoreHint?: Store['peekOrcadMigrationTerminalRestoreHint'] + consumeOrcadMigrationTerminalRestoreHint?: Store['consumeOrcadMigrationTerminalRestoreHint'] persistPtyBinding?: Store['persistPtyBinding'] admitTerminalPane?: Store['admitTerminalPane'] withdrawTerminalPane?: Store['withdrawTerminalPane'] diff --git a/src/shared/orcad-migration-blob-transfer.test.ts b/src/shared/orcad-migration-blob-transfer.test.ts index 8ca5281f74b..ccc50a650c1 100644 --- a/src/shared/orcad-migration-blob-transfer.test.ts +++ b/src/shared/orcad-migration-blob-transfer.test.ts @@ -121,9 +121,10 @@ describe('orcad migration blob contract', () => { ) }) - it('advertises only the channel until a kind has an installer', () => { + it('advertises the channel and only the kinds that have an installer', () => { expect(advertisedOrcadMigrationBlobCapabilities()).toEqual([ - ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY + ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY, + 'orcad.migration-blob.terminal-restore-hints.v1' ]) }) diff --git a/src/shared/orcad-migration-blob-transfer.ts b/src/shared/orcad-migration-blob-transfer.ts index 677b8c7c787..8cd69866f03 100644 --- a/src/shared/orcad-migration-blob-transfer.ts +++ b/src/shared/orcad-migration-blob-transfer.ts @@ -256,8 +256,10 @@ export function parseOrcadMigrationBlobChunkResult( } } -/** Kinds this build's destination installs after commit; 6b/6c add theirs with their installers. */ -export const ORCAD_MIGRATION_INSTALLABLE_BLOB_KINDS: readonly OrcadMigrationBlobKind[] = [] +/** Kinds this build's destination installs after commit; each is added with its installer. */ +export const ORCAD_MIGRATION_INSTALLABLE_BLOB_KINDS: readonly OrcadMigrationBlobKind[] = [ + 'terminal-restore-hints' +] export function advertisedOrcadMigrationBlobCapabilities(): string[] { return [ diff --git a/src/shared/orcad-migration-terminal-restore-hints.test.ts b/src/shared/orcad-migration-terminal-restore-hints.test.ts new file mode 100644 index 00000000000..dcdf8a24e18 --- /dev/null +++ b/src/shared/orcad-migration-terminal-restore-hints.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest' +import { + MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS, + MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_BYTES, + parseOrcadMigrationTerminalRestoreHintsDocument, + parseOrcadMigrationTerminalRestoreHintsText +} from './orcad-migration-terminal-restore-hints' + +const PANE_KEY = 'tab-1:11111111-1111-4111-8111-111111111111' + +function document(hints: unknown[]): unknown { + return { version: 1, hints } +} + +describe('terminal restore hints document', () => { + it('accepts stable pane keys with absolute POSIX folders and drops unknown fields', () => { + expect( + parseOrcadMigrationTerminalRestoreHintsDocument({ + version: 1, + hints: [{ paneKey: PANE_KEY, cwd: '/home/me/src', extra: true }], + futureField: 1 + }) + ).toEqual({ version: 1, hints: [{ paneKey: PANE_KEY, cwd: '/home/me/src' }] }) + }) + + it.each([ + ['a legacy numeric pane id', 'tab-1:3'], + ['a key without a tab', ':11111111-1111-4111-8111-111111111111'], + ['a key with two separators', 'a:b:11111111-1111-4111-8111-111111111111'], + ['an over-long key', `${'t'.repeat(200)}:11111111-1111-4111-8111-111111111111`] + ])('refuses %s', (_label, paneKey) => { + expect(() => + parseOrcadMigrationTerminalRestoreHintsDocument(document([{ paneKey, cwd: '/home' }])) + ).toThrow('orcad_migration_terminal_restore_hint_pane_key_invalid') + }) + + it.each([ + ['a relative folder', 'src/app'], + ['a UNC-style folder', '//wsl.localhost/Ubuntu/home'], + ['a Windows folder', 'C:\\Users\\me'], + ['a folder with NUL', '/home/\0me'], + ['an over-long folder', `/${'a'.repeat(4096)}`] + ])('refuses %s', (_label, cwd) => { + expect(() => + parseOrcadMigrationTerminalRestoreHintsDocument(document([{ paneKey: PANE_KEY, cwd }])) + ).toThrow('orcad_migration_terminal_restore_hint_cwd_invalid') + }) + + it('refuses duplicate panes, too many hints, other versions and non-JSON', () => { + const hint = { paneKey: PANE_KEY, cwd: '/home' } + expect(() => parseOrcadMigrationTerminalRestoreHintsDocument(document([hint, hint]))).toThrow( + 'orcad_migration_terminal_restore_hint_duplicate' + ) + expect(() => + parseOrcadMigrationTerminalRestoreHintsDocument( + document(Array.from({ length: MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS + 1 }, () => hint)) + ) + ).toThrow('orcad_migration_terminal_restore_hints_invalid') + expect(() => + parseOrcadMigrationTerminalRestoreHintsDocument({ version: 2, hints: [] }) + ).toThrow('orcad_migration_terminal_restore_hints_invalid') + expect(() => parseOrcadMigrationTerminalRestoreHintsText('{')).toThrow( + 'orcad_migration_terminal_restore_hints_not_json' + ) + }) + + it('refuses text over the blob cap before parsing it', () => { + expect(() => + parseOrcadMigrationTerminalRestoreHintsText( + ' '.repeat(MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_BYTES + 1) + ) + ).toThrow('orcad_migration_terminal_restore_hints_too_large') + }) +}) diff --git a/src/shared/orcad-migration-terminal-restore-hints.ts b/src/shared/orcad-migration-terminal-restore-hints.ts new file mode 100644 index 00000000000..23914296207 --- /dev/null +++ b/src/shared/orcad-migration-terminal-restore-hints.ts @@ -0,0 +1,101 @@ +/** + * The `terminal-restore-hints` blob: each moved pane's last working directory on the source, so + * the destination reopens it there instead of at the worktree root. One JSON document per + * migration; a later format is a new capability, never a silent reinterpretation of this one. + */ +import { z } from 'zod' +import { MAX_PANE_KEY_LEN } from './agent-hook-listener/listener-limits' +import { ORCAD_MIGRATION_BLOB_KIND_CONTRACTS } from './orcad-migration-blob-transfer' +import { parsePaneKey } from './stable-pane-id' + +export const ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_VERSION = 1 +export const ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_KEY = 'migration' +export const MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS = 8192 +// Linux PATH_MAX; a longer cwd cannot be a directory a shell could have been in. +export const MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINT_CWD_LENGTH = 4096 +export const MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_BYTES = + ORCAD_MIGRATION_BLOB_KIND_CONTRACTS['terminal-restore-hints'].maxBytes + +export type OrcadMigrationTerminalRestoreHint = { paneKey: string; cwd: string } + +export type OrcadMigrationTerminalRestoreHintsDocument = { + version: typeof ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_VERSION + hints: OrcadMigrationTerminalRestoreHint[] +} + +export function isOrcadMigrationTerminalRestoreHintPaneKey(value: string): boolean { + return value.length <= MAX_PANE_KEY_LEN && parsePaneKey(value) !== null +} + +/** Absolute POSIX; `//` is refused because a UNC-style spelling never names a distro path. */ +export function isOrcadMigrationTerminalRestoreHintCwd(value: string): boolean { + return ( + value.length <= MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINT_CWD_LENGTH && + /^\/(?!\/)/.test(value) && + !value.includes('\0') + ) +} + +const HintSchema = z.object({ + paneKey: z.string().refine(isOrcadMigrationTerminalRestoreHintPaneKey, { + message: 'orcad_migration_terminal_restore_hint_pane_key_invalid' + }), + cwd: z.string().refine(isOrcadMigrationTerminalRestoreHintCwd, { + message: 'orcad_migration_terminal_restore_hint_cwd_invalid' + }) +}) + +// Not strict: a same-version sender may add optional fields this build ignores. +export const OrcadMigrationTerminalRestoreHintsDocumentSchema = z + .object({ + version: z.literal(ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_VERSION), + hints: z.array(HintSchema).max(MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS) + }) + .superRefine((value, context) => { + const seen = new Set() + for (const hint of value.hints) { + if (seen.has(hint.paneKey)) { + context.addIssue({ + code: 'custom', + message: 'orcad_migration_terminal_restore_hint_duplicate' + }) + return + } + seen.add(hint.paneKey) + } + }) + +export function parseOrcadMigrationTerminalRestoreHintsDocument( + value: unknown +): OrcadMigrationTerminalRestoreHintsDocument { + const parsed = OrcadMigrationTerminalRestoreHintsDocumentSchema.safeParse(value) + if (!parsed.success) { + const message = parsed.error.issues[0]?.message ?? '' + // Zod's own messages (wrong version, missing field) collapse into one stable code. + throw new Error( + message.startsWith('orcad_migration_terminal_restore_') + ? message + : 'orcad_migration_terminal_restore_hints_invalid' + ) + } + return { + version: parsed.data.version, + hints: parsed.data.hints.map((hint) => ({ paneKey: hint.paneKey, cwd: hint.cwd })) + } +} + +/** The blob's UTF-8 JSON text, size-checked before it is parsed. */ +export function parseOrcadMigrationTerminalRestoreHintsText( + text: string +): OrcadMigrationTerminalRestoreHintsDocument { + if (text.length > MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_BYTES) { + throw new Error('orcad_migration_terminal_restore_hints_too_large') + } + let value: unknown + try { + value = JSON.parse(text) + } catch { + throw new Error('orcad_migration_terminal_restore_hints_not_json') + } + return parseOrcadMigrationTerminalRestoreHintsDocument(value) +} From 02d936619884e01c031ab748fbe4b3b95bad27f0 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:57:07 -0400 Subject: [PATCH 23/53] refactor(tabs): mint a workspace's root group with the loader's id (#28884) A workspace with no groups got a random root group id from the window. It now gets the name-based id the shared loader already mints for that workspace's group, so main can derive the same id without the window. Co-authored-by: Claude Opus 5.5 --- .../store/slices/tabs/tabs-group-actions.ts | 3 +- .../store/slices/tabs/tabs-root-group.test.ts | 35 +++++++++++++++++++ .../workspace-layout-load-order.ts | 3 +- .../workspace-layout-minted-ids.ts | 10 ++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/store/slices/tabs/tabs-root-group.test.ts diff --git a/src/renderer/src/store/slices/tabs/tabs-group-actions.ts b/src/renderer/src/store/slices/tabs/tabs-group-actions.ts index 4ef4c6c9ea3..ef1cc396a52 100644 --- a/src/renderer/src/store/slices/tabs/tabs-group-actions.ts +++ b/src/renderer/src/store/slices/tabs/tabs-group-actions.ts @@ -7,6 +7,7 @@ import { collapseGroupLayout, replaceLeaf } from '../../../../../shared/workspace-layout/tab-group-layout-tree' +import { workspaceRootGroupId } from '../../../../../shared/workspace-layout/workspace-layout-minted-ids' import { ownsGlobalSelection } from '../../global-selection-owner' export function createTabsGroupActions( @@ -23,7 +24,7 @@ export function createTabsGroupActions( return get().activeGroupIdByWorktree[worktreeId] ?? existingGroups[0].id } - const groupId = createBrowserUuid() + const groupId = workspaceRootGroupId(worktreeId) set((state) => ({ // Why: a zero-tab worktree still needs a canonical root group so new tabs and splits land in a deterministic place. groupsByWorktree: { diff --git a/src/renderer/src/store/slices/tabs/tabs-root-group.test.ts b/src/renderer/src/store/slices/tabs/tabs-root-group.test.ts new file mode 100644 index 00000000000..31d26881aef --- /dev/null +++ b/src/renderer/src/store/slices/tabs/tabs-root-group.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it, vi } from 'vitest' +import { LOCAL_EXECUTION_HOST_ID } from '../../../../../shared/execution-host' +import { loadWorkspaceLayout } from '../../../../../shared/workspace-layout/workspace-layout-load' +import { nameBasedLoadContext } from '../../../../../shared/workspace-layout/workspace-layout-minted-ids' +import { + addWorkspace, + emptySession, + FOLDER_KEY, + GIT_KEY, + leaf +} from '../../../../../shared/workspace-layout/workspace-layout-session.test-fixture' +import { createTabsSliceMockApi } from '../tabs-slice-test-harness' +import { createTestStore } from '../store-test-helpers' + +vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } })) + +createTabsSliceMockApi() + +describe('ensureWorktreeRootGroup', () => { + it.each([GIT_KEY, FOLDER_KEY])('mints the group main mints for %s', (workspaceKey) => { + const stored = addWorkspace(emptySession(), workspaceKey, [ + { id: 'g1', tabs: [{ id: 'tab-a', leaves: [[leaf(1), 'pty-a']] }] } + ]) + stored.unifiedTabs = {} + stored.tabGroups = {} + stored.tabGroupLayouts = {} + const loaded = loadWorkspaceLayout(LOCAL_EXECUTION_HOST_ID, stored, nameBasedLoadContext()) + + const groupId = createTestStore().getState().ensureWorktreeRootGroup(workspaceKey) + + expect(loaded.layout.workspaces[workspaceKey]?.groups.map((group) => group.id)).toEqual([ + groupId + ]) + }) +}) diff --git a/src/shared/workspace-layout/workspace-layout-load-order.ts b/src/shared/workspace-layout/workspace-layout-load-order.ts index 8a75d0f8c15..6b52b44e555 100644 --- a/src/shared/workspace-layout/workspace-layout-load-order.ts +++ b/src/shared/workspace-layout/workspace-layout-load-order.ts @@ -4,6 +4,7 @@ import type { TabGroup } from '../tab-types' import type { LayoutGroup } from './workspace-layout-model' +import { rootGroupSeed } from './workspace-layout-minted-ids' export type OrderCandidate = { id: string @@ -46,7 +47,7 @@ export function resolveGroupOrder(args: { for (const candidate of [...unplaced].sort(compareUnplaced)) { let group = groups.find((entry) => entry.id === candidate.groupId) ?? groups[0] if (!group) { - group = { id: args.mintId(`group:${args.workspaceKey}`), tabOrder: [] } + group = { id: args.mintId(rootGroupSeed(args.workspaceKey)), tabOrder: [] } groups.push(group) } group.tabOrder.push(candidate.id) diff --git a/src/shared/workspace-layout/workspace-layout-minted-ids.ts b/src/shared/workspace-layout/workspace-layout-minted-ids.ts index 576daa48ccd..168aa188924 100644 --- a/src/shared/workspace-layout/workspace-layout-minted-ids.ts +++ b/src/shared/workspace-layout/workspace-layout-minted-ids.ts @@ -28,3 +28,13 @@ export function nameBasedUuid(name: string): string { export function nameBasedLoadContext(): WorkspaceLayoutLoadContext { return { mintId: nameBasedUuid, mintLeafId: nameBasedUuid } } + +/** Seed of the group the Loader mints for a workspace's tabs when none was stored. */ +export function rootGroupSeed(workspaceKey: string): string { + return `group:${workspaceKey}` +} + +/** The id the Loader mints for that group, so a window minting a root group picks the same one. */ +export function workspaceRootGroupId(workspaceKey: string): string { + return nameBasedUuid(rootGroupSeed(workspaceKey)) +} From a90ff597bef55b64b46e81cb196f03a6256364a3 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:04:24 -0700 Subject: [PATCH 24/53] Show whole credit balances in usage displays (#28887) * fix: limit credit balances to two decimal places * fix: show whole credits in usage displays --- src/shared/credit-count-format.test.ts | 13 ++++++++++++- src/shared/credit-count-format.ts | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/shared/credit-count-format.test.ts b/src/shared/credit-count-format.test.ts index 07ab4554252..1079f5eebe9 100644 --- a/src/shared/credit-count-format.test.ts +++ b/src/shared/credit-count-format.test.ts @@ -3,10 +3,21 @@ import { formatCreditCount } from './credit-count-format' describe('formatCreditCount', () => { it('uses the viewer locale for grouped credit counts', () => { - const expected = new Intl.NumberFormat(undefined, { maximumFractionDigits: 20 }).format(1234) + const expected = new Intl.NumberFormat().format(1234) expect(formatCreditCount(1234)).toBe(expected) }) + it.each([ + [51809.811786, 51810], + [1234.56789, 1235], + [500.1, 500], + [500.5, 501], + [500, 500], + [0.1, 0] + ])('rounds %s to whole credits', (count, rounded) => { + expect(formatCreditCount(count)).toBe(new Intl.NumberFormat().format(rounded)) + }) + it('normalizes invalid and negative counts', () => { expect(formatCreditCount(Number.NaN)).toBe('0') expect(formatCreditCount(-5)).toBe('0') diff --git a/src/shared/credit-count-format.ts b/src/shared/credit-count-format.ts index 14cfde62bfb..8510944603f 100644 --- a/src/shared/credit-count-format.ts +++ b/src/shared/credit-count-format.ts @@ -1,4 +1,4 @@ -const CREDIT_COUNT_FORMATTER = new Intl.NumberFormat(undefined, { maximumFractionDigits: 20 }) +const CREDIT_COUNT_FORMATTER = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }) export function formatCreditCount(count: number): string { const safeCount = Number.isFinite(count) ? Math.max(0, count) : 0 From f9ef559781ba79e498e48a5f53b16b1f9a2b64d9 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:05:32 -0400 Subject: [PATCH 25/53] refactor(tabs): choose new tab ids before creating them at simple create sites (#28888) Ten create sites now mint the new tab's id (or browser page id), pass it to the create action, and key every follow-up (select, focus, tab order, group focus, remote page handle) off that id instead of the returned record. Creates stay synchronous, so the follow-ups still run immediately. Co-authored-by: Claude Opus 5.5 --- .../FloatingTerminalPanel.shortcuts.test.tsx | 6 +- ...oatingTerminalPanel.tab-lifecycle.test.tsx | 10 ++- .../floating-terminal-panel-test-fixtures.ts | 7 +- .../use-floating-terminal-create-actions.ts | 6 +- .../right-sidebar/PortsPanel.test.tsx | 31 +++++--- ...bGroupCreationCommands.local-shell.test.ts | 7 +- .../tab-group/useTabGroupCreationCommands.ts | 8 +- .../useTabGroupWorkspaceModel.focus.test.ts | 7 +- .../terminal/terminal-tab-actions.test.ts | 10 ++- .../terminal/terminal-tab-create.ts | 14 ++-- .../use-terminal-create-actions.test.tsx | 16 ++-- .../components/use-terminal-create-actions.ts | 8 +- .../tab-lifecycle-ipc-bridge.test.ts | 16 +++- .../ipc-events/tab-lifecycle-ipc-bridge.ts | 8 +- ...pcEvents-terminal-create-surfacing.test.ts | 2 +- .../lib/ensure-simulator-tab-behavior.test.ts | 75 ++++++++++--------- src/renderer/src/lib/ensure-simulator-tab.ts | 32 +++++--- .../lib/floating-workspace-tab-creation.ts | 15 ++-- ...loating-workspace-terminal-actions.test.ts | 25 +++---- .../src/lib/workspace-port-actions.ts | 12 +-- 20 files changed, 186 insertions(+), 129 deletions(-) diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.shortcuts.test.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.shortcuts.test.tsx index 597a3fff646..a4d103846e3 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.shortcuts.test.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.shortcuts.test.tsx @@ -225,7 +225,8 @@ describe('FloatingTerminalPanel close behavior', () => { expect(mocks.createTab).toHaveBeenCalledWith( FLOATING_TERMINAL_WORKTREE_ID, 'floating-group', - undefined + undefined, + { id: expect.any(String) } ) expect(mocks.activateTab).not.toHaveBeenCalled() }) @@ -321,7 +322,8 @@ describe('FloatingTerminalPanel close behavior', () => { expect(mocks.createTab).toHaveBeenCalledWith( FLOATING_TERMINAL_WORKTREE_ID, 'floating-group', - undefined + undefined, + { id: expect.any(String) } ) expect(mocks.activateTab).not.toHaveBeenCalled() }) diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tab-lifecycle.test.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tab-lifecycle.test.tsx index 9337edcde1c..a8991056fed 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tab-lifecycle.test.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tab-lifecycle.test.tsx @@ -202,10 +202,11 @@ describe('FloatingTerminalPanel close behavior', () => { expect(mocks.createTab).toHaveBeenCalledWith( FLOATING_TERMINAL_WORKTREE_ID, 'floating-group', - undefined + undefined, + { id: expect.any(String) } ) expect(mocks.activateTab).not.toHaveBeenCalled() - expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('created-tab') + expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith(mocks.createTab.mock.calls[0][3]?.id) }) it('keeps floating browser create and duplicate local during active web runtime sessions', async () => { @@ -488,10 +489,11 @@ describe('FloatingTerminalPanel close behavior', () => { expect(mocks.createTab).toHaveBeenCalledWith( FLOATING_TERMINAL_WORKTREE_ID, 'floating-group', - undefined + undefined, + { id: expect.any(String) } ) expect(mocks.activateTab).not.toHaveBeenCalled() - expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('created-tab') + expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith(mocks.createTab.mock.calls[0][3]?.id) ;(tabBar.props.onClose as (tabId: string) => void)('tab-1') expect(mocks.closeWebRuntimeSessionTab).not.toHaveBeenCalled() diff --git a/src/renderer/src/components/floating-terminal/floating-terminal-panel-test-fixtures.ts b/src/renderer/src/components/floating-terminal/floating-terminal-panel-test-fixtures.ts index cd061b5ccf0..48e3044bac6 100644 --- a/src/renderer/src/components/floating-terminal/floating-terminal-panel-test-fixtures.ts +++ b/src/renderer/src/components/floating-terminal/floating-terminal-panel-test-fixtures.ts @@ -29,7 +29,12 @@ export type FloatingPanelStoreState = { worktreeId: string, groupId?: string, shellOverride?: string, - options?: { activate?: boolean; pendingActivationSpawn?: boolean; initialPtyId?: string } + options?: { + activate?: boolean + pendingActivationSpawn?: boolean + initialPtyId?: string + id?: string + } ) => TerminalTab createBrowserTab: ( worktreeId: string, diff --git a/src/renderer/src/components/floating-terminal/use-floating-terminal-create-actions.ts b/src/renderer/src/components/floating-terminal/use-floating-terminal-create-actions.ts index 23f87a7ea90..44839079112 100644 --- a/src/renderer/src/components/floating-terminal/use-floating-terminal-create-actions.ts +++ b/src/renderer/src/components/floating-terminal/use-floating-terminal-create-actions.ts @@ -7,6 +7,7 @@ import { ensureClientCreationActionAllowed } from '@/lib/client-creation-action- import { openDocumentInFloatingWorkspace } from '@/lib/open-document-in-floating-workspace' import { extractIpcErrorMessage } from '@/lib/ipc-error' import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface' +import { createBrowserUuid } from '@/lib/browser-uuid' import { translate } from '@/i18n/i18n' import { useAppStore } from '@/store' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' @@ -66,8 +67,9 @@ export function useFloatingTerminalCreateActions({ const createFloatingTerminalTab = useCallback( (shellOverride?: string) => { - const tab = createTab(FLOATING_TERMINAL_WORKTREE_ID, activeGroup?.id, shellOverride) - focusTerminalTabSurface(tab.id) + const tabId = createBrowserUuid() + createTab(FLOATING_TERMINAL_WORKTREE_ID, activeGroup?.id, shellOverride, { id: tabId }) + focusTerminalTabSurface(tabId) }, [activeGroup, createTab] ) diff --git a/src/renderer/src/components/right-sidebar/PortsPanel.test.tsx b/src/renderer/src/components/right-sidebar/PortsPanel.test.tsx index 08bb15828f4..d57e624852b 100644 --- a/src/renderer/src/components/right-sidebar/PortsPanel.test.tsx +++ b/src/renderer/src/components/right-sidebar/PortsPanel.test.tsx @@ -329,7 +329,7 @@ describe('PortsPanel runtime routing', () => { _meta: { runtimeId: 'runtime-1' } }) ) - const createBrowserTab = vi.fn(() => ({ activePageId: 'local-page-1' })) + const createBrowserTab = vi.fn() const setRemoteBrowserPageHandle = vi.fn() await expect( @@ -368,13 +368,17 @@ describe('PortsPanel runtime routing', () => { 'http://127.0.0.1:63468', { activate: true, - browserRuntimeEnvironmentId: 'env-1' + browserRuntimeEnvironmentId: 'env-1', + browserPageId: expect.any(String) + } + ) + expect(setRemoteBrowserPageHandle).toHaveBeenCalledWith( + createBrowserTab.mock.calls[0][2].browserPageId, + { + environmentId: 'env-1', + remotePageId: 'remote-browser-page-1' } ) - expect(setRemoteBrowserPageHandle).toHaveBeenCalledWith('local-page-1', { - environmentId: 'env-1', - remotePageId: 'remote-browser-page-1' - }) }) it('rejects remote port browser creation before RPC without the screencast provider', async () => { @@ -581,7 +585,7 @@ describe('PortsPanel runtime routing', () => { _meta: { runtimeId: 'runtime-1' } }) ) - const createBrowserTab = vi.fn(() => ({ activePageId: 'local-page-1' })) + const createBrowserTab = vi.fn() const setRemoteBrowserPageHandle = vi.fn() await expect( @@ -602,12 +606,15 @@ describe('PortsPanel runtime routing', () => { expect(createBrowserTab).toHaveBeenCalledWith( 'repo::/workspace/app', 'http://127.0.0.1:63468', - { activate: true, browserRuntimeEnvironmentId: 'env-1' } + { activate: true, browserRuntimeEnvironmentId: 'env-1', browserPageId: expect.any(String) } + ) + expect(setRemoteBrowserPageHandle).toHaveBeenCalledWith( + createBrowserTab.mock.calls[0][2].browserPageId, + { + environmentId: 'env-1', + remotePageId: 'remote-browser-page-1' + } ) - expect(setRemoteBrowserPageHandle).toHaveBeenCalledWith('local-page-1', { - environmentId: 'env-1', - remotePageId: 'remote-browser-page-1' - }) }) it('defaults unknown protocols to http for built-in browser opens', () => { diff --git a/src/renderer/src/components/tab-group/useTabGroupCreationCommands.local-shell.test.ts b/src/renderer/src/components/tab-group/useTabGroupCreationCommands.local-shell.test.ts index a6714e864a4..f1e34ece928 100644 --- a/src/renderer/src/components/tab-group/useTabGroupCreationCommands.local-shell.test.ts +++ b/src/renderer/src/components/tab-group/useTabGroupCreationCommands.local-shell.test.ts @@ -77,7 +77,6 @@ describe('tab group "+" menu shell launch on a locally-owned workspace', () => { vi.clearAllMocks() storeState.activeWorkspaceExecutionHostId = 'local' mocks.getRuntimeEnvironmentIdForWorktree.mockReturnValue(null) - mocks.createTab.mockReturnValue({ id: 'local-tab-1' }) stubRuntimeEnvironmentBridge({ call: mocks.runtimeCall }) }) @@ -97,8 +96,10 @@ describe('tab group "+" menu shell launch on a locally-owned workspace', () => { await vi.waitFor(() => expect(mocks.createTab).toHaveBeenCalled()) expect(mocks.runtimeCall).not.toHaveBeenCalled() - expect(mocks.createTab).toHaveBeenCalledWith(WORKTREE_ID, GROUP_ID, 'powershell.exe') - expect(mocks.setActiveTab).toHaveBeenCalledWith('local-tab-1') + expect(mocks.createTab).toHaveBeenCalledWith(WORKTREE_ID, GROUP_ID, 'powershell.exe', { + id: expect.any(String) + }) + expect(mocks.setActiveTab).toHaveBeenCalledWith(mocks.createTab.mock.calls[0][3].id) }) it('leaves the workspace on its own execution host', async () => { diff --git a/src/renderer/src/components/tab-group/useTabGroupCreationCommands.ts b/src/renderer/src/components/tab-group/useTabGroupCreationCommands.ts index d2cd4418e25..83dbe1cccb9 100644 --- a/src/renderer/src/components/tab-group/useTabGroupCreationCommands.ts +++ b/src/renderer/src/components/tab-group/useTabGroupCreationCommands.ts @@ -3,6 +3,7 @@ import { toast } from 'sonner' import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { useAppStore } from '../../store' import { focusTerminalTabSurface } from '../../lib/focus-terminal-tab-surface' +import { createBrowserUuid } from '../../lib/browser-uuid' import { createWebRuntimeSessionBrowserTab } from '../../runtime/web-runtime-browser-creation' import { createWebRuntimeSessionTerminal } from '../../runtime/web-runtime-terminal-creation' import { isWebRuntimeSessionActive } from '../../runtime/web-runtime-session-environment' @@ -159,10 +160,11 @@ export function useTabGroupCreationCommands({ if (outcome.status === 'created' || isWebRuntimeSessionActive(environmentId)) { return } - const terminal = createTab(worktreeId, groupId, shellOverride) - setActiveTab(terminal.id) + const tabId = createBrowserUuid() + createTab(worktreeId, groupId, shellOverride, { id: tabId }) + setActiveTab(tabId) setActiveTabType('terminal', worktreeId) - focusTerminalTabSurface(terminal.id) + focusTerminalTabSurface(tabId) })() } } diff --git a/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts b/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts index 99e063b4cd7..6473a15fdd2 100644 --- a/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts +++ b/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts @@ -254,15 +254,16 @@ describe('useTabGroupWorkspaceModel terminal activation focus', () => { }) it('falls back to a local shell when the typed remote-create outcome is unavailable', async () => { - mocks.createTab.mockReturnValue({ id: 'terminal-new' }) const { useTabGroupWorkspaceModel } = await import('./useTabGroupWorkspaceModel') const model = useTabGroupWorkspaceModel({ groupId: 'group-1', worktreeId: 'wt-1' }) model.commands.newTerminalWithShell('zsh') await vi.waitFor(() => expect(mocks.createTab).toHaveBeenCalled()) - expect(mocks.createTab).toHaveBeenCalledWith('wt-1', 'group-1', 'zsh') - expect(mocks.setActiveTab).toHaveBeenCalledWith('terminal-new') + expect(mocks.createTab).toHaveBeenCalledWith('wt-1', 'group-1', 'zsh', { + id: expect.any(String) + }) + expect(mocks.setActiveTab).toHaveBeenCalledWith(mocks.createTab.mock.calls[0][3].id) }) it('returns keyboard focus to the active split pane leaf when a terminal tab is activated', async () => { diff --git a/src/renderer/src/components/terminal/terminal-tab-actions.test.ts b/src/renderer/src/components/terminal/terminal-tab-actions.test.ts index 1bb5cd76372..502e2779c06 100644 --- a/src/renderer/src/components/terminal/terminal-tab-actions.test.ts +++ b/src/renderer/src/components/terminal/terminal-tab-actions.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const { activateWebRuntimeSessionTabMock, + createBrowserUuidMock, closeWebRuntimeSessionTabMock, createWebRuntimeSessionTerminalMock, getLatestWebSessionTabsPublicationEpochMock, @@ -12,6 +13,7 @@ const { toHostSessionTabIdMock } = vi.hoisted(() => ({ activateWebRuntimeSessionTabMock: vi.fn(), + createBrowserUuidMock: vi.fn(() => 'tab-1'), closeWebRuntimeSessionTabMock: vi.fn(), createWebRuntimeSessionTerminalMock: vi.fn(), getLatestWebSessionTabsPublicationEpochMock: vi.fn(() => 'epoch-1'), @@ -28,6 +30,10 @@ vi.mock('@/store', () => ({ } })) +vi.mock('@/lib/browser-uuid', () => ({ + createBrowserUuid: createBrowserUuidMock +})) + vi.mock('@/runtime/web-runtime-session-tab-lifecycle', () => ({ activateWebRuntimeSessionTab: activateWebRuntimeSessionTabMock, closeWebRuntimeSessionTab: closeWebRuntimeSessionTabMock @@ -84,13 +90,14 @@ describe('createNewTerminalTab', () => { createNewTerminalTab('wt-1', 'zsh') - expect(createTab).toHaveBeenCalledWith('wt-1', undefined, 'zsh', undefined) + expect(createTab).toHaveBeenCalledWith('wt-1', undefined, 'zsh', { id: 'tab-1' }) expect(setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1') expect(setTabBarOrder).toHaveBeenCalledWith('wt-1', ['tab-1']) expect(createWebRuntimeSessionTerminalMock).not.toHaveBeenCalled() }) it('keeps open browser tabs before the new terminal when no order is saved yet', () => { + createBrowserUuidMock.mockReturnValueOnce('term-new') const setTabBarOrder = vi.fn() getStateMock .mockReturnValueOnce({ @@ -178,6 +185,7 @@ describe('createNewTerminalTab', () => { createNewTerminalTab('wt-1', undefined, { startupCwd: '/repo/packages/app' }) expect(createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { + id: 'tab-1', startupCwd: '/repo/packages/app' }) expect(setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1') diff --git a/src/renderer/src/components/terminal/terminal-tab-create.ts b/src/renderer/src/components/terminal/terminal-tab-create.ts index ab3d6531c42..bcfeac9fcd5 100644 --- a/src/renderer/src/components/terminal/terminal-tab-create.ts +++ b/src/renderer/src/components/terminal/terminal-tab-create.ts @@ -3,6 +3,7 @@ import { createWebRuntimeSessionTerminal } from '@/runtime/web-runtime-terminal- import { isWebRuntimeSessionActive } from '@/runtime/web-runtime-session-environment' import { resolveTerminalWorktreeRoute } from '@/lib/terminal-worktree-route' import { persistAgentLaunchTabOrder } from '@/lib/launch-agent-tab-order' +import { createBrowserUuid } from '@/lib/browser-uuid' export function createNewTerminalTab( activeWorktreeId: string | null, @@ -31,14 +32,13 @@ export function createNewTerminalTab( }) return } - const newTab = state.createTab( - activeWorktreeId, - undefined, - shellOverride, - options?.startupCwd ? { startupCwd: options.startupCwd } : undefined - ) + const tabId = createBrowserUuid() + state.createTab(activeWorktreeId, undefined, shellOverride, { + id: tabId, + ...(options?.startupCwd ? { startupCwd: options.startupCwd } : {}) + }) state.setActiveTabType('terminal', activeWorktreeId) // Why: without a saved order the tab bar falls back to terminals-first, so // persist the current visual order with the new terminal last. - persistAgentLaunchTabOrder(activeWorktreeId, newTab.id) + persistAgentLaunchTabOrder(activeWorktreeId, tabId) } diff --git a/src/renderer/src/components/use-terminal-create-actions.test.tsx b/src/renderer/src/components/use-terminal-create-actions.test.tsx index db033f32f34..95c19c5562f 100644 --- a/src/renderer/src/components/use-terminal-create-actions.test.tsx +++ b/src/renderer/src/components/use-terminal-create-actions.test.tsx @@ -132,12 +132,16 @@ describe('useTerminalCreateActions creation gates', () => { tabBarOrderByWorktree: { [WORKTREE_ID]: ['browser-1', 'term-a', 'browser-1'] }, setTabBarOrder } - mocks.createTab.mockImplementation(() => { - terminals.push({ id: 'term-new' }) - return { id: 'term-new' } - }) + mocks.createTab.mockImplementation( + (_worktreeId: string, _groupId: undefined, _shell: string, options: { id: string }) => { + terminals.push({ id: options.id }) + } + ) renderActions().handleNewTab('/bin/zsh') - expect(mocks.createTab).toHaveBeenCalledWith(WORKTREE_ID, undefined, '/bin/zsh') - expect(setTabBarOrder).toHaveBeenCalledWith(WORKTREE_ID, ['browser-1', 'term-a', 'term-new']) + expect(mocks.createTab).toHaveBeenCalledWith(WORKTREE_ID, undefined, '/bin/zsh', { + id: expect.any(String) + }) + const tabId = mocks.createTab.mock.calls[0][3].id + expect(setTabBarOrder).toHaveBeenCalledWith(WORKTREE_ID, ['browser-1', 'term-a', tabId]) }) }) diff --git a/src/renderer/src/components/use-terminal-create-actions.ts b/src/renderer/src/components/use-terminal-create-actions.ts index baee50194ce..2801af6e2a9 100644 --- a/src/renderer/src/components/use-terminal-create-actions.ts +++ b/src/renderer/src/components/use-terminal-create-actions.ts @@ -3,6 +3,7 @@ import { toast } from 'sonner' import type { TuiAgent } from '../../../shared/tui-agent' import { useAppStore } from '../store' import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface' +import { createBrowserUuid } from '@/lib/browser-uuid' import { createWebRuntimeSessionBrowserTab } from '@/runtime/web-runtime-browser-creation' import { createWebRuntimeSessionTerminal } from '@/runtime/web-runtime-terminal-creation' import { isWebRuntimeSessionActive } from '@/runtime/web-runtime-session-environment' @@ -52,10 +53,11 @@ export function useTerminalCreateActions(controller: TerminalColdActivationContr void openNewTerminalTabInActiveWorkspace(targetGroupId) return } - const newTab = createTab(activeWorktreeId, undefined, shellOverride) + const tabId = createBrowserUuid() + createTab(activeWorktreeId, undefined, shellOverride, { id: tabId }) setActiveTabType('terminal', activeWorktreeId) - persistAgentLaunchTabOrder(activeWorktreeId, newTab.id) - focusTerminalTabSurface(newTab.id) + persistAgentLaunchTabOrder(activeWorktreeId, tabId) + focusTerminalTabSurface(tabId) }, [activeWorktreeId, createTab, openNewTerminalTabInActiveWorkspace, setActiveTabType] ) diff --git a/src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.test.ts b/src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.test.ts index 99955b12767..f637c3a91aa 100644 --- a/src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.test.ts +++ b/src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.test.ts @@ -54,9 +54,13 @@ describe('registerTabLifecycleIpcBridge new terminal tab', () => { openFiles: [], browserTabsByWorktree: { [WORKTREE_ID]: [{ id: 'browser-1' }] }, tabBarOrderByWorktree: { [WORKTREE_ID]: ['browser-1', 'term-a', 'browser-1'] }, - createTab: () => { - terminals.push({ id: 'term-new' }) - return { id: 'term-new' } + createTab: ( + _worktreeId: string, + _groupId: undefined, + _shell: undefined, + options: { id: string } + ) => { + terminals.push({ id: options.id }) }, setActiveTabType: vi.fn(), setTabBarOrder @@ -64,6 +68,10 @@ describe('registerTabLifecycleIpcBridge new terminal tab', () => { registerTabLifecycleIpcBridge([]) mocks.listeners.onNewTerminalTab() await vi.waitFor(() => expect(setTabBarOrder).toHaveBeenCalled()) - expect(setTabBarOrder).toHaveBeenCalledWith(WORKTREE_ID, ['browser-1', 'term-a', 'term-new']) + expect(setTabBarOrder).toHaveBeenCalledWith(WORKTREE_ID, [ + 'browser-1', + 'term-a', + terminals[1].id + ]) }) }) diff --git a/src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.ts b/src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.ts index 189c3e4da33..8aac9061eb1 100644 --- a/src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.ts +++ b/src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.ts @@ -1,4 +1,5 @@ import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface' +import { createBrowserUuid } from '@/lib/browser-uuid' import { persistAgentLaunchTabOrder } from '@/lib/launch-agent-tab-order' import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' import { createWebRuntimeSessionTerminal } from '@/runtime/web-runtime-terminal-creation' @@ -41,10 +42,11 @@ export function registerTabLifecycleIpcBridge(unsubs: (() => void)[]): void { if (outcome.status === 'created' || isWebRuntimeSessionActive(environmentId)) { return } - const newTab = store.createTab(worktreeId) + const tabId = createBrowserUuid() + store.createTab(worktreeId, undefined, undefined, { id: tabId }) store.setActiveTabType('terminal', worktreeId) - persistAgentLaunchTabOrder(worktreeId, newTab.id) - focusTerminalTabSurface(newTab.id) + persistAgentLaunchTabOrder(worktreeId, tabId) + focusTerminalTabSurface(tabId) })() }) ) diff --git a/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts b/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts index e489341fe5d..27528ece83f 100644 --- a/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts @@ -41,7 +41,7 @@ describe('useIpcEvents updater integration', () => { environmentId: null, activate: true }) - expect(createTab).toHaveBeenCalledWith('wt-1') + expect(createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, expect.anything()) expect(setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1') // Exact regression sequence: Local default -> connect/navigate Windows 2 -> diff --git a/src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts b/src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts index 451caca9d45..5ca8c137b1b 100644 --- a/src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts +++ b/src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts @@ -11,6 +11,11 @@ const mockStoreState = vi.hoisted(() => ({ createEmptySplitGroup: vi.fn(), createUnifiedTab: vi.fn(), createUnifiedTabInSplit: vi.fn(), + getTab: vi.fn((tabId: string) => + Object.values(mockStoreState.unifiedTabsByWorktree) + .flat() + .find((tab) => tab.id === tabId) + ), dropUnifiedTab: vi.fn(), folderWorkspaces: [] as { id: string @@ -38,6 +43,21 @@ const mockStoreState = vi.hoisted(() => ({ > })) +function stubCreatedSimulatorTab(groupId: string): void { + mockStoreState.createUnifiedTab.mockImplementation( + (worktreeId: string, contentType: string, init: { id: string }) => { + mockStoreState.unifiedTabsByWorktree[worktreeId] = [ + ...(mockStoreState.unifiedTabsByWorktree[worktreeId] ?? []), + { id: init.id, groupId, contentType } + ] + } + ) +} + +function createdSimulatorTabId(): string { + return mockStoreState.createUnifiedTab.mock.calls[0][2].id +} + vi.mock('@/store', () => ({ useAppStore: { getState: () => mockStoreState @@ -100,14 +120,10 @@ describe('ensureSimulatorTab', () => { } ] } - mockStoreState.createUnifiedTab.mockReturnValue({ - id: 'sim-remote', - groupId: 'group-1', - contentType: 'simulator' - }) + stubCreatedSimulatorTab('group-1') const { ensureSimulatorTab } = await import('./ensure-simulator-tab') - expect(ensureSimulatorTab('wt-1')).toBe('sim-remote') + expect(ensureSimulatorTab('wt-1')).toBe(createdSimulatorTabId()) expect(mockStoreState.createUnifiedTab).toHaveBeenCalled() }) @@ -249,22 +265,19 @@ describe('ensureSimulatorTab', () => { } } mockStoreState.unifiedTabsByWorktree = { 'wt-1': [] } - mockStoreState.createUnifiedTab.mockReturnValue({ - id: 'sim-2', - groupId: 'group-2', - contentType: 'simulator' - }) + stubCreatedSimulatorTab('group-2') const { ensureSimulatorTab } = await import('./ensure-simulator-tab') - expect(ensureSimulatorTab('wt-1', { placement: 'rightSplit' })).toBe('sim-2') + expect(ensureSimulatorTab('wt-1', { placement: 'rightSplit' })).toBe(createdSimulatorTabId()) expect(mockStoreState.createUnifiedTabInSplit).not.toHaveBeenCalled() expect(mockStoreState.createUnifiedTab).toHaveBeenCalledWith('wt-1', 'simulator', { + id: expect.any(String), label: 'Mobile Emulator', targetGroupId: 'group-2', activate: true }) - expect(mockStoreState.activateTab).toHaveBeenCalledWith('sim-2') + expect(mockStoreState.activateTab).toHaveBeenCalledWith(createdSimulatorTabId()) expect(mockStoreState.focusGroup).toHaveBeenCalledWith('wt-1', 'group-2') expect(mockStoreState.setActiveTabType).toHaveBeenCalledWith('simulator', 'wt-1') }) @@ -272,14 +285,10 @@ describe('ensureSimulatorTab', () => { it('falls back to the source group when atomic right split creation fails', async () => { mockStoreState.unifiedTabsByWorktree = { 'wt-1': [] } mockStoreState.createUnifiedTabInSplit.mockReturnValue(null) - mockStoreState.createUnifiedTab.mockReturnValue({ - id: 'sim-3', - groupId: 'group-1', - contentType: 'simulator' - }) + stubCreatedSimulatorTab('group-1') const { ensureSimulatorTab } = await import('./ensure-simulator-tab') - expect(ensureSimulatorTab('wt-1', { placement: 'rightSplit' })).toBe('sim-3') + expect(ensureSimulatorTab('wt-1', { placement: 'rightSplit' })).toBe(createdSimulatorTabId()) expect(mockStoreState.createUnifiedTabInSplit).toHaveBeenCalledWith( 'wt-1', @@ -294,6 +303,7 @@ describe('ensureSimulatorTab', () => { } ) expect(mockStoreState.createUnifiedTab).toHaveBeenCalledWith('wt-1', 'simulator', { + id: expect.any(String), label: 'Mobile Emulator', targetGroupId: 'group-1', activate: true @@ -304,20 +314,17 @@ describe('ensureSimulatorTab', () => { it('does not create a split for background auto-attach', async () => { mockStoreState.unifiedTabsByWorktree = { 'wt-1': [] } - mockStoreState.createUnifiedTab.mockReturnValue({ - id: 'sim-4', - groupId: 'group-1', - contentType: 'simulator' - }) + stubCreatedSimulatorTab('group-1') const { ensureSimulatorTab } = await import('./ensure-simulator-tab') expect(ensureSimulatorTab('wt-1', { placement: 'rightSplit', surfacePane: false })).toBe( - 'sim-4' + createdSimulatorTabId() ) expect(mockStoreState.dropUnifiedTab).not.toHaveBeenCalled() expect(mockStoreState.createUnifiedTabInSplit).not.toHaveBeenCalled() expect(mockStoreState.createUnifiedTab).toHaveBeenCalledWith('wt-1', 'simulator', { + id: expect.any(String), label: 'Mobile Emulator', targetGroupId: 'group-1', activate: false @@ -335,17 +342,14 @@ describe('ensureSimulatorTab', () => { { id: 'wt-1', hostId: 'runtime:host-b' } ]) mockStoreState.unifiedTabsByWorktree = { 'wt-1': [] } - mockStoreState.createUnifiedTab.mockReturnValue({ - id: 'sim-local', - groupId: 'group-1', - contentType: 'simulator' - }) + stubCreatedSimulatorTab('group-1') const { ensureSimulatorTab } = await import('./ensure-simulator-tab') expect(ensureSimulatorTab('wt-1', { surfacePane: false, executionHostId: 'local' })).toBe( - 'sim-local' + createdSimulatorTabId() ) expect(mockStoreState.createUnifiedTab).toHaveBeenCalledWith('wt-1', 'simulator', { + id: expect.any(String), label: 'Mobile Emulator', targetGroupId: 'group-1', activate: false, @@ -367,17 +371,14 @@ describe('ensureSimulatorTab', () => { } ] } - mockStoreState.createUnifiedTab.mockReturnValue({ - id: 'sim-local', - groupId: 'group-1', - contentType: 'simulator' - }) + stubCreatedSimulatorTab('group-1') const { ensureSimulatorTab } = await import('./ensure-simulator-tab') expect(ensureSimulatorTab('wt-1', { surfacePane: false, executionHostId: 'local' })).toBe( - 'sim-local' + createdSimulatorTabId() ) expect(mockStoreState.createUnifiedTab).toHaveBeenCalledWith('wt-1', 'simulator', { + id: expect.any(String), label: 'Mobile Emulator', targetGroupId: 'group-1', activate: false, diff --git a/src/renderer/src/lib/ensure-simulator-tab.ts b/src/renderer/src/lib/ensure-simulator-tab.ts index 49fce8d1b40..e44533b4a9a 100644 --- a/src/renderer/src/lib/ensure-simulator-tab.ts +++ b/src/renderer/src/lib/ensure-simulator-tab.ts @@ -3,6 +3,7 @@ import { findReusableRightSplitGroupId } from './emulator-right-split-target' import { cancelPendingSimulatorPaneShutdown } from './simulator-pane-shutdown-scheduler' import { shouldShutdownSimulatorForPaneUnmountFromTabs } from './simulator-tab-shutdown' import { translate } from '@/i18n/i18n' +import { createBrowserUuid } from './browser-uuid' import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../shared/execution-host' import { findAmbiguousWorktreeIds, @@ -126,16 +127,16 @@ export function ensureSimulatorTab( sourceGroupId ) if (reusableRightGroupId) { - const tab = store.createUnifiedTab(worktreeId, 'simulator', { + const tabId = createBrowserUuid() + store.createUnifiedTab(worktreeId, 'simulator', { + id: tabId, label: translate('auto.lib.ensure.simulator.tab.372d21d428', 'Mobile Emulator'), targetGroupId: reusableRightGroupId, activate: true, ...(executionHostId ? { executionHostId } : {}) }) - store.activateTab(tab.id) - store.setActiveTabType('simulator', worktreeId) - store.focusGroup(worktreeId, tab.groupId) - return tab.id + surfaceCreatedSimulatorTab(worktreeId, tabId) + return tabId } // Why: publish the simulator directly in its split group. A two-step @@ -158,18 +159,29 @@ export function ensureSimulatorTab( } } - const tab = store.createUnifiedTab(worktreeId, 'simulator', { + const tabId = createBrowserUuid() + store.createUnifiedTab(worktreeId, 'simulator', { + id: tabId, label: translate('auto.lib.ensure.simulator.tab.372d21d428', 'Mobile Emulator'), targetGroupId: sourceGroupId, activate: shouldSurface, ...(executionHostId ? { executionHostId } : {}) }) if (shouldSurface) { - store.activateTab(tab.id) - store.setActiveTabType('simulator', worktreeId) - store.focusGroup(worktreeId, tab.groupId) + surfaceCreatedSimulatorTab(worktreeId, tabId) + } + return tabId +} + +function surfaceCreatedSimulatorTab(worktreeId: string, tabId: string): void { + const store = useAppStore.getState() + store.activateTab(tabId) + store.setActiveTabType('simulator', worktreeId) + // Why read by id: placement falls back to the active group when the requested one is gone. + const groupId = store.getTab(tabId)?.groupId + if (groupId) { + store.focusGroup(worktreeId, groupId) } - return tab.id } export { shouldShutdownSimulatorForPaneUnmountFromTabs } diff --git a/src/renderer/src/lib/floating-workspace-tab-creation.ts b/src/renderer/src/lib/floating-workspace-tab-creation.ts index 677215ae824..bd176af5847 100644 --- a/src/renderer/src/lib/floating-workspace-tab-creation.ts +++ b/src/renderer/src/lib/floating-workspace-tab-creation.ts @@ -1,11 +1,10 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' -import type { BrowserTab } from '../../../shared/browser-workspace-types' -import type { TerminalTab } from '../../../shared/terminal-tab-types' import { createUntitledMarkdownFileWithTemplateSelection } from './create-untitled-markdown' import { getConnectionId } from './connection-context' import { detectLanguage } from './language-detect' import type { AppState } from '@/store/types' import { focusTerminalTabSurface } from './focus-terminal-tab-surface' +import { createBrowserUuid } from './browser-uuid' import { translate } from '@/i18n/i18n' import { assertClientCreationActionAvailable } from './client-creation-action-policy' @@ -23,19 +22,19 @@ export async function createFloatingWorkspaceTerminalTab( shellOverride?: string, /** A split group's own "+"; omitted, the tab lands in the focused group. */ groupId?: string -): Promise { +): Promise { const targetGroupId = groupId ?? store.activeGroupIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID] // Why: the floating workspace is a local scratchpad; a focused remote runtime // must not own its SSH/tmux terminals or prune them via session snapshots. - const tab = store.createTab(FLOATING_TERMINAL_WORKTREE_ID, targetGroupId, shellOverride) - focusTerminalTabSurface(tab.id) - return tab + const tabId = createBrowserUuid() + store.createTab(FLOATING_TERMINAL_WORKTREE_ID, targetGroupId, shellOverride, { id: tabId }) + focusTerminalTabSurface(tabId) } export async function createFloatingWorkspaceBrowserTab( store: FloatingWorkspaceBrowserStore -): Promise { +): Promise { assertClientCreationActionAvailable( store as AppState, FLOATING_TERMINAL_WORKTREE_ID, @@ -46,7 +45,7 @@ export async function createFloatingWorkspaceBrowserTab( // Why: browser tabs in the floating workspace share the same local-only // ownership rule as floating terminals. - return store.createBrowserTab(FLOATING_TERMINAL_WORKTREE_ID, url, { + store.createBrowserTab(FLOATING_TERMINAL_WORKTREE_ID, url, { title: translate('auto.lib.floating.workspace.tab.creation.f3785eddc2', 'New Browser Tab'), focusAddressBar: true, targetGroupId, diff --git a/src/renderer/src/lib/floating-workspace-terminal-actions.test.ts b/src/renderer/src/lib/floating-workspace-terminal-actions.test.ts index 3ea0cefdac7..7d956d015ed 100644 --- a/src/renderer/src/lib/floating-workspace-terminal-actions.test.ts +++ b/src/renderer/src/lib/floating-workspace-terminal-actions.test.ts @@ -451,49 +451,49 @@ describe('createFloatingWorkspaceTerminalTab', () => { }) it('creates and focuses a local floating workspace terminal in the active floating group', async () => { - const tab = makeTab('floating-tab-1') const store = { activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-group' }, settings: { activeRuntimeEnvironmentId: null }, - createTab: vi.fn().mockReturnValue(tab), + createTab: vi.fn(), activateTab: vi.fn() } createWebRuntimeSessionTerminalMock.mockResolvedValue(false) - await expect(createFloatingWorkspaceTerminalTab(store as never)).resolves.toBe(tab) + await createFloatingWorkspaceTerminalTab(store) expect(createWebRuntimeSessionTerminalMock).not.toHaveBeenCalled() expect(store.createTab).toHaveBeenCalledWith( FLOATING_TERMINAL_WORKTREE_ID, 'floating-group', - undefined + undefined, + { id: expect.any(String) } ) // Why: createTab itself activates the new tab within the floating group. expect(store.activateTab).not.toHaveBeenCalled() - expect(focusTerminalTabSurfaceMock).toHaveBeenCalledWith('floating-tab-1') + expect(focusTerminalTabSurfaceMock).toHaveBeenCalledWith(store.createTab.mock.calls[0][3].id) }) it('ignores the active runtime and keeps floating workspace terminals local', async () => { - const tab = makeTab('floating-tab-runtime') const store = { activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-group' }, settings: { activeRuntimeEnvironmentId: 'env-1' }, - createTab: vi.fn().mockReturnValue(tab), + createTab: vi.fn(), activateTab: vi.fn() } createWebRuntimeSessionTerminalMock.mockResolvedValue(true) - await expect(createFloatingWorkspaceTerminalTab(store as never, 'pwsh')).resolves.toBe(tab) + await createFloatingWorkspaceTerminalTab(store, 'pwsh') expect(createWebRuntimeSessionTerminalMock).not.toHaveBeenCalled() expect(store.createTab).toHaveBeenCalledWith( FLOATING_TERMINAL_WORKTREE_ID, 'floating-group', - 'pwsh' + 'pwsh', + { id: expect.any(String) } ) // Why: createTab itself activates the new tab within the floating group. expect(store.activateTab).not.toHaveBeenCalled() - expect(focusTerminalTabSurfaceMock).toHaveBeenCalledWith('floating-tab-runtime') + expect(focusTerminalTabSurfaceMock).toHaveBeenCalledWith(store.createTab.mock.calls[0][3].id) }) }) @@ -503,16 +503,15 @@ describe('createFloatingWorkspaceBrowserTab', () => { }) it('creates floating browser tabs in the active floating group', async () => { - const browserTab = { id: 'browser-1' } const store = { activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-group' }, browserDefaultUrl: 'about:blank', settings: { activeRuntimeEnvironmentId: null }, - createBrowserTab: vi.fn().mockReturnValue(browserTab) + createBrowserTab: vi.fn() } createWebRuntimeSessionBrowserTabMock.mockResolvedValue(false) - await expect(createFloatingWorkspaceBrowserTab(store as never)).resolves.toBe(browserTab) + await createFloatingWorkspaceBrowserTab(store) expect(createWebRuntimeSessionBrowserTabMock).not.toHaveBeenCalled() expect(store.createBrowserTab).toHaveBeenCalledWith( diff --git a/src/renderer/src/lib/workspace-port-actions.ts b/src/renderer/src/lib/workspace-port-actions.ts index b2bb5f30ffa..6cfedb8c173 100644 --- a/src/renderer/src/lib/workspace-port-actions.ts +++ b/src/renderer/src/lib/workspace-port-actions.ts @@ -12,6 +12,7 @@ import type { import type { LocalhostWorktreeLabelRoute } from '../../../shared/localhost-worktree-labels' import { runWorkspacePortScanForTarget } from './workspace-port-scan-client' import { browserUrlForPort } from './workspace-port-urls' +import { createBrowserUuid } from './browser-uuid' import { BROWSER_SCREENCAST_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' import { RUNTIME_BROWSER_UNAVAILABLE_MESSAGE } from './client-creation-action-policy' @@ -148,14 +149,13 @@ export async function openWorkspacePortInBrowser(args: { { worktree: toRuntimeWorktreeSelector(worktreeId), url }, { timeoutMs: 30_000 } ) - const tab = args.createBrowserTab(worktreeId, url, { + const browserPageId = createBrowserUuid() + args.createBrowserTab(worktreeId, url, { activate: true, - browserRuntimeEnvironmentId: args.runtimeTarget.environmentId + browserRuntimeEnvironmentId: args.runtimeTarget.environmentId, + browserPageId }) - if (!tab.activePageId) { - return { ok: false, reason: 'Failed to create a browser page.' } - } - args.setRemoteBrowserPageHandle(tab.activePageId, { + args.setRemoteBrowserPageHandle(browserPageId, { environmentId: args.runtimeTarget.environmentId, remotePageId: remotePage.browserPageId }) From 8a6cc6c13846f7d38fbaf42a545e07ae06f4da17 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:10:33 -0400 Subject: [PATCH 26/53] fix(terminal): bring each pane back after a terminal service restart when it is shown (#28880) * fix(terminal): bring each pane back after a terminal service restart when it is shown After Settings -> "Restart terminal service", every pane that was open kept its dead process: typing did nothing and agents never resumed until Orca relaunched. A restart-replaced exit now arms the pane: the visible pane spawns its replacement at once, any other pane when it is next shown, through the same fresh spawn and cold-restore agent resume the hibernation wake uses, so old output stays above the new prompt. The exit keeps the agent status the resume reads, and clears the dead binding so the replacement binds as a first spawn. Local spawns wait for an in-flight restart so they reach the new daemon. Claude-Session: p2-headless * fix(terminal): resume from the live recovery record; skip a pane another flow rebound A restart exit is a process death to main's agent-status store, which clears the pane's row before the window sees the exit, so keeping the window's copy changed nothing. The resume reads the live recovery record every agent turn already writes, as a relaunch after a crash does. The replacement also stands down when the pane's stored binding already names another process. Claude-Session: p2-headless * fix(terminal): send every restart exit down one path A finished-turn agent pane took the hibernation wake, which gives up when its record changes before the pane is shown. Every restart exit now arms the replacement directly. Claude-Session: p2-headless --- src/main/daemon/daemon-restart-state.test.ts | 34 +++ src/main/daemon/daemon-restart-state.ts | 8 + src/main/ipc/pty/register-handlers.ts | 8 +- ...pty-connection-restart-replacement.test.ts | 275 ++++++++++++++++++ .../pty-connection/pty-exit-hibernate.ts | 14 +- .../restart-replacement-on-show.ts | 38 +++ .../session-reconcile-dispose.ts | 2 + .../workspace-layout-oracle-known-on-main.ts | 7 - 8 files changed, 374 insertions(+), 12 deletions(-) create mode 100644 src/main/daemon/daemon-restart-state.test.ts create mode 100644 src/renderer/src/components/terminal-pane/pty-connection-restart-replacement.test.ts create mode 100644 src/renderer/src/components/terminal-pane/pty-connection/restart-replacement-on-show.ts diff --git a/src/main/daemon/daemon-restart-state.test.ts b/src/main/daemon/daemon-restart-state.test.ts new file mode 100644 index 00000000000..f9280702050 --- /dev/null +++ b/src/main/daemon/daemon-restart-state.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { runCoalescedDaemonRestart, settledDaemonRestart } from './daemon-restart-state' + +describe('settledDaemonRestart', () => { + it('is undefined with no restart running', () => { + expect(settledDaemonRestart()).toBeUndefined() + }) + + it('holds until the running restart settles, whether it succeeds or fails', async () => { + for (const outcome of ['resolve', 'reject'] as const) { + let finish!: () => void + const restart = runCoalescedDaemonRestart( + () => + new Promise((resolve, reject) => { + finish = () => + outcome === 'resolve' ? resolve({ killedCount: 1 }) : reject(new Error('x')) + }) + ) + restart.catch(() => {}) + let settled = false + void settledDaemonRestart()?.then(() => { + settled = true + }) + await Promise.resolve() + expect(settled).toBe(false) + + finish() + await restart.catch(() => {}) + await Promise.resolve() + expect(settled).toBe(true) + expect(settledDaemonRestart()).toBeUndefined() + } + }) +}) diff --git a/src/main/daemon/daemon-restart-state.ts b/src/main/daemon/daemon-restart-state.ts index e37fad69709..70918375898 100644 --- a/src/main/daemon/daemon-restart-state.ts +++ b/src/main/daemon/daemon-restart-state.ts @@ -9,6 +9,14 @@ export function isDaemonRestartInFlight(): boolean { return restartInFlight !== null } +/** Settles once an in-flight restart has swapped in its provider (or failed); undefined when none. */ +export function settledDaemonRestart(): Promise | undefined { + return restartInFlight?.then( + () => undefined, + () => undefined + ) +} + export function runCoalescedDaemonRestart( run: () => Promise ): Promise { diff --git a/src/main/ipc/pty/register-handlers.ts b/src/main/ipc/pty/register-handlers.ts index 4010711e28b..b40aececc21 100644 --- a/src/main/ipc/pty/register-handlers.ts +++ b/src/main/ipc/pty/register-handlers.ts @@ -1,3 +1,4 @@ +import { settledDaemonRestart } from '../../daemon/daemon-restart-state' import { getAppEnvironment } from '../../../shared/app-environment' import type { OrcaRuntimeService } from '../../runtime/orca-runtime' import type { Store } from '../../persistence' @@ -104,7 +105,12 @@ export function registerPtyHandlers( return undefined } // Why: during cold start the daemon provider swap overlaps first paint, so local spawns must wait; SSH/headless don't use the desktop daemon. - return options?.awaitLocalPtyStartup?.() + const startup = options?.awaitLocalPtyStartup?.() + // A pane replaced by a restart spawns while it runs; it must reach the new daemon, not the old. + const restart = settledDaemonRestart() + return startup && restart + ? Promise.all([startup, restart]).then(() => undefined) + : (startup ?? restart) } const getLocalPtyProviderStartupPromise = ( diff --git a/src/renderer/src/components/terminal-pane/pty-connection-restart-replacement.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-restart-replacement.test.ts new file mode 100644 index 00000000000..bbcd67e7ec1 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/pty-connection-restart-replacement.test.ts @@ -0,0 +1,275 @@ +import type * as React from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { flushAsyncTicks } from './pty-connection-test-async' +import { + leafIdForPane, + createMockTransport, + createPane, + createManager +} from './pty-connection-test-pane-fixtures' +import { buildPaneConnectionDeps } from './pty-connection-test-deps' +import { createInitialStoreState } from './pty-connection-test-store-fixtures' +import type { StoreState } from './pty-connection-test-store-state' +import type { MockTransport } from './pty-connection-test-pane-fixtures' +import { + installTerminalTestGlobals, + restoreTerminalTestGlobals +} from './pty-connection-test-environment' + +const { + resetAndRefreshAllTerminalWebglAtlases, + scheduleTerminalWebglAtlasRecovery, + scheduleRuntimeGraphSync, + shouldSeedCacheTimerOnInitialTitle, + toastInfo, + notifyCodexPaneBoundForStaleSweep +} = vi.hoisted(() => ({ + resetAndRefreshAllTerminalWebglAtlases: vi.fn(), + scheduleTerminalWebglAtlasRecovery: vi.fn(), + scheduleRuntimeGraphSync: vi.fn(), + shouldSeedCacheTimerOnInitialTitle: vi.fn(() => false), + toastInfo: vi.fn(), + notifyCodexPaneBoundForStaleSweep: vi.fn() +})) + +let mockStoreState: StoreState +let transportFactoryQueue: MockTransport[] = [] +let createdTransportOptions: Record[] = [] +let storeSubscribers: ((state: StoreState) => void)[] = [] + +vi.mock('@/runtime/sync-runtime-graph', () => ({ + scheduleRuntimeGraphSync +})) + +vi.mock('@/lib/pane-manager/pane-manager-registry', async (importOriginal) => ({ + ...(await importOriginal>()), + resetAndRefreshAllTerminalWebglAtlases +})) + +vi.mock('./terminal-webgl-atlas-recovery', () => ({ + scheduleTerminalWebglAtlasRecovery +})) + +vi.mock('@/store', () => ({ + useAppStore: { + getState: () => mockStoreState, + subscribe: (listener: (state: StoreState) => void) => { + storeSubscribers.push(listener) + return () => { + storeSubscribers = storeSubscribers.filter((candidate) => candidate !== listener) + } + } + } +})) + +vi.mock('@/lib/agent-status', async (importOriginal) => { + const { buildAgentStatusModuleMock } = await import('./pty-connection-test-environment') + return buildAgentStatusModuleMock(await importOriginal>()) +}) + +vi.mock('./cache-timer-seeding', () => ({ + shouldSeedCacheTimerOnInitialTitle +})) + +vi.mock('sonner', () => ({ + toast: { + info: toastInfo + } +})) + +vi.mock('@/lib/codex-stale-pane-sweep', () => ({ + notifyCodexPaneBoundForStaleSweep +})) + +// Why: the working→idle test invokes the real useNotificationDispatch hook outside React, so useCallback must pass through (safe suite-wide: no test here renders React). +vi.mock('react', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useCallback: unknown>(fn: T): T => fn + } +}) + +vi.mock('./pty-transport', () => ({ + createIpcPtyTransport: vi.fn((options: Record) => { + createdTransportOptions.push(options) + const nextTransport = transportFactoryQueue.shift() + if (!nextTransport) { + throw new Error('No mock transport queued') + } + return nextTransport + }) +})) + +vi.mock('./remote-runtime-pty-transport', () => ({ + createRemoteRuntimePtyTransport: vi.fn( + (_environmentId: string, options: Record) => { + createdTransportOptions.push(options) + const nextTransport = transportFactoryQueue.shift() + if (!nextTransport) { + throw new Error('No mock transport queued') + } + return nextTransport + } + ) +})) + +// Why: stub only getEagerPtyBufferHandle so tests can simulate a live eager buffer (adopt path) without standing up the real IPC dispatcher. +vi.mock('./pty-dispatcher', async (importOriginal) => { + const actual = await importOriginal>() + return { + ...actual, + getEagerPtyBufferHandle: vi.fn(() => undefined) + } +}) + +function createDeps(overrides: Record = {}) { + return buildPaneConnectionDeps(() => mockStoreState, overrides) +} + +describe('connectPanePty', () => { + beforeEach(async () => { + vi.resetModules() + vi.clearAllMocks() + transportFactoryQueue = [] + createdTransportOptions = [] + storeSubscribers = [] + mockStoreState = createInitialStoreState(() => mockStoreState) + await installTerminalTestGlobals() + }) + + afterEach(async () => { + await restoreTerminalTestGlobals() + }) + /** A pane running an agent, with the live recovery record the app keeps for every agent turn. */ + async function connectWithAgent(opts: { visible: boolean; turnDone?: boolean }) { + const { connectPanePty } = await import('./pty-connection') + const { deliverPtyExitToHandlers } = await import('./pty-exit-delivery') + const transport = createMockTransport('pty-pane-2') + transportFactoryQueue.push(transport) + const deps = createDeps({ isVisibleRef: { current: opts.visible } }) + const pane = createPane(2) + const paneKey = `tab-1:${leafIdForPane(2)}` + mockStoreState.removeAgentStatus.mockImplementation((key: string) => { + delete mockStoreState.agentStatusByPaneKey[key] + }) + mockStoreState.sleepingAgentSessionsByPaneKey[paneKey] = { + paneKey, + tabId: 'tab-1', + worktreeId: 'wt-1', + agent: 'claude', + providerSession: { key: 'session_id', id: 'sess-1' }, + prompt: 'fix it', + state: opts.turnDone ? 'done' : 'working', + capturedAt: 1, + updatedAt: 1, + origin: 'live' + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixtures implement the pane, manager and deps members connectPanePty reads. + const args = [pane, createManager(1), deps] as unknown as Parameters + const binding = connectPanePty(...args) + await flushAsyncTicks() + const onPtyExit = createdTransportOptions[0]?.onPtyExit + if (typeof onPtyExit !== 'function') { + throw new Error('expected the transport exit handler') + } + const exit = async (replacedByRestart: boolean): Promise => { + deliverPtyExitToHandlers({ + ptyId: 'tab-pty', + code: -1, + replacedByRestart, + primary: (code) => onPtyExit('tab-pty', code), + sidecars: [] + }) + await flushAsyncTicks() + } + const connects = (): number => transport.connect.mock.calls.length + const lastCommand = (): string | undefined => transport.connect.mock.calls.at(-1)?.[0]?.command + return { binding, exit, connects, lastCommand } + } + + it('resumes a restart-replaced hidden pane once, when it is next shown', async () => { + const pane = await connectWithAgent({ visible: false }) + const before = pane.connects() + + await pane.exit(true) + expect(pane.connects()).toBe(before) + + pane.binding.noteVisibilityResume() + await flushAsyncTicks() + expect(pane.connects()).toBe(before + 1) + expect(pane.lastCommand()).toContain('sess-1') + + pane.binding.noteVisibilityResume() + await flushAsyncTicks() + expect(pane.connects()).toBe(before + 1) + }) + + it('resumes a restart-replaced pane at once when it is visible', async () => { + const pane = await connectWithAgent({ visible: true }) + const before = pane.connects() + + await pane.exit(true) + + expect(pane.connects()).toBe(before + 1) + expect(pane.lastCommand()).toContain('sess-1') + }) + + it('spawns nothing for a restart-replaced pane closed before it is shown', async () => { + const pane = await connectWithAgent({ visible: false }) + await pane.exit(true) + const before = pane.connects() + + pane.binding.dispose() + pane.binding.noteVisibilityResume() + await flushAsyncTicks() + + expect(pane.connects()).toBe(before) + }) + + it('spawns nothing when another flow rebound the pane before it is shown', async () => { + const pane = await connectWithAgent({ visible: false }) + await pane.exit(true) + const before = pane.connects() + mockStoreState.terminalLayoutsByTabId = { + ...mockStoreState.terminalLayoutsByTabId, + 'tab-1': { + root: { type: 'leaf', leafId: leafIdForPane(2) }, + activeLeafId: leafIdForPane(2), + expandedLeafId: null, + ptyIdsByLeafId: { [leafIdForPane(2)]: 'pty-replacement' } + } + } + + pane.binding.noteVisibilityResume() + await flushAsyncTicks() + + expect(pane.connects()).toBe(before) + }) + + it('resumes an agent whose turn had finished the same way, once, when shown', async () => { + const pane = await connectWithAgent({ visible: false, turnDone: true }) + const before = pane.connects() + await pane.exit(true) + expect(pane.connects()).toBe(before) + + pane.binding.noteVisibilityResume() + await flushAsyncTicks() + pane.binding.noteVisibilityResume() + await flushAsyncTicks() + + expect(pane.connects()).toBe(before + 1) + expect(pane.lastCommand()).toContain('sess-1') + }) + + it('leaves a pane lost to any other -1 exit without a process', async () => { + const pane = await connectWithAgent({ visible: false }) + const before = pane.connects() + await pane.exit(false) + + pane.binding.noteVisibilityResume() + await flushAsyncTicks() + + expect(pane.connects()).toBe(before) + }) +}) diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pty-exit-hibernate.ts b/src/renderer/src/components/terminal-pane/pty-connection/pty-exit-hibernate.ts index e592c745ee2..7bc1ddf0402 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pty-exit-hibernate.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pty-exit-hibernate.ts @@ -19,6 +19,7 @@ import type { PtyPaneStartup } from '../pty-connection-types' import type { ConnectPanePtySession } from './connect-pane-pty-session' import { isPtyExitReplacedByRestart } from '../pty-exit-delivery' +import { armRestartReplacement } from './restart-replacement-on-show' // Why live+done arms too: kept from #16308's wake-sweep widening; hibernation itself only writes worktree-sleep notes. export function noteArmsHibernatedPaneWake(record: SleepingAgentSessionRecord): boolean { @@ -219,15 +220,15 @@ export function installPtyExitHibernate(session: ConnectPanePtySession): void { // Why: main clears gate state on PTY exit too; this only resets the // pane-local marker so a reused pane cannot skip re-marking a new PTY. session.releaseHiddenRendererPtyDelivery() + const replacedByRestart = isPtyExitReplacedByRestart(ptyId) // A synthetic host-loss exit only retires this transport. Keep the mounted // leaf↔PTY identity so reconnect/replay can adopt it after the host returns. - if (!isUnverifiedExit) { + // A restart's process is gone for good, so its replacement binds as a first spawn. + if (!isUnverifiedExit || replacedByRestart) { session.clearPanePtyFitBinding() } const isSuppressedExit = - session.deps.consumeSuppressedPtyExit(ptyId) || - preserveRendererBinding || - isPtyExitReplacedByRestart(ptyId) + session.deps.consumeSuppressedPtyExit(ptyId) || preserveRendererBinding || replacedByRestart if (!isSuppressedExit && !isUnverifiedExit) { session.clearExitedPanePtyLayoutBinding(ptyId) } @@ -262,6 +263,11 @@ export function installPtyExitHibernate(session: ConnectPanePtySession): void { // Why: the action that suppressed the exit owns whether the leaf binding // is a wake hint or should be discarded; runtime cleanup above is enough. session.manager.setPaneGpuRendering(session.pane.id, true) + if (replacedByRestart) { + // Nothing else will replace this process; the pane takes it back when shown. + armRestartReplacement(session, ptyId) + return + } const sleepingRecordEntry = session.getSleepingRecordForPane(useAppStore.getState()) if (sleepingRecordEntry && noteArmsHibernatedPaneWake(sleepingRecordEntry.record)) { // Why: hibernation killed this pane's PTY while hidden. The frozen TUI diff --git a/src/renderer/src/components/terminal-pane/pty-connection/restart-replacement-on-show.ts b/src/renderer/src/components/terminal-pane/pty-connection/restart-replacement-on-show.ts new file mode 100644 index 00000000000..411fa206b99 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/pty-connection/restart-replacement-on-show.ts @@ -0,0 +1,38 @@ +import { useAppStore } from '@/store' +import type { ConnectPanePtySession } from './connect-pane-pty-session' + +/** + * A terminal service restart ended this pane's process and nothing replaced it. The pane gets its + * replacement the way a relaunch would (fresh shell, cold-restore agent resume, old output kept), + * but only once it is shown, so a restart never resumes every background agent at once. + */ +export function armRestartReplacement(session: ConnectPanePtySession, ptyId: string): void { + session.restartReplacementPtyId = ptyId + if (session.deps.isVisibleRef.current) { + // Deferred off the exit handler, as the hibernation wake is. + queueMicrotask(() => consumeRestartReplacement(session)) + } +} + +/** Spawns the armed replacement, unless the pane closed or another flow already rebound it. */ +export function consumeRestartReplacement(session: ConnectPanePtySession): void { + const ptyId: string | null = session.restartReplacementPtyId ?? null + if (!ptyId) { + return + } + session.restartReplacementPtyId = null + const currentPtyId = session.transport.getPtyId() + const storedPtyId = + useAppStore.getState().terminalLayoutsByTabId[session.deps.tabId]?.ptyIdsByLeafId?.[ + session.pane.leafId + ] + if ( + session.disposed || + session.deps.paneTransportsRef.current.get(session.pane.id) !== session.transport || + (currentPtyId !== null && currentPtyId !== ptyId) || + (storedPtyId !== undefined && storedPtyId !== ptyId) + ) { + return + } + void session.startFreshColdRestoreAgentResume() +} diff --git a/src/renderer/src/components/terminal-pane/pty-connection/session-reconcile-dispose.ts b/src/renderer/src/components/terminal-pane/pty-connection/session-reconcile-dispose.ts index 8f2f9c91b0f..267074e766c 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/session-reconcile-dispose.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/session-reconcile-dispose.ts @@ -1,4 +1,5 @@ import { useAppStore } from '@/store' +import { consumeRestartReplacement } from './restart-replacement-on-show' import { shouldReconcileMissingSession, type HasPty } from '../terminal-dead-session-reconcile' import { cancelPendingSafeFitContinuations } from '@/lib/pane-manager/pane-tree-ops' import { PANE_PTY_RESIZE_HOLD_FLUSH_EVENT } from '@/lib/pane-manager/pane-pty-resize-hold' @@ -75,6 +76,7 @@ export function installSessionReconcileDispose(session: ConnectPanePtySession): session.claimPendingVisibleRemoteViewport() session.ptySizeReassertion.request({ fit: false }) session.consumeHibernatedAgentWake() + consumeRestartReplacement(session) session.resumeDeferredSpawnOnReveal() session.requestKnownWindowsShiftEnterReconfirmation() session.sampleVisiblePaneForegroundAgent() diff --git a/tests/e2e/workspace-layout-oracle-known-on-main.ts b/tests/e2e/workspace-layout-oracle-known-on-main.ts index bb4aae8c7f0..e32d096b537 100644 --- a/tests/e2e/workspace-layout-oracle-known-on-main.ts +++ b/tests/e2e/workspace-layout-oracle-known-on-main.ts @@ -24,13 +24,6 @@ type KnownOnMain = { const TRANSIENT = /\(transient\)$/ export const LAYOUT_ORACLE_KNOWN_ON_MAIN: readonly KnownOnMain[] = [ - // The in-app daemon restart keeps every pane, but nothing starts a new process in them. - { - scenario: 'live-daemon-restart', - check: 'marker', - step: 'after daemon restart', - cause: 'panes stay without a process after a daemon restart' - }, // A headless host lists a worktree with no saved tab bar (and, after a restart, every worktree) // under a synthesized `headless-terminals:` group, while the layout stream names the same group // by its saved or minted id: same tabs and order, two group ids. One projection in PR 6. From 39815a086fece015330ebf1f5b1111dc7c39ba7f Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:10:55 -0700 Subject: [PATCH 27/53] Reduce CI import, frame-capture, teardown, and shard costs (#28879) * Avoid unused module imports and trim cosmetic test coverage * Capture hidden popup frames and attach CDP before guest creation * Overlap relay test projects within the existing worker budget * Consolidate native preparation and add an opt-in transform cache comparison * Release inherited debugger sockets during final E2E teardown * Refresh unit scheduling weights from all five qualified ARM shards * Avoid retaining unasserted RPC call history in the delivery model * Use the supported unconditional background wait syntax * Overlap unconditional output upload with native preparation * Verify relay integration stays enabled for ordinary unit callers --- .github/workflows/cloud-verify.yml | 7 +- .github/workflows/e2e.yml | 101 +- .../workflows/unit-module-cache-benchmark.yml | 49 + .github/workflows/unit-tests.yml | 87 +- cloud/apps/relay/package.json | 4 +- .../src/postgres-lost-reply-postgres.test.ts | 7 +- cloud/apps/relay/vitest.config.ts | 14 +- cloud/dev/scripts/run-relay-tests.mjs | 126 + cloud/dev/scripts/run-relay-tests.test.mjs | 274 + cloud/package.json | 22 +- cloud/pnpm-lock.yaml | 3 + config/scripts/ci-e2e-job-selection.test.mjs | 10 +- config/scripts/ci-native-cache.test.mjs | 137 +- config/scripts/ci-shard-timings.json | 23845 ++++++++-------- config/scripts/ci-shard-timings.md | 29 +- .../scripts/ci-unit-module-cache-contents.mjs | 217 + .../ci-unit-module-cache-contents.test.mjs | 313 + .../scripts/ci-unit-module-cache-trailer.mjs | 118 + config/scripts/ci-unit-module-cache.mjs | 239 + config/scripts/ci-unit-module-cache.test.mjs | 167 + config/scripts/ci-unit-timing-reporter.mjs | 126 +- .../scripts/ci-unit-timing-reporter.test.mjs | 223 +- config/scripts/orcad-e2e-routing.test.mjs | 2 +- .../pr-auxiliary-preflight-admission.test.mjs | 10 +- .../release-cut-token-permissions.test.mjs | 1 - config/vitest.config.ts | 3 + ...browser-popup-compatibility-cdp-fixture.ts | 44 +- ...er-popup-compatibility-electron-fixture.ts | 2 + ...rowser-popup-compatibility-page-fixture.ts | 17 + .../hidden-renderer-frame-capture-fixture.ts | 47 + ...mobile-subscribe-idle-pane-hydrate.test.ts | 1 + .../opencode-wsl-folder-launch-host.test.ts | 1 + .../rpc/dispatcher-transport-parity.test.ts | 1 + .../agent-launch-restart-replay.test.ts | 1 + .../rpc/methods/managed-server.test.ts | 1 + ...mobile-web-bundle-read-concurrency.test.ts | 1 + .../mobile-web-bundle-window-refusals.test.ts | 1 + .../rpc/methods/mobile-web-bundle.test.ts | 1 + ...ructured-agent-session-attachments.test.ts | 1 + ...on-legacy-compatibility-dispatcher.test.ts | 1 + .../startup/codex-hook-startup-wiring.test.ts | 59 +- ...-process-runtime-launch-activation.test.ts | 8 + .../editor/MonacoEditor.font-family.test.tsx | 1 + .../editor/MonacoEditor.system-theme.test.tsx | 1 + .../CommitArea.primary-icons.test.tsx | 141 - .../settings/ShortcutCommandBlock.test.tsx | 45 - .../terminal-preview-content.test.ts.snap | 19 - .../appearance-interface-summary.test.ts | 27 - .../settings/terminal-preview-content.test.ts | 26 - .../e2e/helpers/electron-process-shutdown.ts | 52 +- .../electron-process-shutdown.unit.test.ts | 46 + tests/e2e/helpers/electron-shutdown-timing.ts | 21 + .../helpers/hidden-renderer-frame-capture.ts | 44 +- tests/e2e/helpers/orca-app.ts | 11 +- ...terminal-input-delivery-model.unit.test.ts | 4 +- ...inal-windows-shell-paste-ownership.spec.ts | 9 +- tests/playwright.config.ts | 4 + 57 files changed, 14718 insertions(+), 12054 deletions(-) create mode 100644 .github/workflows/unit-module-cache-benchmark.yml create mode 100644 cloud/dev/scripts/run-relay-tests.mjs create mode 100644 cloud/dev/scripts/run-relay-tests.test.mjs create mode 100644 config/scripts/ci-unit-module-cache-contents.mjs create mode 100644 config/scripts/ci-unit-module-cache-contents.test.mjs create mode 100644 config/scripts/ci-unit-module-cache-trailer.mjs create mode 100644 config/scripts/ci-unit-module-cache.mjs create mode 100644 config/scripts/ci-unit-module-cache.test.mjs create mode 100644 src/main/browser/browser-popup-compatibility-page-fixture.ts create mode 100644 src/main/browser/hidden-renderer-frame-capture-fixture.ts delete mode 100644 src/renderer/src/components/right-sidebar/CommitArea.primary-icons.test.tsx delete mode 100644 src/renderer/src/components/settings/ShortcutCommandBlock.test.tsx delete mode 100644 src/renderer/src/components/settings/__snapshots__/terminal-preview-content.test.ts.snap delete mode 100644 src/renderer/src/components/settings/appearance-interface-summary.test.ts delete mode 100644 src/renderer/src/components/settings/terminal-preview-content.test.ts create mode 100644 tests/e2e/helpers/electron-shutdown-timing.ts diff --git a/.github/workflows/cloud-verify.yml b/.github/workflows/cloud-verify.yml index 6c4cb9a53a1..fe4877d47de 100644 --- a/.github/workflows/cloud-verify.yml +++ b/.github/workflows/cloud-verify.yml @@ -4,12 +4,14 @@ on: pull_request: paths: - cloud/** + - src/packages/process-host/** - .github/workflows/cloud-*.yml - .github/actions/cloud-sql-rollout-lease/** push: branches: [main] paths: - cloud/** + - src/packages/process-host/** - .github/workflows/cloud-*.yml - .github/actions/cloud-sql-rollout-lease/** @@ -93,8 +95,8 @@ jobs: - run: pnpm build - run: pnpm typecheck - # Runs in parallel with build. `pnpm test` compiles the one workspace - # package it needs through the relay pretest hook, so it does not depend on + # Runs in parallel with build. `pnpm test` compiles the workspace + # packages it needs through pretest hooks, so it does not depend on # `pnpm build` having run. test: runs-on: ubuntu-22.04 @@ -113,6 +115,7 @@ jobs: --health-timeout 5s --health-retries 10 env: + ORCA_BACKGROUND_LAUNCH: '1' ORCA_PUSH_TEST_DATABASE_URL: postgres://relay_test:relay_test@127.0.0.1:5432/orca_relay_test ORCA_RELAY_TEST_POSTGRES_URL: postgres://relay_test:relay_test@127.0.0.1:5432/orca_relay_test steps: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2408eeaa91e..82c8aa889ec 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -52,7 +52,9 @@ jobs: name: build e2e app if: inputs.test_files == '' || github.event_name != 'pull_request' || inputs.needs_build runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 15 + env: + ORCA_BACKGROUND_LAUNCH: '1' steps: - name: Checkout @@ -60,10 +62,44 @@ jobs: with: ref: ${{ inputs.ref || github.ref }} + - name: Install native build tools + env: + ORCA_E2E_APT_PACKAGES: build-essential python3 + run: &install_e2e_tools | + if [ "$ORCA_E2E_APT_PACKAGES" = 'build-essential python3' ] && + command -v make >/dev/null && command -v g++ >/dev/null && command -v python3 >/dev/null; then + exit 0 + fi + read -r -a packages <<< "$ORCA_E2E_APT_PACKAGES" + # Why apt-mirrors.txt: runner images resolve mirror+file: sources through it, so editing the lists alone leaves Azure in use. + for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do + if [ -f "$source" ]; then + sudo sed -i 's|https*://azure\.archive\.ubuntu\.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' "$source" + fi + done + sudo tee /etc/apt/apt.conf.d/99-orca-e2e >/dev/null <<'APTCONF' + Acquire::http::Timeout "15"; + Acquire::https::Timeout "15"; + Acquire::Retries "1"; + APTCONF + timeout 120 sudo apt-get update + timeout 300 sudo apt-get install -y "${packages[@]}" || timeout 300 sudo apt-get install -y --fix-missing "${packages[@]}" + # Why: the build's plain-Node daemon smoke load resolves node-pty. - uses: ./.github/actions/install-node-dependencies + id: deps with: native-runtime: node + persist-native-cache: 'false' + cache-electron-package: 'true' + + # Save Node's ABI before the Electron preparation replaces the same build paths. + - name: Save compiled Node native modules + if: steps.deps.outputs.native-cache-key != '' && steps.deps.outputs.native-cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ${{ steps.deps.outputs.native-cache-path }} + key: ${{ steps.deps.outputs.native-cache-key }} # Why: building here avoids parallel builds inside Playwright globalSetup; # paired-browser specs also need the standalone web bundle. @@ -88,7 +124,19 @@ jobs: - wait: e2e-cli + - name: Check shared native runtime action + id: native-runtime-action + run: | + if [ -f .github/actions/prepare-native-runtime/action.yml ]; then + echo 'available=true' >> "$GITHUB_OUTPUT" + else + echo 'available=false' >> "$GITHUB_OUTPUT" + fi + + # Only the completed output upload overlaps the change from Node to Electron ABI. - name: Upload E2E build output + id: e2e-build-upload + background: true uses: actions/upload-artifact@v7 with: name: e2e-build-out${{ inputs.artifact_suffix }} @@ -97,46 +145,25 @@ jobs: retention-days: 1 if-no-files-found: error - # Build Electron-native dependencies once per workflow. Consumer shards restore - # this immutable cache instead of compiling the same ABI concurrently. - prepare-native-cache: - name: prepare Electron native cache - if: inputs.test_files == '' || github.event_name != 'pull_request' || inputs.needs_build - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - name: Checkout - uses: actions/checkout@v6 + - name: Prepare shared Electron native runtime + id: electron-native-runtime + if: steps.native-runtime-action.outputs.available == 'true' && steps.deps.outputs.node-version != '' + uses: ./.github/actions/prepare-native-runtime with: - ref: ${{ inputs.ref || github.ref }} + native-runtime: electron + node-version: ${{ steps.deps.outputs.node-version }} - - name: Install native build tools - env: - ORCA_E2E_APT_PACKAGES: build-essential python3 - run: &install_e2e_tools | - read -r -a packages <<< "$ORCA_E2E_APT_PACKAGES" - # Why apt-mirrors.txt: runner images resolve mirror+file: sources through it, so editing the lists alone leaves Azure in use. - for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do - if [ -f "$source" ]; then - sudo sed -i 's|https*://azure\.archive\.ubuntu\.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' "$source" - fi - done - sudo tee /etc/apt/apt.conf.d/99-orca-e2e >/dev/null <<'APTCONF' - Acquire::http::Timeout "15"; - Acquire::https::Timeout "15"; - Acquire::Retries "1"; - APTCONF - timeout 120 sudo apt-get update - timeout 300 sudo apt-get install -y "${packages[@]}" || timeout 300 sudo apt-get install -y --fix-missing "${packages[@]}" - - - uses: ./.github/actions/install-node-dependencies + - name: Prepare Electron natives for older refs + if: steps.native-runtime-action.outputs.available != 'true' || steps.deps.outputs.node-version == '' + uses: ./.github/actions/install-node-dependencies with: native-runtime: electron + - wait: e2e-build-upload + e2e: name: e2e ${{ matrix.shard_name }} - needs: [build, prepare-native-cache] + needs: [build] if: inputs.test_files == '' runs-on: ubuntu-latest timeout-minutes: 30 @@ -263,7 +290,7 @@ jobs: changed-e2e: name: changed e2e specs - needs: [build, prepare-native-cache] + needs: [build] if: inputs.test_files != '' && (github.event_name != 'pull_request' || inputs.run_changed_e2e) runs-on: ubuntu-latest timeout-minutes: 45 @@ -351,7 +378,7 @@ jobs: orcad-serve-mode-switch: name: orca serve Electron/orcad mode switch (D7) - needs: [build, prepare-native-cache] + needs: [build] if: >- inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/orcad-serve-mode-switch.spec.ts') || @@ -411,7 +438,7 @@ jobs: # orcad template for the fixture's target (Debian, linux-x64-glibc), which only this job builds. orcad-ssh-docker: name: ssh on managed orcad (Docker ${{ matrix.shard }}) - needs: [build, prepare-native-cache] + needs: [build] if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/ssh-orcad-') runs-on: ubuntu-latest timeout-minutes: 60 diff --git a/.github/workflows/unit-module-cache-benchmark.yml b/.github/workflows/unit-module-cache-benchmark.yml new file mode 100644 index 00000000000..d15f262d235 --- /dev/null +++ b/.github/workflows/unit-module-cache-benchmark.yml @@ -0,0 +1,49 @@ +name: Unit transform cache comparison + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: unit-module-cache-comparison-${{ github.ref }} + cancel-in-progress: true + +jobs: + baseline: + uses: ./.github/workflows/unit-tests.yml + with: + node_versions: '["24.21.0"]' + runner: ubuntu-24.04-arm + shards: '[{"index":1,"count":5},{"index":2,"count":5},{"index":3,"count":5},{"index":4,"count":5},{"index":5,"count":5}]' + artifact_prefix: baseline-unit + phase_diagnostics: true + run_relay_integration: false + + producer: + needs: baseline + uses: ./.github/workflows/unit-tests.yml + with: + node_versions: '["24.21.0"]' + runner: ubuntu-24.04-arm + shards: '[{"index":1,"count":5},{"index":2,"count":5},{"index":3,"count":5},{"index":4,"count":5},{"index":5,"count":5}]' + artifact_prefix: producer-unit + module_cache: cold + module_cache_namespace: ${{ github.run_id }}-${{ github.run_attempt }} + module_cache_seed: true + phase_diagnostics: true + run_relay_integration: false + + warm: + needs: producer + uses: ./.github/workflows/unit-tests.yml + with: + node_versions: '["24.21.0"]' + runner: ubuntu-24.04-arm + shards: '[{"index":1,"count":5},{"index":2,"count":5},{"index":3,"count":5},{"index":4,"count":5},{"index":5,"count":5}]' + artifact_prefix: warm-unit + module_cache: warm + module_cache_namespace: ${{ github.run_id }}-${{ github.run_attempt }} + phase_diagnostics: true + run_relay_integration: false diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 99d57c48888..330070c0b36 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -31,13 +31,43 @@ on: required: true type: string + module_cache: + description: Transform cache policy (off, cold producer, or warm exact restore). + default: 'off' + type: string + + module_cache_namespace: + description: Isolate a same-source cache comparison from existing archives. + default: '' + type: string + + module_cache_seed: + description: Publish comparison caches from a manually dispatched workflow. + default: false + type: boolean + + phase_diagnostics: + description: Attach bounded per-file worker phase diagnostics. + default: false + type: boolean + + artifact_prefix: + description: Distinguish evidence from repeated same-source comparison lanes. + default: unit + type: string + + run_relay_integration: + description: Run relay contracts alongside normal unit checks. + default: true + type: boolean + permissions: contents: read jobs: test: if: inputs.run_shards - name: tests node ${{ matrix.node }} ${{ matrix.shard.index }}/${{ matrix.shard.count }} + name: ${{ inputs.artifact_prefix != 'unit' && format('{0} ', inputs.artifact_prefix) || '' }}tests node ${{ matrix.node }} ${{ matrix.shard.index }}/${{ matrix.shard.count }} runs-on: ${{ inputs.runner }} timeout-minutes: 60 strategy: @@ -65,17 +95,51 @@ jobs: with: bun-version-file: config/.bun-version + - name: Resolve unit module cache + id: module-cache + if: inputs.module_cache == 'cold' || inputs.module_cache == 'warm' + env: + ORCA_UNIT_SHARD_INDEX: ${{ matrix.shard.index }} + ORCA_UNIT_SHARD_COUNT: ${{ matrix.shard.count }} + ORCA_UNIT_CACHE_NAMESPACE: ${{ inputs.module_cache_namespace }} + run: node config/scripts/ci-unit-module-cache.mjs identity + + - name: Restore exact unit module cache + id: module-cache-restore + if: inputs.module_cache == 'warm' && steps.module-cache.outputs.key != '' + continue-on-error: true + uses: actions/cache/restore@v5 + with: + path: ${{ steps.module-cache.outputs.path }} + key: ${{ steps.module-cache.outputs.key }} + + - name: Validate restored unit module cache or use cold transforms + if: steps.module-cache.outputs.key != '' + env: + ORCA_UNIT_CACHE_RESTORE_HIT: ${{ steps.module-cache-restore.outputs.cache-hit }} + run: node config/scripts/ci-unit-module-cache.mjs prepare + - uses: actions/download-artifact@v8 continue-on-error: true with: name: unit-selection-attempt-${{ github.run_attempt }} path: ci-shards/ + - name: Record full unit discovery + if: inputs.phase_diagnostics + continue-on-error: true + run: >- + node --input-type=module -e ' + import { discoverUnitFiles } from "./config/scripts/ci-unit-files.mjs"; + import { writeAssignment } from "./config/scripts/ci-shard-assignment.mjs"; + writeAssignment("ci-shards/unit-discovery.json", { files: discoverUnitFiles() });' + - name: Test shard env: NODE_COMPILE_CACHE: ${{ runner.temp }}/orca-node-compile-cache-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.node }}-${{ matrix.shard.index }} ORCA_BALANCE_UNIT_SHARDS: '1' ORCA_BACKGROUND_LAUNCH: '1' + ORCA_UNIT_PHASE_REPORT: ${{ inputs.phase_diagnostics && 'ci-shards/unit-phases.json' || '' }} run: | ORCA_SHARD_SOURCE_SHA="$(git rev-parse HEAD)" export ORCA_SHARD_SOURCE_SHA @@ -83,18 +147,37 @@ jobs: --shard=${{ matrix.shard.index }}/${{ matrix.shard.count }} \ ${{ inputs.runner == 'ubuntu-24.04-arm' && '--maxWorkers=4' || '' }} + - name: Seal unit module cache + id: module-cache-seal + if: >- + success() && steps.module-cache.outputs.key != '' && + steps.module-cache-restore.outputs.cache-hit != 'true' && + (github.event_name == 'pull_request' || + github.ref == 'refs/heads/main' && github.event_name != 'pull_request' || + inputs.module_cache_seed && github.event_name == 'workflow_dispatch') + run: node config/scripts/ci-unit-module-cache.mjs seal + + - name: Save unit module cache + if: steps.module-cache-seal.outputs.ready == 'true' + continue-on-error: true + uses: actions/cache/save@v5 + with: + path: ${{ steps.module-cache.outputs.path }} + key: ${{ steps.module-cache.outputs.key }} + - name: Upload unit shard assignment if: always() # Diagnostic upload outages must not change the test verdict. continue-on-error: true uses: actions/upload-artifact@v7 with: - name: unit-shard-node-${{ matrix.node }}-${{ matrix.shard.index }}-attempt-${{ github.run_attempt }} + name: ${{ inputs.artifact_prefix }}-shard-node-${{ matrix.node }}-${{ matrix.shard.index }}-attempt-${{ github.run_attempt }} path: ci-shards/ retention-days: 14 if-no-files-found: warn relay_integration: + if: inputs.run_relay_integration name: relay integration node ${{ matrix.node }} strategy: fail-fast: false diff --git a/cloud/apps/relay/package.json b/cloud/apps/relay/package.json index 0a85bc9343e..431156103d2 100644 --- a/cloud/apps/relay/package.json +++ b/cloud/apps/relay/package.json @@ -1,7 +1,7 @@ { "name": "@orca-cloud/relay", - "private": true, "version": "0.0.0", + "private": true, "type": "module", "main": "dist/index.js", "scripts": { @@ -11,7 +11,7 @@ "lint": "tsc -p tsconfig.json --noEmit", "pretest": "pnpm --filter @orca-cloud/postgres-schema build && pnpm --filter @orca-cloud/relay-contract build", "start": "node dist/index.js", - "test": "vitest run", + "test": "node --import tsx --conditions=orca-source ../../dev/scripts/run-relay-tests.mjs", "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { diff --git a/cloud/apps/relay/src/postgres-lost-reply-postgres.test.ts b/cloud/apps/relay/src/postgres-lost-reply-postgres.test.ts index d189e10f060..d89c6dcbf5f 100644 --- a/cloud/apps/relay/src/postgres-lost-reply-postgres.test.ts +++ b/cloud/apps/relay/src/postgres-lost-reply-postgres.test.ts @@ -22,6 +22,7 @@ const PROBE_TABLE = 'relay_lost_reply_probe' async function blackholeProxy(target: URL, trigger: string, delayMs?: number) { const sockets = new Set() const afterTrigger: Buffer[] = [] + let forwardingResumed = false const server: Server = createServer((client) => { const upstream = connect(Number(target.port || 5432), target.hostname) sockets.add(client) @@ -33,7 +34,7 @@ async function blackholeProxy(target: URL, trigger: string, delayMs?: number) { upstream.write(chunk) }) upstream.on('data', (chunk: Buffer) => { - if (!swallowing) client.write(chunk) + if (!swallowing || forwardingResumed) client.write(chunk) else if (delayMs !== undefined) setTimeout(() => client.write(chunk), delayMs) }) for (const socket of [client, upstream]) { @@ -52,6 +53,9 @@ async function blackholeProxy(target: URL, trigger: string, delayMs?: number) { url.port = String(address.port) return { url: url.toString(), + resumeForwarding: () => { + forwardingResumed = true + }, sentAfterTrigger: () => Buffer.concat(afterTrigger).toString('latin1'), close: async () => { for (const socket of sockets) socket.destroy() @@ -174,6 +178,7 @@ describePostgres('a lost PostgreSQL reply against a real server', () => { { probe: 'stalled_reply_probe' } ]) expect(performance.now() - startedAt).toBeGreaterThan(6_000) + proxy.resumeForwarding() // Not destroyed: the one pooled connection is still the same backend. expect(await pid()).toBe(before) }, 30_000) diff --git a/cloud/apps/relay/vitest.config.ts b/cloud/apps/relay/vitest.config.ts index f56bbd7be3a..90621de9e9d 100644 --- a/cloud/apps/relay/vitest.config.ts +++ b/cloud/apps/relay/vitest.config.ts @@ -1,26 +1,22 @@ import { readdirSync, readFileSync } from 'node:fs' +import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { defaultExclude, defineConfig } from 'vitest/config' -// Every test file that opens ORCA_RELAY_TEST_POSTGRES_URL shares one CI -// database, and those tests take session-level locks with a 1s lock_timeout. -// Running them alongside anything else collides into lock timeouts, capacity -// exhaustion, and afterAll hangs. Keep them in their own serialized project so -// only they give up file parallelism; the rest of the suite opens SQLite data -// directories and stays fully parallel. +// PostgreSQL files share session locks and a small connection budget. +// The runner overlaps this serialized project with SQLite tests after other packages finish. const sourceDirectory = fileURLToPath(new URL('src', import.meta.url)) const sharedPostgresTests = readdirSync(sourceDirectory) .filter((entry) => entry.endsWith('.test.ts')) .filter((entry) => - readFileSync(`${sourceDirectory}/${entry}`, 'utf8').includes( - 'ORCA_RELAY_TEST_POSTGRES_URL' - ) + readFileSync(join(sourceDirectory, entry), 'utf8').includes('ORCA_RELAY_TEST_POSTGRES_URL') ) .map((entry) => `src/${entry}`) const timeouts = { testTimeout: 15_000, hookTimeout: 15_000 } export default defineConfig({ + cacheDir: process.env.ORCA_RELAY_TEST_CACHE_DIR, test: { projects: [ { diff --git a/cloud/dev/scripts/run-relay-tests.mjs b/cloud/dev/scripts/run-relay-tests.mjs new file mode 100644 index 00000000000..0cbc689c070 --- /dev/null +++ b/cloud/dev/scripts/run-relay-tests.mjs @@ -0,0 +1,126 @@ +import { runProcess } from '@orca/process-host' +import { + forceTerminateProcessTree, + signalProcessTree +} from '@orca/process-host/process-tree-termination' +import { mkdtempSync, mkdirSync, realpathSync, rmSync } from 'node:fs' +import { createRequire } from 'node:module' +import { availableParallelism, tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const require = createRequire(import.meta.url) +const vitest = join(dirname(require.resolve('vitest/package.json')), 'vitest.mjs') +const relayDirectory = fileURLToPath(new URL('../../apps/relay', import.meta.url)) + +export async function runRelayTests({ + args = [], + cwd = relayDirectory, + env = process.env, + parallelism = availableParallelism(), + cli = vitest, + run = runProcess, + events = process, + report = console.error, + scratchParent = tmpdir() +} = {}) { + const workerBudget = Math.max(parallelism - 1, 1) + // Explicit selectors and alternate database fixtures retain their existing execution path. + const serial = + args.length > 0 || workerBudget <= 1 || env.ORCA_IDLE_REHOME_POSTGRES_URL || env.DATABASE_URL + const roles = serial + ? [{ name: 'all', args }] + : [ + { name: 'relay', args: ['--project=relay', `--maxWorkers=${workerBudget - 1}`] }, + { name: 'relay-postgres', args: ['--project=relay-postgres', '--maxWorkers=1'] } + ] + const scratch = realpathSync(mkdtempSync(join(scratchParent, 'orca-relay-tests-'))) + const abort = new AbortController() + const closed = new Set() + const terminatedTrees = new Set() + let signalExitCode + const onInterrupt = () => { + signalExitCode ??= 130 + abort.abort() + } + const onTerminate = () => { + signalExitCode ??= 143 + abort.abort() + } + events.on('SIGINT', onInterrupt) + events.on('SIGTERM', onTerminate) + + async function runRole(role) { + const owned = join(scratch, role.name) + try { + mkdirSync(owned) + return await run({ + program: process.execPath, + args: [cli, 'run', ...role.args], + cwd, + env: { + ...env, + TMPDIR: owned, + TMP: owned, + TEMP: owned, + ORCA_BACKGROUND_LAUNCH: '1', + ORCA_RELAY_TEST_CACHE_DIR: join(owned, 'cache') + }, + stdio: 'inherit', + timeoutMs: null, + signal: abort.signal, + terminationBarrier: { + async signal(child, signal) { + const terminated = await signalProcessTree(child, signal) + if (process.platform === 'win32' && terminated) { + terminatedTrees.add(role.name) + } + return terminated + }, + async force(child) { + const terminated = await forceTerminateProcessTree(child) + if (terminated) { + terminatedTrees.add(role.name) + } + return terminated + } + }, + onChildTerminated: () => closed.add(role.name) + }) + } catch (error) { + abort.abort() + throw error + } + } + + let exitCode = 0 + try { + const results = await Promise.allSettled(roles.map(runRole)) + for (const result of results) { + if (result.status === 'rejected') { + report(result.reason) + exitCode ||= 1 + } else if (result.value.code !== 0 || result.value.signal || result.value.timedOut) { + exitCode ||= result.value.code || 1 + } + } + } finally { + events.removeListener('SIGINT', onInterrupt) + events.removeListener('SIGTERM', onTerminate) + // A cancelled root's close event alone does not prove its descendants stopped using scratch. + if ( + closed.size === roles.length && + (!abort.signal.aborted || terminatedTrees.size === roles.length) + ) { + rmSync(scratch, { recursive: true, force: true }) + } else { + report(`Relay test process closure is unverifiable; retained ${scratch}`) + exitCode ||= 1 + } + } + return signalExitCode ?? exitCode +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exitCode = await runRelayTests({ args: process.argv.slice(2) }) +} diff --git a/cloud/dev/scripts/run-relay-tests.test.mjs b/cloud/dev/scripts/run-relay-tests.test.mjs new file mode 100644 index 00000000000..1f806f183dc --- /dev/null +++ b/cloud/dev/scripts/run-relay-tests.test.mjs @@ -0,0 +1,274 @@ +import assert from 'node:assert/strict' +import { EventEmitter } from 'node:events' +import { + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + realpathSync, + rmSync, + symlinkSync, + writeFileSync +} from 'node:fs' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { setImmediate } from 'node:timers/promises' +import { pathToFileURL } from 'node:url' +import test from 'node:test' +import { runProcess } from '@orca/process-host' +import { runRelayTests } from './run-relay-tests.mjs' + +const success = { code: 0, signal: null, timedOut: false } +const databaseUrl = 'postgres://relay_test:relay_test@127.0.0.1:5432/orca_relay_test' +const require = createRequire(import.meta.url) +const cloud = resolve(import.meta.dirname, '../..') + +function temporary(t) { + const directory = realpathSync(mkdtempSync(join(tmpdir(), 'orca-relay-runner-contract-'))) + t.after(() => rmSync(directory, { recursive: true, force: true })) + return directory +} + +function start(t, options = {}) { + const scratchParent = temporary(t) + const calls = [] + const reports = [] + const events = new EventEmitter() + const env = { ORCA_RELAY_TEST_POSTGRES_URL: databaseUrl, PRESERVED: 'yes', ...options.env } + const pending = runRelayTests({ + cwd: join(scratchParent, 'relay'), + cli: join(scratchParent, 'vitest.mjs'), + parallelism: 4, + ...options, + env, + events, + scratchParent, + report: (message) => reports.push(message), + run: (spec) => { + const deferred = Promise.withResolvers() + calls.push({ spec, ...deferred }) + return deferred.promise + } + }) + return { pending, calls, reports, events, scratchParent, env } +} + +function finish(call, result = success, closed = true) { + if (closed) { + call.spec.onChildTerminated() + } + call.resolve(result) +} + +function assertClean(harness) { + assert.deepEqual(readdirSync(harness.scratchParent), []) + assert.equal(harness.events.listenerCount('SIGINT'), 0) + assert.equal(harness.events.listenerCount('SIGTERM'), 0) +} + +test('overlaps both projects within the worker budget and isolates their scratch and caches', async (t) => { + const h = start(t) + assert.equal(h.calls.length, 2) + const [regular, postgres] = h.calls.map((call) => call.spec) + assert.deepEqual(regular.args.slice(1), ['run', '--project=relay', '--maxWorkers=2']) + assert.deepEqual(postgres.args.slice(1), ['run', '--project=relay-postgres', '--maxWorkers=1']) + assert.notEqual(regular.env.TMPDIR, postgres.env.TMPDIR) + assert.notEqual(regular.env.ORCA_RELAY_TEST_CACHE_DIR, postgres.env.ORCA_RELAY_TEST_CACHE_DIR) + for (const spec of [regular, postgres]) { + assert.equal(spec.env.TMP, spec.env.TMPDIR) + assert.equal(spec.env.TEMP, spec.env.TMPDIR) + assert.equal(spec.env.ORCA_RELAY_TEST_CACHE_DIR, join(spec.env.TMPDIR, 'cache')) + assert.equal(spec.env.ORCA_RELAY_TEST_POSTGRES_URL, databaseUrl) + assert.equal(spec.env.PRESERVED, 'yes') + assert.equal(spec.env.ORCA_BACKGROUND_LAUNCH, '1') + assert.ok(existsSync(spec.env.TMPDIR)) + assert.equal(spec.timeoutMs, null) + assert.equal(typeof spec.terminationBarrier.force, 'function') + } + assert.equal(regular.signal, postgres.signal) + let settled = false + h.pending.then(() => { + settled = true + }) + finish(h.calls[0], { ...success, code: 7 }) + await setImmediate() + assert.equal(settled, false) + assert.equal(postgres.signal.aborted, false) + assert.ok(existsSync(postgres.env.TMPDIR)) + finish(h.calls[1]) + assert.equal(await h.pending, 7) + assertClean(h) +}) + +for (const [name, options] of [ + ['selectors', { args: ['src/assignment.test.ts', '-t', 'requested behavior'] }], + [ + 'alternate PostgreSQL', + { env: { ORCA_IDLE_REHOME_POSTGRES_URL: 'postgres://localhost:55440/relay' } } + ], + ['application database', { env: { DATABASE_URL: databaseUrl } }], + ['one CPU', { parallelism: 1 }], + ['two CPUs', { parallelism: 2 }] +]) { + test(`retains the original single run for ${name}`, async (t) => { + const h = start(t, options) + assert.equal(h.calls.length, 1) + const { spec } = h.calls[0] + assert.deepEqual(spec.args.slice(1), ['run', ...(options.args ?? [])]) + for (const [key, value] of Object.entries(h.env)) { + assert.equal(spec.env[key], value) + } + finish(h.calls[0]) + assert.equal(await h.pending, 0) + assertClean(h) + }) +} + +for (const [name, result] of [ + ['null exit code', { ...success, code: null }], + ['termination signal', { ...success, signal: 'SIGTERM' }], + ['timeout', { ...success, timedOut: true }] +]) { + test(`fails a ${name} even when other result fields indicate success`, async (t) => { + const h = start(t) + finish(h.calls[0], result) + finish(h.calls[1]) + assert.equal(await h.pending, 1) + assertClean(h) + }) +} + +test('fails and retains scratch when a child never reports closure', async (t) => { + const h = start(t) + finish(h.calls[0], success, false) + finish(h.calls[1]) + assert.equal(await h.pending, 1) + assert.ok(existsSync(h.calls[0].spec.env.TMPDIR)) + assert.match(String(h.reports[0]), /closure is unverifiable; retained/) + assert.equal(h.events.listenerCount('SIGINT'), 0) + assert.equal(h.events.listenerCount('SIGTERM'), 0) +}) + +for (const [signal, exitCode, laterSignal] of [ + ['SIGINT', 130, 'SIGTERM'], + ['SIGTERM', 143, 'SIGINT'] +]) { + test(`${signal} cancels every child and retains scratch despite root close callbacks`, async (t) => { + const h = start(t) + h.events.emit(signal) + h.events.emit(laterSignal) + for (const call of h.calls) { + assert.equal(call.spec.signal.aborted, true) + finish(call) + } + assert.equal(await h.pending, exitCode) + for (const call of h.calls) { + assert.ok(existsSync(call.spec.env.TMPDIR)) + } + assert.match(String(h.reports[0]), /closure is unverifiable; retained/) + assert.equal(h.events.listenerCount('SIGINT'), 0) + assert.equal(h.events.listenerCount('SIGTERM'), 0) + }) +} + +for (const verified of [1, 2]) { + test(`requires verification of both cancelled trees (${verified} verified)`, async (t) => { + const h = start(t) + h.events.emit('SIGINT') + for (const call of h.calls.slice(0, verified)) { + // An unspawned handle proves absence without signalling a real process. + assert.equal(await call.spec.terminationBarrier.force({ kill: () => true }), true) + } + for (const call of h.calls) { + finish(call) + } + assert.equal(await h.pending, 130) + if (verified === 2) { + assertClean(h) + } else { + assert.ok(existsSync(h.calls[0].spec.env.TMPDIR)) + } + }) +} + +test('reports a spawn failure and waits for its cancelled peer before returning', async (t) => { + const h = start(t) + const failure = new Error('fixture spawn failed') + h.calls[0].reject(failure) + let settled = false + h.pending.then(() => { + settled = true + }) + await setImmediate() + assert.equal(h.calls[1].spec.signal.aborted, true) + assert.equal(settled, false) + finish(h.calls[1]) + assert.equal(await h.pending, 1) + assert.equal(h.reports[0], failure) + assert.ok(existsSync(h.calls[1].spec.env.TMPDIR)) +}) + +test('launches with only the Cloud dependency link and no root install or compiled host package', async (t) => { + const root = temporary(t) + const standalone = join(root, 'cloud') + const manifest = JSON.parse(readFileSync(join(cloud, 'package.json'), 'utf8')) + const dependency = manifest.devDependencies['@orca/process-host'] + assert.equal(dependency, 'link:../src/packages/process-host') + const packageDir = resolve(standalone, dependency.slice('link:'.length)) + mkdirSync(packageDir, { recursive: true }) + for (const file of ['package.json', 'src']) { + cpSync(join(cloud, '..', 'src', 'packages', 'process-host', file), join(packageDir, file), { + recursive: true + }) + } + const scripts = join(standalone, 'dev', 'scripts') + mkdirSync(scripts, { recursive: true }) + cpSync(join(cloud, 'dev', 'scripts', 'run-relay-tests.mjs'), join(scripts, 'run-relay-tests.mjs')) + writeFileSync(join(standalone, 'package.json'), JSON.stringify(manifest)) + mkdirSync(join(standalone, 'apps', 'relay'), { recursive: true }) + mkdirSync(join(standalone, 'node_modules', '@orca'), { recursive: true }) + symlinkSync( + packageDir, + join(standalone, 'node_modules', '@orca', 'process-host'), + process.platform === 'win32' ? 'junction' : 'dir' + ) + const fakeVitest = join(standalone, 'node_modules', 'vitest') + mkdirSync(fakeVitest) + writeFileSync(join(fakeVitest, 'package.json'), '{"name":"vitest","version":"0.0.0"}') + writeFileSync( + join(fakeVitest, 'vitest.mjs'), + 'console.log(JSON.stringify({args:process.argv.slice(2), background:process.env.ORCA_BACKGROUND_LAUNCH, database:process.env.ORCA_RELAY_TEST_POSTGRES_URL}))' + ) + assert.equal(existsSync(join(root, 'node_modules')), false) + assert.equal(existsSync(join(packageDir, 'dist')), false) + const result = await runProcess({ + program: process.execPath, + args: [ + '--import', + pathToFileURL(require.resolve('tsx')).href, + '--conditions=orca-source', + join(scripts, 'run-relay-tests.mjs'), + 'src/requested.test.ts' + ], + cwd: standalone, + timeoutMs: 15_000, + terminationBarrier: true, + env: { + ...process.env, + NODE_PATH: '', + NODE_OPTIONS: '', + ORCA_BACKGROUND_LAUNCH: '1', + ORCA_RELAY_TEST_POSTGRES_URL: databaseUrl + } + }) + assert.equal(result.code, 0, result.stderr) + assert.deepEqual(JSON.parse(result.stdout), { + args: ['run', 'src/requested.test.ts'], + background: '1', + database: databaseUrl + }) + assert.equal(existsSync(join(packageDir, 'dist')), false) +}) diff --git a/cloud/package.json b/cloud/package.json index 84c33de4853..c49f6ca9c66 100644 --- a/cloud/package.json +++ b/cloud/package.json @@ -1,12 +1,7 @@ { "name": "orca-cloud", - "private": true, "version": "0.0.0", - "packageManager": "pnpm@10.34.6", - "engines": { - "node": ">=24 <27", - "pnpm": ">=10" - }, + "private": true, "scripts": { "build": "pnpm -r build", "dev": "pnpm --filter @orca-cloud/relay dev", @@ -21,14 +16,21 @@ "load:relay:model": "node dev/scripts/run-relay-load-model.mjs", "load:relay:recovery-gate": "node dev/scripts/run-relay-recovery-wave-gate.mjs", "ops:relay": "pnpm --filter @orca-cloud/relay-ops dev", - "pretest": "node --test dev/scripts/capture-terraform-plan-baseline.test.mjs dev/scripts/operate-relay-asia-admission.test.mjs dev/scripts/prepare-relay-asia-director-cells.test.mjs dev/scripts/prepare-relay-asia-topology-input.test.mjs dev/scripts/production-cloud-sql-rollout-lock.test.mjs dev/scripts/read-relay-serving-regional-placement-version.test.mjs dev/scripts/relay-asia-rollout-evidence.test.mjs dev/scripts/relay-asia-topology-workflow.test.mjs dev/scripts/relay-cloud-sql-connection-budget.test.mjs dev/scripts/relay-load-reader-evidence.test.mjs dev/scripts/relay-control-flag-alerts.test.mjs dev/scripts/relay-db-fault-alerts.test.mjs dev/scripts/relay-lock-contention-alerts.test.mjs dev/scripts/relay-reconnect-herd-alerts.test.mjs dev/scripts/relay-reserve-dead-man-alert.test.mjs dev/scripts/relay-region-hint-metrics.test.mjs dev/scripts/relay-staging-deploy-identity.test.mjs dev/scripts/sanitize-relay-asia-admission-result.test.mjs dev/scripts/terraform-root-partition.test.mjs dev/scripts/validate-relay-asia-topology-plan.test.mjs ../.github/actions/cloud-sql-rollout-lease/action-contract.test.mjs ../.github/actions/cloud-sql-rollout-lease/storage-lease.test.mjs", - "test": "pnpm -r test && node --test dev/scripts/check-relay-same-cap-headroom.test.mjs dev/scripts/classify-relay-production-capacity-director.test.mjs dev/scripts/classify-relay-staging-bootstrap.test.mjs dev/scripts/deploy-relay-blue-green.test.mjs dev/scripts/deploy-relay-gce-candidate.test.mjs dev/scripts/deploy-relay-gce-multi-target.test.mjs dev/scripts/drive-relay-director-deploy.test.mjs dev/scripts/github-smoke-token.test.mjs dev/scripts/infra.test.mjs dev/scripts/measure-relay-drain-disconnect-gap.test.mjs dev/scripts/operate-relay-cell-flags.test.mjs dev/scripts/operate-relay-regional-rehome.test.mjs dev/scripts/power-staging-relay.test.mjs dev/scripts/prepare-relay-capacity-canary.test.mjs dev/scripts/prepare-relay-production-capacity-canary.test.mjs dev/scripts/probe-relay-legacy-admission.test.mjs dev/scripts/probe-relay-rehome-trust.test.mjs dev/scripts/production-cell-image-digest-consistency.test.mjs dev/scripts/push-gateway-workflow.test.mjs dev/scripts/push-gateway-recovery.test.mjs dev/scripts/read-relay-production-capacity-identity.test.mjs dev/scripts/relay-admin-endpoint-retry-workflow.test.mjs dev/scripts/relay-admin-transient-retry.test.mjs dev/scripts/relay-admission-selector.test.mjs dev/scripts/relay-gce-terraform-fence.test.mjs dev/scripts/relay-load-connection-failure.test.mjs dev/scripts/relay-load-control-peer.test.mjs dev/scripts/relay-load-director-capacity-gate.test.mjs dev/scripts/relay-load-duplicate-assign.test.mjs dev/scripts/relay-load-model.test.mjs dev/scripts/relay-load-phase-barrier.test.mjs dev/scripts/relay-load-placement-boundary.test.mjs dev/scripts/relay-load-profile.test.mjs dev/scripts/relay-load-rebind-boundary.test.mjs dev/scripts/relay-load-region-behavior.test.mjs dev/scripts/relay-load-request-unit-boundary.test.mjs dev/scripts/relay-load-run-lifecycle.test.mjs dev/scripts/relay-load-splice-retry.test.mjs dev/scripts/relay-monitor-evidence.test.mjs dev/scripts/relay-production-capacity-wave.test.mjs dev/scripts/relay-production-capacity-workflow.test.mjs dev/scripts/relay-production-identity-boundaries.test.mjs dev/scripts/relay-production-same-cap-wave.test.mjs dev/scripts/relay-public-workflow-contract.test.mjs dev/scripts/relay-recovery-wave-gate.test.mjs dev/scripts/relay-region-observation-evidence.test.mjs dev/scripts/relay-rehome-aggregate-evidence.test.mjs dev/scripts/relay-repository.test.mjs dev/scripts/relay-same-cap-job-mode-conditions.test.mjs dev/scripts/relay-same-cap-script-census.test.mjs dev/scripts/relay-same-cap-shadow-gate.test.mjs dev/scripts/relay-staging-c4-refresh-workflow.test.mjs dev/scripts/relay-staging-capacity-identity.test.mjs dev/scripts/staging-relay-apply-guard.test.mjs dev/scripts/validate-relay-capacity-plan.test.mjs dev/scripts/verify-relay-capacity-transition.test.mjs dev/scripts/verify-relay-legacy-bootstrap.test.mjs dev/scripts/workload-identity-attribute-conditions.test.mjs", - "typecheck": "pnpm -r typecheck" + "pretest": "node --test dev/scripts/capture-terraform-plan-baseline.test.mjs dev/scripts/operate-relay-asia-admission.test.mjs dev/scripts/prepare-relay-asia-director-cells.test.mjs dev/scripts/prepare-relay-asia-topology-input.test.mjs dev/scripts/production-cloud-sql-rollout-lock.test.mjs dev/scripts/read-relay-serving-regional-placement-version.test.mjs dev/scripts/relay-asia-rollout-evidence.test.mjs dev/scripts/relay-asia-topology-workflow.test.mjs dev/scripts/relay-cloud-sql-connection-budget.test.mjs dev/scripts/relay-load-reader-evidence.test.mjs dev/scripts/relay-control-flag-alerts.test.mjs dev/scripts/relay-db-fault-alerts.test.mjs dev/scripts/relay-lock-contention-alerts.test.mjs dev/scripts/relay-reconnect-herd-alerts.test.mjs dev/scripts/relay-reserve-dead-man-alert.test.mjs dev/scripts/relay-region-hint-metrics.test.mjs dev/scripts/relay-staging-deploy-identity.test.mjs dev/scripts/sanitize-relay-asia-admission-result.test.mjs dev/scripts/terraform-root-partition.test.mjs dev/scripts/validate-relay-asia-topology-plan.test.mjs ../.github/actions/cloud-sql-rollout-lease/action-contract.test.mjs ../.github/actions/cloud-sql-rollout-lease/storage-lease.test.mjs && pnpm test:runner", + "test": "pnpm -r --filter \"!@orca-cloud/relay\" test && pnpm --filter @orca-cloud/relay test && node --test dev/scripts/check-relay-same-cap-headroom.test.mjs dev/scripts/classify-relay-production-capacity-director.test.mjs dev/scripts/classify-relay-staging-bootstrap.test.mjs dev/scripts/deploy-relay-blue-green.test.mjs dev/scripts/deploy-relay-gce-candidate.test.mjs dev/scripts/deploy-relay-gce-multi-target.test.mjs dev/scripts/drive-relay-director-deploy.test.mjs dev/scripts/github-smoke-token.test.mjs dev/scripts/infra.test.mjs dev/scripts/measure-relay-drain-disconnect-gap.test.mjs dev/scripts/operate-relay-cell-flags.test.mjs dev/scripts/operate-relay-regional-rehome.test.mjs dev/scripts/power-staging-relay.test.mjs dev/scripts/prepare-relay-capacity-canary.test.mjs dev/scripts/prepare-relay-production-capacity-canary.test.mjs dev/scripts/probe-relay-legacy-admission.test.mjs dev/scripts/probe-relay-rehome-trust.test.mjs dev/scripts/production-cell-image-digest-consistency.test.mjs dev/scripts/push-gateway-workflow.test.mjs dev/scripts/push-gateway-recovery.test.mjs dev/scripts/read-relay-production-capacity-identity.test.mjs dev/scripts/relay-admin-endpoint-retry-workflow.test.mjs dev/scripts/relay-admin-transient-retry.test.mjs dev/scripts/relay-admission-selector.test.mjs dev/scripts/relay-gce-terraform-fence.test.mjs dev/scripts/relay-load-connection-failure.test.mjs dev/scripts/relay-load-control-peer.test.mjs dev/scripts/relay-load-director-capacity-gate.test.mjs dev/scripts/relay-load-duplicate-assign.test.mjs dev/scripts/relay-load-model.test.mjs dev/scripts/relay-load-phase-barrier.test.mjs dev/scripts/relay-load-placement-boundary.test.mjs dev/scripts/relay-load-profile.test.mjs dev/scripts/relay-load-rebind-boundary.test.mjs dev/scripts/relay-load-region-behavior.test.mjs dev/scripts/relay-load-request-unit-boundary.test.mjs dev/scripts/relay-load-run-lifecycle.test.mjs dev/scripts/relay-load-splice-retry.test.mjs dev/scripts/relay-monitor-evidence.test.mjs dev/scripts/relay-production-capacity-wave.test.mjs dev/scripts/relay-production-capacity-workflow.test.mjs dev/scripts/relay-production-identity-boundaries.test.mjs dev/scripts/relay-production-same-cap-wave.test.mjs dev/scripts/relay-public-workflow-contract.test.mjs dev/scripts/relay-recovery-wave-gate.test.mjs dev/scripts/relay-region-observation-evidence.test.mjs dev/scripts/relay-rehome-aggregate-evidence.test.mjs dev/scripts/relay-repository.test.mjs dev/scripts/relay-same-cap-job-mode-conditions.test.mjs dev/scripts/relay-same-cap-script-census.test.mjs dev/scripts/relay-same-cap-shadow-gate.test.mjs dev/scripts/relay-staging-c4-refresh-workflow.test.mjs dev/scripts/relay-staging-capacity-identity.test.mjs dev/scripts/staging-relay-apply-guard.test.mjs dev/scripts/validate-relay-capacity-plan.test.mjs dev/scripts/verify-relay-capacity-transition.test.mjs dev/scripts/verify-relay-legacy-bootstrap.test.mjs dev/scripts/workload-identity-attribute-conditions.test.mjs", + "typecheck": "pnpm -r typecheck", + "test:runner": "node --import tsx --conditions=orca-source --test dev/scripts/run-relay-tests.test.mjs" }, "devDependencies": { + "@orca/process-host": "link:../src/packages/process-host", "@types/node": "^24.19.0", "tsx": "^4.23.15", "typescript": "^5.9.3", "vitest": "^4.1.11" - } + }, + "engines": { + "node": ">=24 <27", + "pnpm": ">=10" + }, + "packageManager": "pnpm@10.34.6" } diff --git a/cloud/pnpm-lock.yaml b/cloud/pnpm-lock.yaml index c688889cb50..2720b9e6474 100644 --- a/cloud/pnpm-lock.yaml +++ b/cloud/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@orca/process-host': + specifier: link:../src/packages/process-host + version: link:../src/packages/process-host '@types/node': specifier: ^24.19.0 version: 24.19.0 diff --git a/config/scripts/ci-e2e-job-selection.test.mjs b/config/scripts/ci-e2e-job-selection.test.mjs index 29d7b4c0a7a..2ecf9ea5b3c 100644 --- a/config/scripts/ci-e2e-job-selection.test.mjs +++ b/config/scripts/ci-e2e-job-selection.test.mjs @@ -97,7 +97,7 @@ it('keeps Electron build and native prerequisites for every consumer requiring t } expect(workflow.jobs['ssh-browser-network-route'].needs).toBeUndefined() for (const name of ['e2e', 'changed-e2e']) { - expect(workflow.jobs[name].needs, name).toEqual(['build', 'prepare-native-cache']) + expect(workflow.jobs[name].needs, name).toEqual(['build']) } }) @@ -132,11 +132,9 @@ it('applies allocation hints only to PRs and retains other callers and full refe default: true }) } - for (const name of ['build', 'prepare-native-cache']) { - expect(workflow.jobs[name].if).toBe( - "inputs.test_files == '' || github.event_name != 'pull_request' || inputs.needs_build" - ) - } + expect(workflow.jobs.build.if).toBe( + "inputs.test_files == '' || github.event_name != 'pull_request' || inputs.needs_build" + ) expect(workflow.jobs.e2e.if).toBe("inputs.test_files == ''") const changed = workflow.jobs['changed-e2e'] expect(changed.if).toContain("github.event_name != 'pull_request' || inputs.run_changed_e2e") diff --git a/config/scripts/ci-native-cache.test.mjs b/config/scripts/ci-native-cache.test.mjs index 91c51b7919e..4ec73876617 100644 --- a/config/scripts/ci-native-cache.test.mjs +++ b/config/scripts/ci-native-cache.test.mjs @@ -1,4 +1,4 @@ -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { parse } from 'yaml' @@ -10,6 +10,7 @@ const installerPath = '.github/actions/install-node-dependencies/action.yml' const nativePath = '.github/actions/prepare-native-runtime/action.yml' const installer = parse(readFileSync(installerPath, 'utf8')) const native = parse(readFileSync(nativePath, 'utf8')) +const e2e = parse(readFileSync('.github/workflows/e2e.yml', 'utf8')) const identity = native.runs.steps.find((step) => step.id === 'native-cache-scope') const inputs = [...identity.env.NATIVE_SOURCE_HASH.matchAll(/'([^']+)'/g)].map((match) => match[1]) const paths = [ @@ -176,4 +177,138 @@ describe('CI native cache ownership', () => { } } ) + + it('prepares both native ABIs in the E2E build before releasing its consumers', () => { + expect(e2e.jobs['prepare-native-cache']).toBeUndefined() + const { steps } = e2e.jobs.build + const install = steps.find((step) => step.id === 'deps') + const save = steps.find((step) => step.name === 'Save compiled Node native modules') + const build = steps.find((step) => step.name === 'Build E2E outputs') + const cliWait = steps.find((step) => step.wait === 'e2e-cli') + const electron = steps.find((step) => step.id === 'electron-native-runtime') + const upload = steps.find((step) => step.name === 'Upload E2E build output') + const uploadWait = steps.find((step) => step.wait === upload.id) + expect(install.with).toMatchObject({ + 'native-runtime': 'node', + 'persist-native-cache': 'false', + 'cache-electron-package': 'true' + }) + expect(save.if).toContain("steps.deps.outputs.native-cache-key != ''") + expect(save.if).toContain("steps.deps.outputs.native-cache-hit != 'true'") + expect(save.with).toEqual({ + key: '${{ steps.deps.outputs.native-cache-key }}', + path: '${{ steps.deps.outputs.native-cache-path }}' + }) + const ordered = [install, save, build, cliWait, upload, electron, uploadWait] + expect(ordered.map((step) => steps.indexOf(step))).toEqual( + ordered.map((step) => steps.indexOf(step)).sort((left, right) => left - right) + ) + expect(upload.background).toBe(true) + expect(upload.if).toBeUndefined() + expect(electron.background).toBeUndefined() + expect(electron.uses).toBe('./.github/actions/prepare-native-runtime') + expect(electron.with).toEqual({ + 'native-runtime': 'electron', + 'node-version': '${{ steps.deps.outputs.node-version }}' + }) + expect(uploadWait).toEqual({ wait: upload.id }) + for (const name of ['e2e', 'changed-e2e', 'orcad-serve-mode-switch', 'orcad-ssh-docker']) { + expect(e2e.jobs[name].needs, name).toEqual(['build']) + expect( + e2e.jobs[name].steps.find( + (step) => step.uses === './.github/actions/install-node-dependencies' + ).with['native-runtime'], + name + ).toBe('electron') + } + }) + + it.skipIf(process.platform === 'win32').each([false, true])( + 'detects shared native action availability=%s for older dispatch refs', + (available) => { + const directory = mkdtempSync(join(tmpdir(), 'orca-e2e-native-action-')) + try { + const output = join(directory, 'output') + if (available) { + mkdirSync(join(directory, '.github/actions/prepare-native-runtime'), { recursive: true }) + writeFileSync(join(directory, nativePath), '') + } + const step = e2e.jobs.build.steps.find( + (candidate) => candidate.id === 'native-runtime-action' + ) + const execution = runProcessSync({ + program: 'bash', + args: ['-e', '-o', 'pipefail', '-c', step.run], + cwd: directory, + env: { ...process.env, GITHUB_OUTPUT: output }, + timeoutMs: 10_000 + }) + expect(execution.code, execution.stderr).toBe(0) + expect(readFileSync(output, 'utf8')).toBe(`available=${available}\n`) + const fallback = e2e.jobs.build.steps.find( + (candidate) => candidate.name === 'Prepare Electron natives for older refs' + ) + expect(fallback.if).toBe( + "steps.native-runtime-action.outputs.available != 'true' || steps.deps.outputs.node-version == ''" + ) + expect(fallback.uses).toBe('./.github/actions/install-node-dependencies') + expect(fallback.with['native-runtime']).toBe('electron') + } finally { + rmSync(directory, { recursive: true, force: true }) + } + } + ) + + it.skipIf(process.platform === 'win32').each([ + ['true', 'build-essential python3', '', 0, false], + ['false', 'build-essential python3', '', 0, true], + ['true', 'build-essential python3 xvfb', '', 0, true], + ['false', 'build-essential python3', 'update', 7, true], + ['false', 'build-essential python3', 'install', 7, true] + ])( + 'tools=%s packages=%s failure=%s retains bounded setup', + (present, packages, failure, status, installs) => { + const directory = mkdtempSync(join(tmpdir(), 'orca-e2e-native-tools-')) + try { + const log = join(directory, 'commands') + writeFileSync(log, '') + const step = e2e.jobs.build.steps.find( + (candidate) => candidate.name === 'Install native build tools' + ) + const execution = runProcessSync({ + program: 'bash', + args: [ + '-e', + '-o', + 'pipefail', + '-c', + ` + command() { [ "$TOOLS_PRESENT" = true ]; } + sudo() { printf 'sudo %s\\n' "$*" >> "$COMMAND_LOG"; cat >/dev/null; } + timeout() { + printf 'timeout %s\\n' "$*" >> "$COMMAND_LOG" + if [ "$4" = "$FAIL_OPERATION" ]; then return 7; fi + } + ${step.run}` + ], + env: { + ...process.env, + TOOLS_PRESENT: present, + ORCA_E2E_APT_PACKAGES: packages, + FAIL_OPERATION: failure, + COMMAND_LOG: log + }, + timeoutMs: 10_000 + }) + expect(execution.code, execution.stderr).toBe(status) + const commands = readFileSync(log, 'utf8') + expect(commands.includes('timeout 120 sudo apt-get update')).toBe(installs) + expect(commands.includes(`timeout 300 sudo apt-get install -y ${packages}`)).toBe( + installs && failure !== 'update' + ) + } finally { + rmSync(directory, { recursive: true, force: true }) + } + } + ) }) diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index c0df38efda9..45f16bf2035 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -1,11724 +1,12327 @@ { "unit": { - "runId": "38086200856", + "runId": "38646706136", "runAttempt": "1", - "sourceSha": "311827966ba81ad0e2b40bf5b98a2a5f1b6166eb", + "sourceSha": "8d7f46198b99432efd104ac7803174173b367c66", "nodeVersion": "24.21.0", - "bunVersion": "1.4.2", + "bunVersion": "1.4.3", "metric": "module-duration-v1", "overheadMs": 0, "timings": { - "config/scripts/adhoc-build-pinned-commit.test.mjs": 226, - "config/scripts/adhoc-build-version.test.mjs": 95, - "config/scripts/agent-state-rules-bundle.test.mjs": 327, - "config/scripts/app-store-performance-plugin.test.mjs": 1010, - "config/scripts/assert-github-release-is-draft.test.mjs": 352, - "config/scripts/audit-localization-coverage.test.mjs": 270, - "config/scripts/benchmark-artifact-comparison.test.mjs": 171, - "config/scripts/benchmark-sample-summary.test.mjs": 57, - "config/scripts/build-linux-local.test.mjs": 56, - "config/scripts/build-mac-local.test.mjs": 57, - "config/scripts/build-mobile-web-app-bundle.test.mjs": 419, - "config/scripts/build-native-for-platform.test.mjs": 52, - "config/scripts/build-orcad-prebuilds.test.mjs": 105, - "config/scripts/build-orcad-template.test.mjs": 145, - "config/scripts/build-windows-cli-launcher.test.mjs": 117, - "config/scripts/casting-code-quality.test.mjs": 854, - "config/scripts/check-changed-code-quality.test.mjs": 3252, - "config/scripts/check-daemon-protocol-crossing.test.mjs": 194, - "config/scripts/check-max-lines-ratchet.test.mjs": 658, - "config/scripts/check-node-runtime-pin.test.mjs": 696, - "config/scripts/check-focus-read-ratchet.test.mjs": 86, - "config/scripts/check-process-host-imports.test.mjs": 301, - "config/scripts/check-react-doctor-changed.test.mjs": 229, - "config/scripts/check-readme-local-links.test.mjs": 414, - "config/scripts/check-reliability-gates.test.mjs": 216, - "config/scripts/check-root-directory-entries.test.mjs": 790, - "config/scripts/check-runtime-electron-ratchet-graph.test.mjs": 163, - "config/scripts/check-runtime-electron-ratchet.test.mjs": 3736, - "config/scripts/check-runtime-launcher-protocol-ratchet.test.mjs": 185, - "config/scripts/check-terminal-perf-report-budgets.test.mjs": 1481, - "config/scripts/check-ts-nocheck-ratchet.test.mjs": 68, - "config/scripts/ci-closed-pr-caches.test.mjs": 123, - "config/scripts/ci-closed-pr-cancellation.test.mjs": 128, - "config/scripts/ci-dependency-download-cache.test.mjs": 557, - "config/scripts/ci-e2e-failure-summary.test.mjs": 56, - "config/scripts/ci-e2e-job-selection.test.mjs": 326, - "config/scripts/ci-e2e-shard-selection.test.mjs": 2569, - "config/scripts/ci-native-cache.test.mjs": 267, - "config/scripts/ci-native-toolchain.test.mjs": 118, - "config/scripts/ci-pnpm-store-mode.test.mjs": 550, - "config/scripts/ci-pnpm-verification-cache.test.mjs": 331, - "config/scripts/ci-runner-metrics.test.mjs": 65, - "config/scripts/ci-shard-assignment.test.mjs": 576, - "config/scripts/ci-unit-dependency-graph.test.mjs": 340, - "config/scripts/ci-unit-exclusion-coverage.test.mjs": 936, - "config/scripts/ci-unit-import-resolution-budget.test.mjs": 270, - "config/scripts/ci-unit-plan.test.mjs": 3110, - "config/scripts/ci-unit-selection-review.test.mjs": 43, - "config/scripts/ci-unit-selection.test.mjs": 276, - "config/scripts/ci-unit-sequencer.test.mjs": 318, - "config/scripts/ci-unit-timing-reporter.test.mjs": 268, - "config/scripts/cloud-security-history-workflow.test.mjs": 259, - "config/scripts/codex-primary-home-tripwire.test.ts": 359, - "config/scripts/computer-use-smoke.test.mjs": 491, - "config/scripts/counterbalanced-benchmark-schedule.test.mjs": 57, - "config/scripts/create-draft-release.test.mjs": 71, - "config/scripts/daemon-protocol-facts.test.mjs": 57, - "config/scripts/daily-build-version.test.mjs": 90, - "config/scripts/dev-channel-base-version.test.mjs": 51, - "config/scripts/dev-cli-terminal-wrapper.test.mjs": 57, - "config/scripts/dev-electron-bundle-cache.test.ts": 49, - "config/scripts/dev-electron-bundle-identity.test.ts": 59, - "config/scripts/e2e-cli-artifact-capability.test.mjs": 262, - "config/scripts/electron-builder-config.test.mjs": 906, - "config/scripts/electron-builder-mac-channel-config.test.mjs": 396, - "config/scripts/electron-builder-native-rebuild.test.mjs": 57, - "config/scripts/electron-builder-runtime-resources.test.mjs": 540, - "config/scripts/electron-builder-speech-config.test.mjs": 322, - "config/scripts/electron-runtime-floor.test.ts": 49, - "config/scripts/electron-vite-output-contract.test.ts": 741, - "config/scripts/ensure-native-runtime-job-ownership.test.mjs": 73, - "config/scripts/ensure-native-runtime.test.mjs": 783, - "config/scripts/ephemeral-vm-runtime-store-cross-version.test.ts": 42, - "config/scripts/foreign-sqlite-reader-worker-smoke.test.mjs": 461, - "config/scripts/generate-bundled-skill-guides.test.mjs": 367, - "config/scripts/generate-monaco-associations.test.mjs": 344, - "config/scripts/generate-runtime-required-english-catalog.test.mjs": 432, - "config/scripts/generate-skill-bundle-manifest.test.mjs": 537, - "config/scripts/generate-terminal-perf-html-report.test.mjs": 107, + "config/scripts/adhoc-build-pinned-commit.test.mjs": 250, + "config/scripts/adhoc-build-version.test.mjs": 104, + "config/scripts/agent-state-rules-bundle.test.mjs": 272, + "config/scripts/anti-slop-scope-regression.test.mjs": 2281, + "config/scripts/app-store-performance-plugin.test.mjs": 855, + "config/scripts/appimage-node-runtime.test.mjs": 114, + "config/scripts/assert-github-release-is-draft.test.mjs": 208, + "config/scripts/audit-localization-coverage.test.mjs": 115, + "config/scripts/benchmark-artifact-comparison.test.mjs": 178, + "config/scripts/benchmark-sample-summary.test.mjs": 38, + "config/scripts/build-linux-local.test.mjs": 46, + "config/scripts/build-mac-local.test.mjs": 38, + "config/scripts/build-mobile-web-app-bundle.test.mjs": 430, + "config/scripts/build-native-for-platform.test.mjs": 54, + "config/scripts/build-orcad-prebuilds.test.mjs": 144, + "config/scripts/build-orcad-template.test.mjs": 180, + "config/scripts/build-wayland-clipboard.test.mjs": 351, + "config/scripts/build-windows-cli-launcher.test.mjs": 97, + "config/scripts/bump-android-apk-download-version.test.mjs": 42, + "config/scripts/casting-code-quality.test.mjs": 824, + "config/scripts/check-changed-code-quality.test.mjs": 2564, + "config/scripts/check-compat-boundary-imports.test.mjs": 124, + "config/scripts/check-daemon-protocol-crossing.test.mjs": 132, + "config/scripts/check-focus-read-ratchet.test.mjs": 58, + "config/scripts/check-import-boundaries.test.mjs": 230, + "config/scripts/check-layout-writer-ratchets.test.mjs": 143, + "config/scripts/check-main-sync-spawn-ratchet.test.mjs": 279, + "config/scripts/check-max-lines-ratchet.test.mjs": 726, + "config/scripts/check-node-runtime-pin.test.mjs": 670, + "config/scripts/check-process-host-imports.test.mjs": 165, + "config/scripts/check-react-doctor-changed.test.mjs": 218, + "config/scripts/check-readme-local-links.test.mjs": 401, + "config/scripts/check-reliability-gates.test.mjs": 166, + "config/scripts/check-root-directory-entries.test.mjs": 857, + "config/scripts/check-runtime-electron-ratchet-graph.test.mjs": 105, + "config/scripts/check-runtime-electron-ratchet.test.mjs": 4608, + "config/scripts/check-runtime-launcher-protocol-ratchet.test.mjs": 181, + "config/scripts/check-terminal-perf-report-budgets.test.mjs": 1575, + "config/scripts/check-ts-nocheck-ratchet.test.mjs": 42, + "config/scripts/ci-closed-pr-caches.test.mjs": 47, + "config/scripts/ci-closed-pr-cancellation.test.mjs": 75, + "config/scripts/ci-dependency-download-cache.test.mjs": 469, + "config/scripts/ci-e2e-failure-summary.test.mjs": 45, + "config/scripts/ci-e2e-job-selection.test.mjs": 311, + "config/scripts/ci-e2e-shard-selection.test.mjs": 2628, + "config/scripts/ci-native-cache.test.mjs": 251, + "config/scripts/ci-native-toolchain.test.mjs": 105, + "config/scripts/ci-pnpm-store-mode.test.mjs": 498, + "config/scripts/ci-pnpm-verification-cache.test.mjs": 274, + "config/scripts/ci-runner-metrics.test.mjs": 54, + "config/scripts/ci-shard-assignment.test.mjs": 505, + "config/scripts/ci-unit-dependency-graph.test.mjs": 199, + "config/scripts/ci-unit-exclusion-coverage.test.mjs": 1039, + "config/scripts/ci-unit-import-resolution-budget.test.mjs": 112, + "config/scripts/ci-unit-plan.test.mjs": 2321, + "config/scripts/ci-unit-selection-review.test.mjs": 37, + "config/scripts/ci-unit-selection.test.mjs": 139, + "config/scripts/ci-unit-sequencer.test.mjs": 304, + "config/scripts/ci-unit-timing-reporter.test.mjs": 151, + "config/scripts/cloud-security-history-workflow.test.mjs": 353, + "config/scripts/codex-primary-home-tripwire.test.ts": 332, + "config/scripts/computer-use-smoke.test.mjs": 473, + "config/scripts/counterbalanced-benchmark-schedule.test.mjs": 44, + "config/scripts/create-draft-release.test.mjs": 59, + "config/scripts/daemon-protocol-facts.test.mjs": 54, + "config/scripts/daily-build-version.test.mjs": 58, + "config/scripts/dev-channel-base-version.test.mjs": 32, + "config/scripts/dev-channel-orcad-template-workflow.test.mjs": 131, + "config/scripts/dev-cli-terminal-wrapper.test.mjs": 43, + "config/scripts/dev-electron-bundle-cache.test.ts": 45, + "config/scripts/dev-electron-bundle-identity.test.ts": 39, + "config/scripts/e2e-cli-artifact-capability.test.mjs": 188, + "config/scripts/electron-builder-config.test.mjs": 753, + "config/scripts/electron-builder-mac-channel-config.test.mjs": 251, + "config/scripts/electron-builder-native-rebuild.test.mjs": 41, + "config/scripts/electron-builder-runtime-resources.test.mjs": 2537, + "config/scripts/electron-builder-speech-config.test.mjs": 167, + "config/scripts/electron-runtime-floor.test.ts": 31, + "config/scripts/electron-vite-output-contract.test.ts": 701, + "config/scripts/ensure-native-runtime-job-ownership.test.mjs": 76, + "config/scripts/ensure-native-runtime.test.mjs": 780, + "config/scripts/ephemeral-vm-runtime-store-cross-version.test.ts": 33, + "config/scripts/foreign-sqlite-reader-worker-smoke.test.mjs": 427, + "config/scripts/generate-bundled-skill-guides.test.mjs": 458, + "config/scripts/generate-monaco-associations.test.mjs": 221, + "config/scripts/generate-runtime-required-english-catalog.test.mjs": 301, + "config/scripts/generate-skill-bundle-manifest.test.mjs": 331, + "config/scripts/generate-terminal-perf-html-report.test.mjs": 79, "config/scripts/generated-artifact-line-ending-pin.test.mjs": 54, - "config/scripts/git-pull-request-diff-base.test.mjs": 52, - "config/scripts/github-opened-issue-repository.test.ts": 1032, - "config/scripts/github-pr-assignee-repository.test.ts": 560, - "config/scripts/hang-watchdog-process-metrics.test.mjs": 102, - "config/scripts/happy-dom-mutation-observer-retention.test.ts": 206, - "config/scripts/happy-dom-offscreen-canvas.test.ts": 195, - "config/scripts/headless-detector-compiler-cache.test.mjs": 1063, - "config/scripts/headless-serve-shutdown-matrix.test.mjs": 84, - "config/scripts/hourly-build-version.test.mjs": 82, - "config/scripts/hourly-preflight-workflow.test.mjs": 182, - "config/scripts/idle-cpu-process-sampling.test.mjs": 64, - "config/scripts/install-electron-package-binary.test.mjs": 2694, - "config/scripts/install-node-dependencies-action.test.mjs": 336, - "config/scripts/lag-probe-failures.test.mjs": 60, - "config/scripts/latest-stable-release.test.mjs": 53, - "config/scripts/linear-sdk-runtime.test.mjs": 985, - "config/scripts/lint-staged-worktree-backup.test.mjs": 253, - "config/scripts/linux-package-fixture-cache.test.mjs": 204, - "config/scripts/live-freeze-bounded-history.test.mjs": 58, - "config/scripts/live-remote-bulk-open-freeze-metrics.test.mjs": 86, - "config/scripts/live-remote-freeze-rpc.test.mjs": 64, - "config/scripts/live-remote-status-watchdog.test.mjs": 284, - "config/scripts/locale-brand-prefilter.test.mjs": 215, - "config/scripts/locale-count-fragment-separator.test.mjs": 99, - "config/scripts/locale-generic-ui-terms.test.mjs": 103, - "config/scripts/locale-ko-frozen-terminal-search-keywords.test.mjs": 126, - "config/scripts/locale-ko-key-overrides.test.mjs": 68, - "config/scripts/locale-repair-catalog-missing-leaves.test.mjs": 103, - "config/scripts/locale-translation-policy-ko-round5.test.mjs": 118, - "config/scripts/locale-translation-policy.es-pr.test.mjs": 97, - "config/scripts/locale-translation-policy.es-round5.test.mjs": 78, - "config/scripts/locale-translation-policy.ja-relocalization.test.mjs": 119, - "config/scripts/locale-translation-policy.ja-round5.test.mjs": 112, + "config/scripts/git-pull-request-diff-base.test.mjs": 31, + "config/scripts/github-opened-issue-repository.test.ts": 1347, + "config/scripts/github-pr-assignee-repository.test.ts": 711, + "config/scripts/hang-watchdog-process-metrics.test.mjs": 69, + "config/scripts/happy-dom-mutation-observer-retention.test.ts": 199, + "config/scripts/happy-dom-offscreen-canvas.test.ts": 204, + "config/scripts/headless-detector-compiler-cache.test.mjs": 876, + "config/scripts/headless-serve-shutdown-matrix.test.mjs": 77, + "config/scripts/hourly-build-version.test.mjs": 63, + "config/scripts/hourly-preflight-workflow.test.mjs": 118, + "config/scripts/idle-cpu-process-sampling.test.mjs": 42, + "config/scripts/ime-enter-guard-plugin.test.mjs": 2898, + "config/scripts/install-electron-package-binary.test.mjs": 2480, + "config/scripts/install-node-dependencies-action.test.mjs": 410, + "config/scripts/lag-probe-failures.test.mjs": 54, + "config/scripts/latest-stable-release.test.mjs": 31, + "config/scripts/linear-sdk-runtime.test.mjs": 977, + "config/scripts/lint-staged-worktree-backup.test.mjs": 269, + "config/scripts/linux-package-fixture-cache.test.mjs": 206, + "config/scripts/live-freeze-bounded-history.test.mjs": 41, + "config/scripts/live-remote-bulk-open-freeze-metrics.test.mjs": 74, + "config/scripts/live-remote-freeze-rpc.test.mjs": 35, + "config/scripts/live-remote-status-watchdog.test.mjs": 290, + "config/scripts/locale-brand-prefilter.test.mjs": 245, + "config/scripts/locale-count-fragment-separator.test.mjs": 78, + "config/scripts/locale-generic-ui-terms.test.mjs": 83, + "config/scripts/locale-ko-frozen-terminal-search-keywords.test.mjs": 127, + "config/scripts/locale-repair-catalog-missing-leaves.test.mjs": 77, + "config/scripts/locale-translation-policy-ko-round5.test.mjs": 107, + "config/scripts/locale-translation-policy.es-pr.test.mjs": 77, + "config/scripts/locale-translation-policy.es-round5.test.mjs": 87, + "config/scripts/locale-translation-policy.ja-relocalization.test.mjs": 101, + "config/scripts/locale-translation-policy.ja-round5.test.mjs": 90, "config/scripts/locale-translation-policy.test.mjs": 119, - "config/scripts/locale-translation-policy.zh-round5.test.mjs": 191, - "config/scripts/locale-translation-policy.zh-status-bar-usage.test.mjs": 105, - "config/scripts/localization-extraction-call-syntax.test.mjs": 292, - "config/scripts/localization-extraction-change-scope.test.mjs": 308, - "config/scripts/mac-build-compatibility.test.mjs": 47, - "config/scripts/macos-computer-helper-owner-loss-group-recovery.test.mjs": 64, - "config/scripts/macos-computer-helper-owner-loss-processes.test.mjs": 65, - "config/scripts/macos-tcc-prompt-localization.test.mjs": 287, - "config/scripts/managed-data-account-runtime.test.ts": 1001, - "config/scripts/merge-orcad-prebuilds.test.mjs": 82, - "config/scripts/mobile-entry-static-closure-work-budget.test.mjs": 127, - "config/scripts/mobile-pairing-qrcode-import-plugin.test.mjs": 374, - "config/scripts/mobile-process-host-tooling.test.mjs": 978, - "config/scripts/mobile-release-check-scope.test.mjs": 701, - "config/scripts/mobile-test-change-scope.test.mjs": 126, - "config/scripts/mobile-web-app-agent-history-render.test.mjs": 363, - "config/scripts/mobile-web-app-browser-pane-render.test.mjs": 365, - "config/scripts/mobile-web-app-browser-pane-text-inputs.test.mjs": 374, - "config/scripts/mobile-web-app-clipboard-image-resize-render.test.mjs": 368, - "config/scripts/mobile-web-app-drawer-render.test.mjs": 365, - "config/scripts/mobile-web-app-editable-host-font-size.test.mjs": 291, - "config/scripts/mobile-web-app-external-link-seam.test.mjs": 247, - "config/scripts/mobile-web-app-external-navigation-grant.test.mjs": 68, - "config/scripts/mobile-web-app-files-external-links.test.mjs": 228, - "config/scripts/mobile-web-app-files-render.test.mjs": 356, - "config/scripts/mobile-web-app-frame-budget-sweep.test.ts": 322, - "config/scripts/mobile-web-app-handoff-grants-render.test.mjs": 395, - "config/scripts/mobile-web-app-haptics-seam.test.mjs": 295, - "config/scripts/mobile-web-app-held-press-render.test.mjs": 367, - "config/scripts/mobile-web-app-html-preview-render.test.mjs": 517, - "config/scripts/mobile-web-app-media-preview-render.test.mjs": 360, - "config/scripts/mobile-web-app-mermaid-render.test.mjs": 354, - "config/scripts/mobile-web-app-native-chat-visual-links.test.mjs": 365, - "config/scripts/mobile-web-app-native-parity-render.test.mjs": 399, - "config/scripts/mobile-web-app-page-closure-families.test.mjs": 74, - "config/scripts/mobile-web-app-page-grant-call-sites.test.mjs": 302, - "config/scripts/mobile-web-app-push-subsystem-closure.test.mjs": 64, - "config/scripts/mobile-web-app-render.test.mjs": 403, - "config/scripts/mobile-web-app-rich-markdown-render.test.mjs": 721, - "config/scripts/mobile-web-app-route-manifest.test.mjs": 164, - "config/scripts/mobile-web-app-route-snapshot.test.mjs": 121, - "config/scripts/mobile-web-app-safe-area-render.test.mjs": 365, - "config/scripts/mobile-web-app-screencast-lane-grant.test.mjs": 69, - "config/scripts/mobile-web-app-session-dictation-capture.test.mjs": 248, - "config/scripts/mobile-web-app-session-external-links.test.mjs": 197, - "config/scripts/mobile-web-app-session-media-picker.test.mjs": 270, - "config/scripts/mobile-web-app-session-render.test.mjs": 372, - "config/scripts/mobile-web-app-session-terminal-closure.test.mjs": 231, - "config/scripts/mobile-web-app-session-terminal-init.test.mjs": 352, - "config/scripts/mobile-web-app-session-text-inputs.test.mjs": 237, - "config/scripts/mobile-web-app-session-webview-consumers.test.mjs": 67, - "config/scripts/mobile-web-app-source-control-external-links.test.mjs": 215, - "config/scripts/mobile-web-app-source-control-keyboard.test.mjs": 56, - "config/scripts/mobile-web-app-source-control-render.test.mjs": 325, - "config/scripts/mobile-web-app-source-control-text-inputs.test.mjs": 334, - "config/scripts/mobile-web-app-stack-transition-render.test.mjs": 357, - "config/scripts/mobile-web-app-tasks-external-links.test.mjs": 251, - "config/scripts/mobile-web-app-tasks-render.test.mjs": 369, - "config/scripts/mobile-web-app-terminal-live-input-render.test.mjs": 332, - "config/scripts/mobile-web-app-terminal-render-fixture.test.mjs": 380, - "config/scripts/mobile-web-app-terminal-render.test.mjs": 352, - "config/scripts/mobile-web-app-working-spinner-render.test.mjs": 382, - "config/scripts/mobile-web-bundle-manifest.test.mjs": 224, - "config/scripts/mobile-web-bundle-serializer-parity.test.mjs": 237, - "config/scripts/mobile-web-page-routes.test.mjs": 250, - "config/scripts/mobile-web-source-line-endings.test.mjs": 261, - "config/scripts/mobile-web-terminal-engine-closure.test.mjs": 60, - "config/scripts/moved-code-normalization-budget.test.mjs": 125, - "config/scripts/msbuild-file-tracking.test.mjs": 44, - "config/scripts/native-chat-compaction-pagination.test.mjs": 446, - "config/scripts/node-old-space-limit.test.mjs": 50, - "config/scripts/node-server-change-scope.test.mjs": 2687, + "config/scripts/locale-translation-policy.zh-round5.test.mjs": 83, + "config/scripts/locale-translation-policy.zh-status-bar-usage.test.mjs": 73, + "config/scripts/localization-extraction-call-syntax.test.mjs": 347, + "config/scripts/localization-extraction-change-scope.test.mjs": 252, + "config/scripts/mac-build-compatibility.test.mjs": 31, + "config/scripts/macos-computer-helper-owner-loss-group-recovery.test.mjs": 40, + "config/scripts/macos-computer-helper-owner-loss-processes.test.mjs": 40, + "config/scripts/macos-folder-usage-descriptions.test.mjs": 117, + "config/scripts/macos-tcc-prompt-localization.test.mjs": 250, + "config/scripts/macos-terminal-host-bundle.test.mjs": 153, + "config/scripts/managed-data-account-runtime.test.ts": 925, + "config/scripts/merge-orcad-prebuilds.test.mjs": 66, + "config/scripts/mobile-entry-static-closure-work-budget.test.mjs": 90, + "config/scripts/mobile-pairing-qrcode-import-plugin.test.mjs": 367, + "config/scripts/mobile-process-host-tooling.test.mjs": 751, + "config/scripts/mobile-release-check-scope.test.mjs": 775, + "config/scripts/mobile-test-change-scope.test.mjs": 130, + "config/scripts/mobile-typecheck-cache.test.mjs": 70, + "config/scripts/mobile-web-app-agent-history-render.test.mjs": 453, + "config/scripts/mobile-web-app-browser-pane-render.test.mjs": 191, + "config/scripts/mobile-web-app-clipboard-image-resize-render.test.mjs": 211, + "config/scripts/mobile-web-app-drawer-render.test.mjs": 218, + "config/scripts/mobile-web-app-editable-host-font-size.test.mjs": 150, + "config/scripts/mobile-web-app-external-link-seam.test.mjs": 135, + "config/scripts/mobile-web-app-external-navigation-grant.test.mjs": 47, + "config/scripts/mobile-web-app-files-external-links.test.mjs": 112, + "config/scripts/mobile-web-app-files-render.test.mjs": 240, + "config/scripts/mobile-web-app-frame-budget-sweep.test.ts": 201, + "config/scripts/mobile-web-app-handoff-grants-render.test.mjs": 475, + "config/scripts/mobile-web-app-haptics-seam.test.mjs": 131, + "config/scripts/mobile-web-app-held-press-render.test.mjs": 257, + "config/scripts/mobile-web-app-html-preview-render.test.mjs": 291, + "config/scripts/mobile-web-app-media-preview-render.test.mjs": 205, + "config/scripts/mobile-web-app-mermaid-render.test.mjs": 197, + "config/scripts/mobile-web-app-module-closure-entrypoints.test.mjs": 79, + "config/scripts/mobile-web-app-module-entry-resolution.test.mjs": 78, + "config/scripts/mobile-web-app-native-chat-visual-links.test.mjs": 515, + "config/scripts/mobile-web-app-native-parity-render.test.mjs": 214, + "config/scripts/mobile-web-app-page-closure-families.test.mjs": 63, + "config/scripts/mobile-web-app-page-grant-call-sites.test.mjs": 127, + "config/scripts/mobile-web-app-push-subsystem-closure.test.mjs": 44, + "config/scripts/mobile-web-app-render.test.mjs": 621, + "config/scripts/mobile-web-app-rich-markdown-render.test.mjs": 620, + "config/scripts/mobile-web-app-route-manifest.test.mjs": 182, + "config/scripts/mobile-web-app-route-snapshot.test.mjs": 106, + "config/scripts/mobile-web-app-safe-area-render.test.mjs": 616, + "config/scripts/mobile-web-app-screencast-lane-grant.test.mjs": 37, + "config/scripts/mobile-web-app-session-dictation-capture.test.mjs": 207, + "config/scripts/mobile-web-app-session-external-links.test.mjs": 102, + "config/scripts/mobile-web-app-session-media-picker.test.mjs": 141, + "config/scripts/mobile-web-app-session-render.test.mjs": 188, + "config/scripts/mobile-web-app-session-terminal-closure.test.mjs": 129, + "config/scripts/mobile-web-app-session-terminal-init.test.mjs": 205, + "config/scripts/mobile-web-app-session-webview-consumers.test.mjs": 43, + "config/scripts/mobile-web-app-source-control-external-links.test.mjs": 133, + "config/scripts/mobile-web-app-source-control-keyboard.test.mjs": 49, + "config/scripts/mobile-web-app-source-control-render.test.mjs": 233, + "config/scripts/mobile-web-app-source-control-text-inputs.test.mjs": 197, + "config/scripts/mobile-web-app-stack-transition-render.test.mjs": 214, + "config/scripts/mobile-web-app-tasks-external-links.test.mjs": 121, + "config/scripts/mobile-web-app-tasks-render.test.mjs": 207, + "config/scripts/mobile-web-app-terminal-live-input-render.test.mjs": 213, + "config/scripts/mobile-web-app-terminal-render-fixture.test.mjs": 445, + "config/scripts/mobile-web-app-terminal-render.test.mjs": 241, + "config/scripts/mobile-web-app-text-input-discovery.test.mjs": 89, + "config/scripts/mobile-web-app-working-spinner-render.test.mjs": 572, + "config/scripts/mobile-web-bundle-manifest.test.mjs": 275, + "config/scripts/mobile-web-bundle-serializer-parity.test.mjs": 233, + "config/scripts/mobile-web-page-routes.test.mjs": 240, + "config/scripts/mobile-web-source-line-endings.test.mjs": 243, + "config/scripts/mobile-web-terminal-engine-closure.test.mjs": 40, + "config/scripts/moved-code-normalization-budget.test.mjs": 94, + "config/scripts/msbuild-file-tracking.test.mjs": 33, + "config/scripts/native-chat-compaction-pagination.test.mjs": 456, + "config/scripts/native-chat-design-tokens-plugin.test.mjs": 1531, + "config/scripts/node-old-space-limit.test.mjs": 41, + "config/scripts/node-server-change-scope.test.mjs": 2683, "config/scripts/node-server-concurrency.test.mjs": 173, - "config/scripts/node-server-qualification.test.mjs": 62, - "config/scripts/nsis-process-check.test.mjs": 59, - "config/scripts/orca-dev-bin.test.mjs": 118, - "config/scripts/orcad-artifact-version.test.mjs": 493, - "config/scripts/orcad-daemon-protocol-crossing.test.mjs": 57, - "config/scripts/orcad-e2e-routing.test.mjs": 184, - "config/scripts/orcad-prebuild-slot-contents.test.mjs": 81, - "config/scripts/orcad-terminal-smoke-change-scope.test.mjs": 2562, - "config/scripts/orcad-watcher-package.test.mjs": 107, - "config/scripts/orcad-windows-prebuild-cache.test.mjs": 446, - "config/scripts/orcad-windows-prebuild-workflow.test.mjs": 371, - "config/scripts/orcad-windows-process-tree.test.mjs": 94, - "config/scripts/oxc-cli-invocation.test.mjs": 102, - "config/scripts/oxlint-cli-invocation.test.mjs": 73, - "config/scripts/package-electron-runtime-contract.test.mjs": 169, - "config/scripts/package-linux-formats-appimage.test.mjs": 577, - "config/scripts/package-linux-formats.test.mjs": 315, - "config/scripts/packaged-node-pty-prebuild-prune.test.mjs": 68, - "config/scripts/packaged-orcad-template.test.mjs": 185, - "config/scripts/packaged-process-host.test.mjs": 653, - "config/scripts/packaged-source-map-prune.test.mjs": 69, - "config/scripts/patched-dependencies-frozen-install.test.mjs": 574, - "config/scripts/pdfjs-viewer-assets-stream.test.ts": 317, - "config/scripts/plain-node-entry-guard.test.ts": 680, - "config/scripts/playwright-focused-workflow-selection.test.mjs": 3051, - "config/scripts/pnpm-cli-invocation.test.mjs": 63, - "config/scripts/pr-auxiliary-preflight-admission.test.mjs": 417, - "config/scripts/pr-code-change-scope-cross-version-launch-route.test.mjs": 49, - "config/scripts/pr-code-change-scope-cross-version-send-path.test.mjs": 55, - "config/scripts/pr-code-change-scope-daemon-protocol.test.mjs": 44, - "config/scripts/pr-code-change-scope-process-host.test.mjs": 54, - "config/scripts/pr-code-change-scope.test.mjs": 551, - "config/scripts/pr-codex-unit-only-selection.test.mjs": 45, - "config/scripts/pr-e2e-gate-contract.test.mjs": 264, - "config/scripts/pr-e2e-native-only-routing.test.mjs": 153, - "config/scripts/pr-e2e-pane-close-routing.test.mjs": 61, - "config/scripts/pr-preflight-gates.test.mjs": 203, - "config/scripts/pr-ready-check-gate.test.mjs": 338, - "config/scripts/pr-ready-check-reuse.test.mjs": 171, - "config/scripts/pr-test-loc-summary.test.mjs": 179, - "config/scripts/pr-unit-support-routing.test.mjs": 64, - "config/scripts/pr-workflow-parallelism.test.mjs": 151, - "config/scripts/process-failure-message.test.mjs": 44, - "config/scripts/process-host-build-dist.test.mjs": 32253, - "config/scripts/process-host-dev-rebuild.test.ts": 9293, - "config/scripts/profile-state-worker-smoke.test.mjs": 2753, - "config/scripts/project-renderer-web-client.test.mjs": 623, - "config/scripts/pty-transcript-secret-scan.test.mjs": 168, - "config/scripts/publish-complete-draft-releases.test.mjs": 151, - "config/scripts/pullfrog-review-scope.test.mjs": 69, - "config/scripts/quadratic-buffer-concat-plugin.test.mjs": 1652, - "config/scripts/readme-downloads-badge.test.mjs": 37, - "config/scripts/rebuild-native-deps-node-pty.test.mjs": 1261, - "config/scripts/rebuild-native-deps-windows-process-tree.test.mjs": 45, - "config/scripts/rebuild-native-deps.test.mjs": 1263, - "config/scripts/reclaim-dev-electron-bundles.test.ts": 64, - "config/scripts/regenerate-xterm-patches.test.mjs": 249, - "config/scripts/windows-process-tree-prepared-addon.test.mjs": 168, - "config/scripts/release-cut-token-permissions.test.mjs": 199, - "config/scripts/release-javascript-artifact.test.mjs": 253, - "config/scripts/release-javascript-parity.test.mjs": 98, - "config/scripts/release-mac-build-workflow-dispatch.test.mjs": 56, - "config/scripts/release-policy.test.mjs": 62, - "config/scripts/release-rc-history.test.mjs": 147, - "config/scripts/release-tag-pattern-census.test.mjs": 97, - "config/scripts/renderer-boot-graph.test.mjs": 88, - "config/scripts/renderer-scrollbar-style-plugin.test.mjs": 2564, - "config/scripts/replace-cached-nsis-elevate.test.mjs": 1852, - "config/scripts/resolve-7za-path.test.mjs": 1333, - "config/scripts/run-codex-real-account-validation.test.ts": 3394, - "config/scripts/run-internal-dev-setup.test.mjs": 59, - "config/scripts/run-linux-packaged-node-pty-floor-smoke.test.mjs": 58, - "config/scripts/run-terminal-scale-perf-report-gate.test.mjs": 77, - "config/scripts/run-typecheck-projects-in-parallel.test.mjs": 59, - "config/scripts/scan-release-artifacts-antivirus.test.mjs": 68, - "config/scripts/script-entry-detection.test.mjs": 91, - "config/scripts/script-module-dependencies.test.mjs": 63, - "config/scripts/session-scanner-service-smoke.test.mjs": 549, - "config/scripts/shared-electron-dist-cache.test.ts": 105, - "config/scripts/shebang-script-line-ending-pin.test.mjs": 103, - "config/scripts/skill-description-length.test.mjs": 107, - "config/scripts/skill-recipe-shell.test.mjs": 103, - "config/scripts/sort-comparator-performance-plugin.test.mjs": 283, - "config/scripts/space-sharing-copy.test.ts": 96, - "config/scripts/ssh-browser-e2e-routing.test.mjs": 176, - "config/scripts/stable-release-tag-selection-budget.test.mjs": 149, - "config/scripts/static-appimage-package-contract.test.mjs": 83, - "config/scripts/static-module-specifiers.test.mjs": 277, - "config/scripts/structured-agent-session-start-failure-row-render.test.mjs": 2675, - "config/scripts/summarize-terminal-perf-report.test.mjs": 106, - "config/scripts/telemetry-bundle-constant-patterns.test.mjs": 60, - "config/scripts/terminal-ime-engagement-receipt.test.mjs": 60, - "config/scripts/terminal-partial-escape-native-continuation.test.mjs": 2681, - "config/scripts/terminal-perf-dependency-preparation.test.mjs": 613, - "config/scripts/terminal-perf-report-annotations.test.mjs": 59, - "config/scripts/test-only-source-path.test.mjs": 42, - "config/scripts/trim-windows-icon-source.test.mjs": 234, - "config/scripts/update-node-runtime-pin.test.mjs": 89, - "config/scripts/verify-cli-bin.test.mjs": 106, - "config/scripts/verify-dev-channel-packaging.test.mjs": 285, - "config/scripts/verify-linux-glibc-floor.test.mjs": 124, - "config/scripts/verify-linux-glibc-payloads.test.mjs": 156, - "config/scripts/verify-localization-catalog.test.mjs": 514, - "config/scripts/verify-localization-catalogs.test.mjs": 2042, - "config/scripts/verify-localization-extraction.test.mjs": 49, - "config/scripts/verify-packaged-browser-participation.test.mjs": 61, - "config/scripts/verify-packaged-daemon-entry.test.mjs": 132, - "config/scripts/verify-packaged-mobile-web-bundle.test.mjs": 330, - "config/scripts/verify-packaged-node-pty-job-ownership.test.mjs": 109, - "config/scripts/verify-packaged-orcad-template.test.mjs": 443, - "config/scripts/verify-packaged-plugin-resources.test.mjs": 91, - "config/scripts/verify-release-required-assets.test.mjs": 55, - "config/scripts/verify-skills-cli-runtime.test.mjs": 289, - "config/scripts/verify-windows-inner-signature.test.mjs": 68, - "config/scripts/verify-wsl-e2e-participation.test.mjs": 77, - "config/scripts/vitest-node-runtime-boundary.test.ts": 2060, - "config/scripts/vitest-node-runtime-pool.test.ts": 341, - "config/scripts/vitest-real-agent-home-write-guard.test.ts": 66, - "config/scripts/vitest-runtime-boundary.test.ts": 114, - "config/scripts/vitest-runtime-sequencer.test.mjs": 194, - "config/scripts/vitest-sqlite-runtime-boundary.test.ts": 3901, - "config/scripts/websocket-server-loopback-bind.test.ts": 536, - "config/scripts/win-crash-survival-e2e.test.mjs": 584, - "config/scripts/win32-test-lane-registration.test.mjs": 2948, - "config/scripts/windows-bin-spawn-predicate.test.mjs": 236, - "config/scripts/windows-cmd-shim-spawn-boundary.test.mjs": 1437, - "config/scripts/windows-pe-imports.test.mjs": 48, - "config/scripts/windows-pe-machine.test.mjs": 66, - "config/scripts/windows-process-tree-creation-time.test.mjs": 45, - "config/scripts/windows-process-tree-gyp-path.test.mjs": 39, - "config/scripts/windows-process-tree-gyp-rebuild.test.mjs": 80, - "config/scripts/windows-process-tree-patch-contract.test.mjs": 562, - "config/scripts/windows-pty-table-stress-observer.test.mjs": 97, - "config/scripts/windows-uninstaller-signing.test.mjs": 63, - "config/scripts/workflow-ref-mirror-case-safety.test.mjs": 534, - "config/scripts/workflow-ref-reachability.test.mjs": 662, - "config/scripts/workspace-source-exports.test.mjs": 120, - "config/scripts/wsl-e2e-lane-contract.test.mjs": 166, - "config/scripts/xterm-image-lifecycle-contract.test.mjs": 231, - "config/scripts/xterm-image-memory-contract.test.mjs": 380, - "config/scripts/xterm-image-resize-contract.test.mjs": 73, - "config/scripts/xterm-image-wasm-budget-contract.test.mjs": 479, - "config/scripts/zip-extractor-command.test.mjs": 76, - "src/cli/agent-context.test.ts": 125, - "src/cli/agent-session-search-format.test.ts": 71, - "src/cli/args-repeatable-flags.test.ts": 69, - "src/cli/args.test.ts": 123, - "src/cli/automation-format.test.ts": 102, - "src/cli/automation-owner-conflict-recovery.test.ts": 482, - "src/cli/browser-cookie-credentials-empty-value.test.ts": 206, - "src/cli/browser-storage-empty-value.test.ts": 185, - "src/cli/browser.test.ts": 309, + "config/scripts/node-server-packaged-chat-selection.test.mjs": 146, + "config/scripts/node-server-policy-self-tests.test.mjs": 3143, + "config/scripts/node-server-qualification.test.mjs": 43, + "config/scripts/nsis-process-check.test.mjs": 50, + "config/scripts/orca-dev-bin.test.mjs": 87, + "config/scripts/orcad-artifact-version.test.mjs": 515, + "config/scripts/orcad-daemon-protocol-crossing.test.mjs": 60, + "config/scripts/orcad-e2e-routing.test.mjs": 131, + "config/scripts/orcad-entry-build.test.mjs": 2783, + "config/scripts/orcad-prebuild-slot-contents.test.mjs": 89, + "config/scripts/orcad-terminal-smoke-change-scope.test.mjs": 2306, + "config/scripts/orcad-watcher-package.test.mjs": 91, + "config/scripts/orcad-windows-cli-launcher.test.mjs": 79, + "config/scripts/orcad-windows-prebuild-cache.test.mjs": 392, + "config/scripts/orcad-windows-prebuild-workflow.test.mjs": 368, + "config/scripts/orcad-windows-process-tree.test.mjs": 90, + "config/scripts/orcad-windows-security.test.mjs": 153, + "config/scripts/oxc-cli-invocation.test.mjs": 82, + "config/scripts/oxlint-cli-invocation.test.mjs": 76, + "config/scripts/package-electron-runtime-contract.test.mjs": 172, + "config/scripts/package-linux-formats-appimage.test.mjs": 535, + "config/scripts/package-linux-formats.test.mjs": 253, + "config/scripts/packaged-node-pty-prebuild-prune.test.mjs": 47, + "config/scripts/packaged-orcad-template.test.mjs": 196, + "config/scripts/packaged-process-host.test.mjs": 465, + "config/scripts/packaged-source-map-prune.test.mjs": 75, + "config/scripts/patched-dependencies-frozen-install.test.mjs": 623, + "config/scripts/pdfjs-viewer-assets-stream.test.ts": 275, + "config/scripts/pinned-node-downloads.test.mjs": 116, + "config/scripts/plain-node-entry-guard.test.ts": 710, + "config/scripts/playwright-focused-workflow-selection.test.mjs": 1489, + "config/scripts/pnpm-cli-invocation.test.mjs": 52, + "config/scripts/pr-auxiliary-preflight-admission.test.mjs": 2289, + "config/scripts/pr-code-change-scope-cross-version-launch-route.test.mjs": 35, + "config/scripts/pr-code-change-scope-cross-version-send-path.test.mjs": 52, + "config/scripts/pr-code-change-scope-daemon-protocol.test.mjs": 36, + "config/scripts/pr-code-change-scope-process-host.test.mjs": 43, + "config/scripts/pr-code-change-scope.test.mjs": 583, + "config/scripts/pr-codex-unit-only-selection.test.mjs": 34, + "config/scripts/pr-e2e-gate-contract.test.mjs": 224, + "config/scripts/pr-e2e-layout-oracle-routing.test.mjs": 229, + "config/scripts/pr-e2e-native-only-routing.test.mjs": 124, + "config/scripts/pr-e2e-pane-close-routing.test.mjs": 48, + "config/scripts/pr-preflight-gates.test.mjs": 606, + "config/scripts/pr-ready-check-gate.test.mjs": 378, + "config/scripts/pr-ready-check-reuse.test.mjs": 167, + "config/scripts/pr-test-loc-summary.test.mjs": 159, + "config/scripts/pr-unit-support-routing.test.mjs": 58, + "config/scripts/pr-workflow-parallelism.test.mjs": 183, + "config/scripts/process-failure-message.test.mjs": 27, + "config/scripts/process-host-build-dist.test.mjs": 12388, + "config/scripts/process-host-dev-rebuild.test.ts": 8977, + "config/scripts/profile-state-worker-smoke.test.mjs": 2936, + "config/scripts/project-renderer-web-client.test.mjs": 654, + "config/scripts/pty-transcript-secret-scan.test.mjs": 142, + "config/scripts/publish-complete-draft-releases.test.mjs": 160, + "config/scripts/pullfrog-review-scope.test.mjs": 46, + "config/scripts/quadratic-buffer-concat-plugin.test.mjs": 1492, + "config/scripts/ratchet-baseline.test.mjs": 53, + "config/scripts/readme-downloads-badge.test.mjs": 30, + "config/scripts/rebuild-native-deps-node-pty.test.mjs": 1338, + "config/scripts/rebuild-native-deps-windows-process-tree.test.mjs": 44, + "config/scripts/rebuild-native-deps.test.mjs": 1191, + "config/scripts/reclaim-dev-electron-bundles.test.ts": 168, + "config/scripts/reclaim-electron-dists.test.ts": 273, + "config/scripts/reclaim-release-test-caches.test.ts": 172, + "config/scripts/reclaim-worktree-space.test.mjs": 985, + "config/scripts/regenerate-xterm-patches.test.mjs": 240, + "config/scripts/relay-era-ref-refusal-workflow.test.mjs": 221, + "config/scripts/release-cut-token-permissions.test.mjs": 188, + "config/scripts/release-javascript-artifact.test.mjs": 174, + "config/scripts/release-javascript-parity.test.mjs": 67, + "config/scripts/release-mac-build-workflow-dispatch.test.mjs": 46, + "config/scripts/release-policy.test.mjs": 44, + "config/scripts/release-rc-history.test.mjs": 150, + "config/scripts/release-tag-pattern-census.test.mjs": 67, + "config/scripts/renderer-boot-graph.test.mjs": 87, + "config/scripts/renderer-scrollbar-style-plugin.test.mjs": 3027, + "config/scripts/replace-cached-nsis-elevate.test.mjs": 1400, + "config/scripts/required-pr-check-name.test.mjs": 852, + "config/scripts/resolve-7za-path.test.mjs": 1513, + "config/scripts/run-codex-real-account-validation.test.ts": 2916, + "config/scripts/run-internal-dev-setup.test.mjs": 33, + "config/scripts/run-linux-packaged-node-pty-floor-smoke.test.mjs": 31, + "config/scripts/run-terminal-scale-perf-report-gate.test.mjs": 64, + "config/scripts/run-typecheck-bun-queue.test.mjs": 420, + "config/scripts/run-typecheck-projects-in-parallel.test.mjs": 135, + "config/scripts/runtime-serve-terminal-smoke.test.mjs": 175, + "config/scripts/scan-release-artifacts-antivirus.test.mjs": 44, + "config/scripts/script-entry-detection.test.mjs": 73, + "config/scripts/script-module-dependencies.test.mjs": 41, + "config/scripts/session-scanner-service-smoke.test.mjs": 633, + "config/scripts/shared-electron-dist-cache.test.ts": 97, + "config/scripts/skill-description-length.test.mjs": 64, + "config/scripts/skill-recipe-shell.test.mjs": 1238, + "config/scripts/sort-comparator-performance-plugin.test.mjs": 249, + "config/scripts/space-sharing-copy.test.ts": 121, + "config/scripts/ssh-browser-e2e-routing.test.mjs": 118, + "config/scripts/stable-release-tag-selection-budget.test.mjs": 118, + "config/scripts/static-appimage-package-contract.test.mjs": 80, + "config/scripts/static-module-specifiers.test.mjs": 175, + "config/scripts/structured-agent-session-start-failure-row-render.test.mjs": 2891, + "config/scripts/summarize-terminal-perf-report.test.mjs": 62, + "config/scripts/telemetry-bundle-constant-patterns.test.mjs": 36, + "config/scripts/terminal-ime-engagement-receipt.test.mjs": 44, + "config/scripts/terminal-partial-escape-native-continuation.test.mjs": 2393, + "config/scripts/terminal-perf-dependency-preparation.test.mjs": 558, + "config/scripts/terminal-perf-report-annotations.test.mjs": 31, + "config/scripts/test-only-source-path.test.mjs": 35, + "config/scripts/trim-windows-icon-source.test.mjs": 242, + "config/scripts/update-node-runtime-pin.test.mjs": 63, + "config/scripts/verify-cli-bin.test.mjs": 100, + "config/scripts/verify-dev-channel-packaging.test.mjs": 207, + "config/scripts/verify-linux-glibc-floor.test.mjs": 143, + "config/scripts/verify-linux-glibc-payloads.test.mjs": 161, + "config/scripts/verify-localization-catalog.test.mjs": 364, + "config/scripts/verify-localization-catalogs.test.mjs": 3034, + "config/scripts/verify-localization-extraction.test.mjs": 37, + "config/scripts/verify-packaged-browser-participation.test.mjs": 46, + "config/scripts/verify-packaged-daemon-entry.test.mjs": 1866, + "config/scripts/verify-packaged-mobile-web-bundle.test.mjs": 201, + "config/scripts/verify-packaged-node-pty-job-ownership.test.mjs": 75, + "config/scripts/verify-packaged-orcad-template.test.mjs": 378, + "config/scripts/verify-packaged-plugin-resources.test.mjs": 71, + "config/scripts/verify-packaged-windows-agent-hook.test.mjs": 38, + "config/scripts/verify-release-required-assets.test.mjs": 37, + "config/scripts/verify-skill-update-roundtrip.test.mjs": 51, + "config/scripts/verify-skills-cli-runtime.test.mjs": 194, + "config/scripts/verify-windows-inner-signature.test.mjs": 56, + "config/scripts/verify-wsl-e2e-participation.test.mjs": 38, + "config/scripts/vitest-node-runtime-boundary.test.ts": 1990, + "config/scripts/vitest-node-runtime-pool.test.ts": 383, + "config/scripts/vitest-real-agent-home-write-guard.test.ts": 61, + "config/scripts/vitest-runtime-boundary.test.ts": 95, + "config/scripts/vitest-runtime-sequencer.test.mjs": 144, + "config/scripts/vitest-sqlite-runtime-boundary.test.ts": 3579, + "config/scripts/websocket-server-loopback-bind.test.ts": 405, + "config/scripts/win-crash-survival-e2e.test.mjs": 452, + "config/scripts/win32-test-lane-registration.test.mjs": 3005, + "config/scripts/windows-bin-spawn-predicate.test.mjs": 127, + "config/scripts/windows-cmd-shim-spawn-boundary.test.mjs": 1457, + "config/scripts/windows-pe-imports.test.mjs": 35, + "config/scripts/windows-pe-machine.test.mjs": 40, + "config/scripts/windows-process-tree-creation-time.test.mjs": 35, + "config/scripts/windows-process-tree-gyp-path.test.mjs": 25, + "config/scripts/windows-process-tree-gyp-rebuild.test.mjs": 76, + "config/scripts/windows-process-tree-prepared-addon.test.mjs": 152, + "config/scripts/windows-pty-table-stress-observer.test.mjs": 73, + "config/scripts/windows-uninstaller-signing.test.mjs": 54, + "config/scripts/workflow-ref-mirror-case-safety.test.mjs": 433, + "config/scripts/workflow-ref-reachability.test.mjs": 663, + "config/scripts/workspace-source-exports.test.mjs": 101, + "config/scripts/wsl-e2e-lane-contract.test.mjs": 170, + "config/scripts/xterm-image-cell-payload-contract.test.mjs": 149, + "config/scripts/xterm-image-lifecycle-contract.test.mjs": 144, + "config/scripts/xterm-image-memory-contract.test.mjs": 371, + "config/scripts/xterm-image-resize-contract.test.mjs": 46, + "config/scripts/xterm-image-wasm-budget-contract.test.mjs": 421, + "config/scripts/zip-extractor-command.test.mjs": 59, + "src/cli/agent-context.test.ts": 113, + "src/cli/agent-session-search-format.test.ts": 62, + "src/cli/args-repeatable-flags.test.ts": 58, + "src/cli/args.test.ts": 125, + "src/cli/automation-format.test.ts": 79, + "src/cli/automation-owner-conflict-recovery.test.ts": 507, + "src/cli/browser-empty-values.test.ts": 165, + "src/cli/browser.test.ts": 476, "src/cli/cli-version.test.ts": 56, - "src/cli/codex-command-classification.test.ts": 56, - "src/cli/command-suggestion-budget.test.ts": 55, - "src/cli/command-suggestion.test.ts": 53, - "src/cli/computer-format.test.ts": 102, - "src/cli/emulator-logcat-format.test.ts": 48, - "src/cli/execution-host-flag.test.ts": 94, - "src/cli/format-recovery.test.ts": 355, - "src/cli/format.test.ts": 618, - "src/cli/handler-group-manifest.test.ts": 1856, - "src/cli/handlers/account.test.ts": 924, - "src/cli/handlers/agent-hooks-load-boundary.test.ts": 533, - "src/cli/handlers/agent-hooks-prepare-codex-isolated-home.test.ts": 531, - "src/cli/handlers/agent-hooks.test.ts": 1290, - "src/cli/handlers/artifacts.test.ts": 475, - "src/cli/handlers/automation-destination-fencing.test.ts": 515, - "src/cli/handlers/automation-owner-fencing.test.ts": 506, - "src/cli/handlers/computer-action-routing.test.ts": 200, - "src/cli/handlers/computer-action-validation.test.ts": 250, - "src/cli/handlers/computer-state-formatting.test.ts": 191, - "src/cli/handlers/computer.test.ts": 224, - "src/cli/handlers/core.test.ts": 385, - "src/cli/handlers/data-account-commands.test.ts": 419, - "src/cli/handlers/emulator.test.ts": 207, - "src/cli/handlers/file-absolute-paths.test.ts": 200, - "src/cli/handlers/file.test.ts": 181, - "src/cli/handlers/interactive-login-interruption.test.ts": 69, - "src/cli/handlers/linear.test.ts": 267, - "src/cli/handlers/managed-server.test.ts": 432, - "src/cli/handlers/orchestration-caller-identity-cli.test.ts": 378, - "src/cli/handlers/orchestration-check-identity.test.ts": 473, - "src/cli/handlers/orchestration-federated-legacy-settlement.test.ts": 504, - "src/cli/handlers/orchestration-gate-cli.test.ts": 263, - "src/cli/handlers/orchestration-legacy-read-only.test.ts": 476, - "src/cli/handlers/orchestration-lifecycle-json-rejection.test.ts": 433, - "src/cli/handlers/orchestration-lifecycle-rejection.test.ts": 484, - "src/cli/handlers/orchestration-migration.test.ts": 555, - "src/cli/handlers/orchestration-module-boundaries.test.ts": 534, - "src/cli/handlers/orchestration-request-show-cli.test.ts": 442, - "src/cli/handlers/orchestration-run-cli.test.ts": 455, - "src/cli/handlers/orchestration-send-receipt-warnings.test.ts": 375, - "src/cli/handlers/orchestration-task-create-cli.test.ts": 561, - "src/cli/handlers/orchestration-task-list-brief.test.ts": 436, - "src/cli/handlers/orchestration-windows-ask-cli.test.ts": 496, - "src/cli/handlers/orchestration-worker-cli.test.ts": 472, - "src/cli/handlers/orchestration-worker-show-wait-cli.test.ts": 376, - "src/cli/handlers/orchestration.test.ts": 805, - "src/cli/handlers/orchestration/mutation-request.test.ts": 3412, - "src/cli/handlers/orchestration/worker-list-run-scope.test.ts": 534, - "src/cli/handlers/orchestration/worker-output.test.ts": 66, - "src/cli/handlers/profile-state-recovery-admission.test.ts": 2338, - "src/cli/handlers/profile-state.test.ts": 1185, - "src/cli/handlers/repo-visibility.test.ts": 584, - "src/cli/handlers/search.test.ts": 359, - "src/cli/handlers/skill-sharing.test.ts": 492, - "src/cli/handlers/terminal.test.ts": 571, - "src/cli/handlers/worktree-gitlab-link.test.ts": 347, - "src/cli/host-selector-alternatives.test.ts": 82, - "src/cli/index-automation-extra-agent-args.test.ts": 318, - "src/cli/index-automation-identifiers.test.ts": 293, - "src/cli/index-automation-schedule.test.ts": 256, - "src/cli/index-automation-session-reuse.test.ts": 312, - "src/cli/index-automation-source-context.test.ts": 254, - "src/cli/index-automation-target.test.ts": 246, - "src/cli/index-device-commands.test.ts": 180, - "src/cli/index-environment-commands.test.ts": 408, - "src/cli/index-local-command-routing-flags.test.ts": 764, - "src/cli/index-memory-diagnostics.test.ts": 244, - "src/cli/index-omitted-host-scope-selectors.test.ts": 317, - "src/cli/index-orchestration.test.ts": 292, - "src/cli/index-project-setup.test.ts": 245, - "src/cli/index-reference.test.ts": 233, - "src/cli/index-serve-command.test.ts": 444, - "src/cli/index-terminal-commands.test.ts": 233, - "src/cli/index-terminal-list-host-scope.test.ts": 229, - "src/cli/index-vm-recipe-doctor.test.ts": 469, - "src/cli/index-worktree-create-agent.test.ts": 253, - "src/cli/index-worktree-create-linear.test.ts": 235, - "src/cli/index-worktree-create-parent.test.ts": 310, - "src/cli/index-worktree-create-target.test.ts": 273, - "src/cli/index-worktree-gitlab-links.test.ts": 225, - "src/cli/index-worktree-linked-items.test.ts": 299, - "src/cli/index-worktree-selector-resolution.test.ts": 527, - "src/cli/index-worktree-set.test.ts": 281, - "src/cli/index.test.ts": 1022, - "src/cli/linear-format.test.ts": 66, - "src/cli/orchestration-dispatch-refusal-format.test.ts": 101, - "src/cli/orchestration-mutation-recovery.test.ts": 461, - "src/cli/orchestration-session-caller-cli.test.ts": 608, - "src/cli/orchestration-structured-sender-identity.test.ts": 285, - "src/cli/orchestration-structured-session-no-identity.test.ts": 448, - "src/cli/quote-stripped-json-flag.test.ts": 62, - "src/cli/reference-command-arguments.test.ts": 132, - "src/cli/retry-request-flag.test.ts": 513, - "src/cli/runtime-client-deferral.test.ts": 939, - "src/cli/runtime-client.test.ts": 532, - "src/cli/runtime/client-recovery.test.ts": 1089, - "src/cli/runtime/client-timeout-policy.test.ts": 371, - "src/cli/runtime/envelope-schema.test.ts": 261, - "src/cli/runtime/environments.test.ts": 409, - "src/cli/runtime/launch-serve-runtime.test.ts": 328, - "src/cli/runtime/launch-user-data-path.test.ts": 264, - "src/cli/runtime/launch.test.ts": 369, - "src/cli/runtime/orchestration-recovery-command.test.ts": 59, - "src/cli/runtime/profile-state-recovery-launch.test.ts": 301, - "src/cli/runtime/runtime-access-denied.test.ts": 433, - "src/cli/runtime/runtime-source-fence.test.ts": 454, - "src/cli/runtime/serve-orcad-launch.test.ts": 287, - "src/cli/runtime/serve-signal-exit-diagnostic.test.ts": 92, - "src/cli/runtime/status-caller.test.ts": 660, - "src/cli/runtime/status.test.ts": 377, - "src/cli/runtime/transport-framing.test.ts": 337, - "src/cli/runtime/transport.test.ts": 789, - "src/cli/runtime/types.test.ts": 48, - "src/cli/runtime/websocket-transport-error.test.ts": 375, - "src/cli/runtime/websocket-transport.test.ts": 552, - "src/cli/search-command-arguments.test.ts": 352, + "src/cli/codex-command-classification.test.ts": 54, + "src/cli/command-suggestion-budget.test.ts": 113, + "src/cli/command-suggestion-destructive-families.test.ts": 212, + "src/cli/command-suggestion.test.ts": 57, + "src/cli/computer-format.test.ts": 88, + "src/cli/emulator-logcat-format.test.ts": 43, + "src/cli/execution-host-flag.test.ts": 89, + "src/cli/format-recovery.test.ts": 483, + "src/cli/format.test.ts": 540, + "src/cli/handler-group-manifest.test.ts": 2724, + "src/cli/handlers/account.test.ts": 1145, + "src/cli/handlers/agent-hooks-load-boundary.test.ts": 462, + "src/cli/handlers/agent-hooks-prepare-codex-isolated-home.test.ts": 634, + "src/cli/handlers/agent-hooks.test.ts": 1543, + "src/cli/handlers/artifacts.test.ts": 596, + "src/cli/handlers/automation-destination-fencing.test.ts": 617, + "src/cli/handlers/automation-owner-fencing.test.ts": 584, + "src/cli/handlers/computer-action-routing.test.ts": 192, + "src/cli/handlers/computer-action-validation.test.ts": 222, + "src/cli/handlers/computer-state-formatting.test.ts": 218, + "src/cli/handlers/computer.test.ts": 274, + "src/cli/handlers/core.test.ts": 342, + "src/cli/handlers/data-account-commands.test.ts": 527, + "src/cli/handlers/emulator.test.ts": 202, + "src/cli/handlers/file-absolute-paths.test.ts": 223, + "src/cli/handlers/file.test.ts": 266, + "src/cli/handlers/interactive-login-interruption.test.ts": 65, + "src/cli/handlers/linear.test.ts": 300, + "src/cli/handlers/managed-server.test.ts": 543, + "src/cli/handlers/orchestration-caller-identity-cli.test.ts": 533, + "src/cli/handlers/orchestration-check-identity.test.ts": 439, + "src/cli/handlers/orchestration-federated-legacy-settlement.test.ts": 519, + "src/cli/handlers/orchestration-gate-cli.test.ts": 476, + "src/cli/handlers/orchestration-legacy-read-only.test.ts": 531, + "src/cli/handlers/orchestration-lifecycle-json-rejection.test.ts": 509, + "src/cli/handlers/orchestration-lifecycle-rejection.test.ts": 483, + "src/cli/handlers/orchestration-migration.test.ts": 527, + "src/cli/handlers/orchestration-module-boundaries.test.ts": 575, + "src/cli/handlers/orchestration-request-show-cli.test.ts": 478, + "src/cli/handlers/orchestration-run-cli.test.ts": 499, + "src/cli/handlers/orchestration-send-receipt-warnings.test.ts": 467, + "src/cli/handlers/orchestration-task-create-cli.test.ts": 563, + "src/cli/handlers/orchestration-task-list-brief.test.ts": 408, + "src/cli/handlers/orchestration-windows-ask-cli.test.ts": 467, + "src/cli/handlers/orchestration-worker-cli.test.ts": 446, + "src/cli/handlers/orchestration-worker-show-wait-cli.test.ts": 745, + "src/cli/handlers/orchestration.test.ts": 717, + "src/cli/handlers/orchestration/mutation-request.test.ts": 3679, + "src/cli/handlers/orchestration/worker-list-run-scope.test.ts": 496, + "src/cli/handlers/orchestration/worker-output.test.ts": 56, + "src/cli/handlers/profile-state-recovery-admission.test.ts": 2820, + "src/cli/handlers/profile-state.test.ts": 1205, + "src/cli/handlers/repo-visibility.test.ts": 611, + "src/cli/handlers/search.test.ts": 447, + "src/cli/handlers/serve-restore.test.ts": 73, + "src/cli/handlers/serve-service.test.ts": 157, + "src/cli/handlers/skill-sharing.test.ts": 477, + "src/cli/handlers/terminal-daemon.test.ts": 576, + "src/cli/handlers/terminal.test.ts": 585, + "src/cli/handlers/worktree-gitlab-link.test.ts": 435, + "src/cli/host-selector-alternatives.test.ts": 69, + "src/cli/index-agent-launch.test.ts": 526, + "src/cli/index-automation-extra-agent-args.test.ts": 448, + "src/cli/index-automation-identifiers.test.ts": 438, + "src/cli/index-automation-schedule.test.ts": 472, + "src/cli/index-automation-session-reuse.test.ts": 432, + "src/cli/index-automation-source-context.test.ts": 452, + "src/cli/index-automation-target.test.ts": 438, + "src/cli/index-device-commands.test.ts": 188, + "src/cli/index-environment-commands.test.ts": 368, + "src/cli/index-jira.test.ts": 451, + "src/cli/index-local-command-routing-flags.test.ts": 823, + "src/cli/index-memory-diagnostics.test.ts": 206, + "src/cli/index-omitted-host-scope-selectors.test.ts": 672, + "src/cli/index-orchestration.test.ts": 509, + "src/cli/index-project-setup.test.ts": 259, + "src/cli/index-reference-jira.test.ts": 516, + "src/cli/index-reference.test.ts": 649, + "src/cli/index-serve-command.test.ts": 467, + "src/cli/index-serve-restore.test.ts": 240, + "src/cli/index-terminal-commands.test.ts": 415, + "src/cli/index-terminal-list-host-scope.test.ts": 430, + "src/cli/index-vm-recipe-doctor.test.ts": 464, + "src/cli/index-worktree-create-agent.test.ts": 525, + "src/cli/index-worktree-create-linear.test.ts": 517, + "src/cli/index-worktree-create-parent.test.ts": 611, + "src/cli/index-worktree-create-target.test.ts": 503, + "src/cli/index-worktree-gitlab-links.test.ts": 578, + "src/cli/index-worktree-linked-items.test.ts": 523, + "src/cli/index-worktree-selector-resolution.test.ts": 1033, + "src/cli/index-worktree-set.test.ts": 464, + "src/cli/index.test.ts": 680, + "src/cli/jira-command-surface.test.ts": 132, + "src/cli/linear-format.test.ts": 67, + "src/cli/orchestration-dispatch-refusal-format.test.ts": 95, + "src/cli/orchestration-mutation-recovery.test.ts": 531, + "src/cli/orchestration-session-caller-cli.test.ts": 677, + "src/cli/orchestration-structured-sender-identity.test.ts": 479, + "src/cli/orchestration-structured-session-no-identity.test.ts": 483, + "src/cli/quote-stripped-json-flag.test.ts": 47, + "src/cli/reference-command-arguments.test.ts": 128, + "src/cli/retry-request-flag.test.ts": 577, + "src/cli/runtime-client-deferral.test.ts": 773, + "src/cli/runtime-client.test.ts": 422, + "src/cli/runtime/client-execution-host.test.ts": 390, + "src/cli/runtime/client-recovery.test.ts": 715, + "src/cli/runtime/client-timeout-policy.test.ts": 403, + "src/cli/runtime/envelope-schema.test.ts": 238, + "src/cli/runtime/environments.test.ts": 312, + "src/cli/runtime/launch-serve-preflight.test.ts": 312, + "src/cli/runtime/launch-serve-runtime.test.ts": 383, + "src/cli/runtime/launch-serve-supervise.test.ts": 1261, + "src/cli/runtime/launch-user-data-path.test.ts": 313, + "src/cli/runtime/launch.test.ts": 406, + "src/cli/runtime/orchestration-recovery-command.test.ts": 62, + "src/cli/runtime/profile-state-recovery-launch.test.ts": 277, + "src/cli/runtime/runtime-access-denied.test.ts": 375, + "src/cli/runtime/runtime-listener-probe.test.ts": 104, + "src/cli/runtime/runtime-source-fence.test.ts": 449, + "src/cli/runtime/serve-appimage-update.test.ts": 214, + "src/cli/runtime/serve-bundle-park.test.ts": 158, + "src/cli/runtime/serve-electron-preflight.test.ts": 282, + "src/cli/runtime/serve-install-hold.test.ts": 75, + "src/cli/runtime/serve-orcad-launch.test.ts": 262, + "src/cli/runtime/serve-pairing-redaction.test.ts": 122, + "src/cli/runtime/serve-service-unit.test.ts": 88, + "src/cli/runtime/serve-signal-exit-diagnostic.test.ts": 90, + "src/cli/runtime/serve-supervised-service.test.ts": 99, + "src/cli/runtime/serve-supervisor-policy.test.ts": 52, + "src/cli/runtime/serve-supervisor-relocation.test.ts": 215, + "src/cli/runtime/serve-update-supervisor-gate.test.ts": 104, + "src/cli/runtime/status-caller.test.ts": 489, + "src/cli/runtime/status.test.ts": 731, + "src/cli/runtime/transport-framing.test.ts": 313, + "src/cli/runtime/transport.test.ts": 798, + "src/cli/runtime/types.test.ts": 60, + "src/cli/runtime/websocket-transport-error.test.ts": 433, + "src/cli/runtime/websocket-transport.test.ts": 599, + "src/cli/search-command-arguments.test.ts": 359, "src/cli/serve-electron-flag-parity.test.ts": 85, - "src/cli/shell-command-quote.test.ts": 53, - "src/cli/skills-reference-selector.test.ts": 206, - "src/cli/skills.test.ts": 1353, - "src/cli/specs/account.test.ts": 94, - "src/cli/specs/bundled-guide-flags.test.ts": 201, - "src/cli/specs/file.test.ts": 97, - "src/cli/specs/profile-state.test.ts": 87, - "src/cli/specs/search.test.ts": 181, - "src/cli/terminal-format-draft.test.ts": 51, - "src/cli/terminal-format.test.ts": 78, - "src/cli/terminal-list-host-scope-format.test.ts": 80, - "src/cli/terminal-read-screen.test.ts": 215, - "src/cli/vocabulary-policy.test.ts": 110, - "src/cli/worktree-ps-terminal-verdict.test.ts": 48, - "src/cli/worktree-selector-caller-host.test.ts": 69, - "src/cli/worktree-selector-wsl-posix-path.test.ts": 78, - "src/main/acp/acp-agent-connection.test.ts": 354, - "src/main/acp/acp-compaction-journal-fixture.test.ts": 298, - "src/main/acp/acp-dialects/grok-subagents.test.ts": 307, - "src/main/acp/acp-dialects/omp-dialect.test.ts": 258, - "src/main/acp/acp-fixture-privacy.test.ts": 137, - "src/main/acp/acp-json-rpc-peer.test.ts": 274, - "src/main/acp/acp-model-catalog-probe.test.ts": 426, - "src/main/acp/acp-permission-requests.test.ts": 355, - "src/main/acp/acp-prompt-content.test.ts": 641, - "src/main/acp/acp-recovery-history.test.ts": 502, - "src/main/acp/acp-session-agent-turns.test.ts": 297, - "src/main/acp/acp-session-lifecycle.test.ts": 273, - "src/main/acp/acp-session-notifications.test.ts": 323, - "src/main/acp/acp-session-runtime.test.ts": 459, - "src/main/acp/acp-structured-auth.test.ts": 1129, - "src/main/acp/acp-structured-background-task-stop.test.ts": 4451, - "src/main/acp/acp-structured-broken-stream.test.ts": 10364, - "src/main/acp/acp-structured-child-session-end.test.ts": 2649, - "src/main/acp/acp-structured-child-stop.test.ts": 2928, - "src/main/acp/acp-structured-child-work-admission.test.ts": 1464, - "src/main/acp/acp-structured-child-work.test.ts": 2920, - "src/main/acp/acp-structured-compaction.test.ts": 895, - "src/main/acp/acp-structured-context-clear.test.ts": 490, - "src/main/acp/acp-structured-electron-free.test.ts": 276, - "src/main/acp/acp-structured-grok-subagent-recordings.test.ts": 1106, - "src/main/acp/acp-structured-host-configured-default.test.ts": 1598, - "src/main/acp/acp-structured-host-crash.test.ts": 1006, - "src/main/acp/acp-structured-host-lifecycle.test.ts": 2036, - "src/main/acp/acp-structured-host-live-listing.test.ts": 1281, - "src/main/acp/acp-structured-host-option-hang.test.ts": 1480, - "src/main/acp/acp-structured-host-restore-failed.test.ts": 1530, - "src/main/acp/acp-structured-host-start-aborts.test.ts": 1092, - "src/main/acp/acp-structured-host-stop.test.ts": 1968, - "src/main/acp/acp-structured-host-subagent-stop.test.ts": 1665, - "src/main/acp/acp-structured-image-prompts.test.ts": 806, - "src/main/acp/acp-structured-launch-resolution-omp.test.ts": 415, - "src/main/acp/acp-structured-launch-resolution-opencode.test.ts": 317, - "src/main/acp/acp-structured-launch-resolution.test.ts": 524, - "src/main/acp/acp-structured-launch-visuals.test.ts": 364, - "src/main/acp/acp-structured-missing-model.test.ts": 361, - "src/main/acp/acp-structured-omp-recordings.test.ts": 953, - "src/main/acp/acp-structured-opencode-recordings.test.ts": 690, - "src/main/acp/acp-structured-options.test.ts": 398, - "src/main/acp/acp-structured-prompt-failure.test.ts": 759, - "src/main/acp/acp-structured-request-admission.test.ts": 824, - "src/main/acp/acp-structured-session-adapter-lifecycle.test.ts": 4918, - "src/main/acp/acp-structured-session-adapter-recordings.test.ts": 684, - "src/main/acp/acp-structured-session-adapter-resume.test.ts": 5492, - "src/main/acp/acp-structured-session-adapter-teardown.test.ts": 1211, - "src/main/acp/acp-structured-session-adapter.test.ts": 1251, - "src/main/acp/acp-structured-sign-in.test.ts": 813, - "src/main/acp/acp-structured-steer.test.ts": 1740, - "src/main/acp/acp-subagent-timeline.test.ts": 476, - "src/main/acp/acp-text-message-ownership.test.ts": 1219, - "src/main/acp/acp-text-stream-identity.test.ts": 817, - "src/main/acp/acp-timeline-background-task-evidence.test.ts": 698, - "src/main/acp/acp-timeline-background-task-results.test.ts": 594, - "src/main/acp/acp-timeline-background-task-words.test.ts": 1032, - "src/main/acp/acp-timeline-background-tasks.test.ts": 1081, - "src/main/acp/acp-timeline-dialect.test.ts": 618, - "src/main/acp/acp-timeline-generic.test.ts": 974, - "src/main/acp/acp-timeline-grok-subagent-retention.test.ts": 1206, - "src/main/acp/acp-timeline-grok-subagents.test.ts": 1171, - "src/main/acp/acp-timeline-joins.test.ts": 561, - "src/main/acp/acp-timeline-open-enums.test.ts": 585, - "src/main/acp/acp-timeline-opencode-dialect.test.ts": 415, - "src/main/acp/acp-timeline-recordings.test.ts": 610, - "src/main/acp/acp-timeline-recovery.test.ts": 929, - "src/main/acp/acp-timeline-routing.test.ts": 725, - "src/main/acp/acp-timeline-turn-failures.test.ts": 863, - "src/main/acp/acp-visuals-skill.test.ts": 53, - "src/main/acp/acp-write-queue-retention.test.ts": 466, - "src/main/active-view-persistence-boundary.test.ts": 1716, - "src/main/active-view-preference-sync-flush-veto.test.ts": 784, - "src/main/active-view-preference.test.ts": 62, - "src/main/agent-auth-restart-preservation.test.ts": 75, - "src/main/agent-awake-service-platform-assertions.test.ts": 93, - "src/main/agent-awake-service.test.ts": 127, - "src/main/agent-cli-version-probe.test.ts": 63, - "src/main/agent-hooks/branch-rename-failure-output.test.ts": 51, - "src/main/agent-hooks/ended-process-reconciliation.test.ts": 638, - "src/main/agent-hooks/first-work-branch-rename.test.ts": 870, - "src/main/agent-hooks/first-work-folder-rename.test.ts": 118, - "src/main/agent-hooks/grok-replay-guard.test.ts": 149, - "src/main/agent-hooks/hook-process-capture.test.ts": 100, - "src/main/agent-hooks/hook-script-outside-orca.test.ts": 277, - "src/main/agent-hooks/hook-status-session-tabs-republish.test.ts": 647, - "src/main/agent-hooks/install-telemetry.test.ts": 50, - "src/main/agent-hooks/installer-utils-remote.test.ts": 71, - "src/main/agent-hooks/installer-utils.test.ts": 189, - "src/main/agent-hooks/local-agent-cli-presence.test.ts": 64, - "src/main/agent-hooks/local-tmux-managed-pty.test.ts": 49, - "src/main/agent-hooks/managed-agent-hook-controls.test.ts": 127, - "src/main/agent-hooks/managed-hook-command-contract.test.ts": 389, - "src/main/agent-hooks/managed-hook-detection-commands.test.ts": 68, - "src/main/agent-hooks/managed-hook-install-lock.test.ts": 332, - "src/main/agent-hooks/managed-hook-owner-identity.test.ts": 99, - "src/main/agent-hooks/managed-hook-script-refresh-main-thread.test.ts": 410, - "src/main/agent-hooks/managed-hook-script-refresh.test.ts": 498, - "src/main/agent-hooks/managed-hook-stdin-lifecycle.test.ts": 1579, - "src/main/agent-hooks/managed-hook-timeout.test.ts": 1842, - "src/main/agent-hooks/managed-toml-ownership.test.ts": 65, - "src/main/agent-hooks/manual-compact-hook-stream.test.ts": 806, - "src/main/agent-hooks/manual-compact-status-cleanup.test.ts": 635, - "src/main/agent-hooks/migration-unsupported-pty-state.test.ts": 44, - "src/main/agent-hooks/posix-hook-json-stdin-reader.test.ts": 3465, - "src/main/agent-hooks/remote-hook-service-installers.test.ts": 453, - "src/main/agent-hooks/remote-hook-service-registry-coverage.test.ts": 430, - "src/main/agent-hooks/restored-status-dead-terminal-sweep.test.ts": 821, - "src/main/agent-hooks/server-agent-presence.test.ts": 732, - "src/main/agent-hooks/server-ai-vault-liveness.test.ts": 669, - "src/main/agent-hooks/server-amp-normalization.test.ts": 693, - "src/main/agent-hooks/server-authority-evidence.test.ts": 729, - "src/main/agent-hooks/server-claude-cancel-captures.test.ts": 761, - "src/main/agent-hooks/server-claude-child-permission-lifecycle.test.ts": 713, - "src/main/agent-hooks/server-claude-normalization.test.ts": 621, - "src/main/agent-hooks/server-claude-owed-notification-expiry.test.ts": 758, - "src/main/agent-hooks/server-claude-permission-visibility.test.ts": 827, - "src/main/agent-hooks/server-claude-statusline.test.ts": 737, - "src/main/agent-hooks/server-claude-task-wakeup-lifecycle.test.ts": 6962, - "src/main/agent-hooks/server-claude-terminal-interrupt.test.ts": 728, - "src/main/agent-hooks/server-closed-tab-suppression.test.ts": 755, - "src/main/agent-hooks/server-codex-ctrl-c-inference.test.ts": 663, - "src/main/agent-hooks/server-codex-interrupt-hook.test.ts": 558, - "src/main/agent-hooks/server-codex-normalization.test.ts": 684, - "src/main/agent-hooks/server-codex-subagent-transcript.test.ts": 4877, - "src/main/agent-hooks/server-codex-turn-interruption.test.ts": 4222, - "src/main/agent-hooks/server-copilot-normalization.test.ts": 940, - "src/main/agent-hooks/server-cursor-normalization.test.ts": 653, - "src/main/agent-hooks/server-endpoint-file-lifecycle.test.ts": 770, - "src/main/agent-hooks/server-escape-navigation-inference.test.ts": 696, - "src/main/agent-hooks/server-grok-background-status.test.ts": 650, - "src/main/agent-hooks/server-grok-cancel.test.ts": 677, - "src/main/agent-hooks/server-grok-discovery.test.ts": 1216, - "src/main/agent-hooks/server-hook-http-ingest.test.ts": 742, - "src/main/agent-hooks/server-ingest-remote.test.ts": 648, - "src/main/agent-hooks/server-ingest-structured-children.test.ts": 615, - "src/main/agent-hooks/server-ingest-structured-state-clock.test.ts": 679, - "src/main/agent-hooks/server-ingest-structured-status.test.ts": 719, - "src/main/agent-hooks/server-ingest-terminal-status.test.ts": 711, - "src/main/agent-hooks/server-interrupt-inference-guards.test.ts": 580, - "src/main/agent-hooks/server-interrupt-inference-resurrection.test.ts": 644, - "src/main/agent-hooks/server-interrupt-inference-validation.test.ts": 670, - "src/main/agent-hooks/server-last-status-child-held-main-agent.test.ts": 814, - "src/main/agent-hooks/server-last-status-hydrate-confirmation.test.ts": 742, - "src/main/agent-hooks/server-last-status-hydrate-validation.test.ts": 854, - "src/main/agent-hooks/server-last-status-lead-boundary.test.ts": 852, - "src/main/agent-hooks/server-last-status-main-agent-fact.test.ts": 802, - "src/main/agent-hooks/server-last-status-restored-children.test.ts": 776, - "src/main/agent-hooks/server-last-status-write.test.ts": 1017, - "src/main/agent-hooks/server-main-agent-turn-verdicts.test.ts": 1703, - "src/main/agent-hooks/server-muse-session-log-poll.test.ts": 2581, - "src/main/agent-hooks/server-observation-provenance.test.ts": 656, - "src/main/agent-hooks/server-omp-retired-ingress.test.ts": 649, - "src/main/agent-hooks/server-omp-retirement-ack.test.ts": 606, - "src/main/agent-hooks/server-opencode-binder.test.ts": 884, - "src/main/agent-hooks/server-opencode-lifecycle.test.ts": 867, - "src/main/agent-hooks/server-opencode-tui-identity.test.ts": 914, - "src/main/agent-hooks/server-pane-authority.test.ts": 875, - "src/main/agent-hooks/server-pi-normalization.test.ts": 868, - "src/main/agent-hooks/server-process-lifetime-status.test.ts": 964, - "src/main/agent-hooks/server-prompt-sent-telemetry.test.ts": 691, - "src/main/agent-hooks/server-relay-listener-replay.test.ts": 657, - "src/main/agent-hooks/server-relayed-claude-cancel.test.ts": 960, - "src/main/agent-hooks/server-reminted-pane-key.test.ts": 739, - "src/main/agent-hooks/server-removed-worktree-foreign-authority.test.ts": 739, - "src/main/agent-hooks/server-removed-worktree-status.test.ts": 574, - "src/main/agent-hooks/server-replay-evidence-clock.test.ts": 631, - "src/main/agent-hooks/server-retired-pane-new-turn.test.ts": 650, - "src/main/agent-hooks/server-start-failure-lifecycle.test.ts": 656, - "src/main/agent-hooks/server-startup-prompt-control.test.ts": 534, - "src/main/agent-hooks/server-status-listener-fanout.test.ts": 725, - "src/main/agent-hooks/server-status-persist-throttle.test.ts": 962, - "src/main/agent-hooks/server-structured-canonical-status.test.ts": 678, - "src/main/agent-hooks/server-tmux-canonical.test.ts": 675, - "src/main/agent-hooks/server-tmux-wire.test.ts": 644, - "src/main/agent-hooks/server-transport-interference.test.ts": 11601, - "src/main/agent-hooks/server-turn-started-at.test.ts": 691, - "src/main/agent-hooks/server.claude-interactive-question.test.ts": 717, - "src/main/agent-hooks/server.test-fixtures.test.ts": 473, - "src/main/agent-hooks/server/server-row-ownership.test.ts": 612, - "src/main/agent-hooks/spool.test.ts": 974, - "src/main/agent-hooks/terminal-handle-row-identity.test.ts": 656, - "src/main/agent-hooks/windows-batch-hook-launcher.test.ts": 99, - "src/main/agent-hooks/windows-cmd-hook-command-unicode.test.ts": 124, - "src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts": 107, - "src/main/agent-hooks/windows-hook-command-hosts.test.ts": 75, - "src/main/agent-hooks/windows-hook-payload-delivery.test.ts": 129, - "src/main/agent-hooks/windows-hook-post-interpreter.test.ts": 419, - "src/main/agent-hooks/windows-powershell-hook-launcher.test.ts": 50, - "src/main/agent-hooks/wsl-guest-plugin-install.test.ts": 56, - "src/main/agent-hooks/wsl-hook-relay-launch.test.ts": 134, - "src/main/agent-hooks/wsl-hook-relay-live.integration.test.ts": 2021, - "src/main/agent-hooks/wsl-hook-relay-manager.test.ts": 2367, - "src/main/agent-hooks/wsl-hook-relay-reattach.test.ts": 971, - "src/main/agent-hooks/wsl-hook-relay-recovery.test.ts": 49, - "src/main/agent-hooks/wsl-hook-relay-sentinel.test.ts": 64, - "src/main/agent-hooks/wsl-opencode-install-race.test.ts": 869, - "src/main/agent-hooks/wsl-opencode-settings-lifetime.test.ts": 1862, - "src/main/agent-hooks/wsl-opencode-settings.test.ts": 1034, - "src/main/agent-launch/agent-launch-decided-mode-contract.test.ts": 48, - "src/main/agent-launch/agent-launch-executor.test.ts": 1177, - "src/main/agent-launch/agent-launch-legacy-host-policy.test.ts": 894, - "src/main/agent-launch/agent-launch-opencode-model.test.ts": 757, - "src/main/agent-launch/agent-launch-pane-attachment.test.ts": 273, - "src/main/agent-launch/startup-agent-worktree-create.test.ts": 1128, - "src/main/agent-state-file-reader.test.ts": 103, - "src/main/agent-trust-presets.test.ts": 300, - "src/main/agent-workspace-trust-guard-failure.test.ts": 119, - "src/main/agent-workspace-trust-spawn.test.ts": 274, - "src/main/agent-workspace-trust.test.ts": 113, - "src/main/ai-vault-search/session-search-agent-compatibility.test.ts": 319, - "src/main/ai-vault-search/session-search-child-service.test.ts": 534, - "src/main/ai-vault-search/session-search-content-hash.test.ts": 65, - "src/main/ai-vault-search/session-search-cwd-key.test.ts": 72, - "src/main/ai-vault-search/session-search-deleted-sources.test.ts": 416, - "src/main/ai-vault-search/session-search-directory-listings.test.ts": 47, - "src/main/ai-vault-search/session-search-engine.test.ts": 1393, - "src/main/ai-vault-search/session-search-file-id.test.ts": 274, - "src/main/ai-vault-search/session-search-file-write.test.ts": 500, - "src/main/ai-vault-search/session-search-fts5-contract.test.ts": 152, - "src/main/ai-vault-search/session-search-hit-ranking.test.ts": 73, - "src/main/ai-vault-search/session-search-host-registration.test.ts": 1096, - "src/main/ai-vault-search/session-search-identifier-split.test.ts": 51, - "src/main/ai-vault-search/session-search-index-consumer.test.ts": 280, - "src/main/ai-vault-search/session-search-index-generation.test.ts": 421, - "src/main/ai-vault-search/session-search-index-pass.test.ts": 468, - "src/main/ai-vault-search/session-search-index-writer.test.ts": 482, - "src/main/ai-vault-search/session-search-indexer.test.ts": 2536, - "src/main/ai-vault-search/session-search-instance.test.ts": 1147, - "src/main/ai-vault-search/session-search-lifecycle-matrix.test.ts": 2756, - "src/main/ai-vault-search/session-search-live-transcript.test.ts": 423, - "src/main/ai-vault-search/session-search-merged-roots.test.ts": 489, - "src/main/ai-vault-search/session-search-message-rows.test.ts": 149, - "src/main/ai-vault-search/session-search-native-chat-indexing.test.ts": 476, - "src/main/ai-vault-search/session-search-opencode-index.test.ts": 807, - "src/main/ai-vault-search/session-search-orphan-rows.test.ts": 235, - "src/main/ai-vault-search/session-search-paging.test.ts": 601, - "src/main/ai-vault-search/session-search-query-planner.test.ts": 59, - "src/main/ai-vault-search/session-search-read-decision.test.ts": 49, - "src/main/ai-vault-search/session-search-retention-delete.test.ts": 198, - "src/main/ai-vault-search/session-search-retention-policy.test.ts": 275, - "src/main/ai-vault-search/session-search-retrieval-statement-compilation.test.ts": 501, - "src/main/ai-vault-search/session-search-root-refresh.test.ts": 623, - "src/main/ai-vault-search/session-search-row-filter.test.ts": 195, - "src/main/ai-vault-search/session-search-row-identity.test.ts": 148, - "src/main/ai-vault-search/session-search-scan-roots.test.ts": 230, - "src/main/ai-vault-search/session-search-schema.test.ts": 531, - "src/main/ai-vault-search/session-search-scope-entry-points.test.ts": 4400, - "src/main/ai-vault-search/session-search-scope-resolution.test.ts": 106, - "src/main/ai-vault-search/session-search-scope-service.test.ts": 288, - "src/main/ai-vault-search/session-search-service-registry.test.ts": 297, - "src/main/ai-vault-search/session-search-service.test.ts": 311, - "src/main/ai-vault-search/session-search-sidebar-parity.test.ts": 241, - "src/main/ai-vault-search/session-search-snippet-marks.test.ts": 179, - "src/main/ai-vault-search/session-search-sqlite-support.test.ts": 62, - "src/main/ai-vault-search/session-search-store-is-memory.test.ts": 827, - "src/main/ai-vault-search/session-search-store-scope-catalog.test.ts": 52, - "src/main/ai-vault-search/session-search-synthetic-corpus.test.ts": 313, - "src/main/ai-vault-search/session-search-typo-policy.test.ts": 160, - "src/main/ai-vault-search/session-search-typo-scope.test.ts": 148, - "src/main/ai-vault-search/session-search-write-lifetime.test.ts": 351, - "src/main/ai-vault/ai-vault-scan-cancellation.test.ts": 46, - "src/main/ai-vault/ai-vault-scan-coordinator.test.ts": 55, - "src/main/ai-vault/antigravity-ide-history.test.ts": 322, - "src/main/ai-vault/antigravity-index-file-admission.test.ts": 115, - "src/main/ai-vault/antigravity-opening-prompt-workspace.test.ts": 157, - "src/main/ai-vault/antigravity-recorded-history.test.ts": 281, - "src/main/ai-vault/cached-session-list-wsl-probe.test.ts": 472, - "src/main/ai-vault/cached-session-list.test.ts": 626, - "src/main/ai-vault/claude-project-dir-encoding.test.ts": 52, - "src/main/ai-vault/codex-session-collection.test.ts": 149, - "src/main/ai-vault/codex-session-root-dedup.test.ts": 156, - "src/main/ai-vault/local-log-tail-reader.test.ts": 69, + "src/cli/shell-command-quote.test.ts": 50, + "src/cli/skills-reference-selector.test.ts": 169, + "src/cli/skills.test.ts": 1324, + "src/cli/specs/account.test.ts": 66, + "src/cli/specs/bundled-guide-flags.test.ts": 233, + "src/cli/specs/cli-command-names-parity.test.ts": 160, + "src/cli/specs/profile-state.test.ts": 67, + "src/cli/terminal-format.test.ts": 97, + "src/cli/terminal-list-host-scope-format.test.ts": 78, + "src/cli/terminal-read-screen.test.ts": 486, + "src/cli/vocabulary-policy.test.ts": 112, + "src/cli/worktree-ps-terminal-verdict.test.ts": 47, + "src/cli/worktree-selector-caller-host.test.ts": 61, + "src/cli/worktree-selector-wsl-posix-path.test.ts": 79, + "src/main/acp/acp-agent-connection.test.ts": 316, + "src/main/acp/acp-compaction-journal-fixture.test.ts": 313, + "src/main/acp/acp-dialects/grok-subagents.test.ts": 279, + "src/main/acp/acp-dialects/omp-dialect.test.ts": 212, + "src/main/acp/acp-fixture-privacy.test.ts": 124, + "src/main/acp/acp-json-rpc-peer.test.ts": 292, + "src/main/acp/acp-model-catalog-probe.test.ts": 676, + "src/main/acp/acp-permission-requests.test.ts": 352, + "src/main/acp/acp-prompt-content.test.ts": 470, + "src/main/acp/acp-recovery-history.test.ts": 524, + "src/main/acp/acp-session-agent-turns.test.ts": 313, + "src/main/acp/acp-session-events.test.ts": 1023, + "src/main/acp/acp-session-lifecycle.test.ts": 413, + "src/main/acp/acp-session-notifications.test.ts": 389, + "src/main/acp/acp-session-runtime.test.ts": 400, + "src/main/acp/acp-structured-auth.test.ts": 1049, + "src/main/acp/acp-structured-background-task-stop.test.ts": 4804, + "src/main/acp/acp-structured-broken-stream.test.ts": 10857, + "src/main/acp/acp-structured-child-session-end.test.ts": 2107, + "src/main/acp/acp-structured-child-stop.test.ts": 3015, + "src/main/acp/acp-structured-child-work-admission.test.ts": 864, + "src/main/acp/acp-structured-child-work.test.ts": 3411, + "src/main/acp/acp-structured-compaction.test.ts": 1845, + "src/main/acp/acp-structured-context-clear.test.ts": 689, + "src/main/acp/acp-structured-electron-free.test.ts": 212, + "src/main/acp/acp-structured-grok-subagent-recordings.test.ts": 1446, + "src/main/acp/acp-structured-host-configured-default.test.ts": 1777, + "src/main/acp/acp-structured-host-crash.test.ts": 1513, + "src/main/acp/acp-structured-host-lifecycle.test.ts": 1920, + "src/main/acp/acp-structured-host-live-listing.test.ts": 1914, + "src/main/acp/acp-structured-host-option-hang.test.ts": 1447, + "src/main/acp/acp-structured-host-restore-failed.test.ts": 2022, + "src/main/acp/acp-structured-host-start-aborts.test.ts": 1886, + "src/main/acp/acp-structured-host-stop.test.ts": 2080, + "src/main/acp/acp-structured-host-subagent-stop.test.ts": 2207, + "src/main/acp/acp-structured-image-prompts.test.ts": 848, + "src/main/acp/acp-structured-launch-resolution-omp.test.ts": 718, + "src/main/acp/acp-structured-launch-resolution-opencode.test.ts": 682, + "src/main/acp/acp-structured-launch-resolution.test.ts": 784, + "src/main/acp/acp-structured-launch-visuals.test.ts": 768, + "src/main/acp/acp-structured-missing-model.test.ts": 358, + "src/main/acp/acp-structured-omp-recordings.test.ts": 1593, + "src/main/acp/acp-structured-opencode-recordings.test.ts": 1307, + "src/main/acp/acp-structured-options.test.ts": 264, + "src/main/acp/acp-structured-prompt-failure.test.ts": 1260, + "src/main/acp/acp-structured-request-admission.test.ts": 1010, + "src/main/acp/acp-structured-session-adapter-lifecycle.test.ts": 5319, + "src/main/acp/acp-structured-session-adapter-recordings.test.ts": 1296, + "src/main/acp/acp-structured-session-adapter-resume.test.ts": 5169, + "src/main/acp/acp-structured-session-adapter-teardown.test.ts": 1688, + "src/main/acp/acp-structured-session-adapter.test.ts": 1688, + "src/main/acp/acp-structured-sign-in.test.ts": 1285, + "src/main/acp/acp-structured-steer.test.ts": 2132, + "src/main/acp/acp-text-message-ownership.test.ts": 1104, + "src/main/acp/acp-text-stream-identity.test.ts": 931, + "src/main/acp/acp-timeline-background-task-evidence.test.ts": 1539, + "src/main/acp/acp-timeline-background-task-results.test.ts": 1133, + "src/main/acp/acp-timeline-background-task-words.test.ts": 1060, + "src/main/acp/acp-timeline-background-tasks.test.ts": 1760, + "src/main/acp/acp-timeline-dialect.test.ts": 746, + "src/main/acp/acp-timeline-generic.test.ts": 1493, + "src/main/acp/acp-timeline-grok-subagent-retention.test.ts": 1665, + "src/main/acp/acp-timeline-grok-subagents.test.ts": 1432, + "src/main/acp/acp-timeline-joins.test.ts": 733, + "src/main/acp/acp-timeline-open-enums.test.ts": 743, + "src/main/acp/acp-timeline-opencode-dialect.test.ts": 733, + "src/main/acp/acp-timeline-recordings.test.ts": 1220, + "src/main/acp/acp-timeline-recovery.test.ts": 1001, + "src/main/acp/acp-timeline-routing.test.ts": 1145, + "src/main/acp/acp-timeline-turn-failures.test.ts": 1327, + "src/main/acp/acp-visuals-skill.test.ts": 58, + "src/main/acp/acp-write-queue-retention.test.ts": 579, + "src/main/active-view-persistence-boundary.test.ts": 1839, + "src/main/active-view-preference-sync-flush-veto.test.ts": 827, + "src/main/active-view-preference.test.ts": 40, + "src/main/agent-auth-restart-preservation.test.ts": 53, + "src/main/agent-awake-service-platform-assertions.test.ts": 64, + "src/main/agent-awake-service.test.ts": 111, + "src/main/agent-cli-version-probe.test.ts": 60, + "src/main/agent-hooks/agent-home-login-shell.test.ts": 57, + "src/main/agent-hooks/branch-rename-failure-output.test.ts": 33, + "src/main/agent-hooks/ended-process-reconciliation.test.ts": 658, + "src/main/agent-hooks/first-work-branch-rename-wsl-move.test.ts": 611, + "src/main/agent-hooks/first-work-branch-rename.test.ts": 1084, + "src/main/agent-hooks/first-work-folder-rename.test.ts": 114, + "src/main/agent-hooks/grok-replay-guard.test.ts": 138, + "src/main/agent-hooks/hook-process-capture.test.ts": 96, + "src/main/agent-hooks/hook-script-outside-orca.test.ts": 275, + "src/main/agent-hooks/hook-status-session-tabs-republish.test.ts": 692, + "src/main/agent-hooks/install-telemetry.test.ts": 38, + "src/main/agent-hooks/installer-utils-remote.test.ts": 48, + "src/main/agent-hooks/installer-utils.test.ts": 201, + "src/main/agent-hooks/local-agent-cli-presence.test.ts": 66, + "src/main/agent-hooks/local-tmux-managed-pty.test.ts": 35, + "src/main/agent-hooks/locked-executable-rename.test.ts": 96, + "src/main/agent-hooks/managed-agent-hook-controls.test.ts": 131, + "src/main/agent-hooks/managed-hook-command-contract.test.ts": 398, + "src/main/agent-hooks/managed-hook-detection-commands.test.ts": 61, + "src/main/agent-hooks/managed-hook-install-lock.test.ts": 307, + "src/main/agent-hooks/managed-hook-owner-identity.test.ts": 71, + "src/main/agent-hooks/managed-hook-script-refresh-main-thread.test.ts": 388, + "src/main/agent-hooks/managed-hook-script-refresh.test.ts": 455, + "src/main/agent-hooks/managed-hook-stdin-lifecycle.test.ts": 1585, + "src/main/agent-hooks/managed-hook-timeout.test.ts": 1809, + "src/main/agent-hooks/managed-toml-ownership.test.ts": 52, + "src/main/agent-hooks/manual-compact-hook-stream.test.ts": 694, + "src/main/agent-hooks/manual-compact-status-cleanup.test.ts": 669, + "src/main/agent-hooks/migration-unsupported-pty-state.test.ts": 34, + "src/main/agent-hooks/posix-hook-json-stdin-reader.test.ts": 3473, + "src/main/agent-hooks/remote-hook-service-installers.test.ts": 496, + "src/main/agent-hooks/remote-hook-service-registry-coverage.test.ts": 410, + "src/main/agent-hooks/restored-status-dead-terminal-sweep.test.ts": 889, + "src/main/agent-hooks/server-agent-presence.test.ts": 963, + "src/main/agent-hooks/server-ai-vault-liveness.test.ts": 618, + "src/main/agent-hooks/server-amp-normalization.test.ts": 682, + "src/main/agent-hooks/server-authority-evidence.test.ts": 694, + "src/main/agent-hooks/server-claude-cancel-captures.test.ts": 844, + "src/main/agent-hooks/server-claude-child-permission-lifecycle.test.ts": 819, + "src/main/agent-hooks/server-claude-normalization.test.ts": 710, + "src/main/agent-hooks/server-claude-owed-notification-expiry.test.ts": 867, + "src/main/agent-hooks/server-claude-permission-visibility.test.ts": 767, + "src/main/agent-hooks/server-claude-statusline.test.ts": 731, + "src/main/agent-hooks/server-claude-task-wakeup-lifecycle.test.ts": 6114, + "src/main/agent-hooks/server-claude-terminal-interrupt.test.ts": 838, + "src/main/agent-hooks/server-closed-tab-suppression.test.ts": 762, + "src/main/agent-hooks/server-codex-capacity.test.ts": 800, + "src/main/agent-hooks/server-codex-ctrl-c-inference.test.ts": 658, + "src/main/agent-hooks/server-codex-interrupt-hook.test.ts": 723, + "src/main/agent-hooks/server-codex-normalization.test.ts": 673, + "src/main/agent-hooks/server-codex-subagent-transcript.test.ts": 4690, + "src/main/agent-hooks/server-codex-turn-interruption.test.ts": 4334, + "src/main/agent-hooks/server-copilot-normalization.test.ts": 974, + "src/main/agent-hooks/server-cursor-normalization.test.ts": 650, + "src/main/agent-hooks/server-endpoint-file-lifecycle.test.ts": 585, + "src/main/agent-hooks/server-escape-navigation-inference.test.ts": 771, + "src/main/agent-hooks/server-grok-background-status.test.ts": 753, + "src/main/agent-hooks/server-grok-cancel.test.ts": 686, + "src/main/agent-hooks/server-grok-discovery.test.ts": 1163, + "src/main/agent-hooks/server-hook-http-ingest.test.ts": 811, + "src/main/agent-hooks/server-ingest-remote.test.ts": 702, + "src/main/agent-hooks/server-ingest-structured-children.test.ts": 704, + "src/main/agent-hooks/server-ingest-structured-state-clock.test.ts": 687, + "src/main/agent-hooks/server-ingest-structured-status.test.ts": 789, + "src/main/agent-hooks/server-ingest-terminal-status.test.ts": 633, + "src/main/agent-hooks/server-interrupt-inference-guards.test.ts": 747, + "src/main/agent-hooks/server-interrupt-inference-resurrection.test.ts": 656, + "src/main/agent-hooks/server-interrupt-inference-validation.test.ts": 759, + "src/main/agent-hooks/server-last-status-child-held-main-agent.test.ts": 1034, + "src/main/agent-hooks/server-last-status-hydrate-confirmation.test.ts": 944, + "src/main/agent-hooks/server-last-status-hydrate-validation.test.ts": 711, + "src/main/agent-hooks/server-last-status-lead-boundary.test.ts": 756, + "src/main/agent-hooks/server-last-status-main-agent-fact.test.ts": 803, + "src/main/agent-hooks/server-last-status-restored-children.test.ts": 870, + "src/main/agent-hooks/server-last-status-write.test.ts": 813, + "src/main/agent-hooks/server-main-agent-turn-verdicts.test.ts": 1813, + "src/main/agent-hooks/server-muse-session-log-poll.test.ts": 2514, + "src/main/agent-hooks/server-nested-agent-model.test.ts": 609, + "src/main/agent-hooks/server-observation-provenance.test.ts": 715, + "src/main/agent-hooks/server-omp-retired-ingress.test.ts": 620, + "src/main/agent-hooks/server-omp-retirement-ack.test.ts": 686, + "src/main/agent-hooks/server-opencode-binder.test.ts": 879, + "src/main/agent-hooks/server-opencode-lifecycle.test.ts": 835, + "src/main/agent-hooks/server-opencode-tui-identity.test.ts": 915, + "src/main/agent-hooks/server-pane-authority.test.ts": 786, + "src/main/agent-hooks/server-pi-normalization.test.ts": 883, + "src/main/agent-hooks/server-process-lifetime-status.test.ts": 800, + "src/main/agent-hooks/server-prompt-sent-telemetry.test.ts": 612, + "src/main/agent-hooks/server-relay-listener-replay.test.ts": 672, + "src/main/agent-hooks/server-relayed-claude-cancel.test.ts": 1209, + "src/main/agent-hooks/server-reminted-pane-key.test.ts": 783, + "src/main/agent-hooks/server-removed-worktree-foreign-authority.test.ts": 807, + "src/main/agent-hooks/server-removed-worktree-status.test.ts": 712, + "src/main/agent-hooks/server-replay-evidence-clock.test.ts": 639, + "src/main/agent-hooks/server-retired-pane-new-turn.test.ts": 665, + "src/main/agent-hooks/server-start-failure-lifecycle.test.ts": 733, + "src/main/agent-hooks/server-startup-prompt-control.test.ts": 783, + "src/main/agent-hooks/server-status-listener-fanout.test.ts": 738, + "src/main/agent-hooks/server-status-persist-throttle.test.ts": 1095, + "src/main/agent-hooks/server-structured-canonical-status.test.ts": 769, + "src/main/agent-hooks/server-tmux-canonical.test.ts": 649, + "src/main/agent-hooks/server-tmux-wire.test.ts": 618, + "src/main/agent-hooks/server-transport-interference.test.ts": 1870, + "src/main/agent-hooks/server-turn-started-at.test.ts": 835, + "src/main/agent-hooks/server.claude-interactive-question.test.ts": 696, + "src/main/agent-hooks/server.test-fixtures.test.ts": 764, + "src/main/agent-hooks/server/server-row-ownership.test.ts": 660, + "src/main/agent-hooks/spool.test.ts": 782, + "src/main/agent-hooks/terminal-handle-row-identity.test.ts": 860, + "src/main/agent-hooks/windows-batch-hook-launcher.test.ts": 90, + "src/main/agent-hooks/windows-cmd-hook-command-unicode.test.ts": 110, + "src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts": 90, + "src/main/agent-hooks/windows-hook-command-hosts.test.ts": 82, + "src/main/agent-hooks/windows-hook-payload-delivery.test.ts": 208, + "src/main/agent-hooks/windows-hook-post-interpreter.test.ts": 393, + "src/main/agent-hooks/windows-native-hook-exe-hosts.test.ts": 101, + "src/main/agent-hooks/windows-powershell-hook-launcher.test.ts": 44, + "src/main/agent-hooks/wsl-guest-plugin-install.test.ts": 45, + "src/main/agent-hooks/wsl-hook-relay-launch.test.ts": 96, + "src/main/agent-hooks/wsl-hook-relay-live.integration.test.ts": 1971, + "src/main/agent-hooks/wsl-hook-relay-manager.test.ts": 2166, + "src/main/agent-hooks/wsl-hook-relay-reattach.test.ts": 1178, + "src/main/agent-hooks/wsl-hook-relay-recovery.test.ts": 46, + "src/main/agent-hooks/wsl-hook-relay-sentinel.test.ts": 54, + "src/main/agent-hooks/wsl-opencode-install-race.test.ts": 1081, + "src/main/agent-hooks/wsl-opencode-settings-lifetime.test.ts": 1458, + "src/main/agent-hooks/wsl-opencode-settings.test.ts": 1055, + "src/main/agent-launch/agent-launch-account-env.test.ts": 1307, + "src/main/agent-launch/agent-launch-decided-mode-contract.test.ts": 25, + "src/main/agent-launch/agent-launch-executor.test.ts": 1401, + "src/main/agent-launch/agent-launch-legacy-host-policy.test.ts": 1463, + "src/main/agent-launch/agent-launch-opencode-model.test.ts": 1254, + "src/main/agent-launch/agent-launch-pane-attachment.test.ts": 36, + "src/main/agent-launch/startup-agent-worktree-create.test.ts": 1204, + "src/main/agent-state-file-reader.test.ts": 61, + "src/main/agent-trust-presets.test.ts": 347, + "src/main/agent-workspace-trust-guard-failure.test.ts": 132, + "src/main/agent-workspace-trust-spawn.test.ts": 345, + "src/main/agent-workspace-trust.test.ts": 169, + "src/main/ai-vault-search/session-search-agent-compatibility.test.ts": 248, + "src/main/ai-vault-search/session-search-child-service.test.ts": 376, + "src/main/ai-vault-search/session-search-content-hash.test.ts": 38, + "src/main/ai-vault-search/session-search-cwd-key.test.ts": 52, + "src/main/ai-vault-search/session-search-deleted-sources.test.ts": 395, + "src/main/ai-vault-search/session-search-dependent-transcripts.test.ts": 739, + "src/main/ai-vault-search/session-search-directory-listings.test.ts": 39, + "src/main/ai-vault-search/session-search-engine.test.ts": 1664, + "src/main/ai-vault-search/session-search-file-id.test.ts": 185, + "src/main/ai-vault-search/session-search-file-write.test.ts": 618, + "src/main/ai-vault-search/session-search-fts5-contract.test.ts": 143, + "src/main/ai-vault-search/session-search-hit-ranking.test.ts": 60, + "src/main/ai-vault-search/session-search-host-registration.test.ts": 979, + "src/main/ai-vault-search/session-search-identifier-split.test.ts": 38, + "src/main/ai-vault-search/session-search-index-consumer.test.ts": 244, + "src/main/ai-vault-search/session-search-index-generation.test.ts": 449, + "src/main/ai-vault-search/session-search-index-pass.test.ts": 507, + "src/main/ai-vault-search/session-search-index-writer.test.ts": 402, + "src/main/ai-vault-search/session-search-indexer.test.ts": 2684, + "src/main/ai-vault-search/session-search-instance.test.ts": 1182, + "src/main/ai-vault-search/session-search-lifecycle-matrix.test.ts": 3442, + "src/main/ai-vault-search/session-search-list-reparse.test.ts": 384, + "src/main/ai-vault-search/session-search-live-transcript.test.ts": 344, + "src/main/ai-vault-search/session-search-merged-roots.test.ts": 494, + "src/main/ai-vault-search/session-search-message-rows.test.ts": 139, + "src/main/ai-vault-search/session-search-native-chat-indexing.test.ts": 547, + "src/main/ai-vault-search/session-search-opencode-index.test.ts": 788, + "src/main/ai-vault-search/session-search-orphan-rows.test.ts": 244, + "src/main/ai-vault-search/session-search-paging.test.ts": 642, + "src/main/ai-vault-search/session-search-query-planner.test.ts": 42, + "src/main/ai-vault-search/session-search-read-decision.test.ts": 31, + "src/main/ai-vault-search/session-search-retention-delete.test.ts": 206, + "src/main/ai-vault-search/session-search-retention-policy.test.ts": 224, + "src/main/ai-vault-search/session-search-retrieval-statement-compilation.test.ts": 516, + "src/main/ai-vault-search/session-search-root-refresh.test.ts": 500, + "src/main/ai-vault-search/session-search-row-filter.test.ts": 205, + "src/main/ai-vault-search/session-search-row-identity.test.ts": 131, + "src/main/ai-vault-search/session-search-scan-roots.test.ts": 198, + "src/main/ai-vault-search/session-search-schema.test.ts": 403, + "src/main/ai-vault-search/session-search-scope-entry-points.test.ts": 4341, + "src/main/ai-vault-search/session-search-scope-resolution.test.ts": 98, + "src/main/ai-vault-search/session-search-scope-service.test.ts": 293, + "src/main/ai-vault-search/session-search-service-registry.test.ts": 257, + "src/main/ai-vault-search/session-search-service.test.ts": 329, + "src/main/ai-vault-search/session-search-sidebar-parity.test.ts": 318, + "src/main/ai-vault-search/session-search-snippet-marks.test.ts": 233, + "src/main/ai-vault-search/session-search-sqlite-support.test.ts": 37, + "src/main/ai-vault-search/session-search-store-is-memory.test.ts": 1107, + "src/main/ai-vault-search/session-search-store-scope-catalog.test.ts": 45, + "src/main/ai-vault-search/session-search-synthetic-corpus.test.ts": 341, + "src/main/ai-vault-search/session-search-transcript-dependencies.test.ts": 389, + "src/main/ai-vault-search/session-search-typo-policy.test.ts": 86, + "src/main/ai-vault-search/session-search-typo-scope.test.ts": 123, + "src/main/ai-vault-search/session-search-write-lifetime.test.ts": 326, + "src/main/ai-vault/ai-vault-scan-cancellation.test.ts": 31, + "src/main/ai-vault/ai-vault-scan-coordinator.test.ts": 35, + "src/main/ai-vault/ai-vault-scan-progress.test.ts": 313, + "src/main/ai-vault/antigravity-ide-history.test.ts": 344, + "src/main/ai-vault/antigravity-index-file-admission.test.ts": 129, + "src/main/ai-vault/antigravity-opening-prompt-workspace.test.ts": 173, + "src/main/ai-vault/antigravity-recorded-history.test.ts": 239, + "src/main/ai-vault/cached-session-list-wsl-probe.test.ts": 316, + "src/main/ai-vault/cached-session-list.test.ts": 383, + "src/main/ai-vault/claude-project-dir-encoding.test.ts": 40, + "src/main/ai-vault/codex-session-collection.test.ts": 91, + "src/main/ai-vault/codex-session-root-dedup.test.ts": 122, + "src/main/ai-vault/local-log-tail-reader.test.ts": 50, "src/main/ai-vault/omp-session-root.test.ts": 234, - "src/main/ai-vault/openclaw-session-layout.test.ts": 172, - "src/main/ai-vault/opencode-wsl-runtime-preparation.test.ts": 1150, - "src/main/ai-vault/remote-session-content-lines.test.ts": 84, - "src/main/ai-vault/runtime-session-scanner.test.ts": 256, - "src/main/ai-vault/runtime-session-search-call.test.ts": 44, - "src/main/ai-vault/session-cwd-bucket-layouts.test.ts": 59, - "src/main/ai-vault/session-delete-target.test.ts": 224, - "src/main/ai-vault/session-delete.test.ts": 220, - "src/main/ai-vault/session-document-stream-boundaries.test.ts": 155, - "src/main/ai-vault/session-document-stream-contract.test.ts": 681, - "src/main/ai-vault/session-document-stream-parity.test.ts": 201, - "src/main/ai-vault/session-document-stream-projection.test.ts": 145, - "src/main/ai-vault/session-first-user-prompt-opencode.test.ts": 232, - "src/main/ai-vault/session-first-user-prompt-read.test.ts": 302, - "src/main/ai-vault/session-list-result-validation.test.ts": 235, - "src/main/ai-vault/session-list-results.test.ts": 136, - "src/main/ai-vault/session-newest-files.test.ts": 120, - "src/main/ai-vault/session-parse-cache-antigravity-upgrade.test.ts": 278, - "src/main/ai-vault/session-parse-cache-bounds.test.ts": 498, - "src/main/ai-vault/session-parse-cache-devin-upgrade.test.ts": 244, - "src/main/ai-vault/session-parse-cache-persistence.test.ts": 348, - "src/main/ai-vault/session-scan-cutoff.test.ts": 125, - "src/main/ai-vault/session-scanner-agent-root-overrides.test.ts": 601, - "src/main/ai-vault/session-scanner-antigravity-parser.test.ts": 114, - "src/main/ai-vault/session-scanner-antigravity-source.test.ts": 337, - "src/main/ai-vault/session-scanner-claude-cwd-drift.test.ts": 265, - "src/main/ai-vault/session-scanner-claude-subagent-prune.test.ts": 326, - "src/main/ai-vault/session-scanner-claude-subagents.test.ts": 186, - "src/main/ai-vault/session-scanner-claude-title.test.ts": 333, - "src/main/ai-vault/session-scanner-claude-unicode-scope.test.ts": 353, - "src/main/ai-vault/session-scanner-cline-parser.test.ts": 311, - "src/main/ai-vault/session-scanner-codebuddy-scope.test.ts": 236, - "src/main/ai-vault/session-scanner-codebuddy-source.test.ts": 212, - "src/main/ai-vault/session-scanner-codex-dual-root.test.ts": 327, - "src/main/ai-vault/session-scanner-codex-fast-path.test.ts": 352, - "src/main/ai-vault/session-scanner-codex-non-user-origin.test.ts": 99, - "src/main/ai-vault/session-scanner-codex-parser.test.ts": 152, - "src/main/ai-vault/session-scanner-codex-title-index.test.ts": 255, - "src/main/ai-vault/session-scanner-codex-tool-records.test.ts": 156, - "src/main/ai-vault/session-scanner-codex-workers.test.ts": 306, - "src/main/ai-vault/session-scanner-core-parser-wsl-stall.test.ts": 318, - "src/main/ai-vault/session-scanner-cursor-chat-meta.test.ts": 433, - "src/main/ai-vault/session-scanner-cutoff.test.ts": 599, - "src/main/ai-vault/session-scanner-dedup-batches.test.ts": 510, - "src/main/ai-vault/session-scanner-devin-contention.test.ts": 367, - "src/main/ai-vault/session-scanner-devin-db.test.ts": 493, - "src/main/ai-vault/session-scanner-devin-dedup.test.ts": 383, - "src/main/ai-vault/session-scanner-devin-parser.test.ts": 127, - "src/main/ai-vault/session-scanner-directory-reader.test.ts": 97, - "src/main/ai-vault/session-scanner-discovery-wsl-gate.test.ts": 120, - "src/main/ai-vault/session-scanner-first-user-prompt.test.ts": 63, - "src/main/ai-vault/session-scanner-graph-parsers.test.ts": 138, - "src/main/ai-vault/session-scanner-grok-parser.test.ts": 124, - "src/main/ai-vault/session-scanner-grok-user-text.test.ts": 51, - "src/main/ai-vault/session-scanner-index-cache-wsl-stall.test.ts": 121, - "src/main/ai-vault/session-scanner-injected-title.test.ts": 295, - "src/main/ai-vault/session-scanner-jcode-parser.test.ts": 127, - "src/main/ai-vault/session-scanner-jsonl-reader.test.ts": 324, - "src/main/ai-vault/session-scanner-kimi-index-cache.test.ts": 119, - "src/main/ai-vault/session-scanner-kimi-parser.test.ts": 201, - "src/main/ai-vault/session-scanner-kiro-parser.test.ts": 124, - "src/main/ai-vault/session-scanner-muse.test.ts": 277, - "src/main/ai-vault/session-scanner-omp-subagent-listing.test.ts": 156, - "src/main/ai-vault/session-scanner-omp-subagent-prune.test.ts": 361, - "src/main/ai-vault/session-scanner-omp-subagent-transcripts.test.ts": 122, - "src/main/ai-vault/session-scanner-omp-title.test.ts": 161, - "src/main/ai-vault/session-scanner-opencode-cancellation.test.ts": 678, - "src/main/ai-vault/session-scanner-opencode-native-page-limit.test.ts": 64, - "src/main/ai-vault/session-scanner-opencode-native-worker.test.ts": 721, - "src/main/ai-vault/session-scanner-opencode-parser.test.ts": 126, - "src/main/ai-vault/session-scanner-opencode-process-admission.test.ts": 275, - "src/main/ai-vault/session-scanner-opencode-sources-wsl-stall.test.ts": 124, - "src/main/ai-vault/session-scanner-opencode-sources.test.ts": 53, - "src/main/ai-vault/session-scanner-opencode-sqlite-bounds.test.ts": 1475, - "src/main/ai-vault/session-scanner-opencode-sqlite-coexistence.test.ts": 398, - "src/main/ai-vault/session-scanner-opencode-sqlite-open.test.ts": 3525, - "src/main/ai-vault/session-scanner-opencode-sqlite-paths.test.ts": 53, - "src/main/ai-vault/session-scanner-opencode-sqlite-process.test.ts": 3438, - "src/main/ai-vault/session-scanner-opencode-sqlite-scan-scope.test.ts": 54, - "src/main/ai-vault/session-scanner-opencode-sqlite-worker-client.test.ts": 148, - "src/main/ai-vault/session-scanner-opencode-sqlite-worker-spawn.test.ts": 118, - "src/main/ai-vault/session-scanner-opencode-sqlite.test.ts": 432, - "src/main/ai-vault/session-scanner-opencode-wsl-client.test.ts": 144, - "src/main/ai-vault/session-scanner-opencode-wsl-routing.test.ts": 192, - "src/main/ai-vault/session-scanner-opencode2-message.test.ts": 86, - "src/main/ai-vault/session-scanner-opencode2-sqlite.test.ts": 332, - "src/main/ai-vault/session-scanner-parse-cache-agents.test.ts": 541, - "src/main/ai-vault/session-scanner-parse-cache.test.ts": 310, - "src/main/ai-vault/session-scanner-parse-wsl-stall.test.ts": 275, - "src/main/ai-vault/session-scanner-parser-stream-cleanup.test.ts": 172, - "src/main/ai-vault/session-scanner-pi-scope.test.ts": 394, - "src/main/ai-vault/session-scanner-preview-window-truncation.test.ts": 111, - "src/main/ai-vault/session-scanner-qoder.test.ts": 319, - "src/main/ai-vault/session-scanner-record-budget.test.ts": 393, - "src/main/ai-vault/session-scanner-recoverable-empty.test.ts": 338, - "src/main/ai-vault/session-scanner-roots.test.ts": 184, - "src/main/ai-vault/session-scanner-scope.test.ts": 339, - "src/main/ai-vault/session-scanner-service-cancellation.test.ts": 417, - "src/main/ai-vault/session-scanner-service-client.test.ts": 502, - "src/main/ai-vault/session-scanner-service-entry-path.test.ts": 50, - "src/main/ai-vault/session-scanner-service-entry-subagents.test.ts": 649, - "src/main/ai-vault/session-scanner-service-entry.test.ts": 813, - "src/main/ai-vault/session-scanner-service-env.test.ts": 69, - "src/main/ai-vault/session-scanner-service-restart-policy.test.ts": 54, - "src/main/ai-vault/session-scanner-service-root-request.test.ts": 50, - "src/main/ai-vault/session-scanner-service-root-response.test.ts": 219, - "src/main/ai-vault/session-scanner-service-search-roots.test.ts": 604, - "src/main/ai-vault/session-scanner-service-search.test.ts": 1251, - "src/main/ai-vault/session-scanner-service-spawn.test.ts": 501, - "src/main/ai-vault/session-scanner-timeline.test.ts": 140, - "src/main/ai-vault/session-scanner-unlimited-dedup.test.ts": 3871, - "src/main/ai-vault/session-scanner-values.test.ts": 114, - "src/main/ai-vault/session-scanner-zcode-sources.test.ts": 131, - "src/main/ai-vault/session-scanner-zcode.test.ts": 313, - "src/main/ai-vault/session-scanner.test.ts": 425, - "src/main/ai-vault/session-sidecar-stat.test.ts": 68, - "src/main/ai-vault/session-title-file-reader-wsl-stall.test.ts": 275, - "src/main/ai-vault/session-title-file-reader.test.ts": 126, - "src/main/ai-vault/session-transcript-consumers.test.ts": 386, - "src/main/ai-vault/session-transcript-every-agent-capture.test.ts": 680, - "src/main/ai-vault/session-transcript-message-content.test.ts": 212, - "src/main/ai-vault/session-transcript-record-budget.test.ts": 41, - "src/main/ai-vault/structured-session-ownership.test.ts": 283, - "src/main/amp/hook-service.test.ts": 70, - "src/main/antigravity/hook-service.test.ts": 210, - "src/main/antigravity/native-account-host.test.ts": 43, - "src/main/antigravity/native-account-launch.test.ts": 107, - "src/main/antigravity/native-account-service.test.ts": 84, - "src/main/antigravity/native-account-store.test.ts": 255, - "src/main/antigravity/native-credential-backend.test.ts": 71, + "src/main/ai-vault/openclaw-session-layout.test.ts": 154, + "src/main/ai-vault/opencode-wsl-runtime-preparation.test.ts": 1213, + "src/main/ai-vault/remote-session-content-lines.test.ts": 55, + "src/main/ai-vault/runtime-session-scanner.test.ts": 278, + "src/main/ai-vault/runtime-session-search-call.test.ts": 34, + "src/main/ai-vault/session-cwd-bucket-layouts.test.ts": 38, + "src/main/ai-vault/session-delete-target.test.ts": 169, + "src/main/ai-vault/session-delete.test.ts": 232, + "src/main/ai-vault/session-document-stream-boundaries.test.ts": 176, + "src/main/ai-vault/session-document-stream-contract.test.ts": 229, + "src/main/ai-vault/session-document-stream-parity.test.ts": 155, + "src/main/ai-vault/session-document-stream-projection.test.ts": 114, + "src/main/ai-vault/session-first-user-prompt-opencode.test.ts": 244, + "src/main/ai-vault/session-first-user-prompt-read.test.ts": 250, + "src/main/ai-vault/session-list-fingerprint.test.ts": 36, + "src/main/ai-vault/session-list-result-validation.test.ts": 208, + "src/main/ai-vault/session-list-results.test.ts": 133, + "src/main/ai-vault/session-newest-files.test.ts": 98, + "src/main/ai-vault/session-parse-cache-antigravity-upgrade.test.ts": 265, + "src/main/ai-vault/session-parse-cache-bounds.test.ts": 281, + "src/main/ai-vault/session-parse-cache-checkpoint.test.ts": 3873, + "src/main/ai-vault/session-parse-cache-devin-upgrade.test.ts": 223, + "src/main/ai-vault/session-parse-cache-metadata-checkpoint.test.ts": 299, + "src/main/ai-vault/session-parse-cache-persistence.test.ts": 378, + "src/main/ai-vault/session-parse-cache-save-lane.test.ts": 256, + "src/main/ai-vault/session-scan-cutoff.test.ts": 154, + "src/main/ai-vault/session-scanner-agent-root-overrides.test.ts": 683, + "src/main/ai-vault/session-scanner-antigravity-parser.test.ts": 130, + "src/main/ai-vault/session-scanner-antigravity-source.test.ts": 348, + "src/main/ai-vault/session-scanner-claude-cwd-drift.test.ts": 270, + "src/main/ai-vault/session-scanner-claude-subagent-prune.test.ts": 338, + "src/main/ai-vault/session-scanner-claude-subagents.test.ts": 157, + "src/main/ai-vault/session-scanner-claude-title.test.ts": 270, + "src/main/ai-vault/session-scanner-claude-unicode-scope.test.ts": 332, + "src/main/ai-vault/session-scanner-cline-parser.test.ts": 325, + "src/main/ai-vault/session-scanner-codebuddy-scope.test.ts": 301, + "src/main/ai-vault/session-scanner-codebuddy-source.test.ts": 295, + "src/main/ai-vault/session-scanner-codex-dual-root.test.ts": 369, + "src/main/ai-vault/session-scanner-codex-fast-path.test.ts": 305, + "src/main/ai-vault/session-scanner-codex-non-user-origin.test.ts": 94, + "src/main/ai-vault/session-scanner-codex-parser.test.ts": 147, + "src/main/ai-vault/session-scanner-codex-title-index.test.ts": 233, + "src/main/ai-vault/session-scanner-codex-tool-records.test.ts": 123, + "src/main/ai-vault/session-scanner-codex-workers.test.ts": 300, + "src/main/ai-vault/session-scanner-core-parser-wsl-stall.test.ts": 300, + "src/main/ai-vault/session-scanner-cursor-chat-meta.test.ts": 420, + "src/main/ai-vault/session-scanner-cutoff.test.ts": 473, + "src/main/ai-vault/session-scanner-dedup-batches.test.ts": 366, + "src/main/ai-vault/session-scanner-devin-contention.test.ts": 298, + "src/main/ai-vault/session-scanner-devin-db.test.ts": 542, + "src/main/ai-vault/session-scanner-devin-dedup.test.ts": 302, + "src/main/ai-vault/session-scanner-devin-parser.test.ts": 114, + "src/main/ai-vault/session-scanner-directory-reader.test.ts": 91, + "src/main/ai-vault/session-scanner-discovery-pipeline.test.ts": 174, + "src/main/ai-vault/session-scanner-discovery-wsl-gate.test.ts": 94, + "src/main/ai-vault/session-scanner-first-user-prompt.test.ts": 49, + "src/main/ai-vault/session-scanner-graph-parsers.test.ts": 112, + "src/main/ai-vault/session-scanner-grok-parser.test.ts": 133, + "src/main/ai-vault/session-scanner-grok-user-text.test.ts": 38, + "src/main/ai-vault/session-scanner-index-cache-wsl-stall.test.ts": 157, + "src/main/ai-vault/session-scanner-injected-title.test.ts": 296, + "src/main/ai-vault/session-scanner-jcode-parser.test.ts": 95, + "src/main/ai-vault/session-scanner-jsonl-reader.test.ts": 295, + "src/main/ai-vault/session-scanner-kimi-index-cache.test.ts": 110, + "src/main/ai-vault/session-scanner-kimi-parser.test.ts": 280, + "src/main/ai-vault/session-scanner-kiro-parser.test.ts": 137, + "src/main/ai-vault/session-scanner-muse.test.ts": 340, + "src/main/ai-vault/session-scanner-omp-subagent-listing.test.ts": 121, + "src/main/ai-vault/session-scanner-omp-subagent-prune.test.ts": 313, + "src/main/ai-vault/session-scanner-omp-subagent-transcripts.test.ts": 144, + "src/main/ai-vault/session-scanner-omp-title.test.ts": 129, + "src/main/ai-vault/session-scanner-opencode-cancellation.test.ts": 727, + "src/main/ai-vault/session-scanner-opencode-native-page-limit.test.ts": 65, + "src/main/ai-vault/session-scanner-opencode-native-worker.test.ts": 739, + "src/main/ai-vault/session-scanner-opencode-parser.test.ts": 114, + "src/main/ai-vault/session-scanner-opencode-process-admission.test.ts": 254, + "src/main/ai-vault/session-scanner-opencode-sources-wsl-stall.test.ts": 131, + "src/main/ai-vault/session-scanner-opencode-sources.test.ts": 43, + "src/main/ai-vault/session-scanner-opencode-sqlite-bounds.test.ts": 990, + "src/main/ai-vault/session-scanner-opencode-sqlite-coexistence.test.ts": 677, + "src/main/ai-vault/session-scanner-opencode-sqlite-open.test.ts": 3553, + "src/main/ai-vault/session-scanner-opencode-sqlite-paths.test.ts": 36, + "src/main/ai-vault/session-scanner-opencode-sqlite-process.test.ts": 3322, + "src/main/ai-vault/session-scanner-opencode-sqlite-scan-scope.test.ts": 45, + "src/main/ai-vault/session-scanner-opencode-sqlite-worker-client.test.ts": 107, + "src/main/ai-vault/session-scanner-opencode-sqlite-worker-spawn.test.ts": 142, + "src/main/ai-vault/session-scanner-opencode-sqlite.test.ts": 414, + "src/main/ai-vault/session-scanner-opencode-wsl-client.test.ts": 102, + "src/main/ai-vault/session-scanner-opencode-wsl-routing.test.ts": 162, + "src/main/ai-vault/session-scanner-opencode2-message.test.ts": 67, + "src/main/ai-vault/session-scanner-opencode2-sqlite.test.ts": 260, + "src/main/ai-vault/session-scanner-parse-cache-agents.test.ts": 550, + "src/main/ai-vault/session-scanner-parse-cache.test.ts": 352, + "src/main/ai-vault/session-scanner-parse-wsl-stall.test.ts": 227, + "src/main/ai-vault/session-scanner-parser-stream-cleanup.test.ts": 161, + "src/main/ai-vault/session-scanner-pi-scope.test.ts": 277, + "src/main/ai-vault/session-scanner-preview-window-truncation.test.ts": 88, + "src/main/ai-vault/session-scanner-qoder.test.ts": 271, + "src/main/ai-vault/session-scanner-record-budget.test.ts": 520, + "src/main/ai-vault/session-scanner-recoverable-empty.test.ts": 360, + "src/main/ai-vault/session-scanner-roots.test.ts": 174, + "src/main/ai-vault/session-scanner-scope.test.ts": 307, + "src/main/ai-vault/session-scanner-service-cancellation.test.ts": 372, + "src/main/ai-vault/session-scanner-service-client.test.ts": 509, + "src/main/ai-vault/session-scanner-service-entry-path.test.ts": 45, + "src/main/ai-vault/session-scanner-service-entry-subagents.test.ts": 629, + "src/main/ai-vault/session-scanner-service-entry.test.ts": 931, + "src/main/ai-vault/session-scanner-service-env.test.ts": 54, + "src/main/ai-vault/session-scanner-service-request-deadline.test.ts": 71, + "src/main/ai-vault/session-scanner-service-restart-policy.test.ts": 40, + "src/main/ai-vault/session-scanner-service-root-request.test.ts": 44, + "src/main/ai-vault/session-scanner-service-root-response.test.ts": 218, + "src/main/ai-vault/session-scanner-service-search-roots.test.ts": 625, + "src/main/ai-vault/session-scanner-service-search.test.ts": 1338, + "src/main/ai-vault/session-scanner-service-spawn.test.ts": 338, + "src/main/ai-vault/session-scanner-timeline.test.ts": 98, + "src/main/ai-vault/session-scanner-unlimited-dedup.test.ts": 3572, + "src/main/ai-vault/session-scanner-values.test.ts": 107, + "src/main/ai-vault/session-scanner-zcode-sources.test.ts": 98, + "src/main/ai-vault/session-scanner-zcode.test.ts": 406, + "src/main/ai-vault/session-scanner.test.ts": 538, + "src/main/ai-vault/session-sidecar-stat.test.ts": 44, + "src/main/ai-vault/session-title-file-reader-wsl-stall.test.ts": 253, + "src/main/ai-vault/session-title-file-reader.test.ts": 168, + "src/main/ai-vault/session-transcript-consumers.test.ts": 494, + "src/main/ai-vault/session-transcript-every-agent-capture.test.ts": 605, + "src/main/ai-vault/session-transcript-message-content.test.ts": 238, + "src/main/ai-vault/session-transcript-record-budget.test.ts": 46, + "src/main/ai-vault/shell-command-words.test.ts": 392, + "src/main/ai-vault/structured-session-ownership-acp-and-pi.test.ts": 1271, + "src/main/ai-vault/structured-session-ownership-client-audience.test.ts": 1147, + "src/main/ai-vault/structured-session-ownership-shell-commands.test.ts": 1251, + "src/main/ai-vault/structured-session-ownership.test.ts": 1243, + "src/main/amp/hook-service.test.ts": 54, + "src/main/antigravity/hook-service.test.ts": 197, + "src/main/antigravity/native-account-host.test.ts": 37, + "src/main/antigravity/native-account-launch.test.ts": 110, + "src/main/antigravity/native-account-service.test.ts": 79, + "src/main/antigravity/native-account-store.test.ts": 183, + "src/main/antigravity/native-credential-backend.test.ts": 83, "src/main/antigravity/native-credential-codec.test.ts": 44, - "src/main/antigravity/native-macos-credentials.real.test.ts": 73, - "src/main/antigravity/native-macos-credentials.test.ts": 75, - "src/main/antigravity/posix-hook-invocation.test.ts": 5420, - "src/main/antigravity/windows-hook-json-post.test.ts": 9898, - "src/main/antigravity/windows-hook-payload-delivery.test.ts": 126, - "src/main/app-icon.test.ts": 80, - "src/main/app-relaunch.test.ts": 64, - "src/main/appimage-runtime-identity.test.ts": 87, - "src/main/appkit-scene-mutation.test.ts": 49, - "src/main/artifacts/artifact-cloud-config.test.ts": 60, - "src/main/artifacts/artifact-cloud-recovery.test.ts": 577, - "src/main/artifacts/artifact-cloud-service-races.test.ts": 512, - "src/main/artifacts/artifact-cloud-service.test.ts": 1194, - "src/main/artifacts/artifact-create-intent-store.test.ts": 245, - "src/main/artifacts/artifact-recovery-directory-fsync.test.ts": 90, - "src/main/artifacts/artifact-share-record-allocation.test.ts": 179, - "src/main/artifacts/artifact-share-record-store.test.ts": 240, - "src/main/asar-transparent-fs.test.ts": 53, - "src/main/automations/automation-dispatch-host-fence.test.ts": 1715, - "src/main/automations/automation-owner-fencing.test.ts": 1763, - "src/main/automations/automation-owner-migration.test.ts": 80, - "src/main/automations/automation-run-terminal-surface.test.ts": 4629, - "src/main/automations/automation-run-writer.test.ts": 292, - "src/main/automations/automation-skip-coalescing.test.ts": 3604, - "src/main/automations/automation-ssh-readoption-migration.test.ts": 68, - "src/main/automations/automation-update-host-retarget.test.ts": 1497, - "src/main/automations/automation-worker-durability.test.ts": 5967, - "src/main/automations/automation-workspace-host-attribution.test.ts": 1786, - "src/main/automations/automation-zero-grace-tick-latency.test.ts": 1950, - "src/main/automations/dispatch-refusal.test.ts": 71, - "src/main/automations/dispatch-tokens.test.ts": 88, - "src/main/automations/external-automation-manager-cache.test.ts": 83, - "src/main/automations/external-automation-owner-guard.test.ts": 320, - "src/main/automations/external-automation-probe-scheduler.test.ts": 175, - "src/main/automations/external-manager-scoped.test.ts": 404, - "src/main/automations/external-manager.test.ts": 359, - "src/main/automations/headless-automation-agent-launch.test.ts": 845, - "src/main/automations/headless-dispatch-durability.test.ts": 414, - "src/main/automations/headless-run-terminal-retention.test.ts": 120, - "src/main/automations/headless-workspace-create.test.ts": 49, - "src/main/automations/hermes-cron-output.test.ts": 118, - "src/main/automations/precheck-runner.test.ts": 1267, - "src/main/automations/refused-manual-run.test.ts": 6693, - "src/main/automations/retained-run-reconciliation.test.ts": 1759, - "src/main/automations/run-completion-watcher.test.ts": 2643, - "src/main/automations/run-target-resolution.test.ts": 69, - "src/main/automations/runtime-automation-service-retention.test.ts": 1414, - "src/main/automations/runtime-automation-service.test.ts": 1101, - "src/main/automations/runtime-terminal-run-observer-evidence.test.ts": 64, - "src/main/automations/runtime-terminal-run-observer.test.ts": 82, - "src/main/automations/schedule-drift-report.test.ts": 63, - "src/main/automations/service-precheck.test.ts": 1669, - "src/main/automations/service.test.ts": 1985, - "src/main/azure-devops/azure-devops-api-request.test.ts": 62, - "src/main/azure-devops/client.test.ts": 566, - "src/main/azure-devops/pull-request-creation.test.ts": 351, - "src/main/azure-devops/pull-request-mappers.test.ts": 46, - "src/main/azure-devops/repository-ref.test.ts": 362, - "src/main/bitbucket/client.test.ts": 411, - "src/main/bitbucket/credential-connection.test.ts": 130, - "src/main/bitbucket/credential-store.test.ts": 178, - "src/main/bitbucket/pull-request-creation.test.ts": 420, - "src/main/bitbucket/pull-request-mappers.test.ts": 40, - "src/main/bitbucket/repository-ref.test.ts": 360, - "src/main/bitbucket/status-no-decrypt.test.ts": 196, - "src/main/browser/agent-browser-bridge-capture-paint-hold.test.ts": 581, - "src/main/browser/agent-browser-bridge-command-transport.test.ts": 563, - "src/main/browser/agent-browser-bridge-keypress-input.test.ts": 336, - "src/main/browser/agent-browser-bridge-mouse-input.test.ts": 372, - "src/main/browser/agent-browser-bridge-navigation.test.ts": 473, - "src/main/browser/agent-browser-bridge-pointer-input.test.ts": 335, - "src/main/browser/agent-browser-bridge-raw-process-retention.test.ts": 485, - "src/main/browser/agent-browser-bridge-raw-process.test.ts": 1056, - "src/main/browser/agent-browser-bridge-screenshot-format.test.ts": 318, - "src/main/browser/agent-browser-bridge-session-lifecycle.test.ts": 751, - "src/main/browser/agent-browser-bridge-tab-routing.test.ts": 488, - "src/main/browser/agent-browser-bridge-text-input.test.ts": 633, - "src/main/browser/agent-browser-orphan-sweep.test.ts": 63, - "src/main/browser/agent-browser-process-environment.test.ts": 53, - "src/main/browser/agent-browser-session-reset.test.ts": 60, - "src/main/browser/browser-certificate-trust-controller.test.ts": 436, - "src/main/browser/browser-client-download-relay.test.ts": 257, - "src/main/browser/browser-client-download-routing.test.ts": 239, - "src/main/browser/browser-client-file-channel-negotiation.test.ts": 307, - "src/main/browser/browser-client-file-channel-reconnect.test.ts": 536, - "src/main/browser/browser-client-host-attach-request.test.ts": 257, - "src/main/browser/browser-client-host-authority-replacement.test.ts": 250, - "src/main/browser/browser-client-host-command-dispatcher.test.ts": 268, - "src/main/browser/browser-client-host-command-retention.test.ts": 837, - "src/main/browser/browser-client-host-id.test.ts": 63, - "src/main/browser/browser-client-host-placement-preparation.test.ts": 95, - "src/main/browser/browser-client-host-published-url-wiring.test.ts": 537, - "src/main/browser/browser-client-host-reconciliation-command-order.test.ts": 257, - "src/main/browser/browser-client-network-route-registry.test.ts": 292, - "src/main/browser/browser-client-page-automation-runtime.test.ts": 61, - "src/main/browser/browser-client-page-command-executor-fencing.test.ts": 274, - "src/main/browser/browser-client-page-command-executor.test.ts": 311, - "src/main/browser/browser-client-page-command-integration.test.ts": 345, - "src/main/browser/browser-client-page-execution-host-supersession.test.ts": 414, - "src/main/browser/browser-client-page-inventory.test.ts": 232, - "src/main/browser/browser-client-page-metadata-transport.test.ts": 260, - "src/main/browser/browser-client-page-published-url.test.ts": 257, - "src/main/browser/browser-client-page-reconciliation-adapters.test.ts": 394, - "src/main/browser/browser-client-page-renderer-bridge.test.ts": 258, - "src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts": 840, - "src/main/browser/browser-client-page-renderer-runtime.test.ts": 230, - "src/main/browser/browser-client-page-unavailability.test.ts": 316, - "src/main/browser/browser-client-page-upload-routing.test.ts": 366, - "src/main/browser/browser-client-route-cookie-import-partition-parity.test.ts": 388, - "src/main/browser/browser-client-route-cookie-import.test.ts": 89, - "src/main/browser/browser-client-upload-buffer-retention.test.ts": 530, - "src/main/browser/browser-client-upload-command.test.ts": 261, - "src/main/browser/browser-client-upload-staging.test.ts": 311, - "src/main/browser/browser-client-upload-transfer.test.ts": 247, - "src/main/browser/browser-cookie-clear-preserve.test.ts": 136, - "src/main/browser/browser-cookie-clear-store-lifecycle.test.ts": 81, - "src/main/browser/browser-cookie-clear-store.test.ts": 83, - "src/main/browser/browser-cookie-decryption.test.ts": 67, - "src/main/browser/browser-cookie-import-clear-atomicity.test.ts": 75, - "src/main/browser/browser-cookie-import-concurrency.test.ts": 550, - "src/main/browser/browser-cookie-import-google-exclusion.test.ts": 472, - "src/main/browser/browser-cookie-import-partition-fidelity.test.ts": 603, - "src/main/browser/browser-cookie-import-partition-reland.electron.test.ts": 1241, - "src/main/browser/browser-cookie-import-partition-rollback.electron.test.ts": 643, - "src/main/browser/browser-cookie-import-partition-success.electron.test.ts": 948, - "src/main/browser/browser-cookie-import-partition.electron.test.ts": 782, - "src/main/browser/browser-cookie-import-plan-writes.test.ts": 80, - "src/main/browser/browser-cookie-import-policy.test.ts": 202, - "src/main/browser/browser-cookie-import-replace-partition-rollback.electron.test.ts": 918, - "src/main/browser/browser-cookie-import-replacement.test.ts": 539, - "src/main/browser/browser-cookie-import-route-partition-staging.test.ts": 378, - "src/main/browser/browser-cookie-import-scope.test.ts": 403, - "src/main/browser/browser-cookie-import-snapshot-ownership.test.ts": 528, - "src/main/browser/browser-cookie-import-undecryptable.test.ts": 753, - "src/main/browser/browser-cookie-import-validated-partition.electron.test.ts": 723, - "src/main/browser/browser-cookie-import-write.test.ts": 75, - "src/main/browser/browser-cookie-import.comet.test.ts": 939, - "src/main/browser/browser-cookie-import.helium.test.ts": 892, - "src/main/browser/browser-cookie-import.test.ts": 1375, - "src/main/browser/browser-cookie-public-suffix-scope.test.ts": 71, - "src/main/browser/browser-cookie-registrable-family.test.ts": 77, - "src/main/browser/browser-cookie-samesite.electron.test.ts": 796, - "src/main/browser/browser-cookie-source-partition.test.ts": 67, - "src/main/browser/browser-cookie-validation.test.ts": 79, - "src/main/browser/browser-download-destination.test.ts": 72, - "src/main/browser/browser-execution-host-storage-identity.test.ts": 256, - "src/main/browser/browser-google-auth-ua.test.ts": 52, - "src/main/browser/browser-grab-payload.test.ts": 64, - "src/main/browser/browser-guest-shortcut-forwarding.test.ts": 346, - "src/main/browser/browser-guest-wheel-zoom-scroll.test.ts": 54, - "src/main/browser/browser-host-client-identity.test.ts": 81, - "src/main/browser/browser-host-lease-reconnect-delay.test.ts": 45, - "src/main/browser/browser-identity-mode-record.test.ts": 52, - "src/main/browser/browser-identity-mode-store.test.ts": 140, - "src/main/browser/browser-manager-annotation-bridge.test.ts": 382, - "src/main/browser/browser-manager-auth-user-agent.test.ts": 378, - "src/main/browser/browser-manager-client-hosted-downloads.test.ts": 467, - "src/main/browser/browser-manager-destroyed-guest-downloads.test.ts": 499, - "src/main/browser/browser-manager-destroyed-guest.test.ts": 424, - "src/main/browser/browser-manager-downloads.test.ts": 391, - "src/main/browser/browser-manager-grab-capture.test.ts": 365, - "src/main/browser/browser-manager-grab-mode.test.ts": 397, - "src/main/browser/browser-manager-grab-selection.test.ts": 446, - "src/main/browser/browser-manager-grab-shortcuts.test.ts": 413, - "src/main/browser/browser-manager-guest-lifecycle.test.ts": 356, - "src/main/browser/browser-manager-guest-policy-profile.test.ts": 394, - "src/main/browser/browser-manager-guest-shortcuts.test.ts": 388, - "src/main/browser/browser-manager-guest-visibility.test.ts": 398, - "src/main/browser/browser-manager-load-failure-replay.test.ts": 412, - "src/main/browser/browser-manager-popup-child.test.ts": 442, - "src/main/browser/browser-manager-popup-routing.test.ts": 398, - "src/main/browser/browser-manager-tab-identity.test.ts": 387, - "src/main/browser/browser-manager-viewport-crashed-guest.test.ts": 322, - "src/main/browser/browser-manager-viewport-override.test.ts": 422, - "src/main/browser/browser-manager-viewport-ownership.test.ts": 409, - "src/main/browser/browser-manager-viewport-partial-failure.test.ts": 372, - "src/main/browser/browser-manager-viewport-touch-reset.test.ts": 317, - "src/main/browser/browser-manager-worker-request-user-agent.test.ts": 376, - "src/main/browser/browser-network-deferred-socket.test.ts": 45, - "src/main/browser/browser-network-execution-route.test.ts": 236, - "src/main/browser/browser-network-tunnel-client-memory-budget.test.ts": 93, - "src/main/browser/browser-network-tunnel-client.test.ts": 129, - "src/main/browser/browser-network-tunnel-conformance.test.ts": 169, - "src/main/browser/browser-network-tunnel-outbound-memory-budget.test.ts": 52, - "src/main/browser/browser-network-tunnel-session-aggregate-memory.test.ts": 68, - "src/main/browser/browser-network-tunnel-session-stream-scoped-failure.test.ts": 103, - "src/main/browser/browser-network-tunnel-session.test.ts": 140, - "src/main/browser/browser-page-initiated-tab-budget.test.ts": 45, - "src/main/browser/browser-popup-new-tab-intent.test.ts": 38, - "src/main/browser/browser-process-user-agent.test.ts": 55, - "src/main/browser/browser-route-dns-prefetch.electron.test.ts": 4477, - "src/main/browser/browser-route-guest-popups.test.ts": 348, - "src/main/browser/browser-route-h3-egress.electron.test.ts": 18009, - "src/main/browser/browser-route-identity.test.ts": 63, - "src/main/browser/browser-route-partition-binding-capacity.test.ts": 84, - "src/main/browser/browser-route-partition-binding-store.test.ts": 122, - "src/main/browser/browser-route-partition-migration.test.ts": 105, - "src/main/browser/browser-route-partition-stability.test.ts": 292, - "src/main/browser/browser-route-partition-storage-lifecycle.test.ts": 116, - "src/main/browser/browser-route-partition-storage-retirement.test.ts": 270, - "src/main/browser/browser-route-partition-storage-runtime.test.ts": 58, - "src/main/browser/browser-route-persisted-worker-egress.electron.test.ts": 3825, - "src/main/browser/browser-route-prepared-page-rekey.test.ts": 51, - "src/main/browser/browser-route-session-registry.test.ts": 111, - "src/main/browser/browser-route-tcp-egress.electron.test.ts": 2122, - "src/main/browser/browser-route-webcontents-registry.test.ts": 428, - "src/main/browser/browser-route-webrtc-egress.electron.test.ts": 13343, - "src/main/browser/browser-screencast-dialog-settlement.test.ts": 115, - "src/main/browser/browser-screencast-first-frame-timeout.test.ts": 93, - "src/main/browser/browser-screencast-lifecycle.test.ts": 104, - "src/main/browser/browser-screencast-live-frame-allocation.test.ts": 100, - "src/main/browser/browser-screencast-snapshot-scaling.test.ts": 202, - "src/main/browser/browser-screencast-stream.test.ts": 639, + "src/main/antigravity/native-macos-credentials.real.test.ts": 58, + "src/main/antigravity/native-macos-credentials.test.ts": 54, + "src/main/antigravity/posix-hook-invocation.test.ts": 5390, + "src/main/antigravity/windows-hook-json-post.test.ts": 9873, + "src/main/antigravity/windows-hook-payload-delivery.test.ts": 107, + "src/main/app-icon.test.ts": 66, + "src/main/app-relaunch.test.ts": 68, + "src/main/appimage-runtime-identity.test.ts": 71, + "src/main/appkit-scene-mutation.test.ts": 29, + "src/main/artifacts/artifact-cloud-config.test.ts": 45, + "src/main/artifacts/artifact-cloud-recovery.test.ts": 569, + "src/main/artifacts/artifact-cloud-service-races.test.ts": 563, + "src/main/artifacts/artifact-cloud-service.test.ts": 1030, + "src/main/artifacts/artifact-create-intent-store.test.ts": 317, + "src/main/artifacts/artifact-recovery-directory-fsync.test.ts": 96, + "src/main/artifacts/artifact-share-record-allocation.test.ts": 155, + "src/main/artifacts/artifact-share-record-store.test.ts": 194, + "src/main/asar-transparent-fs.test.ts": 41, + "src/main/automations/automation-dispatch-host-fence.test.ts": 2204, + "src/main/automations/automation-owner-fencing.test.ts": 1582, + "src/main/automations/automation-owner-migration.test.ts": 82, + "src/main/automations/automation-run-terminal-surface.test.ts": 4359, + "src/main/automations/automation-run-writer.test.ts": 251, + "src/main/automations/automation-skip-coalescing.test.ts": 2416, + "src/main/automations/automation-ssh-readoption-migration.test.ts": 44, + "src/main/automations/automation-update-host-retarget.test.ts": 1609, + "src/main/automations/automation-worker-durability.test.ts": 3774, + "src/main/automations/automation-workspace-host-attribution.test.ts": 1797, + "src/main/automations/automation-wsl-move-refusal.test.ts": 1902, + "src/main/automations/automation-zero-grace-tick-latency.test.ts": 2120, + "src/main/automations/dispatch-refusal-wsl-move.test.ts": 456, + "src/main/automations/dispatch-refusal.test.ts": 426, + "src/main/automations/dispatch-tokens.test.ts": 49, + "src/main/automations/external-automation-manager-cache.test.ts": 61, + "src/main/automations/external-automation-owner-guard.test.ts": 290, + "src/main/automations/external-automation-probe-scheduler.test.ts": 152, + "src/main/automations/external-manager-scoped.test.ts": 320, + "src/main/automations/external-manager.test.ts": 515, + "src/main/automations/headless-automation-agent-launch.test.ts": 1418, + "src/main/automations/headless-dispatch-durability.test.ts": 455, + "src/main/automations/headless-run-terminal-retention.test.ts": 72, + "src/main/automations/headless-workspace-create.test.ts": 43, + "src/main/automations/precheck-runner.test.ts": 1175, + "src/main/automations/refused-manual-run.test.ts": 8614, + "src/main/automations/retained-run-reconciliation.test.ts": 1517, + "src/main/automations/run-completion-watcher.test.ts": 2121, + "src/main/automations/run-target-resolution.test.ts": 51, + "src/main/automations/runtime-automation-service-retention.test.ts": 1498, + "src/main/automations/runtime-automation-service.test.ts": 1210, + "src/main/automations/runtime-terminal-run-observer-evidence.test.ts": 48, + "src/main/automations/runtime-terminal-run-observer.test.ts": 47, + "src/main/automations/schedule-drift-report.test.ts": 48, + "src/main/automations/service-precheck.test.ts": 1991, + "src/main/automations/service.test.ts": 2458, + "src/main/azure-devops/azure-devops-api-request.test.ts": 33, + "src/main/azure-devops/client.test.ts": 530, + "src/main/azure-devops/pull-request-creation.test.ts": 559, + "src/main/azure-devops/pull-request-mappers.test.ts": 25, + "src/main/azure-devops/repository-ref.test.ts": 536, + "src/main/bitbucket/client.test.ts": 552, + "src/main/bitbucket/credential-connection.test.ts": 128, + "src/main/bitbucket/credential-store.test.ts": 183, + "src/main/bitbucket/pull-request-creation.test.ts": 553, + "src/main/bitbucket/pull-request-mappers.test.ts": 31, + "src/main/bitbucket/repository-ref.test.ts": 631, + "src/main/bitbucket/status-no-decrypt.test.ts": 160, + "src/main/browser-sidecar-owner.test.ts": 43, + "src/main/browser/agent-browser-bridge-capture-paint-hold.test.ts": 352, + "src/main/browser/agent-browser-bridge-command-transport.test.ts": 710, + "src/main/browser/agent-browser-bridge-keypress-input.test.ts": 392, + "src/main/browser/agent-browser-bridge-mouse-input.test.ts": 329, + "src/main/browser/agent-browser-bridge-navigation.test.ts": 486, + "src/main/browser/agent-browser-bridge-pointer-input.test.ts": 588, + "src/main/browser/agent-browser-bridge-raw-process-retention.test.ts": 766, + "src/main/browser/agent-browser-bridge-raw-process.test.ts": 917, + "src/main/browser/agent-browser-bridge-screenshot-format.test.ts": 301, + "src/main/browser/agent-browser-bridge-session-lifecycle.test.ts": 785, + "src/main/browser/agent-browser-bridge-tab-routing.test.ts": 369, + "src/main/browser/agent-browser-bridge-text-input.test.ts": 607, + "src/main/browser/agent-browser-orphan-sweep.test.ts": 41, + "src/main/browser/agent-browser-process-environment.test.ts": 38, + "src/main/browser/agent-browser-session-reset.test.ts": 45, + "src/main/browser/agent-input-focus-guard.test.ts": 50, + "src/main/browser/agent-input-injection.test.ts": 287, + "src/main/browser/browser-certificate-trust-controller.test.ts": 461, + "src/main/browser/browser-client-download-relay.test.ts": 234, + "src/main/browser/browser-client-download-routing.test.ts": 243, + "src/main/browser/browser-client-file-channel-negotiation.test.ts": 333, + "src/main/browser/browser-client-file-channel-reconnect.test.ts": 570, + "src/main/browser/browser-client-host-attach-request.test.ts": 229, + "src/main/browser/browser-client-host-authority-replacement.test.ts": 235, + "src/main/browser/browser-client-host-command-dispatcher.test.ts": 249, + "src/main/browser/browser-client-host-command-retention.test.ts": 764, + "src/main/browser/browser-client-host-id.test.ts": 66, + "src/main/browser/browser-client-host-placement-preparation.test.ts": 272, + "src/main/browser/browser-client-host-published-url-wiring.test.ts": 534, + "src/main/browser/browser-client-host-reconciliation-command-order.test.ts": 246, + "src/main/browser/browser-client-network-route-registry.test.ts": 274, + "src/main/browser/browser-client-page-automation-runtime.test.ts": 31, + "src/main/browser/browser-client-page-command-executor-fencing.test.ts": 256, + "src/main/browser/browser-client-page-command-executor.test.ts": 357, + "src/main/browser/browser-client-page-command-integration.test.ts": 338, + "src/main/browser/browser-client-page-execution-host-supersession.test.ts": 323, + "src/main/browser/browser-client-page-inventory.test.ts": 266, + "src/main/browser/browser-client-page-metadata-transport.test.ts": 210, + "src/main/browser/browser-client-page-published-url.test.ts": 251, + "src/main/browser/browser-client-page-reconciliation-adapters.test.ts": 318, + "src/main/browser/browser-client-page-renderer-bridge.test.ts": 244, + "src/main/browser/browser-client-page-renderer-runtime.test.ts": 218, + "src/main/browser/browser-client-page-unavailability.test.ts": 318, + "src/main/browser/browser-client-page-upload-routing.test.ts": 334, + "src/main/browser/browser-client-route-cookie-import-partition-parity.test.ts": 337, + "src/main/browser/browser-client-route-cookie-import.test.ts": 90, + "src/main/browser/browser-client-upload-buffer-retention.test.ts": 754, + "src/main/browser/browser-client-upload-command.test.ts": 214, + "src/main/browser/browser-client-upload-staging.test.ts": 297, + "src/main/browser/browser-client-upload-transfer.test.ts": 224, + "src/main/browser/browser-cookie-clear-preserve.test.ts": 126, + "src/main/browser/browser-cookie-clear-store-lifecycle.test.ts": 78, + "src/main/browser/browser-cookie-clear-store.test.ts": 100, + "src/main/browser/browser-cookie-decryption.test.ts": 64, + "src/main/browser/browser-cookie-import-clear-atomicity.test.ts": 71, + "src/main/browser/browser-cookie-import-concurrency.test.ts": 573, + "src/main/browser/browser-cookie-import-google-exclusion.test.ts": 518, + "src/main/browser/browser-cookie-import-partition-fidelity.test.ts": 542, + "src/main/browser/browser-cookie-import-partition-reland.electron.test.ts": 1533, + "src/main/browser/browser-cookie-import-partition-rollback.electron.test.ts": 642, + "src/main/browser/browser-cookie-import-partition-success.electron.test.ts": 3984, + "src/main/browser/browser-cookie-import-partition.electron.test.ts": 769, + "src/main/browser/browser-cookie-import-plan-writes.test.ts": 71, + "src/main/browser/browser-cookie-import-policy.test.ts": 201, + "src/main/browser/browser-cookie-import-replace-partition-rollback.electron.test.ts": 1156, + "src/main/browser/browser-cookie-import-replacement.test.ts": 483, + "src/main/browser/browser-cookie-import-route-partition-staging.test.ts": 643, + "src/main/browser/browser-cookie-import-scope.test.ts": 424, + "src/main/browser/browser-cookie-import-snapshot-ownership.test.ts": 722, + "src/main/browser/browser-cookie-import-undecryptable.test.ts": 962, + "src/main/browser/browser-cookie-import-validated-partition.electron.test.ts": 722, + "src/main/browser/browser-cookie-import-write.test.ts": 69, + "src/main/browser/browser-cookie-import.comet.test.ts": 1035, + "src/main/browser/browser-cookie-import.helium.test.ts": 944, + "src/main/browser/browser-cookie-import.test.ts": 1574, + "src/main/browser/browser-cookie-partition-summary.test.ts": 65, + "src/main/browser/browser-cookie-public-suffix-scope.test.ts": 72, + "src/main/browser/browser-cookie-registrable-family.test.ts": 80, + "src/main/browser/browser-cookie-samesite.electron.test.ts": 1072, + "src/main/browser/browser-cookie-source-partition.test.ts": 54, + "src/main/browser/browser-cookie-validation.test.ts": 68, + "src/main/browser/browser-download-destination.test.ts": 43, + "src/main/browser/browser-execution-host-storage-identity.test.ts": 191, + "src/main/browser/browser-google-auth-ua.test.ts": 31, + "src/main/browser/browser-grab-payload.test.ts": 40, + "src/main/browser/browser-guest-shortcut-forwarding.test.ts": 395, + "src/main/browser/browser-guest-wheel-zoom-scroll.test.ts": 40, + "src/main/browser/browser-host-client-identity.test.ts": 74, + "src/main/browser/browser-host-lease-reconnect-delay.test.ts": 27, + "src/main/browser/browser-identity-mode-record.test.ts": 32, + "src/main/browser/browser-identity-mode-store.test.ts": 96, + "src/main/browser/browser-manager-annotation-bridge.test.ts": 383, + "src/main/browser/browser-manager-auth-user-agent.test.ts": 382, + "src/main/browser/browser-manager-client-hosted-downloads.test.ts": 540, + "src/main/browser/browser-manager-destroyed-guest-downloads.test.ts": 358, + "src/main/browser/browser-manager-destroyed-guest.test.ts": 448, + "src/main/browser/browser-manager-downloads.test.ts": 392, + "src/main/browser/browser-manager-grab-capture.test.ts": 384, + "src/main/browser/browser-manager-grab-mode.test.ts": 407, + "src/main/browser/browser-manager-grab-selection.test.ts": 433, + "src/main/browser/browser-manager-grab-shortcuts.test.ts": 385, + "src/main/browser/browser-manager-guest-lifecycle.test.ts": 361, + "src/main/browser/browser-manager-guest-policy-profile.test.ts": 380, + "src/main/browser/browser-manager-guest-shortcuts.test.ts": 382, + "src/main/browser/browser-manager-guest-visibility.test.ts": 350, + "src/main/browser/browser-manager-load-failure-replay.test.ts": 385, + "src/main/browser/browser-manager-popup-child.test.ts": 350, + "src/main/browser/browser-manager-popup-openerless-tab.test.ts": 417, + "src/main/browser/browser-manager-popup-routing.test.ts": 477, + "src/main/browser/browser-manager-tab-identity.test.ts": 390, + "src/main/browser/browser-manager-viewport-crashed-guest.test.ts": 333, + "src/main/browser/browser-manager-viewport-override.test.ts": 392, + "src/main/browser/browser-manager-viewport-ownership.test.ts": 378, + "src/main/browser/browser-manager-viewport-partial-failure.test.ts": 400, + "src/main/browser/browser-manager-viewport-touch-reset.test.ts": 355, + "src/main/browser/browser-manager-worker-request-user-agent.test.ts": 341, + "src/main/browser/browser-network-deferred-socket.test.ts": 88, + "src/main/browser/browser-network-execution-route.test.ts": 229, + "src/main/browser/browser-network-tunnel-client-memory-budget.test.ts": 81, + "src/main/browser/browser-network-tunnel-client.test.ts": 70, + "src/main/browser/browser-network-tunnel-conformance.test.ts": 141, + "src/main/browser/browser-network-tunnel-outbound-memory-budget.test.ts": 63, + "src/main/browser/browser-network-tunnel-session-aggregate-memory.test.ts": 61, + "src/main/browser/browser-network-tunnel-session-stream-scoped-failure.test.ts": 67, + "src/main/browser/browser-network-tunnel-session.test.ts": 139, + "src/main/browser/browser-page-initiated-tab-budget.test.ts": 31, + "src/main/browser/browser-popup-native-window.test.ts": 288, + "src/main/browser/browser-popup-new-tab-intent.test.ts": 50, + "src/main/browser/browser-process-user-agent.test.ts": 46, + "src/main/browser/browser-route-guest-popups.test.ts": 336, + "src/main/browser/browser-route-identity.test.ts": 33, + "src/main/browser/browser-route-partition-binding-capacity.test.ts": 78, + "src/main/browser/browser-route-partition-binding-store.test.ts": 109, + "src/main/browser/browser-route-partition-migration.test.ts": 101, + "src/main/browser/browser-route-partition-stability.test.ts": 255, + "src/main/browser/browser-route-partition-storage-lifecycle.test.ts": 95, + "src/main/browser/browser-route-partition-storage-retirement.test.ts": 245, + "src/main/browser/browser-route-partition-storage-runtime.test.ts": 39, + "src/main/browser/browser-route-persisted-worker-egress.electron.test.ts": 2086, + "src/main/browser/browser-route-prepared-page-rekey.test.ts": 33, + "src/main/browser/browser-route-session-registry.test.ts": 86, + "src/main/browser/browser-route-webcontents-registry.test.ts": 321, + "src/main/browser/browser-screencast-dialog-settlement.test.ts": 85, + "src/main/browser/browser-screencast-first-frame-timeout.test.ts": 96, + "src/main/browser/browser-screencast-lifecycle.test.ts": 89, + "src/main/browser/browser-screencast-live-frame-allocation.test.ts": 114, + "src/main/browser/browser-screencast-snapshot-scaling.test.ts": 165, + "src/main/browser/browser-screencast-stream.test.ts": 668, "src/main/browser/browser-session-cookie-staging.scoped.test.ts": 475, - "src/main/browser/browser-session-cookie-staging.test.ts": 316, - "src/main/browser/browser-session-partition-policies.test.ts": 95, - "src/main/browser/browser-session-partition-proxy-install.test.ts": 499, - "src/main/browser/browser-session-proxy.test.ts": 223, - "src/main/browser/browser-session-registry-identity.persistence.test.ts": 504, - "src/main/browser/browser-session-registry.persistence.test.ts": 887, - "src/main/browser/browser-session-registry.test.ts": 839, - "src/main/browser/browser-session-startup.test.ts": 71, - "src/main/browser/browser-session-ua-cloudflare-live.electron.test.ts": 645, - "src/main/browser/browser-session-ua-wire-identity-cross-context.electron.test.ts": 5588, - "src/main/browser/browser-session-ua-wire-identity.electron.test.ts": 6689, - "src/main/browser/browser-session-user-agent-migration-inspection.test.ts": 249, - "src/main/browser/browser-tab-identity.test.ts": 44, - "src/main/browser/browser-text-insertion.test.ts": 72, - "src/main/browser/browser-webauthn-access.test.ts": 55, - "src/main/browser/browser-webauthn-account-picker.test.ts": 70, - "src/main/browser/browser-webauthn-profile-delete.test.ts": 474, - "src/main/browser/cdp-bridge-integration.test.ts": 6649, - "src/main/browser/cdp-bridge-state.test.ts": 73, - "src/main/browser/cdp-client-response-writer.test.ts": 319, + "src/main/browser/browser-session-cookie-staging.test.ts": 335, + "src/main/browser/browser-session-partition-policies.test.ts": 75, + "src/main/browser/browser-session-partition-proxy-install.test.ts": 522, + "src/main/browser/browser-session-proxy.test.ts": 207, + "src/main/browser/browser-session-registry-identity.persistence.test.ts": 473, + "src/main/browser/browser-session-registry.persistence.test.ts": 978, + "src/main/browser/browser-session-registry.test.ts": 821, + "src/main/browser/browser-session-startup.test.ts": 44, + "src/main/browser/browser-session-ua-cloudflare-live.electron.test.ts": 272, + "src/main/browser/browser-session-ua-wire-identity-cross-context.electron.test.ts": 6773, + "src/main/browser/browser-session-ua-wire-identity.electron.test.ts": 11546, + "src/main/browser/browser-session-user-agent-migration-inspection.test.ts": 268, + "src/main/browser/browser-tab-identity.test.ts": 36, + "src/main/browser/browser-text-insertion.test.ts": 34, + "src/main/browser/browser-webauthn-access.test.ts": 25, + "src/main/browser/browser-webauthn-account-picker.test.ts": 43, + "src/main/browser/browser-webauthn-profile-delete.test.ts": 458, + "src/main/browser/cdp-bridge-integration.test.ts": 8568, + "src/main/browser/cdp-bridge-state.test.ts": 82, + "src/main/browser/cdp-client-response-writer.test.ts": 225, "src/main/browser/cdp-keyboard-us-layout.test.ts": 73, - "src/main/browser/cdp-print-to-pdf.test.ts": 64, - "src/main/browser/cdp-screenshot.test.ts": 89, - "src/main/browser/cdp-ws-proxy-focus-replay.test.ts": 293, - "src/main/browser/cdp-ws-proxy.test.ts": 2808, - "src/main/browser/chromium-cookie-snapshot.test.ts": 521, - "src/main/browser/client-route-cookie-import-source-store.test.ts": 114, - "src/main/browser/doc-preview-download-block-notice.test.ts": 65, - "src/main/browser/doc-preview-failure-notice.test.ts": 47, - "src/main/browser/doc-preview-file-reader.test.ts": 106, - "src/main/browser/doc-preview-grant-registry.test.ts": 74, - "src/main/browser/doc-preview-guest-policy.test.ts": 255, - "src/main/browser/doc-preview-protocol.test.ts": 86, - "src/main/browser/doc-preview-response-allocation.test.ts": 155, - "src/main/browser/electron-debugger-lease.test.ts": 46, - "src/main/browser/electron-probe-display-launch.test.ts": 44, - "src/main/browser/execution-route-socket-duplex.test.ts": 91, - "src/main/browser/grab-guest-script.test.ts": 91, - "src/main/browser/guest-cdp-command.test.ts": 41, - "src/main/browser/local-ssh-browser-partition-identity.test.ts": 466, - "src/main/browser/local-ssh-browser-partitions.probe-cache.test.ts": 56, - "src/main/browser/local-ssh-browser-route-target.test.ts": 50, - "src/main/browser/local-ssh-browser-route.test.ts": 84, - "src/main/browser/offscreen-browser-backend-lifecycle.test.ts": 362, - "src/main/browser/paired-runtime-browser-client-host-composition.test.ts": 261, - "src/main/browser/paired-runtime-browser-client-host-identity-wiring.test.ts": 995, - "src/main/browser/paired-runtime-browser-client-host-reconnect.test.ts": 510, - "src/main/browser/paired-runtime-browser-client-host-registry.test.ts": 79, - "src/main/browser/paired-runtime-browser-client-host-route-identity.test.ts": 574, - "src/main/browser/paired-runtime-browser-client-host-runtime.test.ts": 576, - "src/main/browser/paired-runtime-browser-client-host.test.ts": 414, - "src/main/browser/paired-runtime-browser-host-command-admission.test.ts": 266, - "src/main/browser/paired-runtime-browser-host-lease-reconnect.test.ts": 967, - "src/main/browser/paired-runtime-browser-host-lease.test.ts": 943, - "src/main/browser/paired-runtime-browser-host-reconciliation-negotiation.test.ts": 338, - "src/main/browser/paired-runtime-browser-network-route.test.ts": 1296, - "src/main/browser/popup-origin-bar-window.test.ts": 106, - "src/main/browser/remote-browser-socks-buffering.test.ts": 372, - "src/main/browser/remote-browser-socks-server.test.ts": 309, + "src/main/browser/cdp-print-to-pdf.test.ts": 50, + "src/main/browser/cdp-screenshot.test.ts": 80, + "src/main/browser/cdp-ws-proxy-focus-replay.test.ts": 327, + "src/main/browser/cdp-ws-proxy-input-focus.test.ts": 173, + "src/main/browser/cdp-ws-proxy-origin.test.ts": 141, + "src/main/browser/cdp-ws-proxy.test.ts": 2625, + "src/main/browser/chromium-cookie-snapshot.test.ts": 381, + "src/main/browser/client-route-cookie-import-source-store.test.ts": 118, + "src/main/browser/doc-preview-download-block-notice.test.ts": 52, + "src/main/browser/doc-preview-failure-notice.test.ts": 35, + "src/main/browser/doc-preview-file-reader.test.ts": 56, + "src/main/browser/doc-preview-grant-registry.test.ts": 56, + "src/main/browser/doc-preview-guest-policy.test.ts": 304, + "src/main/browser/doc-preview-protocol.test.ts": 76, + "src/main/browser/doc-preview-response-allocation.test.ts": 137, + "src/main/browser/electron-debugger-lease.test.ts": 27, + "src/main/browser/electron-probe-display-launch.test.ts": 34, + "src/main/browser/execution-route-socket-duplex.test.ts": 79, + "src/main/browser/grab-guest-overlay-follow.test.ts": 300, + "src/main/browser/grab-guest-script.test.ts": 76, + "src/main/browser/guest-cdp-command.test.ts": 44, + "src/main/browser/guest-direct-key-input.test.ts": 87, + "src/main/browser/local-ssh-browser-partition-identity.test.ts": 392, + "src/main/browser/local-ssh-browser-partitions.probe-cache.test.ts": 53, + "src/main/browser/local-ssh-browser-route-target.test.ts": 42, + "src/main/browser/local-ssh-browser-route.test.ts": 76, + "src/main/browser/offscreen-browser-backend-lifecycle.test.ts": 377, + "src/main/browser/paired-runtime-browser-client-host-composition.test.ts": 255, + "src/main/browser/paired-runtime-browser-client-host-identity-wiring.test.ts": 1509, + "src/main/browser/paired-runtime-browser-client-host-reconnect.test.ts": 541, + "src/main/browser/paired-runtime-browser-client-host-registry.test.ts": 46, + "src/main/browser/paired-runtime-browser-client-host-route-identity.test.ts": 667, + "src/main/browser/paired-runtime-browser-client-host-runtime.test.ts": 540, + "src/main/browser/paired-runtime-browser-client-host.test.ts": 403, + "src/main/browser/paired-runtime-browser-host-command-admission.test.ts": 250, + "src/main/browser/paired-runtime-browser-host-lease-reconnect.test.ts": 1082, + "src/main/browser/paired-runtime-browser-host-lease.test.ts": 1002, + "src/main/browser/paired-runtime-browser-host-reconciliation-negotiation.test.ts": 368, + "src/main/browser/paired-runtime-browser-network-route.test.ts": 1212, + "src/main/browser/popup-origin-bar-window.test.ts": 246, + "src/main/browser/remote-browser-socks-buffering.test.ts": 363, + "src/main/browser/remote-browser-socks-server.test.ts": 298, "src/main/browser/snapshot-engine-iframe-sessions.test.ts": 96, - "src/main/browser/snapshot-engine.test.ts": 60, - "src/main/browser/ssh-browser-network-execution-route.test.ts": 554, - "src/main/browser/system-ssh-socks-client-socket.test.ts": 71, - "src/main/browser/wsl-browser-network-execution-route.test.ts": 354, - "src/main/browser/wsl-browser-network-relay-launch.test.ts": 83, - "src/main/claude-accounts/claude-account-folder.test.ts": 275, - "src/main/claude-accounts/claude-account-service.test.ts": 792, - "src/main/claude-accounts/claude-command-process.test.ts": 81, - "src/main/claude-accounts/claude-credential-persistence-ratchet.test.ts": 608, - "src/main/claude-accounts/claude-duplicate-account.test.ts": 52, - "src/main/claude-accounts/claude-launch-routing.test.ts": 540, - "src/main/claude-accounts/claude-profile-history-windows.test.ts": 132, - "src/main/claude-accounts/claude-profile-history.test.ts": 107, - "src/main/claude-accounts/claude-profile-paths.test.ts": 80, - "src/main/claude-accounts/claude-profile-provisioning.test.ts": 762, - "src/main/claude-accounts/claude-profile-router.test.ts": 7997, - "src/main/claude-accounts/claude-profile-setup-worker.test.ts": 240, - "src/main/claude-accounts/claude-profile-setup.test.ts": 181, - "src/main/claude-accounts/claude-profile-shell-composition.test.ts": 72, - "src/main/claude-accounts/claude-profile-wsl-router.test.ts": 787, - "src/main/claude-accounts/claude-sign-in-browser.test.ts": 319, - "src/main/claude-accounts/claude-structured-auth-policy.test.ts": 61, - "src/main/claude-accounts/keychain-config-directory-aliases.test.ts": 57, - "src/main/claude-accounts/keychain.test.ts": 56, - "src/main/claude-accounts/runtime-auth-service.test.ts": 297, - "src/main/claude-accounts/runtime-paths.test.ts": 87, - "src/main/claude-accounts/runtime-selection.test.ts": 69, - "src/main/claude-accounts/windows-command-invocation.test.ts": 44, - "src/main/claude-usage/claude-model-pricing.test.ts": 68, - "src/main/claude-usage/claude-usage-report-aggregation.test.ts": 47, - "src/main/claude-usage/persisted-source-authority.test.ts": 320, - "src/main/claude-usage/persisted-token-columns-store.test.ts": 467, - "src/main/claude-usage/persisted-token-columns.test.ts": 91, - "src/main/claude-usage/scanner-incremental-append.test.ts": 191, - "src/main/claude-usage/scanner-incremental-ownership.test.ts": 414, - "src/main/claude-usage/scanner-incremental-reader.test.ts": 666, - "src/main/claude-usage/scanner-large-directory.test.ts": 285, - "src/main/claude-usage/scanner-scan.test.ts": 242, - "src/main/claude-usage/scanner.test.ts": 91, - "src/main/claude-usage/source-cache-compression.test.ts": 89, - "src/main/claude-usage/source-cache-write-retention.test.ts": 305, - "src/main/claude-usage/store-checkpoint-persistence.test.ts": 590, - "src/main/claude-usage/store-source-authority.test.ts": 557, - "src/main/claude-usage/store.test.ts": 403, - "src/main/claude-usage/transcript-file-discovery-errors.test.ts": 60, + "src/main/browser/snapshot-engine.test.ts": 44, + "src/main/browser/ssh-browser-network-execution-route.test.ts": 587, + "src/main/browser/system-ssh-socks-client-socket.test.ts": 50, + "src/main/browser/wsl-browser-network-execution-route.test.ts": 643, + "src/main/browser/wsl-browser-network-relay-launch.test.ts": 70, + "src/main/claude-accounts/claude-account-folder.test.ts": 269, + "src/main/claude-accounts/claude-account-service.test.ts": 1049, + "src/main/claude-accounts/claude-command-process.test.ts": 89, + "src/main/claude-accounts/claude-credential-persistence-ratchet.test.ts": 607, + "src/main/claude-accounts/claude-duplicate-account.test.ts": 42, + "src/main/claude-accounts/claude-launch-routing.test.ts": 443, + "src/main/claude-accounts/claude-profile-history-windows.test.ts": 122, + "src/main/claude-accounts/claude-profile-history.test.ts": 132, + "src/main/claude-accounts/claude-profile-paths.test.ts": 79, + "src/main/claude-accounts/claude-profile-provisioning.test.ts": 858, + "src/main/claude-accounts/claude-profile-router.test.ts": 7998, + "src/main/claude-accounts/claude-profile-setup-worker.test.ts": 242, + "src/main/claude-accounts/claude-profile-setup.test.ts": 241, + "src/main/claude-accounts/claude-profile-shell-composition.test.ts": 78, + "src/main/claude-accounts/claude-profile-wsl-router.test.ts": 835, + "src/main/claude-accounts/claude-sign-in-browser.test.ts": 323, + "src/main/claude-accounts/claude-structured-auth-policy.test.ts": 33, + "src/main/claude-accounts/keychain-config-directory-aliases.test.ts": 43, + "src/main/claude-accounts/keychain.test.ts": 35, + "src/main/claude-accounts/runtime-auth-service.test.ts": 302, + "src/main/claude-accounts/runtime-paths.test.ts": 65, + "src/main/claude-accounts/runtime-selection.test.ts": 31, + "src/main/claude-accounts/windows-command-invocation.test.ts": 33, + "src/main/claude-usage/claude-model-pricing.test.ts": 63, + "src/main/claude-usage/claude-usage-report-aggregation.test.ts": 40, + "src/main/claude-usage/persisted-source-authority.test.ts": 434, + "src/main/claude-usage/persisted-token-columns-store.test.ts": 417, + "src/main/claude-usage/persisted-token-columns.test.ts": 97, + "src/main/claude-usage/scanner-incremental-append.test.ts": 169, + "src/main/claude-usage/scanner-incremental-ownership.test.ts": 329, + "src/main/claude-usage/scanner-incremental-reader.test.ts": 665, + "src/main/claude-usage/scanner-large-directory.test.ts": 231, + "src/main/claude-usage/scanner-scan.test.ts": 210, + "src/main/claude-usage/scanner.test.ts": 74, + "src/main/claude-usage/source-cache-compression.test.ts": 61, + "src/main/claude-usage/source-cache-write-retention.test.ts": 367, + "src/main/claude-usage/store-checkpoint-persistence.test.ts": 670, + "src/main/claude-usage/store-source-authority.test.ts": 770, + "src/main/claude-usage/store.test.ts": 412, + "src/main/claude-usage/transcript-file-discovery-errors.test.ts": 48, "src/main/claude-usage/transcript-projection-integrity.test.ts": 207, - "src/main/claude-usage/transcript-reader-lifetime.test.ts": 66, - "src/main/claude-usage/transcript-record-parser-prefilter.test.ts": 57, - "src/main/claude-usage/transcript-tuple-append.test.ts": 270, - "src/main/claude-usage/worktree-attribution-scaling.test.ts": 60, - "src/main/claude/claude-adapter-capture-replay.test.ts": 669, - "src/main/claude/claude-agent-sdk-contract-pins.test.ts": 3228, - "src/main/claude/claude-agent-sdk-control-requests.test.ts": 54, - "src/main/claude/claude-agent-sdk-exit-proof-identity.test.ts": 113, - "src/main/claude/claude-agent-sdk-exit-proof.test.ts": 8144, - "src/main/claude/claude-agent-sdk-import-boundary.test.ts": 1579, - "src/main/claude/claude-agent-sdk-process-spawn.test.ts": 180, - "src/main/claude/claude-agent-sdk-root-kill-fallback.test.ts": 130, - "src/main/claude/claude-agent-sdk-user-message-queue.test.ts": 151, - "src/main/claude/claude-api-retry-idle-sweep.test.ts": 1549, - "src/main/claude/claude-api-retry-row.test.ts": 651, - "src/main/claude/claude-background-stop-records.test.ts": 828, - "src/main/claude/claude-background-task-resume.test.ts": 70, - "src/main/claude/claude-background-task-retention.test.ts": 476, - "src/main/claude/claude-background-task-row-journal.test.ts": 113, - "src/main/claude/claude-background-task-rows.test.ts": 166, - "src/main/claude/claude-background-task-terminal-frames.test.ts": 94, - "src/main/claude/claude-background-task-tracker.test.ts": 155, - "src/main/claude/claude-background-task-turn-resumption.test.ts": 376, - "src/main/claude/claude-background-wake-retention.test.ts": 1040, - "src/main/claude/claude-child-exit-proof-ladder.test.ts": 5645, - "src/main/claude/claude-child-process-environment.test.ts": 65, - "src/main/claude/claude-child-work-decoder.test.ts": 85, - "src/main/claude/claude-child-work-evidence-retention.test.ts": 483, - "src/main/claude/claude-cli-flag-prewarm.test.ts": 246, - "src/main/claude/claude-cli-flag-support.test.ts": 399, - "src/main/claude/claude-close-turn-end.test.ts": 554, - "src/main/claude/claude-command-lifecycle-frames.test.ts": 378, - "src/main/claude/claude-command-lifecycle.test.ts": 636, - "src/main/claude/claude-command-turn.test.ts": 328, - "src/main/claude/claude-config-dir-pin.test.ts": 65, - "src/main/claude/claude-context-model-switch.test.ts": 435, - "src/main/claude/claude-context-usage-restart.test.ts": 832, - "src/main/claude/claude-context-usage-unloaded-turn.test.ts": 3083, - "src/main/claude/claude-context-usage.test.ts": 298, - "src/main/claude/claude-context-window-hint.test.ts": 473, - "src/main/claude/claude-descendant-escalation-boundary.test.ts": 564, - "src/main/claude/claude-fold-steer-receipt.test.ts": 684, - "src/main/claude/claude-folder-trust-file-wsl-guest.test.ts": 60, - "src/main/claude/claude-folder-trust-file.test.ts": 737, - "src/main/claude/claude-history-window-source-budget.test.ts": 594, - "src/main/claude/claude-hook-event-install.test.ts": 142, - "src/main/claude/claude-hook-event-versions.test.ts": 87, - "src/main/claude/claude-informational-row.test.ts": 615, - "src/main/claude/claude-journal-turn-scope.test.ts": 286, - "src/main/claude/claude-model-catalog-probe.test.ts": 2112, - "src/main/claude/claude-profile-hooks.test.ts": 193, - "src/main/claude/claude-provisional-row-corrections.test.ts": 61, - "src/main/claude/claude-request-end-wait.test.ts": 49, - "src/main/claude/claude-settings-schema-install.test.ts": 181, - "src/main/claude/claude-slash-command-catalog.test.ts": 59, - "src/main/claude/claude-status-line-install.test.ts": 124, - "src/main/claude/claude-stop-ack-race.test.ts": 1348, - "src/main/claude/claude-stream-json-connection-close.test.ts": 653, - "src/main/claude/claude-stream-json-connection.test.ts": 10922, - "src/main/claude/claude-streamed-text-checkpoints.test.ts": 64, - "src/main/claude/claude-structured-applied-options.test.ts": 631, - "src/main/claude/claude-structured-auth-parity.test.ts": 458, - "src/main/claude/claude-structured-child-work-captures.test.ts": 1096, - "src/main/claude/claude-structured-child-work-producer.test.ts": 1124, - "src/main/claude/claude-structured-configured-default.test.ts": 833, - "src/main/claude/claude-structured-content-parts.test.ts": 352, - "src/main/claude/claude-structured-control-actions.test.ts": 393, - "src/main/claude/claude-structured-conversation-stop.test.ts": 562, - "src/main/claude/claude-structured-dispatch-admission.test.ts": 567, - "src/main/claude/claude-structured-dispatch-attachment-rejection.test.ts": 291, - "src/main/claude/claude-structured-dispatch-boundary.test.ts": 622, - "src/main/claude/claude-structured-dispatch-content.test.ts": 270, - "src/main/claude/claude-structured-dispatch-queue-boundary.test.ts": 659, - "src/main/claude/claude-structured-dispatch.test.ts": 1937, - "src/main/claude/claude-structured-effort-default-at-rest.test.ts": 714, - "src/main/claude/claude-structured-effort-reporting.test.ts": 715, - "src/main/claude/claude-structured-history-window.test.ts": 378, - "src/main/claude/claude-structured-in-turn-stop.test.ts": 607, - "src/main/claude/claude-structured-inbound-control.test.ts": 75, - "src/main/claude/claude-structured-journal-prompt-cards.test.ts": 60, - "src/main/claude/claude-structured-journal-prompt-retry.test.ts": 332, - "src/main/claude/claude-structured-journal-translation-background-tasks.test.ts": 506, - "src/main/claude/claude-structured-journal-translation-context-usage.test.ts": 456, - "src/main/claude/claude-structured-journal-translation-subagents.test.ts": 400, - "src/main/claude/claude-structured-journal-translation-turn-timing.test.ts": 730, - "src/main/claude/claude-structured-journal-translation.plan.test.ts": 328, - "src/main/claude/claude-structured-journal-translation.test.ts": 726, - "src/main/claude/claude-structured-launch-account-routing.test.ts": 631, - "src/main/claude/claude-structured-launch-args.test.ts": 40, - "src/main/claude/claude-structured-launch-resolution.test.ts": 647, - "src/main/claude/claude-structured-launch-visuals.test.ts": 477, - "src/main/claude/claude-structured-live-provider-process.test.ts": 587, - "src/main/claude/claude-structured-model-confirmation.test.ts": 673, - "src/main/claude/claude-structured-model-preflight.test.ts": 343, - "src/main/claude/claude-structured-option-confirmation.test.ts": 758, - "src/main/claude/claude-structured-options-context-window.test.ts": 414, - "src/main/claude/claude-structured-options.test.ts": 431, - "src/main/claude/claude-structured-owner-identity.test.ts": 90, - "src/main/claude/claude-structured-permission-mode.test.ts": 58, - "src/main/claude/claude-structured-prompt-items.test.ts": 316, - "src/main/claude/claude-structured-prompt-ownership.test.ts": 700, - "src/main/claude/claude-structured-provider-fallback.test.ts": 550, - "src/main/claude/claude-structured-queued-stop.test.ts": 538, - "src/main/claude/claude-structured-rate-limit-retry.test.ts": 264, - "src/main/claude/claude-structured-real-cli-fold.test.ts": 490, - "src/main/claude/claude-structured-real-cli-visuals.test.ts": 584, - "src/main/claude/claude-structured-real-cli.test.ts": 787, - "src/main/claude/claude-structured-reasoning-lifecycle.test.ts": 350, - "src/main/claude/claude-structured-reasoning.test.ts": 289, - "src/main/claude/claude-structured-requested-stop.test.ts": 557, - "src/main/claude/claude-structured-resume-point.test.ts": 758, - "src/main/claude/claude-structured-retired-model.test.ts": 531, - "src/main/claude/claude-structured-session-acquisition-processless.test.ts": 679, - "src/main/claude/claude-structured-session-adapter-turns.test.ts": 726, - "src/main/claude/claude-structured-session-adapter.test.ts": 824, - "src/main/claude/claude-structured-session-close.test.ts": 635, - "src/main/claude/claude-structured-session-commands.test.ts": 694, - "src/main/claude/claude-structured-session-exit-blame.test.ts": 672, - "src/main/claude/claude-structured-session-reading-control.test.ts": 886, - "src/main/claude/claude-structured-session-recovery.test.ts": 641, - "src/main/claude/claude-structured-session-startup.test.ts": 723, - "src/main/claude/claude-structured-sign-in.test.ts": 317, - "src/main/claude/claude-structured-spawn-options.test.ts": 442, - "src/main/claude/claude-structured-start-fast-mode.test.ts": 554, - "src/main/claude/claude-structured-turn-resume-point.test.ts": 695, - "src/main/claude/claude-subagent-group-row.test.ts": 55, + "src/main/claude-usage/transcript-reader-lifetime.test.ts": 89, + "src/main/claude-usage/transcript-record-parser-prefilter.test.ts": 45, + "src/main/claude-usage/transcript-tuple-append.test.ts": 346, + "src/main/claude-usage/worktree-attribution-scaling.test.ts": 42, + "src/main/claude/claude-adapter-capture-replay.test.ts": 557, + "src/main/claude/claude-agent-sdk-contract-pins.test.ts": 3570, + "src/main/claude/claude-agent-sdk-control-requests.test.ts": 40, + "src/main/claude/claude-agent-sdk-exit-proof-identity.test.ts": 94, + "src/main/claude/claude-agent-sdk-exit-proof.test.ts": 8785, + "src/main/claude/claude-agent-sdk-import-boundary.test.ts": 1558, + "src/main/claude/claude-agent-sdk-process-spawn.test.ts": 150, + "src/main/claude/claude-agent-sdk-root-kill-fallback.test.ts": 129, + "src/main/claude/claude-agent-sdk-user-message-queue.test.ts": 146, + "src/main/claude/claude-api-retry-idle-sweep.test.ts": 1807, + "src/main/claude/claude-api-retry-row.test.ts": 638, + "src/main/claude/claude-background-stop-records.test.ts": 1019, + "src/main/claude/claude-background-task-resume.test.ts": 41, + "src/main/claude/claude-background-task-retention.test.ts": 430, + "src/main/claude/claude-background-task-row-journal.test.ts": 127, + "src/main/claude/claude-background-task-rows.test.ts": 224, + "src/main/claude/claude-background-task-terminal-frames.test.ts": 70, + "src/main/claude/claude-background-task-tracker.test.ts": 150, + "src/main/claude/claude-background-task-turn-resumption.test.ts": 334, + "src/main/claude/claude-background-wake-retention.test.ts": 1429, + "src/main/claude/claude-child-exit-proof-ladder.test.ts": 5711, + "src/main/claude/claude-child-process-environment.test.ts": 38, + "src/main/claude/claude-child-work-decoder.test.ts": 74, + "src/main/claude/claude-child-work-evidence-retention.test.ts": 560, + "src/main/claude/claude-cli-flag-prewarm.test.ts": 286, + "src/main/claude/claude-cli-flag-support.test.ts": 404, + "src/main/claude/claude-close-turn-end.test.ts": 943, + "src/main/claude/claude-command-lifecycle-frames.test.ts": 334, + "src/main/claude/claude-command-lifecycle.test.ts": 789, + "src/main/claude/claude-command-turn.test.ts": 344, + "src/main/claude/claude-config-dir-pin.test.ts": 36, + "src/main/claude/claude-context-model-switch.test.ts": 302, + "src/main/claude/claude-context-usage-restart.test.ts": 831, + "src/main/claude/claude-context-usage-unloaded-turn.test.ts": 3382, + "src/main/claude/claude-context-usage.test.ts": 287, + "src/main/claude/claude-context-window-hint.test.ts": 355, + "src/main/claude/claude-descendant-escalation-boundary.test.ts": 548, + "src/main/claude/claude-fold-steer-receipt.test.ts": 901, + "src/main/claude/claude-folder-trust-file-wsl-guest.test.ts": 59, + "src/main/claude/claude-folder-trust-file.test.ts": 784, + "src/main/claude/claude-history-window-source-budget.test.ts": 603, + "src/main/claude/claude-hook-event-install.test.ts": 182, + "src/main/claude/claude-hook-event-versions.test.ts": 81, + "src/main/claude/claude-informational-row.test.ts": 704, + "src/main/claude/claude-journal-turn-scope.test.ts": 329, + "src/main/claude/claude-model-catalog-probe.test.ts": 1837, + "src/main/claude/claude-profile-hooks.test.ts": 231, + "src/main/claude/claude-provisional-row-corrections.test.ts": 38, + "src/main/claude/claude-request-end-wait.test.ts": 47, + "src/main/claude/claude-settings-schema-install.test.ts": 235, + "src/main/claude/claude-slash-command-catalog.test.ts": 50, + "src/main/claude/claude-status-line-install.test.ts": 143, + "src/main/claude/claude-status-line-windows.test.ts": 133, + "src/main/claude/claude-stop-ack-race.test.ts": 1389, + "src/main/claude/claude-stream-json-connection-close.test.ts": 752, + "src/main/claude/claude-stream-json-connection.test.ts": 8162, + "src/main/claude/claude-streamed-text-checkpoints.test.ts": 43, + "src/main/claude/claude-structured-applied-options.test.ts": 629, + "src/main/claude/claude-structured-auth-parity.test.ts": 700, + "src/main/claude/claude-structured-child-env.test.ts": 42, + "src/main/claude/claude-structured-child-work-captures.test.ts": 1184, + "src/main/claude/claude-structured-child-work-producer.test.ts": 1025, + "src/main/claude/claude-structured-configured-default.test.ts": 930, + "src/main/claude/claude-structured-content-parts.test.ts": 321, + "src/main/claude/claude-structured-control-actions.test.ts": 412, + "src/main/claude/claude-structured-conversation-stop.test.ts": 614, + "src/main/claude/claude-structured-dispatch-admission.test.ts": 400, + "src/main/claude/claude-structured-dispatch-attachment-rejection.test.ts": 371, + "src/main/claude/claude-structured-dispatch-boundary.test.ts": 586, + "src/main/claude/claude-structured-dispatch-content.test.ts": 254, + "src/main/claude/claude-structured-dispatch-queue-boundary.test.ts": 601, + "src/main/claude/claude-structured-dispatch.test.ts": 1843, + "src/main/claude/claude-structured-effort-default-at-rest.test.ts": 836, + "src/main/claude/claude-structured-effort-reporting.test.ts": 607, + "src/main/claude/claude-structured-history-window.test.ts": 742, + "src/main/claude/claude-structured-in-turn-stop.test.ts": 657, + "src/main/claude/claude-structured-inbound-control.test.ts": 46, + "src/main/claude/claude-structured-journal-prompt-cards.test.ts": 65, + "src/main/claude/claude-structured-journal-prompt-retry.test.ts": 336, + "src/main/claude/claude-structured-journal-translation-background-tasks.test.ts": 539, + "src/main/claude/claude-structured-journal-translation-context-usage.test.ts": 461, + "src/main/claude/claude-structured-journal-translation-subagents.test.ts": 365, + "src/main/claude/claude-structured-journal-translation-turn-timing.test.ts": 705, + "src/main/claude/claude-structured-journal-translation.plan.test.ts": 336, + "src/main/claude/claude-structured-journal-translation.test.ts": 677, + "src/main/claude/claude-structured-launch-account-routing.test.ts": 943, + "src/main/claude/claude-structured-launch-args.test.ts": 54, + "src/main/claude/claude-structured-launch-resolution.test.ts": 734, + "src/main/claude/claude-structured-launch-visuals.test.ts": 630, + "src/main/claude/claude-structured-live-provider-process.test.ts": 568, + "src/main/claude/claude-structured-model-confirmation.test.ts": 644, + "src/main/claude/claude-structured-model-preflight.test.ts": 323, + "src/main/claude/claude-structured-option-confirmation.test.ts": 627, + "src/main/claude/claude-structured-options-context-window.test.ts": 447, + "src/main/claude/claude-structured-options.test.ts": 532, + "src/main/claude/claude-structured-owner-identity.test.ts": 63, + "src/main/claude/claude-structured-prompt-items.test.ts": 300, + "src/main/claude/claude-structured-prompt-ownership.test.ts": 761, + "src/main/claude/claude-structured-provider-fallback.test.ts": 636, + "src/main/claude/claude-structured-queued-stop.test.ts": 599, + "src/main/claude/claude-structured-rate-limit-retry.test.ts": 326, + "src/main/claude/claude-structured-real-cli-fold.test.ts": 601, + "src/main/claude/claude-structured-real-cli-stop-task.test.ts": 526, + "src/main/claude/claude-structured-real-cli-visuals.test.ts": 789, + "src/main/claude/claude-structured-real-cli.test.ts": 487, + "src/main/claude/claude-structured-reasoning-lifecycle.test.ts": 401, + "src/main/claude/claude-structured-reasoning.test.ts": 403, + "src/main/claude/claude-structured-requested-stop.test.ts": 570, + "src/main/claude/claude-structured-resume-point.test.ts": 539, + "src/main/claude/claude-structured-retired-model.test.ts": 627, + "src/main/claude/claude-structured-session-acquisition-processless.test.ts": 527, + "src/main/claude/claude-structured-session-adapter-turns.test.ts": 666, + "src/main/claude/claude-structured-session-adapter.test.ts": 593, + "src/main/claude/claude-structured-session-close.test.ts": 636, + "src/main/claude/claude-structured-session-commands.test.ts": 604, + "src/main/claude/claude-structured-session-exit-blame.test.ts": 700, + "src/main/claude/claude-structured-session-reading-control.test.ts": 649, + "src/main/claude/claude-structured-session-recovery.test.ts": 877, + "src/main/claude/claude-structured-session-startup.test.ts": 589, + "src/main/claude/claude-structured-sign-in.test.ts": 401, + "src/main/claude/claude-structured-spawn-options.test.ts": 406, + "src/main/claude/claude-structured-start-fast-mode.test.ts": 574, + "src/main/claude/claude-structured-turn-resume-point.test.ts": 694, "src/main/claude/claude-subagent-id-aliases.test.ts": 53, - "src/main/claude/claude-subagent-journaled-roster.test.ts": 55, - "src/main/claude/claude-subagent-permission-request.test.ts": 2224, - "src/main/claude/claude-subagent-resume-after-restart.test.ts": 798, - "src/main/claude/claude-subagent-roster-groups.test.ts": 70, - "src/main/claude/claude-subagent-roster-retention.test.ts": 116, - "src/main/claude/claude-subagent-roster.test.ts": 371, - "src/main/claude/claude-subagent-task-frames.test.ts": 83, - "src/main/claude/claude-supervised-stop-real-cli.test.ts": 450, - "src/main/claude/claude-supervised-stop.integration.test.ts": 3745, - "src/main/claude/claude-transcript-branch-graph.test.ts": 46, - "src/main/claude/claude-transcript-branch-streaming.test.ts": 162, - "src/main/claude/claude-transcript-entry-uuid.test.ts": 45, - "src/main/claude/claude-transcript-marker-branch-proof.test.ts": 82, - "src/main/claude/claude-transcript-rewind-proof.test.ts": 57, - "src/main/claude/claude-turn-outcome.test.ts": 450, - "src/main/claude/claude-turn-ownership.test.ts": 743, - "src/main/claude/claude-turn-resumption.test.ts": 453, - "src/main/claude/compact-status-registration.test.ts": 220, - "src/main/claude/hook-service.test.ts": 206, - "src/main/claude/statusline-script.test.ts": 179, - "src/main/claude/windows-hook-files.test.ts": 151, - "src/main/cli/appimage-extracted-root.test.ts": 139, - "src/main/cli/appimage-extraction-pruning.test.ts": 102, - "src/main/cli/appimage-payload-removal.reentrancy.test.ts": 51, - "src/main/cli/appimage-payload-removal.test.ts": 62, - "src/main/cli/appimage-registration-lock.test.ts": 71, - "src/main/cli/appimage-stable-launcher.test.ts": 221, - "src/main/cli/cli-command-installation-races.test.ts": 152, - "src/main/cli/cli-command-privileged-transaction.test.ts": 112, - "src/main/cli/cli-installer-appimage-ownership.test.ts": 174, - "src/main/cli/cli-installer-appimage-removal.test.ts": 290, - "src/main/cli/cli-installer-command-conflicts.test.ts": 246, - "src/main/cli/cli-installer-macos-command-path.test.ts": 190, - "src/main/cli/cli-installer-windows-path.test.ts": 178, - "src/main/cli/cli-installer.test.ts": 258, - "src/main/cli/cli-privileged-processes.test.ts": 54, - "src/main/cli/keyed-promise-queue.test.ts": 55, - "src/main/cli/legacy-appimage-cli-wrapper.test.ts": 53, - "src/main/cli/linux-bare-orca-dispatcher.test.ts": 144, - "src/main/cli/linux-terminal-orca-cli-shim.test.ts": 148, - "src/main/cli/native-cli-powershell-argv-boundary.test.ts": 51, - "src/main/cli/orca-cli-child-path.test.ts": 82, - "src/main/cli/packaged-cli-assets.test.ts": 6578, - "src/main/cli/windows-user-path-registry.test.ts": 53, - "src/main/cli/wsl-cli-installer.test.ts": 298, - "src/main/cli/wsl-cli-powershell-boundary.test.ts": 97, - "src/main/cli/wsl-cli-registration-operation.test.ts": 104, - "src/main/cli/wsl-cli-registration-reconciliation.test.ts": 240, - "src/main/cli/wsl-cli-registration-registry.test.ts": 131, - "src/main/cli/wsl-managed-cli.test.ts": 64, - "src/main/cli/wsl-managed-cli.wsl.test.ts": 114, - "src/main/codebuddy/hook-service.test.ts": 102, - "src/main/codex-accounts/async-file-rename.test.ts": 1080, - "src/main/codex-accounts/codex-account-identity-api-key-guard.test.ts": 69, - "src/main/codex-accounts/codex-auth-workspace-identity.test.ts": 58, - "src/main/codex-accounts/codex-config-mirror.test.ts": 315, - "src/main/codex-accounts/codex-credential-absence-grace.test.ts": 162, - "src/main/codex-accounts/codex-login-auth-url.test.ts": 54, - "src/main/codex-accounts/codex-reset-credit-ledger.test.ts": 294, - "src/main/codex-accounts/codex-windows-interactive-login.test.ts": 464, - "src/main/codex-accounts/fs-utils.test.ts": 126, - "src/main/codex-accounts/host-codex-managed-home-ownership.test.ts": 69, - "src/main/codex-accounts/legacy-shared-auth-migration.test.ts": 156, - "src/main/codex-accounts/legacy-shared-config-compatibility.test.ts": 147, - "src/main/codex-accounts/legacy-wsl-runtime-auth-drain-apply-script.test.ts": 12427, - "src/main/codex-accounts/legacy-wsl-runtime-auth-drain-interruption-source.test.ts": 61, - "src/main/codex-accounts/legacy-wsl-runtime-auth-drain-rollback-script.test.ts": 520, - "src/main/codex-accounts/legacy-wsl-runtime-auth-drain.test.ts": 78, - "src/main/codex-accounts/managed-codex-auth-readiness.test.ts": 89, - "src/main/codex-accounts/runtime-home-catalog-account-key.test.ts": 1493, - "src/main/codex-accounts/runtime-home-catalog-probe-prep.test.ts": 381, - "src/main/codex-accounts/runtime-home-legacy-migration.test.ts": 869, - "src/main/codex-accounts/runtime-home-managed-auth-recovery.test.ts": 526, - "src/main/codex-accounts/runtime-home-mirror-login-copy.test.ts": 721, - "src/main/codex-accounts/runtime-home-mirrored-status-home.test.ts": 350, - "src/main/codex-accounts/runtime-home-per-account-homes.test.ts": 1014, - "src/main/codex-accounts/runtime-home-read-only-launch-resolution.test.ts": 480, - "src/main/codex-accounts/runtime-home-real-home-lane-routing.test.ts": 645, - "src/main/codex-accounts/runtime-home-resume-selection-gate.test.ts": 470, - "src/main/codex-accounts/runtime-home-retained-auth-provenance.test.ts": 1229, - "src/main/codex-accounts/runtime-home-service-per-account-migration.test.ts": 479, - "src/main/codex-accounts/runtime-home-session-migration-pass.test.ts": 555, - "src/main/codex-accounts/runtime-home-status-hook-independence.test.ts": 545, - "src/main/codex-accounts/runtime-home-system-default-mirror-readback.test.ts": 1406, - "src/main/codex-accounts/runtime-home-system-default-snapshot.test.ts": 949, - "src/main/codex-accounts/runtime-home-system-resource-materialization.test.ts": 604, - "src/main/codex-accounts/runtime-home-windows-profile-ownership.test.ts": 273, - "src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts": 878, - "src/main/codex-accounts/runtime-home-wsl-session-bridge.test.ts": 875, - "src/main/codex-accounts/runtime-home-wsl-system-default.test.ts": 661, - "src/main/codex-accounts/runtime-selection.test.ts": 47, - "src/main/codex-accounts/service-abandoned-login-supersede.test.ts": 1252, - "src/main/codex-accounts/service-account-add-login.test.ts": 699, - "src/main/codex-accounts/service-account-selection-and-removal.test.ts": 701, - "src/main/codex-accounts/service-add-account-from-home.test.ts": 519, - "src/main/codex-accounts/service-login-process-teardown.test.ts": 465, - "src/main/codex-accounts/service-managed-home-removal-retries.test.ts": 516, - "src/main/codex-accounts/service-quota-refresh-decoupling.test.ts": 395, - "src/main/codex-accounts/service-reauthenticate-activation.test.ts": 433, - "src/main/codex-accounts/service-reset-credit-durability.test.ts": 932, - "src/main/codex-accounts/service-reset-credit-home-ownership.test.ts": 608, - "src/main/codex-accounts/service-reset-credit-target-routing.test.ts": 652, - "src/main/codex-accounts/service-system-default-identity.test.ts": 511, - "src/main/codex-accounts/service-wsl-accounts.test.ts": 740, - "src/main/codex-accounts/service.test.ts": 615, - "src/main/codex-accounts/sta-4422-transient-ownership-error.test.ts": 525, - "src/main/codex-accounts/sta-4734-login-rollback-deletes-authenticated-home.test.ts": 465, - "src/main/codex-accounts/sta-4735-unreadable-host-lane-writes.test.ts": 528, - "src/main/codex-accounts/wsl-codex-command.test.ts": 50, - "src/main/codex-cli/codex-home-process-lock.test.ts": 53, - "src/main/codex-cli/command.test.ts": 90, - "src/main/codex-usage/codex-rollout-file-lifetime.test.ts": 160, - "src/main/codex-usage/codex-rollout-resume-state.test.ts": 75, - "src/main/codex-usage/codex-usage-cost-estimate.test.ts": 51, - "src/main/codex-usage/codex-usage-long-context-billing.test.ts": 491, - "src/main/codex-usage/scanner-attribution-memo.test.ts": 224, - "src/main/codex-usage/scanner-checkpoint-races.test.ts": 240, - "src/main/codex-usage/scanner-file-generation.test.ts": 197, - "src/main/codex-usage/scanner-file-identity.test.ts": 168, - "src/main/codex-usage/scanner-incremental-append.test.ts": 502, - "src/main/codex-usage/scanner-large-directory.test.ts": 673, - "src/main/codex-usage/scanner-paths.test.ts": 400, - "src/main/codex-usage/scanner.test.ts": 138, - "src/main/codex-usage/session-file-discovery-errors.test.ts": 71, - "src/main/codex-usage/store-automation-usage.test.ts": 385, - "src/main/codex-usage/store-model-pricing.test.ts": 431, - "src/main/codex-usage/store-orca-scope.test.ts": 404, - "src/main/codex-usage/store-persistence.test.ts": 360, - "src/main/codex-usage/store-snapshot.benchmark.test.ts": 637, - "src/main/codex/codex-account-session-bridge.test.ts": 376, - "src/main/codex/codex-account-session-index-heal.test.ts": 141, - "src/main/codex/codex-ai-vault-session-resume.test.ts": 145, - "src/main/codex/codex-app-server-capability-cache.test.ts": 65, - "src/main/codex/codex-app-server-client.test.ts": 11768, - "src/main/codex/codex-app-server-connection-exit-order.test.ts": 272, - "src/main/codex/codex-app-server-connection-tree-unproven.test.ts": 247, - "src/main/codex/codex-app-server-connection.test.ts": 1093, - "src/main/codex/codex-app-server-exit-error.test.ts": 202, - "src/main/codex/codex-app-server-session-stop.test.ts": 96, - "src/main/codex/codex-app-server-session-supervision.test.ts": 17428, - "src/main/codex/codex-app-server-session.test.ts": 278, - "src/main/codex/codex-app-server-teardown.integration.test.ts": 20731, - "src/main/codex/codex-background-command-tracker.test.ts": 377, - "src/main/codex/codex-background-task-tracker.test.ts": 326, - "src/main/codex/codex-background-terminals.test.ts": 476, - "src/main/codex/codex-child-work-evidence.test.ts": 416, - "src/main/codex/codex-child-work-translation-retention.test.ts": 424, - "src/main/codex/codex-close-turn-end.test.ts": 350, - "src/main/codex/codex-collab-call-delegation.test.ts": 435, - "src/main/codex/codex-collab-call-rows-on-positional-clients.test.ts": 522, - "src/main/codex/codex-command-turn-claim.test.ts": 332, - "src/main/codex/codex-config-mirror-hook-trust-carry.test.ts": 149, - "src/main/codex/codex-config-mirror-mcp-ownership.test.ts": 151, - "src/main/codex/codex-config-mirror.test.ts": 225, - "src/main/codex/codex-config-settings-removal.test.ts": 68, - "src/main/codex/codex-configured-launch-defaults.test.ts": 58, - "src/main/codex/codex-daemon-socket-path-guard.test.ts": 189, - "src/main/codex/codex-default-mode-subagents.test.ts": 763, - "src/main/codex/codex-goal-journal-rows.test.ts": 82, - "src/main/codex/codex-home-paths.test.ts": 116, - "src/main/codex/codex-hook-approval-first-write.test.ts": 89, - "src/main/codex/codex-hook-command-form.test.ts": 121, - "src/main/codex/codex-hook-file-entry-binary-contract.test.ts": 228, - "src/main/codex/codex-hook-hash-lookup.test.ts": 180, - "src/main/codex/codex-hook-legacy-profile-block.test.ts": 144, - "src/main/codex/codex-hook-real-home-status.test.ts": 321, - "src/main/codex/codex-hook-reconcile.test.ts": 2030, - "src/main/codex/codex-hook-retired-commands.test.ts": 81, - "src/main/codex/codex-hook-sweep-per-agent-opt-out.test.ts": 211, - "src/main/codex/codex-hook-toggle-off-wins.test.ts": 276, - "src/main/codex/codex-hook-trust-cleanup.test.ts": 140, - "src/main/codex/codex-hook-trust-derivation.test.ts": 165, - "src/main/codex/codex-hook-trust-failure-memo.test.ts": 359, - "src/main/codex/codex-hook-trust-grant.test.ts": 594, - "src/main/codex/codex-hook-trust-memo.test.ts": 170, - "src/main/codex/codex-hooks-read-denial-followup.test.ts": 284, - "src/main/codex/codex-index-heal-binary-contract.test.ts": 146, - "src/main/codex/codex-interrupt-hook-trust.test.ts": 69, - "src/main/codex/codex-legacy-session-resume.test.ts": 453, - "src/main/codex/codex-model-catalog-probe.test.ts": 3579, - "src/main/codex/codex-model-provider-config.test.ts": 55, - "src/main/codex/codex-notice-item-translation.test.ts": 341, - "src/main/codex/codex-pane-account-registry-mutations.test.ts": 48, - "src/main/codex/codex-pane-launch-account.test.ts": 58, - "src/main/codex/codex-persistent-command-retention.test.ts": 416, - "src/main/codex/codex-pinned-rollout-proof.test.ts": 87, - "src/main/codex/codex-prompt-registry-retention.test.ts": 711, - "src/main/codex/codex-provider-retry-idle-sweep.test.ts": 1708, - "src/main/codex/codex-provider-retry-row.test.ts": 419, - "src/main/codex/codex-real-home-hook-entries.test.ts": 383, - "src/main/codex/codex-real-home-hook-key-spellings.test.ts": 283, - "src/main/codex/codex-real-home-hook-removal.test.ts": 214, - "src/main/codex/codex-real-home-path.test.ts": 70, - "src/main/codex/codex-real-home-shared-entry.test.ts": 433, - "src/main/codex/codex-real-home-windows-key-spellings.test.ts": 158, - "src/main/codex/codex-requested-close-turn-timing.test.ts": 341, - "src/main/codex/codex-rollout-session-meta.test.ts": 108, - "src/main/codex/codex-server-request-disposition.test.ts": 253, - "src/main/codex/codex-session-backfill-marker.test.ts": 92, - "src/main/codex/codex-session-backfill-scan-dates.test.ts": 68, - "src/main/codex/codex-session-backfill.test.ts": 314, - "src/main/codex/codex-session-bridge.test.ts": 122, - "src/main/codex/codex-session-index-heal-state.test.ts": 506, - "src/main/codex/codex-session-index-heal.test.ts": 2477, - "src/main/codex/codex-session-migration-recent-exits.test.ts": 44, - "src/main/codex/codex-session-migration-scheduler.test.ts": 436, - "src/main/codex/codex-session-resume-home.test.ts": 217, - "src/main/codex/codex-session-resume-preparation.test.ts": 68, - "src/main/codex/codex-session-resume-windows.test.ts": 60, - "src/main/codex/codex-session-resume-wrong-account.test.ts": 75, - "src/main/codex/codex-session-source-home.test.ts": 47, - "src/main/codex/codex-shared-server-fix.test.ts": 74, - "src/main/codex/codex-shared-server-pane.test.ts": 89, - "src/main/codex/codex-shared-server-probe.test.ts": 104, - "src/main/codex/codex-short-lived-app-server-spawn.test.ts": 431, - "src/main/codex/codex-stale-pane-accounts.test.ts": 118, - "src/main/codex/codex-state-db-backfill-recovery-process.test.ts": 75, - "src/main/codex/codex-state-db-backfill-recovery-supervision.test.ts": 8520, - "src/main/codex/codex-state-db-backfill-recovery.test.ts": 286, - "src/main/codex/codex-state-db.test.ts": 127, - "src/main/codex/codex-structured-acquisition-exit-proof.test.ts": 540, - "src/main/codex/codex-structured-child-environment.test.ts": 504, - "src/main/codex/codex-structured-child-work-producer.test.ts": 625, - "src/main/codex/codex-structured-compaction-refusal.test.ts": 377, - "src/main/codex/codex-structured-conversation-stop.test.ts": 808, - "src/main/codex/codex-structured-dispatch-admission.test.ts": 772, - "src/main/codex/codex-structured-dispatch-boundary.test.ts": 696, - "src/main/codex/codex-structured-dispatch-echo.test.ts": 76, - "src/main/codex/codex-structured-item-translation.test.ts": 205, - "src/main/codex/codex-structured-journal-compactions.test.ts": 406, - "src/main/codex/codex-structured-journal-exit-convergence.test.ts": 821, - "src/main/codex/codex-structured-journal-goal-admission.test.ts": 341, - "src/main/codex/codex-structured-journal-goal-resume.test.ts": 386, - "src/main/codex/codex-structured-journal-goal-revision.test.ts": 603, - "src/main/codex/codex-structured-journal-goal-rows.test.ts": 320, - "src/main/codex/codex-structured-journal-repeated-questions.test.ts": 446, - "src/main/codex/codex-structured-journal-restore-turn-grouping.test.ts": 337, - "src/main/codex/codex-structured-journal-settlement-rollback.test.ts": 631, - "src/main/codex/codex-structured-journal-subagent-readers.test.ts": 744, - "src/main/codex/codex-structured-journal-translation-settlement.test.ts": 673, - "src/main/codex/codex-structured-journal-translation-streams.test.ts": 507, - "src/main/codex/codex-structured-journal-translation-subagent-linkage.test.ts": 337, - "src/main/codex/codex-structured-journal-translation-subagents.test.ts": 404, - "src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts": 692, - "src/main/codex/codex-structured-journal-translation-turn-settles-once.test.ts": 363, - "src/main/codex/codex-structured-journal-translation-turn-state.test.ts": 71, - "src/main/codex/codex-structured-journal-translation.test.ts": 447, - "src/main/codex/codex-structured-journal-turn-endings-replay.test.ts": 314, - "src/main/codex/codex-structured-journal-turn-settles-once-journal.test.ts": 472, - "src/main/codex/codex-structured-launch-args.test.ts": 54, - "src/main/codex/codex-structured-launch-resolution.test.ts": 482, - "src/main/codex/codex-structured-live-provider-process.test.ts": 397, - "src/main/codex/codex-structured-owner-identity.test.ts": 83, - "src/main/codex/codex-structured-permission-policy.test.ts": 73, + "src/main/claude/claude-subagent-permission-request.test.ts": 2912, + "src/main/claude/claude-subagent-resume-after-restart.test.ts": 618, + "src/main/claude/claude-subagent-task-frames.test.ts": 54, + "src/main/claude/claude-supervised-stop-real-cli.test.ts": 428, + "src/main/claude/claude-supervised-stop.integration.test.ts": 3753, + "src/main/claude/claude-transcript-branch-graph.test.ts": 37, + "src/main/claude/claude-transcript-branch-streaming.test.ts": 181, + "src/main/claude/claude-transcript-entry-uuid.test.ts": 32, + "src/main/claude/claude-transcript-marker-branch-proof.test.ts": 64, + "src/main/claude/claude-transcript-rewind-proof.test.ts": 35, + "src/main/claude/claude-turn-outcome.test.ts": 422, + "src/main/claude/claude-turn-ownership.test.ts": 750, + "src/main/claude/claude-turn-resumption.test.ts": 490, + "src/main/claude/compact-status-registration.test.ts": 270, + "src/main/claude/hook-service.test.ts": 235, + "src/main/claude/post-compact-hook-command.test.ts": 179, + "src/main/claude/statusline-script.test.ts": 257, + "src/main/claude/windows-hook-files.test.ts": 171, + "src/main/claude/windows-native-hook-registration.test.ts": 151, + "src/main/cli/appimage-copy-pre-extraction.test.ts": 329, + "src/main/cli/appimage-extracted-root.test.ts": 142, + "src/main/cli/appimage-extraction-pruning-namespace.test.ts": 106, + "src/main/cli/appimage-extraction-pruning.test.ts": 126, + "src/main/cli/appimage-generation-lease.test.ts": 64, + "src/main/cli/appimage-generation-residency.test.ts": 40, + "src/main/cli/appimage-generation-stable-launcher.test.ts": 229, + "src/main/cli/appimage-payload-removal.reentrancy.test.ts": 48, + "src/main/cli/appimage-payload-removal.test.ts": 31, + "src/main/cli/appimage-registration-lock.test.ts": 74, + "src/main/cli/appimage-serve-preparation.test.ts": 751, + "src/main/cli/appimage-stable-launcher.test.ts": 233, + "src/main/cli/cli-command-installation-races.test.ts": 150, + "src/main/cli/cli-command-privileged-transaction.test.ts": 102, + "src/main/cli/cli-installer-appimage-ownership.test.ts": 187, + "src/main/cli/cli-installer-appimage-removal.test.ts": 257, + "src/main/cli/cli-installer-command-conflicts.test.ts": 192, + "src/main/cli/cli-installer-macos-command-path.test.ts": 193, + "src/main/cli/cli-installer-windows-path.test.ts": 149, + "src/main/cli/cli-installer.test.ts": 235, + "src/main/cli/cli-privileged-processes.test.ts": 45, + "src/main/cli/keyed-promise-queue.test.ts": 41, + "src/main/cli/legacy-appimage-cli-wrapper.test.ts": 42, + "src/main/cli/linux-bare-orca-dispatcher.test.ts": 133, + "src/main/cli/linux-terminal-orca-cli-shim.test.ts": 161, + "src/main/cli/native-cli-powershell-argv-boundary.test.ts": 41, + "src/main/cli/orca-cli-child-path.test.ts": 89, + "src/main/cli/packaged-cli-assets.test.ts": 6567, + "src/main/cli/windows-user-path-registry.test.ts": 28, + "src/main/cli/wsl-cli-installer.test.ts": 314, + "src/main/cli/wsl-cli-powershell-boundary.test.ts": 85, + "src/main/cli/wsl-cli-registration-operation.test.ts": 91, + "src/main/cli/wsl-cli-registration-reconciliation.test.ts": 209, + "src/main/cli/wsl-cli-registration-registry.test.ts": 151, + "src/main/cli/wsl-managed-cli.test.ts": 56, + "src/main/cli/wsl-managed-cli.wsl.test.ts": 92, + "src/main/codebuddy/hook-service.test.ts": 131, + "src/main/codex-accounts/async-file-rename.test.ts": 1027, + "src/main/codex-accounts/codex-account-identity-api-key-guard.test.ts": 37, + "src/main/codex-accounts/codex-auth-workspace-identity.test.ts": 49, + "src/main/codex-accounts/codex-config-mirror.test.ts": 328, + "src/main/codex-accounts/codex-credential-absence-grace.test.ts": 77, + "src/main/codex-accounts/codex-login-auth-url.test.ts": 39, + "src/main/codex-accounts/codex-reset-credit-ledger.test.ts": 254, + "src/main/codex-accounts/codex-windows-interactive-login.test.ts": 506, + "src/main/codex-accounts/fs-utils.test.ts": 113, + "src/main/codex-accounts/guest-codex-account-homes.test.ts": 60, + "src/main/codex-accounts/host-codex-managed-home-ownership.test.ts": 41, + "src/main/codex-accounts/legacy-shared-auth-migration.test.ts": 103, + "src/main/codex-accounts/legacy-shared-config-compatibility.test.ts": 143, + "src/main/codex-accounts/legacy-wsl-runtime-auth-drain-apply-script.test.ts": 12778, + "src/main/codex-accounts/legacy-wsl-runtime-auth-drain-interruption-source.test.ts": 63, + "src/main/codex-accounts/legacy-wsl-runtime-auth-drain-rollback-script.test.ts": 506, + "src/main/codex-accounts/legacy-wsl-runtime-auth-drain.test.ts": 64, + "src/main/codex-accounts/managed-codex-auth-readiness.test.ts": 60, + "src/main/codex-accounts/runtime-home-catalog-account-key.test.ts": 2360, + "src/main/codex-accounts/runtime-home-catalog-probe-prep.test.ts": 560, + "src/main/codex-accounts/runtime-home-legacy-migration.test.ts": 1057, + "src/main/codex-accounts/runtime-home-managed-auth-recovery.test.ts": 566, + "src/main/codex-accounts/runtime-home-mirror-login-copy.test.ts": 815, + "src/main/codex-accounts/runtime-home-mirrored-status-home.test.ts": 309, + "src/main/codex-accounts/runtime-home-per-account-homes.test.ts": 706, + "src/main/codex-accounts/runtime-home-read-only-launch-resolution.test.ts": 573, + "src/main/codex-accounts/runtime-home-real-home-lane-routing.test.ts": 690, + "src/main/codex-accounts/runtime-home-restart-stopped-distro.test.ts": 442, + "src/main/codex-accounts/runtime-home-resume-selection-gate.test.ts": 559, + "src/main/codex-accounts/runtime-home-retained-auth-provenance.test.ts": 1166, + "src/main/codex-accounts/runtime-home-service-per-account-migration.test.ts": 536, + "src/main/codex-accounts/runtime-home-session-migration-pass.test.ts": 645, + "src/main/codex-accounts/runtime-home-status-hook-independence.test.ts": 713, + "src/main/codex-accounts/runtime-home-system-default-mirror-readback.test.ts": 1295, + "src/main/codex-accounts/runtime-home-system-default-snapshot.test.ts": 785, + "src/main/codex-accounts/runtime-home-system-resource-materialization.test.ts": 633, + "src/main/codex-accounts/runtime-home-windows-profile-ownership.test.ts": 290, + "src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts": 982, + "src/main/codex-accounts/runtime-home-wsl-session-bridge.test.ts": 826, + "src/main/codex-accounts/runtime-home-wsl-system-default.test.ts": 649, + "src/main/codex-accounts/runtime-selection.test.ts": 37, + "src/main/codex-accounts/service-abandoned-login-supersede.test.ts": 886, + "src/main/codex-accounts/service-account-add-login.test.ts": 906, + "src/main/codex-accounts/service-account-selection-and-removal.test.ts": 952, + "src/main/codex-accounts/service-add-account-from-home.test.ts": 505, + "src/main/codex-accounts/service-login-process-teardown.test.ts": 508, + "src/main/codex-accounts/service-managed-home-removal-retries.test.ts": 425, + "src/main/codex-accounts/service-quota-refresh-decoupling.test.ts": 456, + "src/main/codex-accounts/service-reauthenticate-activation.test.ts": 493, + "src/main/codex-accounts/service-reset-credit-durability.test.ts": 845, + "src/main/codex-accounts/service-reset-credit-home-ownership.test.ts": 600, + "src/main/codex-accounts/service-reset-credit-target-routing.test.ts": 719, + "src/main/codex-accounts/service-system-default-identity.test.ts": 492, + "src/main/codex-accounts/service-wsl-accounts.test.ts": 541, + "src/main/codex-accounts/service.test.ts": 605, + "src/main/codex-accounts/sta-4422-transient-ownership-error.test.ts": 538, + "src/main/codex-accounts/sta-4734-login-rollback-deletes-authenticated-home.test.ts": 499, + "src/main/codex-accounts/sta-4735-unreadable-host-lane-writes.test.ts": 517, + "src/main/codex-accounts/wsl-codex-command.test.ts": 39, + "src/main/codex-cli/codex-home-process-lock.test.ts": 39, + "src/main/codex-cli/command.test.ts": 97, + "src/main/codex-usage/codex-rollout-file-lifetime.test.ts": 210, + "src/main/codex-usage/codex-rollout-resume-state.test.ts": 73, + "src/main/codex-usage/codex-usage-cost-estimate.test.ts": 33, + "src/main/codex-usage/codex-usage-long-context-billing.test.ts": 530, + "src/main/codex-usage/scanner-attribution-memo.test.ts": 260, + "src/main/codex-usage/scanner-checkpoint-races.test.ts": 304, + "src/main/codex-usage/scanner-file-generation.test.ts": 248, + "src/main/codex-usage/scanner-file-identity.test.ts": 166, + "src/main/codex-usage/scanner-incremental-append.test.ts": 537, + "src/main/codex-usage/scanner-large-directory.test.ts": 453, + "src/main/codex-usage/scanner-paths.test.ts": 316, + "src/main/codex-usage/scanner.test.ts": 106, + "src/main/codex-usage/session-file-discovery-errors.test.ts": 59, + "src/main/codex-usage/store-automation-usage.test.ts": 381, + "src/main/codex-usage/store-model-pricing.test.ts": 419, + "src/main/codex-usage/store-orca-scope.test.ts": 407, + "src/main/codex-usage/store-persistence.test.ts": 1203, + "src/main/codex-usage/store-snapshot.benchmark.test.ts": 447, + "src/main/codex/codex-account-home-hook.test.ts": 269, + "src/main/codex/codex-account-session-bridge.test.ts": 343, + "src/main/codex/codex-account-session-index-heal.test.ts": 173, + "src/main/codex/codex-ai-vault-session-resume.test.ts": 130, + "src/main/codex/codex-app-server-capability-cache.test.ts": 39, + "src/main/codex/codex-app-server-client.test.ts": 12524, + "src/main/codex/codex-app-server-connection-exit-order.test.ts": 293, + "src/main/codex/codex-app-server-connection-tree-unproven.test.ts": 268, + "src/main/codex/codex-app-server-connection.test.ts": 1089, + "src/main/codex/codex-app-server-exit-error.test.ts": 365, + "src/main/codex/codex-app-server-session-stop.test.ts": 87, + "src/main/codex/codex-app-server-session-supervision.test.ts": 17983, + "src/main/codex/codex-app-server-session.test.ts": 268, + "src/main/codex/codex-app-server-teardown.integration.test.ts": 21370, + "src/main/codex/codex-background-command-tracker.test.ts": 365, + "src/main/codex/codex-background-task-tracker.test.ts": 389, + "src/main/codex/codex-background-terminals.test.ts": 449, + "src/main/codex/codex-child-work-evidence.test.ts": 629, + "src/main/codex/codex-child-work-translation-retention.test.ts": 582, + "src/main/codex/codex-close-turn-end.test.ts": 321, + "src/main/codex/codex-collab-call-delegation.test.ts": 482, + "src/main/codex/codex-collab-call-rows-on-positional-clients.test.ts": 567, + "src/main/codex/codex-command-turn-claim.test.ts": 358, + "src/main/codex/codex-config-mirror-hook-trust-carry.test.ts": 258, + "src/main/codex/codex-config-mirror-mcp-ownership.test.ts": 117, + "src/main/codex/codex-config-mirror.test.ts": 264, + "src/main/codex/codex-config-settings-removal.test.ts": 57, + "src/main/codex/codex-configured-launch-defaults.test.ts": 46, + "src/main/codex/codex-daemon-socket-path-guard.test.ts": 165, + "src/main/codex/codex-default-mode-subagents.test.ts": 709, + "src/main/codex/codex-goal-journal-rows.test.ts": 72, + "src/main/codex/codex-home-paths.test.ts": 134, + "src/main/codex/codex-hook-approval-first-write.test.ts": 112, + "src/main/codex/codex-hook-command-form.test.ts": 172, + "src/main/codex/codex-hook-file-entry-binary-contract.test.ts": 266, + "src/main/codex/codex-hook-hash-lookup.test.ts": 232, + "src/main/codex/codex-hook-legacy-profile-block.test.ts": 119, + "src/main/codex/codex-hook-real-home-status.test.ts": 328, + "src/main/codex/codex-hook-reconcile.test.ts": 2218, + "src/main/codex/codex-hook-retired-commands.test.ts": 75, + "src/main/codex/codex-hook-sweep-per-agent-opt-out.test.ts": 249, + "src/main/codex/codex-hook-toggle-off-wins.test.ts": 437, + "src/main/codex/codex-hook-trust-cleanup.test.ts": 102, + "src/main/codex/codex-hook-trust-derivation.test.ts": 218, + "src/main/codex/codex-hook-trust-failure-memo.test.ts": 410, + "src/main/codex/codex-hook-trust-grant.test.ts": 568, + "src/main/codex/codex-hook-trust-memo.test.ts": 187, + "src/main/codex/codex-hooks-read-denial-followup.test.ts": 231, + "src/main/codex/codex-index-heal-binary-contract.test.ts": 167, + "src/main/codex/codex-interrupt-hook-trust.test.ts": 107, + "src/main/codex/codex-legacy-session-resume.test.ts": 246, + "src/main/codex/codex-minimum-version.test.ts": 797, + "src/main/codex/codex-model-catalog-probe.test.ts": 3355, + "src/main/codex/codex-model-provider-config.test.ts": 47, + "src/main/codex/codex-notice-item-translation.test.ts": 333, + "src/main/codex/codex-pane-account-registry-mutations.test.ts": 38, + "src/main/codex/codex-pane-launch-account.test.ts": 41, + "src/main/codex/codex-persistent-command-retention.test.ts": 498, + "src/main/codex/codex-pinned-rollout-proof.test.ts": 68, + "src/main/codex/codex-prompt-registry-retention.test.ts": 712, + "src/main/codex/codex-provider-retry-idle-sweep.test.ts": 1918, + "src/main/codex/codex-provider-retry-row.test.ts": 527, + "src/main/codex/codex-real-home-hook-entries.test.ts": 543, + "src/main/codex/codex-real-home-hook-key-spellings.test.ts": 310, + "src/main/codex/codex-real-home-hook-removal.test.ts": 325, + "src/main/codex/codex-real-home-path.test.ts": 56, + "src/main/codex/codex-real-home-shared-entry.test.ts": 472, + "src/main/codex/codex-real-home-windows-key-spellings.test.ts": 173, + "src/main/codex/codex-requested-close-turn-timing.test.ts": 373, + "src/main/codex/codex-rollout-session-meta.test.ts": 78, + "src/main/codex/codex-server-request-disposition.test.ts": 249, + "src/main/codex/codex-session-backfill-marker.test.ts": 88, + "src/main/codex/codex-session-backfill-scan-dates.test.ts": 58, + "src/main/codex/codex-session-backfill.test.ts": 351, + "src/main/codex/codex-session-bridge.test.ts": 90, + "src/main/codex/codex-session-index-heal-state.test.ts": 373, + "src/main/codex/codex-session-index-heal.test.ts": 2651, + "src/main/codex/codex-session-migration-recent-exits.test.ts": 34, + "src/main/codex/codex-session-migration-scheduler.test.ts": 423, + "src/main/codex/codex-session-resume-home.test.ts": 114, + "src/main/codex/codex-session-resume-preparation.test.ts": 41, + "src/main/codex/codex-session-resume-windows.test.ts": 33, + "src/main/codex/codex-session-resume-wrong-account.test.ts": 70, + "src/main/codex/codex-session-source-home.test.ts": 30, + "src/main/codex/codex-shared-server-fix.test.ts": 82, + "src/main/codex/codex-shared-server-pane.test.ts": 100, + "src/main/codex/codex-shared-server-probe.test.ts": 152, + "src/main/codex/codex-short-lived-app-server-spawn.test.ts": 186, + "src/main/codex/codex-stale-pane-accounts.test.ts": 128, + "src/main/codex/codex-state-db-backfill-recovery-process.test.ts": 71, + "src/main/codex/codex-state-db-backfill-recovery-supervision.test.ts": 8406, + "src/main/codex/codex-state-db-backfill-recovery.test.ts": 311, + "src/main/codex/codex-state-db.test.ts": 135, + "src/main/codex/codex-structured-acquisition-exit-proof.test.ts": 489, + "src/main/codex/codex-structured-child-environment.test.ts": 630, + "src/main/codex/codex-structured-child-work-producer.test.ts": 736, + "src/main/codex/codex-structured-compaction-refusal.test.ts": 492, + "src/main/codex/codex-structured-conversation-stop.test.ts": 834, + "src/main/codex/codex-structured-dispatch-admission.test.ts": 713, + "src/main/codex/codex-structured-dispatch-boundary.test.ts": 391, + "src/main/codex/codex-structured-dispatch-echo.test.ts": 39, + "src/main/codex/codex-structured-item-translation.test.ts": 181, + "src/main/codex/codex-structured-journal-compactions.test.ts": 430, + "src/main/codex/codex-structured-journal-context-usage.test.ts": 646, + "src/main/codex/codex-structured-journal-exit-convergence.test.ts": 964, + "src/main/codex/codex-structured-journal-goal-admission.test.ts": 349, + "src/main/codex/codex-structured-journal-goal-resume.test.ts": 440, + "src/main/codex/codex-structured-journal-goal-revision.test.ts": 600, + "src/main/codex/codex-structured-journal-goal-rows.test.ts": 262, + "src/main/codex/codex-structured-journal-repeated-questions.test.ts": 656, + "src/main/codex/codex-structured-journal-restore-turn-grouping.test.ts": 430, + "src/main/codex/codex-structured-journal-settlement-rollback.test.ts": 908, + "src/main/codex/codex-structured-journal-subagent-readers.test.ts": 738, + "src/main/codex/codex-structured-journal-translation-settlement.test.ts": 718, + "src/main/codex/codex-structured-journal-translation-streams.test.ts": 458, + "src/main/codex/codex-structured-journal-translation-subagent-linkage.test.ts": 359, + "src/main/codex/codex-structured-journal-translation-subagents.test.ts": 374, + "src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts": 579, + "src/main/codex/codex-structured-journal-translation-turn-settles-once.test.ts": 382, + "src/main/codex/codex-structured-journal-translation-turn-state.test.ts": 51, + "src/main/codex/codex-structured-journal-translation.test.ts": 468, + "src/main/codex/codex-structured-journal-turn-endings-replay.test.ts": 414, + "src/main/codex/codex-structured-journal-turn-settles-once-journal.test.ts": 790, + "src/main/codex/codex-structured-launch-args.test.ts": 61, + "src/main/codex/codex-structured-launch-resolution.test.ts": 645, + "src/main/codex/codex-structured-live-provider-process.test.ts": 485, + "src/main/codex/codex-structured-owner-identity.test.ts": 81, + "src/main/codex/codex-structured-permission-policy.test.ts": 33, "src/main/codex/codex-structured-prompt-items.test.ts": 304, - "src/main/codex/codex-structured-prompt-ownership.test.ts": 648, - "src/main/codex/codex-structured-prompt-replies.test.ts": 370, - "src/main/codex/codex-structured-question-order.test.ts": 1100, - "src/main/codex/codex-structured-real-cli-visuals.test.ts": 228, - "src/main/codex/codex-structured-reasoning-lifecycle.test.ts": 268, - "src/main/codex/codex-structured-rewind.test.ts": 502, - "src/main/codex/codex-structured-service-tier.test.ts": 649, - "src/main/codex/codex-structured-session-adapter-lifecycle.test.ts": 558, - "src/main/codex/codex-structured-session-adapter.test.ts": 645, - "src/main/codex/codex-structured-session-background-tasks.test.ts": 661, - "src/main/codex/codex-structured-session-cancel.test.ts": 566, - "src/main/codex/codex-structured-session-close.test.ts": 579, - "src/main/codex/codex-structured-session-exit-blame.test.ts": 486, - "src/main/codex/codex-structured-session-options-catalog.test.ts": 541, - "src/main/codex/codex-structured-session-options.test.ts": 609, - "src/main/codex/codex-structured-session-prompts.test.ts": 483, - "src/main/codex/codex-structured-session-shutdown.test.ts": 649, - "src/main/codex/codex-structured-sign-in.test.ts": 301, - "src/main/codex/codex-structured-thread-goal.test.ts": 339, - "src/main/codex/codex-structured-thread-open.test.ts": 392, - "src/main/codex/codex-structured-turn-end-settlement.test.ts": 1386, - "src/main/codex/codex-structured-turn-open-wait.test.ts": 1451, - "src/main/codex/codex-structured-turn-steer.test.ts": 800, - "src/main/codex/codex-structured-visuals.test.ts": 479, - "src/main/codex/codex-subagent-execution-projection.test.ts": 490, - "src/main/codex/codex-subagent-executions.test.ts": 83, - "src/main/codex/codex-subagent-interrupts.test.ts": 503, - "src/main/codex/codex-subagent-roster-retention.test.ts": 3130, - "src/main/codex/codex-subagent-roster.test.ts": 383, - "src/main/codex/codex-thread-token-totals.test.ts": 39, - "src/main/codex/codex-tool-identity-translation.test.ts": 105, - "src/main/codex/codex-trust-config-concurrent-launch.test.ts": 250, - "src/main/codex/codex-trust-config-mutation-queue.test.ts": 63, - "src/main/codex/codex-trust-grant-host.test.ts": 117, - "src/main/codex/codex-trust-grant-ledger.test.ts": 102, - "src/main/codex/codex-trust-grant-telemetry.test.ts": 161, - "src/main/codex/codex-turn-ordinals.test.ts": 89, - "src/main/codex/codex-user-hook-trust-moves.test.ts": 122, - "src/main/codex/codex-wsl-hook-install-plan.test.ts": 71, - "src/main/codex/config-features-settings-promotion.test.ts": 155, - "src/main/codex/config-plugin-registration-promotion.test.ts": 285, - "src/main/codex/config-settings-baseline-upgrade.test.ts": 169, - "src/main/codex/config-settings-promotion.test.ts": 280, - "src/main/codex/config-sync-stall.test.ts": 155, - "src/main/codex/config-toml-hook-state-duplicate-repair.test.ts": 123, - "src/main/codex/config-toml-hook-trust-scaling.test.ts": 120, - "src/main/codex/config-toml-mcp-servers.test.ts": 65, - "src/main/codex/config-toml-project-duplicate-repair.test.ts": 95, - "src/main/codex/config-toml-project-trust-spellings.test.ts": 113, - "src/main/codex/config-toml-promoted-setting-values.test.ts": 43, - "src/main/codex/config-toml-trust-hash.test.ts": 78, - "src/main/codex/config-toml-trust-hook-read.test.ts": 145, - "src/main/codex/config-toml-trust-hook-removal.test.ts": 152, - "src/main/codex/config-toml-trust-hook-upsert.test.ts": 194, - "src/main/codex/config-toml-trust-key.test.ts": 116, - "src/main/codex/config-toml-trust-loadability.test.ts": 182, - "src/main/codex/config-toml-trust-paths.test.ts": 100, - "src/main/codex/config-toml-trust-project.test.ts": 114, - "src/main/codex/config-toml-trust-stale-writes.test.ts": 71, - "src/main/codex/hook-service-concurrent-launch-install.test.ts": 1027, - "src/main/codex/hook-service-legacy-cleanup.test.ts": 907, - "src/main/codex/hook-service-managed-codex-hashes.test.ts": 646, - "src/main/codex/hook-service-managed-install.test.ts": 438, - "src/main/codex/hook-service-runtime-trust-repair.test.ts": 362, - "src/main/codex/hook-service-trust-grant.test.ts": 230, - "src/main/codex/hook-service-user-hook-mirroring.test.ts": 469, - "src/main/codex/hook-service-user-hook-trust-spellings.test.ts": 414, - "src/main/codex/hook-service-windows-key-spellings.test.ts": 220, - "src/main/codex/hook-service-wsl-runtime.test.ts": 366, - "src/main/codex/hook-trust-promotion.test.ts": 474, - "src/main/codex/retained-codex-hook-state.test.ts": 41, - "src/main/codex/sta-4735-hook-trust-provenance-overwrite.test.ts": 110, - "src/main/codex/sta-4737-unreadable-is-not-absent.test.ts": 161, - "src/main/codex/sta-4823-shared-state-survives-a-failed-read.test.ts": 193, - "src/main/codex/windows-hook-command.test.ts": 119, - "src/main/codex/windows-hook-upgrade.test.ts": 269, - "src/main/codex/wsl-codex-session-bridge.test.ts": 125, - "src/main/command-code/hook-service.test.ts": 255, - "src/main/computer/computer-action-verification-normalization.test.ts": 38, - "src/main/computer/computer-provider-lifecycle.test.ts": 152, - "src/main/computer/computer-provider-unavailable-message.test.ts": 45, - "src/main/computer/computer-sidecar-diagnostics.test.ts": 53, - "src/main/computer/desktop-script-provider-action-errors.test.ts": 149, - "src/main/computer/desktop-script-provider-actions.test.ts": 136, - "src/main/computer/desktop-script-provider-bridge.test.ts": 50, - "src/main/computer/desktop-script-provider-cache-lifecycle.test.ts": 163, - "src/main/computer/desktop-script-provider-cache.test.ts": 148, - "src/main/computer/desktop-script-provider-client.test.ts": 216, - "src/main/computer/desktop-script-provider-errors.test.ts": 290, - "src/main/computer/desktop-script-provider-paste-validation.test.ts": 123, - "src/main/computer/desktop-script-provider-runtime-host-routing.test.ts": 231, - "src/main/computer/desktop-script-runtime-host.test.ts": 248, - "src/main/computer/desktop-script-runtime-host.win32.test.ts": 80, - "src/main/computer/desktop-script-serve-channel.test.ts": 112, - "src/main/computer/macos-computer-use-permission-status.test.ts": 101, - "src/main/computer/macos-computer-use-permissions.test.ts": 115, - "src/main/computer/macos-native-provider-client.test.ts": 1179, - "src/main/computer/macos-native-provider-line-buffer.test.ts": 63, - "src/main/computer/macos-native-provider-paste-validation.test.ts": 166, - "src/main/computer/macos-native-provider-process-reaping.test.ts": 103, - "src/main/computer/macos-native-provider-reaping.integration.test.ts": 4160, - "src/main/computer/macos-native-provider-socket.test.ts": 60, - "src/main/computer/macos-native-provider-startup-cleanup.test.ts": 82, - "src/main/computer/sidecar-client.test.ts": 130, - "src/main/computer/sidecar-provider-reaping.integration.test.ts": 335, - "src/main/computer/windows-powershell-execution-policy.test.ts": 54, - "src/main/copilot/hook-service.test.ts": 112, - "src/main/crash-reporting/crash-breadcrumb-renderer-attribution.test.ts": 61, - "src/main/crash-reporting/crash-breadcrumb-store-leak.test.ts": 123, - "src/main/crash-reporting/crash-breadcrumb-store-orphan-cleanup.test.ts": 96, - "src/main/crash-reporting/crash-breadcrumb-store.test.ts": 156, - "src/main/crash-reporting/crash-report-copy-text.test.ts": 58, - "src/main/crash-reporting/crash-report-store.test.ts": 455, - "src/main/crash-reporting/crashpad-capture-read-limit.test.ts": 274, - "src/main/crash-reporting/crashpad-capture.test.ts": 102, - "src/main/crash-reporting/durable-crash-breadcrumb.test.ts": 67, - "src/main/crash-reporting/expected-teardown-state.test.ts": 57, - "src/main/crash-reporting/gpu-crash-diagnostics.test.ts": 63, - "src/main/crash-reporting/gpu-crash-fallback-decision.test.ts": 49, - "src/main/crash-reporting/gpu-crash-fallback-field-sessions.test.ts": 48, - "src/main/crash-reporting/gpu-fallback-engagement.test.ts": 78, - "src/main/crash-reporting/gpu-fallback-recovered-launch.test.ts": 53, - "src/main/crash-reporting/gpu-fallback-restart-prompt.test.ts": 55, - "src/main/crash-reporting/low-commit-oom-recovery-gate.test.ts": 61, - "src/main/crash-reporting/main-process-lifecycle-identity.test.ts": 51, - "src/main/crash-reporting/minidump-crash-signature.test.ts": 88, - "src/main/crash-reporting/minidump-file-source.test.ts": 248, - "src/main/crash-reporting/pre-gone-host-memory.test.ts": 114, - "src/main/crash-reporting/process-gone-classification.test.ts": 56, - "src/main/crash-reporting/process-gone-dedupe.test.ts": 52, - "src/main/crash-reporting/process-gone-diagnostics.test.ts": 112, - "src/main/crash-reporting/process-gone-killed-one-ordering.test.ts": 172, - "src/main/crash-reporting/process-gone-recorder.test.ts": 767, - "src/main/crash-reporting/process-gone-sibling-correlation.test.ts": 643, - "src/main/crash-reporting/renderer-recovery-circuit-breaker.test.ts": 66, - "src/main/crash-reporting/self-initiated-tree-kill-log.test.ts": 126, - "src/main/crash-reporting/suppressed-process-gone-breadcrumb.test.ts": 49, + "src/main/codex/codex-structured-prompt-ownership.test.ts": 599, + "src/main/codex/codex-structured-prompt-replies.test.ts": 271, + "src/main/codex/codex-structured-question-order.test.ts": 1568, + "src/main/codex/codex-structured-real-cli-visuals.test.ts": 312, + "src/main/codex/codex-structured-reasoning-lifecycle.test.ts": 385, + "src/main/codex/codex-structured-rewind.test.ts": 424, + "src/main/codex/codex-structured-service-tier.test.ts": 554, + "src/main/codex/codex-structured-session-adapter-lifecycle.test.ts": 478, + "src/main/codex/codex-structured-session-adapter.test.ts": 1004, + "src/main/codex/codex-structured-session-background-tasks.test.ts": 533, + "src/main/codex/codex-structured-session-cancel.test.ts": 449, + "src/main/codex/codex-structured-session-close.test.ts": 507, + "src/main/codex/codex-structured-session-exit-blame.test.ts": 529, + "src/main/codex/codex-structured-session-options-catalog.test.ts": 521, + "src/main/codex/codex-structured-session-options.test.ts": 557, + "src/main/codex/codex-structured-session-prompts.test.ts": 482, + "src/main/codex/codex-structured-session-shutdown.test.ts": 551, + "src/main/codex/codex-structured-sign-in.test.ts": 367, + "src/main/codex/codex-structured-thread-goal.test.ts": 282, + "src/main/codex/codex-structured-thread-open.test.ts": 383, + "src/main/codex/codex-structured-turn-end-settlement.test.ts": 1408, + "src/main/codex/codex-structured-turn-open-wait.test.ts": 1524, + "src/main/codex/codex-structured-turn-steer.test.ts": 744, + "src/main/codex/codex-structured-visuals.test.ts": 775, + "src/main/codex/codex-subagent-activity.test.ts": 49, + "src/main/codex/codex-subagent-execution-projection.test.ts": 384, + "src/main/codex/codex-subagent-executions.test.ts": 53, + "src/main/codex/codex-subagent-interrupts.test.ts": 580, + "src/main/codex/codex-tool-identity-translation.test.ts": 70, + "src/main/codex/codex-trust-config-concurrent-launch.test.ts": 220, + "src/main/codex/codex-trust-config-mutation-queue.test.ts": 44, + "src/main/codex/codex-trust-grant-host.test.ts": 93, + "src/main/codex/codex-trust-grant-ledger.test.ts": 72, + "src/main/codex/codex-trust-grant-telemetry.test.ts": 176, + "src/main/codex/codex-turn-ordinals.test.ts": 88, + "src/main/codex/codex-user-hook-trust-moves.test.ts": 112, + "src/main/codex/codex-wsl-hook-install-plan.test.ts": 48, + "src/main/codex/config-features-settings-promotion.test.ts": 157, + "src/main/codex/config-plugin-registration-promotion.test.ts": 313, + "src/main/codex/config-settings-baseline-upgrade.test.ts": 162, + "src/main/codex/config-settings-promotion.test.ts": 315, + "src/main/codex/config-sync-stall.test.ts": 148, + "src/main/codex/config-toml-hook-state-duplicate-repair.test.ts": 161, + "src/main/codex/config-toml-hook-trust-scaling.test.ts": 113, + "src/main/codex/config-toml-mcp-servers.test.ts": 47, + "src/main/codex/config-toml-project-duplicate-repair.test.ts": 89, + "src/main/codex/config-toml-project-trust-spellings.test.ts": 155, + "src/main/codex/config-toml-promoted-setting-values.test.ts": 47, + "src/main/codex/config-toml-trust-hash.test.ts": 92, + "src/main/codex/config-toml-trust-hook-read.test.ts": 113, + "src/main/codex/config-toml-trust-hook-removal.test.ts": 116, + "src/main/codex/config-toml-trust-hook-upsert.test.ts": 166, + "src/main/codex/config-toml-trust-key.test.ts": 111, + "src/main/codex/config-toml-trust-loadability.test.ts": 163, + "src/main/codex/config-toml-trust-paths.test.ts": 92, + "src/main/codex/config-toml-trust-project.test.ts": 105, + "src/main/codex/config-toml-trust-stale-writes.test.ts": 75, + "src/main/codex/hook-service-concurrent-launch-install.test.ts": 954, + "src/main/codex/hook-service-legacy-cleanup.test.ts": 713, + "src/main/codex/hook-service-managed-codex-hashes.test.ts": 885, + "src/main/codex/hook-service-managed-install.test.ts": 389, + "src/main/codex/hook-service-runtime-trust-repair.test.ts": 366, + "src/main/codex/hook-service-trust-grant.test.ts": 263, + "src/main/codex/hook-service-user-hook-mirroring.test.ts": 540, + "src/main/codex/hook-service-user-hook-trust-spellings.test.ts": 511, + "src/main/codex/hook-service-windows-key-spellings.test.ts": 353, + "src/main/codex/hook-service-wsl-runtime.test.ts": 443, + "src/main/codex/hook-trust-promotion.test.ts": 660, + "src/main/codex/retained-codex-hook-state.test.ts": 43, + "src/main/codex/sta-4735-hook-trust-provenance-overwrite.test.ts": 108, + "src/main/codex/sta-4737-unreadable-is-not-absent.test.ts": 160, + "src/main/codex/sta-4823-shared-state-survives-a-failed-read.test.ts": 231, + "src/main/codex/windows-hook-command.test.ts": 110, + "src/main/codex/windows-hook-upgrade.test.ts": 255, + "src/main/codex/wsl-codex-session-bridge.test.ts": 90, + "src/main/command-code/hook-service.test.ts": 269, + "src/main/computer/computer-action-verification-normalization.test.ts": 32, + "src/main/computer/computer-provider-lifecycle.test.ts": 181, + "src/main/computer/computer-provider-unavailable-message.test.ts": 33, + "src/main/computer/computer-sidecar-diagnostics.test.ts": 39, + "src/main/computer/desktop-script-provider-action-errors.test.ts": 207, + "src/main/computer/desktop-script-provider-actions.test.ts": 202, + "src/main/computer/desktop-script-provider-bridge.test.ts": 35, + "src/main/computer/desktop-script-provider-cache-lifecycle.test.ts": 95, + "src/main/computer/desktop-script-provider-cache.test.ts": 169, + "src/main/computer/desktop-script-provider-client.test.ts": 126, + "src/main/computer/desktop-script-provider-errors.test.ts": 264, + "src/main/computer/desktop-script-provider-paste-validation.test.ts": 97, + "src/main/computer/desktop-script-provider-runtime-host-routing.test.ts": 176, + "src/main/computer/desktop-script-runtime-host.test.ts": 255, + "src/main/computer/desktop-script-runtime-host.win32.test.ts": 56, + "src/main/computer/desktop-script-serve-channel.test.ts": 136, + "src/main/computer/macos-computer-use-permission-status-cleanup.integration.test.ts": 51, + "src/main/computer/macos-computer-use-permission-status.test.ts": 5094, + "src/main/computer/macos-computer-use-permissions.test.ts": 111, + "src/main/computer/macos-native-provider-client.test.ts": 1242, + "src/main/computer/macos-native-provider-line-buffer.test.ts": 44, + "src/main/computer/macos-native-provider-paste-validation.test.ts": 191, + "src/main/computer/macos-native-provider-process-reaping.test.ts": 84, + "src/main/computer/macos-native-provider-reaping.integration.test.ts": 4146, + "src/main/computer/macos-native-provider-socket.test.ts": 36, + "src/main/computer/macos-native-provider-startup-cleanup.test.ts": 44, + "src/main/computer/sidecar-client.test.ts": 88, + "src/main/computer/sidecar-provider-reaping.integration.test.ts": 295, + "src/main/computer/windows-powershell-execution-policy.test.ts": 36, + "src/main/copilot/hook-service.test.ts": 164, + "src/main/crash-reporting/appimage-crashpad-handler-exit.test.ts": 31, + "src/main/crash-reporting/crash-breadcrumb-renderer-attribution.test.ts": 58, + "src/main/crash-reporting/crash-breadcrumb-store-leak.test.ts": 61, + "src/main/crash-reporting/crash-breadcrumb-store-orphan-cleanup.test.ts": 89, + "src/main/crash-reporting/crash-breadcrumb-store.test.ts": 161, + "src/main/crash-reporting/crash-report-copy-text.test.ts": 38, + "src/main/crash-reporting/crash-report-store.test.ts": 392, + "src/main/crash-reporting/crashpad-capture-read-limit.test.ts": 253, + "src/main/crash-reporting/crashpad-capture.test.ts": 90, + "src/main/crash-reporting/durable-crash-breadcrumb.test.ts": 54, + "src/main/crash-reporting/expected-teardown-state.test.ts": 37, + "src/main/crash-reporting/gpu-crash-diagnostics.test.ts": 47, + "src/main/crash-reporting/gpu-crash-fallback-decision.test.ts": 50, + "src/main/crash-reporting/gpu-crash-fallback-field-sessions.test.ts": 34, + "src/main/crash-reporting/gpu-fallback-engagement.test.ts": 48, + "src/main/crash-reporting/gpu-fallback-recovered-launch.test.ts": 54, + "src/main/crash-reporting/gpu-fallback-restart-prompt.test.ts": 37, + "src/main/crash-reporting/low-commit-oom-recovery-gate.test.ts": 49, + "src/main/crash-reporting/main-process-lifecycle-identity.test.ts": 29, + "src/main/crash-reporting/minidump-crash-signature.test.ts": 72, + "src/main/crash-reporting/minidump-file-source.test.ts": 151, + "src/main/crash-reporting/pre-gone-host-memory.test.ts": 98, + "src/main/crash-reporting/process-gone-classification.test.ts": 44, + "src/main/crash-reporting/process-gone-dedupe.test.ts": 35, + "src/main/crash-reporting/process-gone-diagnostics.test.ts": 100, + "src/main/crash-reporting/process-gone-killed-one-ordering.test.ts": 85, + "src/main/crash-reporting/process-gone-recorder.test.ts": 846, + "src/main/crash-reporting/process-gone-sibling-correlation.test.ts": 604, + "src/main/crash-reporting/renderer-heap-allocation-profile.test.ts": 537, + "src/main/crash-reporting/renderer-heap-allocation-sites.test.ts": 52, + "src/main/crash-reporting/renderer-recovery-circuit-breaker.test.ts": 29, + "src/main/crash-reporting/self-initiated-tree-kill-log.test.ts": 96, + "src/main/crash-reporting/suppressed-process-gone-breadcrumb.test.ts": 27, "src/main/credential-file-protection.test.ts": 44, - "src/main/cursor/hook-command.live-shell.test.ts": 562, - "src/main/cursor/hook-service.test.ts": 264, - "src/main/daemon/agent-startup-prompt-latency.node-pty.test.ts": 2622, - "src/main/daemon/bash-prompt-command-composition.test.ts": 3412, - "src/main/daemon/client.test.ts": 426, - "src/main/daemon/cold-restore-payload-cache.test.ts": 58, - "src/main/daemon/daemon-adoption-telemetry-event.test.ts": 399, - "src/main/daemon/daemon-audit-eligibility-event.test.ts": 962, - "src/main/daemon/daemon-authenticated-client-activity.test.ts": 666, - "src/main/daemon/daemon-background-transient-facts.test.ts": 97, - "src/main/daemon/daemon-bundle-staleness.test.ts": 425, - "src/main/daemon/daemon-cgroup-scope.test.ts": 105, - "src/main/daemon/daemon-checkpoint-session-queue.test.ts": 227, - "src/main/daemon/daemon-client-frame-validation.test.ts": 552, - "src/main/daemon/daemon-client-notify-settlement.test.ts": 52, - "src/main/daemon/daemon-client-reader-handoff.test.ts": 61, - "src/main/daemon/daemon-client-rpc-request.test.ts": 80, - "src/main/daemon/daemon-client-stream-keepalive.test.ts": 53, - "src/main/daemon/daemon-color-query-reply-colors.test.ts": 857, - "src/main/daemon/daemon-durable-checkpoint-live-rebase.test.ts": 990, - "src/main/daemon/daemon-endpoint-ownership.test.ts": 648, - "src/main/daemon/daemon-endpoint-publish.test.ts": 219, - "src/main/daemon/daemon-endpoint-windows.test.ts": 61, - "src/main/daemon/daemon-entry-path-layouts.test.ts": 546, - "src/main/daemon/daemon-entry.test.ts": 811, - "src/main/daemon/daemon-errors.test.ts": 293, - "src/main/daemon/daemon-file-log.test.ts": 83, - "src/main/daemon/daemon-folder-access-mismatch.test.ts": 300, - "src/main/daemon/daemon-folder-access-probe-script.test.ts": 194, - "src/main/daemon/daemon-folder-access-probe.test.ts": 66, - "src/main/daemon/daemon-folder-access-reset.test.ts": 296, - "src/main/daemon/daemon-foreground-confirmation-protocol.test.ts": 60, - "src/main/daemon/daemon-health-endpoint-entry.test.ts": 121, - "src/main/daemon/daemon-health-socket-cleanup.test.ts": 124, - "src/main/daemon/daemon-health.test.ts": 3789, - "src/main/daemon/daemon-host-current-version-reclaim.test.ts": 200, - "src/main/daemon/daemon-host-entry-layout.test.ts": 175, - "src/main/daemon/daemon-host-file-hash-cache.test.ts": 164, - "src/main/daemon/daemon-host-process-package.test.ts": 315, - "src/main/daemon/daemon-host-profile-isolation.test.ts": 158, + "src/main/cursor/hook-command.live-shell.test.ts": 614, + "src/main/cursor/hook-service.test.ts": 350, + "src/main/daemon/agent-startup-prompt-latency.node-pty.test.ts": 2497, + "src/main/daemon/automatic-work-run-marker-store.test.ts": 294, + "src/main/daemon/bash-prompt-command-composition.test.ts": 3478, + "src/main/daemon/client.test.ts": 460, + "src/main/daemon/cold-restore-payload-cache.test.ts": 36, + "src/main/daemon/daemon-adoption-telemetry-event.test.ts": 353, + "src/main/daemon/daemon-appimage-copy-host.electron.test.ts": 3055, + "src/main/daemon/daemon-appimage-generation-launch.test.ts": 72, + "src/main/daemon/daemon-appimage-launch.test.ts": 115, + "src/main/daemon/daemon-audit-eligibility-event.test.ts": 825, + "src/main/daemon/daemon-authenticated-client-activity.test.ts": 620, + "src/main/daemon/daemon-automatic-work-guard.test.ts": 317, + "src/main/daemon/daemon-automatic-work-resume.test.ts": 894, + "src/main/daemon/daemon-background-transient-facts.test.ts": 86, + "src/main/daemon/daemon-bundle-staleness.test.ts": 420, + "src/main/daemon/daemon-cgroup-scope.test.ts": 101, + "src/main/daemon/daemon-checkpoint-session-queue.test.ts": 231, + "src/main/daemon/daemon-client-frame-validation.test.ts": 811, + "src/main/daemon/daemon-client-notify-settlement.test.ts": 46, + "src/main/daemon/daemon-client-reader-handoff.test.ts": 72, + "src/main/daemon/daemon-client-rpc-request.test.ts": 88, + "src/main/daemon/daemon-client-stream-keepalive.test.ts": 36, + "src/main/daemon/daemon-color-query-reply-colors.test.ts": 908, + "src/main/daemon/daemon-durable-checkpoint-live-rebase.test.ts": 989, + "src/main/daemon/daemon-electron-to-node-adoption.electron.test.ts": 3107, + "src/main/daemon/daemon-endpoint-ownership.test.ts": 664, + "src/main/daemon/daemon-endpoint-publish.test.ts": 171, + "src/main/daemon/daemon-endpoint-windows.test.ts": 43, + "src/main/daemon/daemon-entry-path-layouts.test.ts": 646, + "src/main/daemon/daemon-entry.test.ts": 680, + "src/main/daemon/daemon-errors.test.ts": 292, + "src/main/daemon/daemon-file-log.test.ts": 52, + "src/main/daemon/daemon-folder-access-mismatch.test.ts": 360, + "src/main/daemon/daemon-folder-access-probe-script.test.ts": 180, + "src/main/daemon/daemon-folder-access-probe.test.ts": 42, + "src/main/daemon/daemon-folder-access-reset.test.ts": 315, + "src/main/daemon/daemon-foreground-confirmation-protocol.test.ts": 41, + "src/main/daemon/daemon-full-disk-access-status.test.ts": 39, + "src/main/daemon/daemon-health-endpoint-entry.test.ts": 104, + "src/main/daemon/daemon-health-socket-cleanup.test.ts": 116, + "src/main/daemon/daemon-health.test.ts": 3807, + "src/main/daemon/daemon-host-current-version-reclaim.test.ts": 301, + "src/main/daemon/daemon-host-entry-layout.test.ts": 151, + "src/main/daemon/daemon-host-file-hash-cache.test.ts": 161, + "src/main/daemon/daemon-host-image.test.ts": 38, + "src/main/daemon/daemon-host-path-budget.test.ts": 120, + "src/main/daemon/daemon-host-process-package.test.ts": 436, + "src/main/daemon/daemon-host-profile-isolation.test.ts": 163, "src/main/daemon/daemon-host-relocation.test.ts": 333, - "src/main/daemon/daemon-idle-inventory-budget.test.ts": 651, - "src/main/daemon/daemon-idle-shutdown.test.ts": 9146, - "src/main/daemon/daemon-incarnation-evidence-main-thread.test.ts": 127, - "src/main/daemon/daemon-incarnation-evidence.test.ts": 130, - "src/main/daemon/daemon-init-child-readiness.test.ts": 674, - "src/main/daemon/daemon-init-child-startup-failure.test.ts": 1005, - "src/main/daemon/daemon-init-endpoint-adoption.test.ts": 1604, - "src/main/daemon/daemon-init-live-session-preservation.test.ts": 961, - "src/main/daemon/daemon-init-packaged-bundle-staleness.test.ts": 539, - "src/main/daemon/daemon-init-provider-installation.test.ts": 509, - "src/main/daemon/daemon-init-replacement-reporting.test.ts": 896, - "src/main/daemon/daemon-init-restart-sequence.test.ts": 1523, - "src/main/daemon/daemon-init-wedged-daemon-grace.test.ts": 801, - "src/main/daemon/daemon-launched-child-identity.test.ts": 82, - "src/main/daemon/daemon-launched-child-spawn.test.ts": 65, - "src/main/daemon/daemon-lifecycle-event.test.ts": 312, - "src/main/daemon/daemon-mac-code-identity.test.ts": 70, - "src/main/daemon/daemon-main.test.ts": 781, - "src/main/daemon/daemon-native-pty-exception.test.ts": 62, - "src/main/daemon/daemon-out-of-process-launcher-host-prune.test.ts": 218, - "src/main/daemon/daemon-pid-file-parse.test.ts": 44, - "src/main/daemon/daemon-preflight-client-replacement.test.ts": 927, - "src/main/daemon/daemon-process-identity-time-zone.test.ts": 57, - "src/main/daemon/daemon-process-inspection.test.ts": 95, - "src/main/daemon/daemon-provider-census.test.ts": 120, - "src/main/daemon/daemon-provider-routing.test.ts": 331, - "src/main/daemon/daemon-pty-adapter-closing-session-inventory.test.ts": 822, - "src/main/daemon/daemon-pty-adapter-cold-restore-reanchor.test.ts": 1311, - "src/main/daemon/daemon-pty-adapter-cold-restore-seed.test.ts": 1180, - "src/main/daemon/daemon-pty-adapter-concurrent-recovery.test.ts": 1076, - "src/main/daemon/daemon-pty-adapter-daemon-recovery.test.ts": 1414, - "src/main/daemon/daemon-pty-adapter-history-checkpoints.test.ts": 2079, - "src/main/daemon/daemon-pty-adapter-history-recovery.test.ts": 1304, - "src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts": 827, - "src/main/daemon/daemon-pty-adapter-live-rebased-checkpoint.test.ts": 1578, - "src/main/daemon/daemon-pty-adapter-protocol-compatibility.test.ts": 1037, - "src/main/daemon/daemon-pty-adapter-replacement-exit-race.test.ts": 870, - "src/main/daemon/daemon-pty-adapter-session-adoption.test.ts": 1432, - "src/main/daemon/daemon-pty-adapter-steady-state-compat.test.ts": 649, - "src/main/daemon/daemon-pty-adapter.test.ts": 1757, - "src/main/daemon/daemon-pty-router-history-handoff.test.ts": 1347, - "src/main/daemon/daemon-pty-router.test.ts": 485, - "src/main/daemon/daemon-pty-startup-delivery.test.ts": 3028, - "src/main/daemon/daemon-pty-upgrade-adoption.test.ts": 836, - "src/main/daemon/daemon-pty-write-settlement-recovery.test.ts": 684, - "src/main/daemon/daemon-ready-identity.test.ts": 56, - "src/main/daemon/daemon-reattach-checkpoint-isolation.test.ts": 949, - "src/main/daemon/daemon-respawn-throttle.test.ts": 106, - "src/main/daemon/daemon-restore-scrollback-depth.test.ts": 2335, - "src/main/daemon/daemon-router-retirement.test.ts": 67, - "src/main/daemon/daemon-scope-death-watch.test.ts": 89, - "src/main/daemon/daemon-self-retirement-respawn.test.ts": 1065, - "src/main/daemon/daemon-server-async-spawn-cancellation.test.ts": 1280, - "src/main/daemon/daemon-server-attach-only.test.ts": 738, - "src/main/daemon/daemon-server-attachment-lifecycle.test.ts": 919, - "src/main/daemon/daemon-server-error-handling.test.ts": 818, - "src/main/daemon/daemon-server-kill-attribution.test.ts": 693, - "src/main/daemon/daemon-server.test.ts": 1370, - "src/main/daemon/daemon-session-owner-resolution.test.ts": 89, - "src/main/daemon/daemon-session-scrollback-window.test.ts": 477, - "src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts": 856, - "src/main/daemon/daemon-shutdown-final-checkpoint-caller-deadline.test.ts": 2550, - "src/main/daemon/daemon-socket-endpoint-path.test.ts": 428, - "src/main/daemon/daemon-spawner.test.ts": 728, - "src/main/daemon/daemon-stream-backpressure-socket.test.ts": 454, - "src/main/daemon/daemon-stream-backpressure.test.ts": 71, - "src/main/daemon/daemon-stream-binary-framing.test.ts": 69, - "src/main/daemon/daemon-stream-binary-negotiation.test.ts": 550, - "src/main/daemon/daemon-stream-binary-unicode.test.ts": 60, - "src/main/daemon/daemon-stream-data-batcher.test.ts": 235, - "src/main/daemon/daemon-stream-data-split.test.ts": 700, - "src/main/daemon/daemon-stream-droppability-lifecycle.test.ts": 644, - "src/main/daemon/daemon-stream-droppable-membership.test.ts": 131, - "src/main/daemon/daemon-stream-framing-replacement.test.ts": 58, - "src/main/daemon/daemon-stream-producer-stall-watchdog.test.ts": 126, - "src/main/daemon/daemon-tcc-attribution-main-thread.test.ts": 132, - "src/main/daemon/daemon-tcc-attribution.test.ts": 101, - "src/main/daemon/daemon-transport-attachment-release.test.ts": 790, - "src/main/daemon/daemon-user-manager-lifetime.test.ts": 77, - "src/main/daemon/degraded-daemon-pty-provider.test.ts": 126, - "src/main/daemon/hangul-cell-width-agreement.test.ts": 100, - "src/main/daemon/headless-emulator-fidelity.fuzz.test.ts": 2924, - "src/main/daemon/headless-emulator-restored-osc-links.test.ts": 161, - "src/main/daemon/headless-emulator-shrink-snapshot.test.ts": 175, - "src/main/daemon/headless-emulator-snapshot-cost.bench.test.ts": 101, - "src/main/daemon/headless-emulator-unicode-width.test.ts": 141, - "src/main/daemon/headless-emulator-wide-char-repaint.test.ts": 1711, - "src/main/daemon/headless-emulator-wide-char-snapshot.test.ts": 273, - "src/main/daemon/headless-emulator.test.ts": 341, - "src/main/daemon/headless-osc-link-ranges.test.ts": 185, - "src/main/daemon/hibernation-cold-restore-repro.test.ts": 203, - "src/main/daemon/history-manager-disabled-sessions-leak.test.ts": 173, - "src/main/daemon/history-manager.test.ts": 371, - "src/main/daemon/history-reader-memory.test.ts": 440, - "src/main/daemon/history-reader.test.ts": 242, - "src/main/daemon/issue-6814-daemon-failure-classification.test.ts": 835, - "src/main/daemon/macos-login-session-death-watch.test.ts": 89, - "src/main/daemon/ndjson.test.ts": 135, - "src/main/daemon/node-pty-error-hints.test.ts": 50, - "src/main/daemon/node-pty-windows-input-error.win32.test.ts": 52, - "src/main/daemon/osc7-file-uri.test.ts": 49, - "src/main/daemon/osc7-uri-extraction.test.ts": 49, - "src/main/daemon/post-ready-flush-gate.test.ts": 67, - "src/main/daemon/process-boundary-ground.test.ts": 230, - "src/main/daemon/process-boundary-host-focus.test.ts": 296, - "src/main/daemon/pty-session-id.test.ts": 68, - "src/main/daemon/pty-subprocess-cwd-cancel-identity.test.ts": 454, - "src/main/daemon/pty-subprocess-env-inheritance.test.ts": 436, - "src/main/daemon/pty-subprocess-env-retention.test.ts": 361, - "src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts": 528, - "src/main/daemon/pty-subprocess-foreground-identity.test.ts": 719, - "src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts": 428, - "src/main/daemon/pty-subprocess-git-credential-guard.test.ts": 439, - "src/main/daemon/pty-subprocess-handle-lifecycle.test.ts": 525, - "src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts": 1307, - "src/main/daemon/pty-subprocess-io-failure-native.test.ts": 2779, - "src/main/daemon/pty-subprocess-managed-agent-env.test.ts": 559, - "src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts": 479, - "src/main/daemon/pty-subprocess-windows-shell-launch.test.ts": 434, - "src/main/daemon/pty-subprocess-wsl-launch.test.ts": 482, - "src/main/daemon/pty-subprocess.test.ts": 441, + "src/main/daemon/daemon-idle-inventory-budget.test.ts": 815, + "src/main/daemon/daemon-idle-shutdown.test.ts": 9356, + "src/main/daemon/daemon-incarnation-evidence-main-thread.test.ts": 116, + "src/main/daemon/daemon-incarnation-evidence.test.ts": 135, + "src/main/daemon/daemon-init-child-readiness.test.ts": 769, + "src/main/daemon/daemon-init-child-startup-failure.test.ts": 1151, + "src/main/daemon/daemon-init-endpoint-adoption.test.ts": 2067, + "src/main/daemon/daemon-init-live-session-preservation.test.ts": 1140, + "src/main/daemon/daemon-init-macos-stable-launch-fallback.test.ts": 914, + "src/main/daemon/daemon-init-packaged-bundle-staleness.test.ts": 819, + "src/main/daemon/daemon-init-provider-installation.test.ts": 747, + "src/main/daemon/daemon-init-replacement-reporting.test.ts": 1282, + "src/main/daemon/daemon-init-restart-sequence.test.ts": 1813, + "src/main/daemon/daemon-init-severed-host-image.test.ts": 1077, + "src/main/daemon/daemon-init-wedged-daemon-grace.test.ts": 1067, + "src/main/daemon/daemon-launched-child-identity.test.ts": 77, + "src/main/daemon/daemon-launched-child-spawn.test.ts": 120, + "src/main/daemon/daemon-lifecycle-event.test.ts": 268, + "src/main/daemon/daemon-liveness-report.test.ts": 560, + "src/main/daemon/daemon-mac-code-identity.test.ts": 36, + "src/main/daemon/daemon-main.test.ts": 673, + "src/main/daemon/daemon-native-pty-exception.test.ts": 33, + "src/main/daemon/daemon-newer-protocol-hold.test.ts": 186, + "src/main/daemon/daemon-out-of-process-launcher-host-prune.test.ts": 326, + "src/main/daemon/daemon-out-of-process-launcher-identity.test.ts": 167, + "src/main/daemon/daemon-pid-file-parse.test.ts": 32, + "src/main/daemon/daemon-pid-identity-native.win32.test.ts": 102, + "src/main/daemon/daemon-pid-identity-windows.test.ts": 74, + "src/main/daemon/daemon-preflight-client-replacement.test.ts": 746, + "src/main/daemon/daemon-process-identity-time-zone.test.ts": 39, + "src/main/daemon/daemon-process-inspection.test.ts": 96, + "src/main/daemon/daemon-protocol-cleanup.test.ts": 80, + "src/main/daemon/daemon-provider-census.test.ts": 257, + "src/main/daemon/daemon-provider-routing.test.ts": 585, + "src/main/daemon/daemon-pty-adapter-closing-session-inventory.test.ts": 1002, + "src/main/daemon/daemon-pty-adapter-cold-restore-reanchor.test.ts": 1308, + "src/main/daemon/daemon-pty-adapter-cold-restore-seed.test.ts": 1218, + "src/main/daemon/daemon-pty-adapter-concurrent-recovery.test.ts": 995, + "src/main/daemon/daemon-pty-adapter-daemon-recovery.test.ts": 1534, + "src/main/daemon/daemon-pty-adapter-full-disk-access.test.ts": 538, + "src/main/daemon/daemon-pty-adapter-history-checkpoints.test.ts": 1706, + "src/main/daemon/daemon-pty-adapter-history-recovery.test.ts": 1372, + "src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts": 917, + "src/main/daemon/daemon-pty-adapter-live-rebased-checkpoint.test.ts": 1595, + "src/main/daemon/daemon-pty-adapter-protocol-compatibility.test.ts": 899, + "src/main/daemon/daemon-pty-adapter-replacement-exit-race.test.ts": 943, + "src/main/daemon/daemon-pty-adapter-session-adoption.test.ts": 1611, + "src/main/daemon/daemon-pty-adapter-spawn-intent.test.ts": 615, + "src/main/daemon/daemon-pty-adapter-steady-state-compat.test.ts": 724, + "src/main/daemon/daemon-pty-adapter-subscription-fanout.test.ts": 38, + "src/main/daemon/daemon-pty-adapter.test.ts": 1940, + "src/main/daemon/daemon-pty-router-history-handoff.test.ts": 1121, + "src/main/daemon/daemon-pty-router-legacy-gone.test.ts": 159, + "src/main/daemon/daemon-pty-router-newer-daemon-hold.test.ts": 108, + "src/main/daemon/daemon-pty-router.test.ts": 830, + "src/main/daemon/daemon-pty-startup-delivery.test.ts": 1037, + "src/main/daemon/daemon-pty-upgrade-adoption.test.ts": 941, + "src/main/daemon/daemon-pty-write-settlement-recovery.test.ts": 582, + "src/main/daemon/daemon-ready-identity.test.ts": 35, + "src/main/daemon/daemon-reattach-checkpoint-isolation.test.ts": 1044, + "src/main/daemon/daemon-respawn-throttle.test.ts": 86, + "src/main/daemon/daemon-restore-scrollback-depth.test.ts": 2519, + "src/main/daemon/daemon-router-retirement.test.ts": 73, + "src/main/daemon/daemon-scope-death-watch.test.ts": 76, + "src/main/daemon/daemon-self-retirement-respawn.test.ts": 1119, + "src/main/daemon/daemon-server-async-spawn-cancellation.test.ts": 1516, + "src/main/daemon/daemon-server-attach-only.test.ts": 651, + "src/main/daemon/daemon-server-attachment-lifecycle.test.ts": 720, + "src/main/daemon/daemon-server-error-handling.test.ts": 731, + "src/main/daemon/daemon-server-kill-attribution.test.ts": 662, + "src/main/daemon/daemon-server.test.ts": 1396, + "src/main/daemon/daemon-session-owner-resolution.test.ts": 61, + "src/main/daemon/daemon-session-scrollback-window.test.ts": 442, + "src/main/daemon/daemon-severed-restart.test.ts": 516, + "src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts": 1051, + "src/main/daemon/daemon-shutdown-final-checkpoint-caller-deadline.test.ts": 2814, + "src/main/daemon/daemon-socket-endpoint-path.test.ts": 473, + "src/main/daemon/daemon-spawner.test.ts": 762, + "src/main/daemon/daemon-stream-backpressure-socket.test.ts": 420, + "src/main/daemon/daemon-stream-backpressure.test.ts": 59, + "src/main/daemon/daemon-stream-binary-framing.test.ts": 91, + "src/main/daemon/daemon-stream-binary-negotiation.test.ts": 1188, + "src/main/daemon/daemon-stream-binary-unicode.test.ts": 85, + "src/main/daemon/daemon-stream-data-batcher.test.ts": 285, + "src/main/daemon/daemon-stream-data-split.test.ts": 617, + "src/main/daemon/daemon-stream-droppability-lifecycle.test.ts": 708, + "src/main/daemon/daemon-stream-droppable-membership.test.ts": 95, + "src/main/daemon/daemon-stream-framing-replacement.test.ts": 73, + "src/main/daemon/daemon-stream-ingest-turn-bound.test.ts": 348, + "src/main/daemon/daemon-stream-producer-stall-watchdog.test.ts": 109, + "src/main/daemon/daemon-tcc-attribution-main-thread.test.ts": 112, + "src/main/daemon/daemon-tcc-attribution.test.ts": 103, + "src/main/daemon/daemon-transport-attachment-release.test.ts": 825, + "src/main/daemon/daemon-user-manager-lifetime.test.ts": 73, + "src/main/daemon/degraded-daemon-pty-provider.test.ts": 130, + "src/main/daemon/desktop-restore-admission.test.ts": 590, + "src/main/daemon/hangul-cell-width-agreement.test.ts": 63, + "src/main/daemon/headless-emulator-fidelity.fuzz.test.ts": 2829, + "src/main/daemon/headless-emulator-restored-osc-links.test.ts": 135, + "src/main/daemon/headless-emulator-shrink-snapshot.test.ts": 157, + "src/main/daemon/headless-emulator-snapshot-cost.bench.test.ts": 93, + "src/main/daemon/headless-emulator-unicode-width.test.ts": 86, + "src/main/daemon/headless-emulator-wide-char-repaint.test.ts": 1429, + "src/main/daemon/headless-emulator-wide-char-snapshot.test.ts": 263, + "src/main/daemon/headless-emulator.test.ts": 332, + "src/main/daemon/headless-osc-link-ranges.test.ts": 195, + "src/main/daemon/hibernation-cold-restore-repro.test.ts": 219, + "src/main/daemon/history-manager-disabled-sessions-leak.test.ts": 175, + "src/main/daemon/history-manager.test.ts": 351, + "src/main/daemon/history-reader-memory.test.ts": 433, + "src/main/daemon/history-reader.test.ts": 287, + "src/main/daemon/host-terminal-ledger.test.ts": 111, + "src/main/daemon/issue-6814-daemon-failure-classification.test.ts": 681, + "src/main/daemon/legacy-restore-records.test.ts": 606, + "src/main/daemon/macos-daemon-bundle-retirement.test.ts": 66, + "src/main/daemon/macos-daemon-bundle.test.ts": 140, + "src/main/daemon/macos-daemon-job-state.test.ts": 41, + "src/main/daemon/macos-daemon-launchd.test.ts": 1494, + "src/main/daemon/macos-login-session-death-watch.test.ts": 66, + "src/main/daemon/ndjson.test.ts": 134, + "src/main/daemon/node-pty-error-hints.test.ts": 58, + "src/main/daemon/node-pty-windows-input-error.win32.test.ts": 42, + "src/main/daemon/osc7-file-uri.test.ts": 34, + "src/main/daemon/osc7-uri-extraction.test.ts": 38, + "src/main/daemon/post-ready-flush-gate.test.ts": 45, + "src/main/daemon/process-boundary-ground.test.ts": 207, + "src/main/daemon/process-boundary-host-focus.test.ts": 284, + "src/main/daemon/pty-process-ceiling.test.ts": 94, + "src/main/daemon/pty-session-id.test.ts": 59, + "src/main/daemon/pty-subprocess-cwd-cancel-identity.test.ts": 416, + "src/main/daemon/pty-subprocess-env-inheritance.test.ts": 447, + "src/main/daemon/pty-subprocess-env-retention.test.ts": 357, + "src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts": 532, + "src/main/daemon/pty-subprocess-foreground-identity.test.ts": 650, + "src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts": 485, + "src/main/daemon/pty-subprocess-git-credential-guard.test.ts": 432, + "src/main/daemon/pty-subprocess-handle-lifecycle.test.ts": 514, + "src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts": 862, + "src/main/daemon/pty-subprocess-io-failure-native.test.ts": 2756, + "src/main/daemon/pty-subprocess-managed-agent-env.test.ts": 472, + "src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts": 465, + "src/main/daemon/pty-subprocess-windows-shell-launch.test.ts": 489, + "src/main/daemon/pty-subprocess-wsl-launch.test.ts": 465, + "src/main/daemon/pty-subprocess.test.ts": 495, "src/main/daemon/pty-subprocess/native-pty-spawn-windows.test.ts": 87, - "src/main/daemon/pty-subprocess/spawn-file-foreground-rejected-agents.test.ts": 148, - "src/main/daemon/pty-subprocess/spawn-preflight-health-probe.test.ts": 91, - "src/main/daemon/reattach-snapshot.test.ts": 889, - "src/main/daemon/repro-12101-mouse-tracking-survives-agent-death.test.ts": 284, - "src/main/daemon/repro-7329-remote-snapshot-corruption.test.ts": 163, - "src/main/daemon/serialize-addon-edge-cases.test.ts": 104, - "src/main/daemon/serialize-grid-cell-descriptors.test.ts": 78, - "src/main/daemon/serialize-grid-transcript-replay.test.ts": 24288, - "src/main/daemon/serialize-grid.differential.fuzz.test.ts": 559, - "src/main/daemon/session-ingest-throughput.bench.test.ts": 203, - "src/main/daemon/session-pending-output.test.ts": 350, - "src/main/daemon/session-producer-pause.test.ts": 59, + "src/main/daemon/pty-subprocess/spawn-file-foreground-rejected-agents.test.ts": 150, + "src/main/daemon/pty-subprocess/spawn-preflight-health-probe.test.ts": 101, + "src/main/daemon/reattach-snapshot.test.ts": 1072, + "src/main/daemon/repro-12101-mouse-tracking-survives-agent-death.test.ts": 367, + "src/main/daemon/repro-7329-remote-snapshot-corruption.test.ts": 162, + "src/main/daemon/serialize-addon-edge-cases.test.ts": 79, + "src/main/daemon/serialize-grid-cell-descriptors.test.ts": 137, + "src/main/daemon/serialize-grid-transcript-replay.test.ts": 27203, + "src/main/daemon/serialize-grid.differential.fuzz.test.ts": 545, + "src/main/daemon/session-ingest-throughput.bench.test.ts": 260, + "src/main/daemon/session-pending-output.test.ts": 403, + "src/main/daemon/session-producer-pause.test.ts": 42, "src/main/daemon/session-reset-input-modes.test.ts": 243, - "src/main/daemon/session-shell-recovery.test.ts": 332, - "src/main/daemon/session-terminal-control.test.ts": 275, - "src/main/daemon/session.test.ts": 359, - "src/main/daemon/shell-ready-bash-wrapper.test.ts": 429, - "src/main/daemon/slow-daemon-session-verification.test.ts": 14993, - "src/main/daemon/startup-device-attributes-responder.test.ts": 57, - "src/main/daemon/terminal-attach-cancellation.test.ts": 40, - "src/main/daemon/terminal-checkpoint-serializer.test.ts": 255, - "src/main/daemon/terminal-checkpoint-writer-bounds.test.ts": 310, - "src/main/daemon/terminal-cursor-line-context.test.ts": 146, - "src/main/daemon/terminal-descendant-shutdown.test.ts": 124, - "src/main/daemon/terminal-history-incremental-restore.test.ts": 443, - "src/main/daemon/terminal-history-large-checkpoint-cold-restore.test.ts": 1922, - "src/main/daemon/terminal-history-log-encode-allocation.test.ts": 87, - "src/main/daemon/terminal-history-log.test.ts": 60, - "src/main/daemon/terminal-history-permissions.test.ts": 251, - "src/main/daemon/terminal-history-restorable-retention.test.ts": 55, - "src/main/daemon/terminal-history-seed-chunks.test.ts": 76, - "src/main/daemon/terminal-history-seed-segments.test.ts": 143, - "src/main/daemon/terminal-history-seed-transfer-registry.test.ts": 49, - "src/main/daemon/terminal-history-session-tombstone-retry.test.ts": 81, - "src/main/daemon/terminal-host-agent-session.test.ts": 391, - "src/main/daemon/terminal-host-attach-only.test.ts": 359, - "src/main/daemon/terminal-host-cheap-tier-ps-scan-volume.test.ts": 237, - "src/main/daemon/terminal-host-closing-session-listing.test.ts": 644, - "src/main/daemon/terminal-host-concurrent-create.test.ts": 397, - "src/main/daemon/terminal-host-cwd-readability.test.ts": 422, - "src/main/daemon/terminal-host-non-agent-foreground.test.ts": 179, - "src/main/daemon/terminal-host-process-inspection-cheap-tier.test.ts": 239, - "src/main/daemon/terminal-host-process-inspection.test.ts": 360, - "src/main/daemon/terminal-host-pty-owner-backend.test.ts": 366, - "src/main/daemon/terminal-host-readiness-reporting.test.ts": 447, - "src/main/daemon/terminal-host-session-reaping-leak.test.ts": 411, - "src/main/daemon/terminal-host-spawn-input-retention.test.ts": 641, - "src/main/daemon/terminal-host-startup-staging.test.ts": 355, - "src/main/daemon/terminal-host-startup.test.ts": 374, - "src/main/daemon/terminal-host-teardown-recreate.test.ts": 501, - "src/main/daemon/terminal-host-wsl-context.test.ts": 380, - "src/main/daemon/terminal-host.test.ts": 605, - "src/main/daemon/terminal-session-teardown.test.ts": 77, - "src/main/daemon/terminal-shell-armed-input-mode-recovery.test.ts": 256, - "src/main/daemon/terminal-shell-lifecycle-scanner.test.ts": 69, - "src/main/daemon/terminal-shell-recovery-barrier.test.ts": 1248, - "src/main/daemon/terminal-shell-recovery-clean-exit-retirement.test.ts": 72, - "src/main/daemon/terminal-snapshot-color-parity.test.ts": 136, - "src/main/daemon/terminal-snapshot-mouse-encoding.test.ts": 139, - "src/main/daemon/terminal-snapshot-osc8-roundtrip.test.ts": 130, - "src/main/daemon/terminal-snapshot-serialize-roundtrip.test.ts": 222, - "src/main/daemon/windows-conpty-warmup.test.ts": 56, - "src/main/daemon/wsl-cold-restore-cwd.test.ts": 47, - "src/main/destination-serialized-local-rename.test.ts": 2512, - "src/main/devin/hook-config-json.test.ts": 86, - "src/main/devin/hook-service.test.ts": 131, - "src/main/diagnostics/main-thread-churn-probe.test.ts": 69, - "src/main/dock/unread-badge.test.ts": 69, - "src/main/droid/hook-service.test.ts": 89, - "src/main/dsh/dsh-home-patch.test.ts": 45, - "src/main/dsh/hook-service.test.ts": 106, - "src/main/durable-file-write-syscall-proof.test.ts": 67, - "src/main/durable-file-write.test.ts": 139, - "src/main/emulator/android/adb-devices.test.ts": 65, - "src/main/emulator/android/android-app-control.test.ts": 48, - "src/main/emulator/android/android-capability-operations.test.ts": 100, - "src/main/emulator/android/android-device-inventory.test.ts": 66, - "src/main/emulator/android/android-input-commands.test.ts": 49, - "src/main/emulator/android/android-input-mapping.test.ts": 63, - "src/main/emulator/android/android-logcat.test.ts": 54, - "src/main/emulator/android/android-permissions.test.ts": 49, - "src/main/emulator/android/android-sdk-discovery.test.ts": 61, - "src/main/emulator/android/avd-manager.test.ts": 61, - "src/main/emulator/android/scrcpy-server-deploy.test.ts": 53, - "src/main/emulator/android/scrcpy-stream-session.test.ts": 419, - "src/main/emulator/android/scrcpy-video-frame-parser.test.ts": 162, - "src/main/emulator/android/uiautomator-tree.test.ts": 63, - "src/main/emulator/backends/android-emulator-backend.test.ts": 220, - "src/main/emulator/backends/ios-emulator-backend.test.ts": 74, - "src/main/emulator/emulator-availability.test.ts": 57, - "src/main/emulator/emulator-bridge.test.ts": 335, - "src/main/emulator/emulator-gesture-sender.test.ts": 191, - "src/main/emulator/emulator-session-registry.test.ts": 62, - "src/main/emulator/emulator-start-lease-registry.test.ts": 111, - "src/main/emulator/mjpeg-frame-parser.test.ts": 52, - "src/main/emulator/mjpeg-frame-stream.test.ts": 92, - "src/main/emulator/scrcpy-video-registry.test.ts": 51, - "src/main/emulator/serve-sim-accessibility-tree.test.ts": 74, - "src/main/emulator/serve-sim-ax-normalization.test.ts": 62, - "src/main/emulator/serve-sim-detached-session.test.ts": 72, - "src/main/emulator/serve-sim-execution.test.ts": 90, - "src/main/emulator/serve-sim-helper-processes.test.ts": 58, - "src/main/emulator/serve-sim-runtime-materializer.test.ts": 168, - "src/main/emulator/simctl-simulator-devices.test.ts": 66, - "src/main/ephemeral-vm-recipe-runner.test.ts": 625, - "src/main/ephemeral-vm-resume-integrity.test.ts": 271, - "src/main/ephemeral-vm-runtime-service.test.ts": 834, - "src/main/ephemeral-vm-runtime-ssh-cleanup.test.ts": 272, - "src/main/execution-host-workspace-trust.test.ts": 164, - "src/main/external-editor-launch.test.ts": 121, - "src/main/fish-history-session.test.ts": 88, - "src/main/fish-xdg-data-dirs-handoff.test.ts": 66, - "src/main/fish-xdg-data-dirs-no-config-launch.test.ts": 301, - "src/main/folder-upgrade-worktree-path.test.ts": 57, - "src/main/foreign-sqlite-readers/foreign-sqlite-reader-client-worker-lifecycle.test.ts": 768, - "src/main/foreign-sqlite-readers/foreign-sqlite-reader-client.test.ts": 245, - "src/main/foreign-sqlite-readers/foreign-sqlite-reader-dispatch.test.ts": 258, - "src/main/foreign-sqlite-readers/foreign-sqlite-reader-entry-path.test.ts": 53, - "src/main/foreign-sqlite-readers/foreign-sqlite-reader-event-loop.test.ts": 2385, - "src/main/foreign-sqlite-readers/foreign-sqlite-reader-spawn.test.ts": 49, - "src/main/foreign-sqlite-readers/readers/cursor-profile.test.ts": 517, - "src/main/foreign-sqlite-readers/readers/opencode-binder-sessions.test.ts": 131, - "src/main/gemini/hook-service.test.ts": 87, - "src/main/ghostty/discovery.test.ts": 78, - "src/main/ghostty/index.test.ts": 118, - "src/main/ghostty/mapper-extended.test.ts": 82, - "src/main/ghostty/mapper.test.ts": 85, - "src/main/ghostty/parser.test.ts": 55, - "src/main/ghostty/theme-import.test.ts": 108, - "src/main/ghostty/theme-resolution.test.ts": 71, - "src/main/git-bash.test.ts": 61, - "src/main/git/add-sparse-worktree.test.ts": 219, - "src/main/git/admission-tier-plumbing.test.ts": 61, - "src/main/git/base-ref-search-selector.test.ts": 200, - "src/main/git/branch-rename.test.ts": 67, - "src/main/git/canonical-repo-key.test.ts": 174, - "src/main/git/check-ignored-paths.test.ts": 60, - "src/main/git/coalesced-probe.test.ts": 61, - "src/main/git/command-runner/gh-exec-file-deadline.test.ts": 151, - "src/main/git/command-runner/git-admission-output-parity.test.ts": 196, - "src/main/git/command-runner/git-admission-span.test.ts": 323, - "src/main/git/command-runner/git-admission-storm-measurement.test.ts": 940, - "src/main/git/command-runner/git-command-timeout-behavior.test.ts": 1957, - "src/main/git/command-runner/git-command-timeout.test.ts": 56, - "src/main/git/command-runner/git-exec-admission-exempt.test.ts": 124, - "src/main/git/command-runner/git-exec-admission-lifetime.test.ts": 665, - "src/main/git/command-runner/git-operation-executor.test.ts": 72, - "src/main/git/command-runner/git-spawn-admission-lifetime.test.ts": 170, - "src/main/git/command-runner/git-stream-admission-lifetime.test.ts": 263, - "src/main/git/command-runner/git-subprocess-admission.test.ts": 373, - "src/main/git/command-runner/hosted-cli-deadline-log.test.ts": 145, - "src/main/git/command-runner/spawned-command-tree-kill.test.ts": 74, - "src/main/git/command-runner/wsl-command-resolution-windows-binary.test.ts": 75, - "src/main/git/command-runner/wsl-host-failure.test.ts": 186, - "src/main/git/commit-object-ref.test.ts": 46, - "src/main/git/commit.test.ts": 231, - "src/main/git/create-event-repo-probe.test.ts": 73, - "src/main/git/exact-ref-probe.test.ts": 65, - "src/main/git/fetch-error-classification.test.ts": 53, - "src/main/git/fork-remote-refspec.test.ts": 64, - "src/main/git/fork-remote-stale-branch-refspec.test.ts": 55, - "src/main/git/gh-rate-limit-breaker.test.ts": 70, - "src/main/git/git-capability-state.test.ts": 43, - "src/main/git/git-network-safety-real-git.test.ts": 642, - "src/main/git/git-status-read-lease-owner.test.ts": 68, - "src/main/git/git-upstream-status-read-owner.test.ts": 52, - "src/main/git/git-username.test.ts": 211, - "src/main/git/hosted-remote-url.test.ts": 48, - "src/main/git/huge-folder-ignore.test.ts": 63, - "src/main/git/local-repo-ref-maintenance-probe-retry.test.ts": 244, - "src/main/git/local-repo-ref-maintenance.test.ts": 339, - "src/main/git/pack-refs-lock-ownership.test.ts": 77, - "src/main/git/porcelain-v1-records.test.ts": 132, - "src/main/git/remote-name-listing.test.ts": 307, - "src/main/git/remote-ref-probe-cache.test.ts": 342, - "src/main/git/remote-url-probe.test.ts": 376, - "src/main/git/remote.test.ts": 345, - "src/main/git/remove-worktree-branch-cleanup.test.ts": 337, - "src/main/git/remove-worktree-clean-preflight.test.ts": 188, - "src/main/git/remove-worktree.test.ts": 240, - "src/main/git/repo-branch-conflict-batched-probe.test.ts": 83, - "src/main/git/repo-branch-conflict-real-git.test.ts": 238, - "src/main/git/repo-branch-conflict.test.ts": 235, - "src/main/git/repo-clone-path.test.ts": 63, - "src/main/git/repo-default-base-timeout.test.ts": 108, - "src/main/git/repo-default-remote.test.ts": 134, - "src/main/git/repo-detection-async.test.ts": 507, - "src/main/git/repo-detection-batching.test.ts": 351, - "src/main/git/repo-detection-command.test.ts": 42, - "src/main/git/repo-detection-failure-parity.test.ts": 158, - "src/main/git/repo-detection.test.ts": 501, - "src/main/git/repo-maintenance-admission.test.ts": 647, - "src/main/git/repo-pack-index-maintenance.test.ts": 156, - "src/main/git/repo-pack-index-state.test.ts": 61, - "src/main/git/repo-ref-maintenance-real-git.test.ts": 3586, - "src/main/git/repo-remote-drift-real.test.ts": 266, - "src/main/git/repo-remote-drift.test.ts": 241, - "src/main/git/repo-search-ref-compat.test.ts": 129, - "src/main/git/repo-username.test.ts": 240, - "src/main/git/repo.test.ts": 1920, - "src/main/git/runner-buffer-cancellation.test.ts": 291, - "src/main/git/runner-command-exec.test.ts": 653, - "src/main/git/runner-gh-account-binding.test.ts": 214, - "src/main/git/runner-gh-host-args.test.ts": 189, - "src/main/git/runner-gh-rate-limit-breaker.test.ts": 217, - "src/main/git/runner-network-ssh-policy.test.ts": 159, - "src/main/git/runner-windows-host-environment.test.ts": 249, - "src/main/git/runner-wsl-direct-read.test.ts": 1856, - "src/main/git/runner-wsl-gh-fallback.test.ts": 1079, - "src/main/git/runner-wsl-linked-gitdir-timeout.test.ts": 195, - "src/main/git/runner-wsl-linked-gitdir-windows.test.ts": 248, - "src/main/git/runner-wsl-login-shell-capture.test.ts": 184, - "src/main/git/runner-wsl-network-ssh-policy.test.ts": 197, - "src/main/git/runner-wsl-read-routing.test.ts": 183, - "src/main/git/runner.test.ts": 212, - "src/main/git/saved-clone-probe-budget.test.ts": 81, - "src/main/git/saved-clone-target.test.ts": 612, - "src/main/git/settled-diff-cache-bounds.test.ts": 76, - "src/main/git/source-control/blob-absence-real-git.test.ts": 447, - "src/main/git/source-control/bulk-pathspec-command-line-budget.test.ts": 192, - "src/main/git/source-control/resolve-git-dir.test.ts": 59, - "src/main/git/source-control/stage-worktree-changes.test.ts": 1305, - "src/main/git/source-control/staging-discard-index-safety.test.ts": 941, - "src/main/git/source-control/wsl-tracked-pathspec-banner.test.ts": 193, - "src/main/git/status-branch-compare-real-ref.test.ts": 403, - "src/main/git/status-branch-compare.test.ts": 239, - "src/main/git/status-branch-line-total-exec-contract.test.ts": 892, - "src/main/git/status-branch-line-total-real-git.test.ts": 836, - "src/main/git/status-conflict-operations.test.ts": 163, - "src/main/git/status-cquoted-paths.test.ts": 374, - "src/main/git/status-diff-settled-cache.test.ts": 322, - "src/main/git/status-diff.test.ts": 260, + "src/main/daemon/session-shell-recovery.test.ts": 348, + "src/main/daemon/session-terminal-control.test.ts": 289, + "src/main/daemon/session.test.ts": 420, + "src/main/daemon/shell-ready-bash-wrapper.test.ts": 474, + "src/main/daemon/slow-daemon-session-verification.test.ts": 3611, + "src/main/daemon/startup-device-attributes-responder.test.ts": 37, + "src/main/daemon/terminal-attach-cancellation.test.ts": 31, + "src/main/daemon/terminal-checkpoint-serializer.test.ts": 257, + "src/main/daemon/terminal-checkpoint-writer-bounds.test.ts": 211, + "src/main/daemon/terminal-cursor-line-context.test.ts": 124, + "src/main/daemon/terminal-descendant-shutdown.test.ts": 120, + "src/main/daemon/terminal-history-incremental-restore.test.ts": 423, + "src/main/daemon/terminal-history-large-checkpoint-cold-restore.test.ts": 2132, + "src/main/daemon/terminal-history-log-encode-allocation.test.ts": 105, + "src/main/daemon/terminal-history-log.test.ts": 41, + "src/main/daemon/terminal-history-permissions.test.ts": 241, + "src/main/daemon/terminal-history-restorable-retention.test.ts": 47, + "src/main/daemon/terminal-history-seed-chunks.test.ts": 40, + "src/main/daemon/terminal-history-seed-segments.test.ts": 127, + "src/main/daemon/terminal-history-seed-transfer-registry.test.ts": 38, + "src/main/daemon/terminal-history-session-tombstone-retry.test.ts": 57, + "src/main/daemon/terminal-host-agent-session.test.ts": 503, + "src/main/daemon/terminal-host-attach-only.test.ts": 288, + "src/main/daemon/terminal-host-cheap-tier-ps-scan-volume.test.ts": 635, + "src/main/daemon/terminal-host-closing-session-listing.test.ts": 815, + "src/main/daemon/terminal-host-concurrent-create.test.ts": 383, + "src/main/daemon/terminal-host-cwd-readability.test.ts": 405, + "src/main/daemon/terminal-host-non-agent-foreground.test.ts": 154, + "src/main/daemon/terminal-host-process-inspection-cheap-tier.test.ts": 597, + "src/main/daemon/terminal-host-process-inspection.test.ts": 301, + "src/main/daemon/terminal-host-pty-owner-backend.test.ts": 333, + "src/main/daemon/terminal-host-readiness-reporting.test.ts": 360, + "src/main/daemon/terminal-host-session-reaping-leak.test.ts": 455, + "src/main/daemon/terminal-host-spawn-input-retention.test.ts": 761, + "src/main/daemon/terminal-host-startup-staging.test.ts": 353, + "src/main/daemon/terminal-host-startup.test.ts": 321, + "src/main/daemon/terminal-host-teardown-recreate.test.ts": 546, + "src/main/daemon/terminal-host-wsl-context.test.ts": 367, + "src/main/daemon/terminal-host.test.ts": 543, + "src/main/daemon/terminal-session-teardown.test.ts": 49, + "src/main/daemon/terminal-shell-armed-input-mode-recovery.test.ts": 302, + "src/main/daemon/terminal-shell-lifecycle-scanner.test.ts": 71, + "src/main/daemon/terminal-shell-recovery-barrier.test.ts": 1222, + "src/main/daemon/terminal-shell-recovery-clean-exit-retirement.test.ts": 54, + "src/main/daemon/terminal-snapshot-color-parity.test.ts": 107, + "src/main/daemon/terminal-snapshot-mouse-encoding.test.ts": 262, + "src/main/daemon/terminal-snapshot-osc8-roundtrip.test.ts": 74, + "src/main/daemon/terminal-snapshot-serialize-roundtrip.test.ts": 309, + "src/main/daemon/terminal-spawn-gate-legacy-restores.test.ts": 323, + "src/main/daemon/terminal-spawn-gate-shared-budget.test.ts": 467, + "src/main/daemon/terminal-spawn-gate.test.ts": 601, + "src/main/daemon/windows-conpty-warmup.test.ts": 42, + "src/main/daemon/wsl-cold-restore-cwd.test.ts": 34, + "src/main/destination-serialized-local-rename.test.ts": 1527, + "src/main/devin/hook-config-json.test.ts": 69, + "src/main/devin/hook-service.test.ts": 139, + "src/main/diagnostics/event-loop-delay-probe.test.ts": 91, + "src/main/diagnostics/main-thread-churn-probe.test.ts": 53, + "src/main/dock/unread-badge.test.ts": 26, + "src/main/droid/hook-service.test.ts": 87, + "src/main/dsh/dsh-home-patch.test.ts": 39, + "src/main/dsh/hook-service.test.ts": 103, + "src/main/durable-file-write-syscall-proof.test.ts": 50, + "src/main/durable-file-write.test.ts": 143, + "src/main/electron-updater-mac-staging-contract.test.ts": 117, + "src/main/emulator/android/adb-devices.test.ts": 53, + "src/main/emulator/android/android-app-control.test.ts": 33, + "src/main/emulator/android/android-capability-operations.test.ts": 40, + "src/main/emulator/android/android-device-inventory.test.ts": 45, + "src/main/emulator/android/android-input-commands.test.ts": 40, + "src/main/emulator/android/android-input-mapping.test.ts": 45, + "src/main/emulator/android/android-logcat.test.ts": 43, + "src/main/emulator/android/android-permissions.test.ts": 33, + "src/main/emulator/android/android-sdk-discovery.test.ts": 43, + "src/main/emulator/android/avd-manager.test.ts": 46, + "src/main/emulator/android/scrcpy-server-deploy.test.ts": 39, + "src/main/emulator/android/scrcpy-stream-session.test.ts": 405, + "src/main/emulator/android/scrcpy-video-frame-parser.test.ts": 155, + "src/main/emulator/android/uiautomator-tree.test.ts": 59, + "src/main/emulator/backends/android-emulator-backend.test.ts": 106, + "src/main/emulator/backends/ios-emulator-backend.test.ts": 132, + "src/main/emulator/emulator-availability.test.ts": 42, + "src/main/emulator/emulator-bridge.test.ts": 407, + "src/main/emulator/emulator-gesture-sender.test.ts": 163, + "src/main/emulator/emulator-session-registry.test.ts": 37, + "src/main/emulator/emulator-start-lease-registry.test.ts": 95, + "src/main/emulator/mjpeg-frame-parser.test.ts": 39, + "src/main/emulator/mjpeg-frame-stream.test.ts": 63, + "src/main/emulator/scrcpy-video-registry.test.ts": 39, + "src/main/emulator/serve-sim-accessibility-tree.test.ts": 49, + "src/main/emulator/serve-sim-ax-normalization.test.ts": 39, + "src/main/emulator/serve-sim-detached-session.test.ts": 32, + "src/main/emulator/serve-sim-execution.test.ts": 77, + "src/main/emulator/serve-sim-helper-processes.test.ts": 45, + "src/main/emulator/serve-sim-kill-contract.test.ts": 1251, + "src/main/emulator/serve-sim-runtime-materializer.test.ts": 179, + "src/main/emulator/simctl-simulator-devices.test.ts": 58, + "src/main/ephemeral-vm-recipe-runner.test.ts": 534, + "src/main/ephemeral-vm-resume-integrity.test.ts": 228, + "src/main/ephemeral-vm-runtime-environment-release.test.ts": 224, + "src/main/ephemeral-vm-runtime-service.test.ts": 979, + "src/main/ephemeral-vm-runtime-ssh-cleanup.test.ts": 325, + "src/main/ephemeral-vm-runtime-ssh.test.ts": 55, + "src/main/execution-host-workspace-trust.test.ts": 204, + "src/main/external-editor-launch.test.ts": 105, + "src/main/fish-history-session.test.ts": 70, + "src/main/fish-xdg-data-dirs-handoff.test.ts": 56, + "src/main/fish-xdg-data-dirs-no-config-launch.test.ts": 374, + "src/main/folder-upgrade-worktree-path.test.ts": 42, + "src/main/foreign-sqlite-readers/foreign-sqlite-reader-client-worker-lifecycle.test.ts": 866, + "src/main/foreign-sqlite-readers/foreign-sqlite-reader-client.test.ts": 281, + "src/main/foreign-sqlite-readers/foreign-sqlite-reader-dispatch.test.ts": 305, + "src/main/foreign-sqlite-readers/foreign-sqlite-reader-entry-path.test.ts": 50, + "src/main/foreign-sqlite-readers/foreign-sqlite-reader-event-loop.test.ts": 2322, + "src/main/foreign-sqlite-readers/foreign-sqlite-reader-spawn.test.ts": 63, + "src/main/foreign-sqlite-readers/readers/cursor-profile.test.ts": 496, + "src/main/foreign-sqlite-readers/readers/opencode-binder-sessions.test.ts": 153, + "src/main/gemini/hook-service.test.ts": 88, + "src/main/ghostty/discovery.test.ts": 69, + "src/main/ghostty/index.test.ts": 95, + "src/main/ghostty/mapper-extended.test.ts": 71, + "src/main/ghostty/mapper.test.ts": 76, + "src/main/ghostty/parser.test.ts": 38, + "src/main/ghostty/theme-import.test.ts": 100, + "src/main/ghostty/theme-resolution.test.ts": 57, + "src/main/git-bash.test.ts": 40, + "src/main/git/add-sparse-worktree.test.ts": 162, + "src/main/git/admission-tier-plumbing.test.ts": 31, + "src/main/git/base-ref-search-selector.test.ts": 199, + "src/main/git/branch-rename.test.ts": 50, + "src/main/git/canonical-repo-key.test.ts": 183, + "src/main/git/check-ignored-paths.test.ts": 39, + "src/main/git/coalesced-probe.test.ts": 29, + "src/main/git/command-runner/gh-exec-file-deadline.test.ts": 115, + "src/main/git/command-runner/git-admission-output-parity.test.ts": 233, + "src/main/git/command-runner/git-admission-span.test.ts": 327, + "src/main/git/command-runner/git-admission-storm-measurement.test.ts": 179, + "src/main/git/command-runner/git-command-resolution-windows-checkout.test.ts": 82, + "src/main/git/command-runner/git-command-timeout-behavior.test.ts": 1935, + "src/main/git/command-runner/git-command-timeout.test.ts": 41, + "src/main/git/command-runner/git-exec-admission-exempt.test.ts": 168, + "src/main/git/command-runner/git-exec-admission-lifetime.test.ts": 647, + "src/main/git/command-runner/git-operation-executor.test.ts": 55, + "src/main/git/command-runner/git-spawn-admission-lifetime.test.ts": 180, + "src/main/git/command-runner/git-stream-admission-lifetime.test.ts": 254, + "src/main/git/command-runner/git-subprocess-admission.test.ts": 293, + "src/main/git/command-runner/hosted-cli-deadline-log.test.ts": 161, + "src/main/git/command-runner/spawned-command-tree-kill.test.ts": 57, + "src/main/git/command-runner/wsl-command-resolution-windows-binary.test.ts": 73, + "src/main/git/command-runner/wsl-host-failure.test.ts": 160, + "src/main/git/commit-object-ref.test.ts": 28, + "src/main/git/commit.test.ts": 187, + "src/main/git/create-event-repo-probe.test.ts": 86, + "src/main/git/exact-ref-probe.test.ts": 40, + "src/main/git/fetch-error-classification.test.ts": 40, + "src/main/git/fork-remote-refspec.test.ts": 46, + "src/main/git/fork-remote-stale-branch-refspec.test.ts": 44, + "src/main/git/gh-rate-limit-breaker.test.ts": 52, + "src/main/git/git-capability-state.test.ts": 48, + "src/main/git/git-network-safety-real-git.test.ts": 299, + "src/main/git/git-status-read-lease-owner.test.ts": 47, + "src/main/git/git-upstream-status-read-owner.test.ts": 42, + "src/main/git/git-username.test.ts": 181, + "src/main/git/hosted-remote-url.test.ts": 35, + "src/main/git/huge-folder-ignore.test.ts": 50, + "src/main/git/local-repo-ref-maintenance-probe-retry.test.ts": 227, + "src/main/git/local-repo-ref-maintenance.test.ts": 295, + "src/main/git/pack-refs-lock-ownership.test.ts": 58, + "src/main/git/porcelain-v1-records.test.ts": 129, + "src/main/git/remote-name-listing.test.ts": 496, + "src/main/git/remote-ref-probe-cache.test.ts": 480, + "src/main/git/remote-url-probe.test.ts": 474, + "src/main/git/remote.test.ts": 333, + "src/main/git/remove-worktree-branch-cleanup.test.ts": 310, + "src/main/git/remove-worktree-clean-preflight.test.ts": 179, + "src/main/git/remove-worktree.test.ts": 261, + "src/main/git/repo-branch-conflict-batched-probe.test.ts": 66, + "src/main/git/repo-branch-conflict-real-git.test.ts": 321, + "src/main/git/repo-branch-conflict.test.ts": 258, + "src/main/git/repo-clone-path.test.ts": 36, + "src/main/git/repo-default-base-timeout.test.ts": 110, + "src/main/git/repo-default-remote.test.ts": 106, + "src/main/git/repo-detection-async.test.ts": 517, + "src/main/git/repo-detection-batching.test.ts": 436, + "src/main/git/repo-detection-command.test.ts": 43, + "src/main/git/repo-detection-failure-parity.test.ts": 163, + "src/main/git/repo-detection.test.ts": 629, + "src/main/git/repo-maintenance-admission.test.ts": 705, + "src/main/git/repo-pack-index-maintenance.test.ts": 141, + "src/main/git/repo-pack-index-state.test.ts": 73, + "src/main/git/repo-ref-maintenance-real-git.test.ts": 2009, + "src/main/git/repo-remote-drift-real.test.ts": 240, + "src/main/git/repo-remote-drift.test.ts": 218, + "src/main/git/repo-search-ref-compat.test.ts": 78, + "src/main/git/repo-username.test.ts": 258, + "src/main/git/repo.test.ts": 2374, + "src/main/git/runner-buffer-cancellation.test.ts": 296, + "src/main/git/runner-command-exec.test.ts": 677, + "src/main/git/runner-gh-account-binding.test.ts": 223, + "src/main/git/runner-gh-host-args.test.ts": 60, + "src/main/git/runner-gh-rate-limit-breaker.test.ts": 240, + "src/main/git/runner-network-ssh-policy.test.ts": 214, + "src/main/git/runner-windows-host-environment.test.ts": 269, + "src/main/git/runner-wsl-direct-read.test.ts": 1817, + "src/main/git/runner-wsl-gh-fallback.test.ts": 1030, + "src/main/git/runner-wsl-linked-gitdir-timeout.test.ts": 158, + "src/main/git/runner-wsl-linked-gitdir-windows.test.ts": 308, + "src/main/git/runner-wsl-login-shell-capture.test.ts": 220, + "src/main/git/runner-wsl-network-ssh-policy.test.ts": 213, + "src/main/git/runner-wsl-read-routing.test.ts": 193, + "src/main/git/runner.test.ts": 192, + "src/main/git/saved-clone-probe-budget.test.ts": 70, + "src/main/git/saved-clone-target.test.ts": 970, + "src/main/git/settled-diff-cache-bounds.test.ts": 61, + "src/main/git/source-control/blob-absence-real-git.test.ts": 406, + "src/main/git/source-control/bulk-pathspec-command-line-budget.test.ts": 266, + "src/main/git/source-control/drive-checkout-suspect-paths.test.ts": 55, + "src/main/git/source-control/drive-checkout-suspect-real-git.test.ts": 235, + "src/main/git/source-control/drive-checkout-suspect-status.test.ts": 258, + "src/main/git/source-control/drive-checkout-suspect-writes-real-git.test.ts": 697, + "src/main/git/source-control/resolve-git-dir.test.ts": 43, + "src/main/git/source-control/stage-worktree-changes.test.ts": 1382, + "src/main/git/source-control/staging-discard-index-safety.test.ts": 563, + "src/main/git/source-control/wsl-tracked-pathspec-banner.test.ts": 167, + "src/main/git/status-branch-compare-real-ref.test.ts": 366, + "src/main/git/status-branch-compare.test.ts": 216, + "src/main/git/status-branch-line-total-exec-contract.test.ts": 806, + "src/main/git/status-branch-line-total-real-git.test.ts": 769, + "src/main/git/status-conflict-operations.test.ts": 223, + "src/main/git/status-cquoted-paths.test.ts": 285, + "src/main/git/status-diff-settled-cache.test.ts": 345, + "src/main/git/status-diff.test.ts": 309, "src/main/git/status-discard-and-bulk-staging.test.ts": 296, - "src/main/git/status-discard-symlink.test.ts": 544, - "src/main/git/status-line-stats-host-paths.test.ts": 77, - "src/main/git/status-pathspec-literals.test.ts": 362, - "src/main/git/status-porcelain-parser.test.ts": 78, - "src/main/git/status-read-coalescing.test.ts": 218, - "src/main/git/status-shared-symlinks.test.ts": 579, - "src/main/git/status-submodule-path-cache.test.ts": 367, - "src/main/git/status-submodule.test.ts": 264, - "src/main/git/status-symlink-probe-budget.test.ts": 179, - "src/main/git/status-upstream-negative-cache.test.ts": 718, - "src/main/git/status-upstream-probe-churn.test.ts": 280, - "src/main/git/status-upstream-ref.test.ts": 42, - "src/main/git/status-wsl-pathspecs.test.ts": 185, - "src/main/git/status.test.ts": 246, - "src/main/git/upstream-deferred-fork-remote-real.test.ts": 243, - "src/main/git/upstream.test.ts": 381, - "src/main/git/worktree-add-creation-config.test.ts": 290, - "src/main/git/worktree-add-local-base-refresh.test.ts": 291, - "src/main/git/worktree-add-local-base-suggestion.test.ts": 274, - "src/main/git/worktree-add-pending-removal.test.ts": 239, - "src/main/git/worktree-add-timeout-override.test.ts": 318, - "src/main/git/worktree-base-divergence-real-git.test.ts": 781, - "src/main/git/worktree-base-divergence.test.ts": 107, - "src/main/git/worktree-base-ref-probe.test.ts": 66, - "src/main/git/worktree-base-refresh-contention.test.ts": 239, - "src/main/git/worktree-base-refresh-lock-real-git.test.ts": 1272, - "src/main/git/worktree-branch-removal-host.test.ts": 185, - "src/main/git/worktree-common-dir-comparison.test.ts": 273, - "src/main/git/worktree-configured-paths-concurrency.test.ts": 91, - "src/main/git/worktree-create-admission-tier.test.ts": 305, - "src/main/git/worktree-create-git-executor-real-git.test.ts": 477, - "src/main/git/worktree-create-preparation-real-git.test.ts": 1597, - "src/main/git/worktree-create-preparation-real-wsl.test.ts": 327, - "src/main/git/worktree-created-description-real-git.test.ts": 1158, - "src/main/git/worktree-created-disk-witness.test.ts": 218, - "src/main/git/worktree-delete-limit.test.ts": 44, - "src/main/git/worktree-diff-stamp-guest-gitdir.test.ts": 62, - "src/main/git/worktree-diff-stamp-host-paths.test.ts": 60, - "src/main/git/worktree-git-capabilities.test.ts": 262, - "src/main/git/worktree-graph-listing.test.ts": 349, - "src/main/git/worktree-include-file.test.ts": 157, - "src/main/git/worktree-list-paths.test.ts": 464, - "src/main/git/worktree-list-porcelain.test.ts": 236, - "src/main/git/worktree-list-reader-cancellation.test.ts": 64, - "src/main/git/worktree-listing-created-sparse-distro.test.ts": 223, - "src/main/git/worktree-listing-sparse-cancellation.test.ts": 176, - "src/main/git/worktree-listing-sparse-distro.test.ts": 191, - "src/main/git/worktree-move.test.ts": 278, - "src/main/git/worktree-mutation-route-invalidation.test.ts": 177, - "src/main/git/worktree-porcelain-parsing.test.ts": 248, - "src/main/git/worktree-posix-path-case-sensitivity.test.ts": 169, - "src/main/git/worktree-preparation-add.test.ts": 194, - "src/main/git/worktree-preparation-base-oid.test.ts": 239, - "src/main/git/worktree-preparation-cancel-latency.bench.test.ts": 286, - "src/main/git/worktree-preparation-discard-real-git.test.ts": 466, - "src/main/git/worktree-preparation-fallback-cleanup-real-git.test.ts": 485, - "src/main/git/worktree-preparation-lock-real-git.test.ts": 922, - "src/main/git/worktree-preparation-lock.test.ts": 87, - "src/main/git/worktree-preparation-tip-refresh-real-git.test.ts": 488, - "src/main/git/worktree-preparation-tip-refresh.test.ts": 108, - "src/main/git/worktree-rebase-update-refs-real-git.test.ts": 273, - "src/main/git/worktree-removal-large-tree.bench.test.ts": 284, - "src/main/git/worktree-removal-real-git.test.ts": 834, - "src/main/git/worktree-remove-branch-deletion.test.ts": 258, - "src/main/git/worktree-safety-real-git.test.ts": 989, - "src/main/git/worktree-scan-cache-annotation-reuse.test.ts": 312, - "src/main/git/worktree-scan-cache-sharing.test.ts": 312, - "src/main/git/worktree-separate-git-dir.test.ts": 543, - "src/main/git/worktree-shared-directories.test.ts": 911, - "src/main/git/worktree-sparse-checkout-cache.test.ts": 206, - "src/main/git/worktree-sparse-checkout.test.ts": 574, - "src/main/git/worktree-sparse-state-host-paths.test.ts": 264, - "src/main/git/worktree-symlink-detection.test.ts": 61, - "src/main/git/wsl-direct-git-read-commands.test.ts": 72, - "src/main/git/wsl-linked-worktree-git-route-invalidation.test.ts": 61, - "src/main/git/wsl-linked-worktree-git-routing.test.ts": 118, - "src/main/git/wsl-process-group-termination.test.ts": 57, - "src/main/gitea/client-scan-invalidation.test.ts": 377, - "src/main/gitea/client.test.ts": 413, - "src/main/gitea/pull-request-creation.test.ts": 395, - "src/main/gitea/pull-request-mappers.test.ts": 44, - "src/main/gitea/repository-ref.test.ts": 373, - "src/main/github/auth-diagnose.test.ts": 66, - "src/main/github/client-create-pr.test.ts": 312, - "src/main/github/client-file-viewed.test.ts": 465, - "src/main/github/client-issue-origin-preference.test.ts": 405, - "src/main/github/client-issue-source.test.ts": 849, - "src/main/github/client-merge-queue-auto-merge.test.ts": 437, - "src/main/github/client-merged-pr-visibility.test.ts": 487, - "src/main/github/client-pr-branch-discovery.test.ts": 441, - "src/main/github/client-pr-check-details.test.ts": 427, - "src/main/github/client-pr-checks.test.ts": 464, - "src/main/github/client-pr-comment-reactions.test.ts": 449, - "src/main/github/client-pr-conflict-summary.test.ts": 478, - "src/main/github/client-pr-fallback-number.test.ts": 442, - "src/main/github/client-pr-linked-lookup.test.ts": 385, - "src/main/github/client-pr-local-runtime.test.ts": 511, - "src/main/github/client-pr-lookup-coalescing.test.ts": 622, - "src/main/github/client-pr-push-target.test.ts": 427, - "src/main/github/client-pr-state.test.ts": 408, - "src/main/github/client-rate-limit-block.test.ts": 377, - "src/main/github/client-ssh-provider-execution-boundary.test.ts": 441, - "src/main/github/client-stack-merge-guard.test.ts": 456, - "src/main/github/client-starred.test.ts": 340, - "src/main/github/client-tracked-upstream-fork-owner.test.ts": 438, - "src/main/github/client-tracked-upstream-snapshot.test.ts": 1192, - "src/main/github/client-work-item-check-summary.test.ts": 444, - "src/main/github/client-work-items-query-paging.test.ts": 651, - "src/main/github/client-work-items.test.ts": 579, - "src/main/github/comment-reactions.test.ts": 74, - "src/main/github/conflict-summary.test.ts": 97, - "src/main/github/default-branch-stale-pr.test.ts": 378, - "src/main/github/gh-account-token.test.ts": 60, - "src/main/github/gh-capability-state.test.ts": 48, - "src/main/github/gh-utils-concurrency.test.ts": 434, - "src/main/github/gh-utils.test.ts": 529, - "src/main/github/github-api-repository-validation.test.ts": 50, - "src/main/github/github-api-repository.test.ts": 563, - "src/main/github/github-enterprise-repository.test.ts": 403, - "src/main/github/github-pr-stack.test.ts": 376, - "src/main/github/github-remote-identity-parsing.test.ts": 58, - "src/main/github/github-repository-identity.fork-owner-repo.test.ts": 538, - "src/main/github/github-repository-identity.gh-account.test.ts": 408, - "src/main/github/github-repository-identity.signed-cache.test.ts": 372, - "src/main/github/github-repository-identity.ssh-host-alias.test.ts": 620, - "src/main/github/issue-mutation-source.test.ts": 436, - "src/main/github/issues.test.ts": 479, - "src/main/github/pr-head-tracking-ref.test.ts": 67, - "src/main/github/pr-mutation-review-invalidation.test.ts": 2411, - "src/main/github/pr-refresh-coordinator-active-burst-pacing.test.ts": 660, - "src/main/github/pr-refresh-coordinator-active-visible-priority.test.ts": 575, - "src/main/github/pr-refresh-coordinator-alias-coalescing.test.ts": 859, - "src/main/github/pr-refresh-coordinator-completion-order.test.ts": 602, - "src/main/github/pr-refresh-coordinator-error-cadence.test.ts": 574, - "src/main/github/pr-refresh-coordinator-foreground-pacing.test.ts": 469, - "src/main/github/pr-refresh-coordinator-rate-limit-budget.test.ts": 858, - "src/main/github/pr-refresh-coordinator-refresh-events.test.ts": 726, - "src/main/github/pr-refresh-coordinator-visibility-policy.test.ts": 1217, - "src/main/github/pr-refresh-coordinator-visible-follow-up.test.ts": 935, - "src/main/github/pr-refresh-error-classification.test.ts": 60, - "src/main/github/pr-refresh-queue-growth-bound.test.ts": 1057, - "src/main/github/pr-refresh-validation-backoff.test.ts": 56, - "src/main/github/pr-review-comment-lines.test.ts": 39, - "src/main/github/pr-start-point-compare-base.test.ts": 64, - "src/main/github/pr-start-point.test.ts": 95, + "src/main/git/status-discard-symlink.test.ts": 603, + "src/main/git/status-line-stats-host-paths.test.ts": 75, + "src/main/git/status-pathspec-literals.test.ts": 460, + "src/main/git/status-porcelain-parser.test.ts": 41, + "src/main/git/status-read-coalescing.test.ts": 192, + "src/main/git/status-shared-symlinks.test.ts": 746, + "src/main/git/status-submodule-path-cache.test.ts": 327, + "src/main/git/status-submodule.test.ts": 248, + "src/main/git/status-symlink-probe-budget.test.ts": 244, + "src/main/git/status-upstream-negative-cache.test.ts": 702, + "src/main/git/status-upstream-probe-churn.test.ts": 223, + "src/main/git/status-upstream-ref.test.ts": 39, + "src/main/git/status-wsl-pathspecs.test.ts": 208, + "src/main/git/status.test.ts": 281, + "src/main/git/upstream-deferred-fork-remote-real.test.ts": 199, + "src/main/git/upstream.test.ts": 401, + "src/main/git/worktree-add-creation-config.test.ts": 301, + "src/main/git/worktree-add-local-base-refresh.test.ts": 317, + "src/main/git/worktree-add-local-base-suggestion.test.ts": 249, + "src/main/git/worktree-add-pending-removal.test.ts": 293, + "src/main/git/worktree-add-timeout-override.test.ts": 198, + "src/main/git/worktree-base-divergence-real-git.test.ts": 1239, + "src/main/git/worktree-base-divergence.test.ts": 78, + "src/main/git/worktree-base-ref-probe.test.ts": 46, + "src/main/git/worktree-base-refresh-contention.test.ts": 262, + "src/main/git/worktree-base-refresh-lock-real-git.test.ts": 1609, + "src/main/git/worktree-branch-removal-host.test.ts": 183, + "src/main/git/worktree-common-dir-comparison.test.ts": 236, + "src/main/git/worktree-configured-paths-concurrency.test.ts": 70, + "src/main/git/worktree-create-admission-tier.test.ts": 298, + "src/main/git/worktree-create-git-executor-real-git.test.ts": 479, + "src/main/git/worktree-create-preparation-real-git.test.ts": 1746, + "src/main/git/worktree-create-preparation-real-wsl.test.ts": 287, + "src/main/git/worktree-created-description-real-git.test.ts": 975, + "src/main/git/worktree-created-disk-witness.test.ts": 190, + "src/main/git/worktree-delete-limit.test.ts": 21, + "src/main/git/worktree-diff-stamp-guest-gitdir.test.ts": 40, + "src/main/git/worktree-diff-stamp-host-paths.test.ts": 41, + "src/main/git/worktree-git-capabilities.test.ts": 288, + "src/main/git/worktree-graph-listing.test.ts": 236, + "src/main/git/worktree-include-file.test.ts": 118, + "src/main/git/worktree-list-paths.test.ts": 475, + "src/main/git/worktree-list-porcelain.test.ts": 192, + "src/main/git/worktree-list-reader-cancellation.test.ts": 73, + "src/main/git/worktree-listing-created-sparse-distro.test.ts": 251, + "src/main/git/worktree-listing-sparse-cancellation.test.ts": 222, + "src/main/git/worktree-listing-sparse-distro.test.ts": 200, + "src/main/git/worktree-move.test.ts": 226, + "src/main/git/worktree-mutation-route-invalidation.test.ts": 224, + "src/main/git/worktree-porcelain-parsing.test.ts": 260, + "src/main/git/worktree-posix-path-case-sensitivity.test.ts": 239, + "src/main/git/worktree-preparation-add.test.ts": 212, + "src/main/git/worktree-preparation-base-oid.test.ts": 210, + "src/main/git/worktree-preparation-cancel-latency.bench.test.ts": 368, + "src/main/git/worktree-preparation-discard-real-git.test.ts": 469, + "src/main/git/worktree-preparation-fallback-cleanup-real-git.test.ts": 590, + "src/main/git/worktree-preparation-lock-real-git.test.ts": 1078, + "src/main/git/worktree-preparation-lock.test.ts": 96, + "src/main/git/worktree-preparation-tip-refresh-real-git.test.ts": 797, + "src/main/git/worktree-preparation-tip-refresh.test.ts": 101, + "src/main/git/worktree-rebase-update-refs-real-git.test.ts": 224, + "src/main/git/worktree-removal-large-tree.bench.test.ts": 280, + "src/main/git/worktree-removal-real-git.test.ts": 1078, + "src/main/git/worktree-remove-branch-deletion.test.ts": 282, + "src/main/git/worktree-safety-real-git.test.ts": 528, + "src/main/git/worktree-scan-cache-annotation-reuse.test.ts": 282, + "src/main/git/worktree-scan-cache-sharing.test.ts": 313, + "src/main/git/worktree-separate-git-dir.test.ts": 537, + "src/main/git/worktree-shared-directories.test.ts": 968, + "src/main/git/worktree-sparse-checkout-cache.test.ts": 196, + "src/main/git/worktree-sparse-checkout.test.ts": 560, + "src/main/git/worktree-sparse-state-host-paths.test.ts": 257, + "src/main/git/worktree-symlink-detection.test.ts": 44, + "src/main/git/wsl-direct-git-read-commands.test.ts": 78, + "src/main/git/wsl-linked-worktree-git-route-invalidation.test.ts": 32, + "src/main/git/wsl-linked-worktree-git-routing.test.ts": 109, + "src/main/git/wsl-process-group-termination.test.ts": 51, + "src/main/git/wsl-windows-checkout-git-config.test.ts": 53, + "src/main/git/wsl-windows-checkout-git-probe.test.ts": 224, + "src/main/gitea/client-scan-invalidation.test.ts": 545, + "src/main/gitea/client.test.ts": 507, + "src/main/gitea/pull-request-creation.test.ts": 460, + "src/main/gitea/pull-request-mappers.test.ts": 38, + "src/main/gitea/repository-ref.test.ts": 469, + "src/main/github/auth-diagnose.test.ts": 35, + "src/main/github/client-create-pr.test.ts": 600, + "src/main/github/client-file-viewed.test.ts": 590, + "src/main/github/client-issue-origin-preference.test.ts": 580, + "src/main/github/client-issue-source.test.ts": 711, + "src/main/github/client-merge-queue-auto-merge.test.ts": 680, + "src/main/github/client-merged-pr-visibility.test.ts": 596, + "src/main/github/client-pr-branch-discovery.test.ts": 581, + "src/main/github/client-pr-check-details.test.ts": 573, + "src/main/github/client-pr-checks.test.ts": 611, + "src/main/github/client-pr-comment-reactions.test.ts": 622, + "src/main/github/client-pr-conflict-summary.test.ts": 644, + "src/main/github/client-pr-fallback-number.test.ts": 613, + "src/main/github/client-pr-linked-lookup.test.ts": 570, + "src/main/github/client-pr-local-runtime.test.ts": 581, + "src/main/github/client-pr-lookup-coalescing.test.ts": 597, + "src/main/github/client-pr-push-target.test.ts": 600, + "src/main/github/client-pr-state.test.ts": 543, + "src/main/github/client-rate-limit-block.test.ts": 949, + "src/main/github/client-ssh-provider-execution-boundary.test.ts": 591, + "src/main/github/client-stack-merge-guard.test.ts": 556, + "src/main/github/client-starred.test.ts": 629, + "src/main/github/client-tracked-upstream-default-branch.test.ts": 467, + "src/main/github/client-tracked-upstream-fork-owner.test.ts": 570, + "src/main/github/client-tracked-upstream-snapshot.test.ts": 907, + "src/main/github/client-work-item-check-summary.test.ts": 512, + "src/main/github/client-work-items-query-paging.test.ts": 663, + "src/main/github/client-work-items.test.ts": 518, + "src/main/github/comment-reactions.test.ts": 30, + "src/main/github/conflict-summary.test.ts": 69, + "src/main/github/default-branch-stale-pr.test.ts": 559, + "src/main/github/gh-account-token.test.ts": 50, + "src/main/github/gh-capability-state.test.ts": 33, + "src/main/github/gh-utils-concurrency.test.ts": 519, + "src/main/github/gh-utils.test.ts": 794, + "src/main/github/github-api-repository-validation.test.ts": 39, + "src/main/github/github-api-repository.test.ts": 686, + "src/main/github/github-enterprise-repository.test.ts": 548, + "src/main/github/github-pr-stack.test.ts": 780, + "src/main/github/github-remote-identity-parsing.test.ts": 34, + "src/main/github/github-repository-identity.fork-owner-repo.test.ts": 555, + "src/main/github/github-repository-identity.gh-account.test.ts": 486, + "src/main/github/github-repository-identity.signed-cache.test.ts": 524, + "src/main/github/github-repository-identity.ssh-host-alias.test.ts": 499, + "src/main/github/issue-mutation-source.test.ts": 597, + "src/main/github/issues.test.ts": 621, + "src/main/github/pr-head-tracking-ref.test.ts": 58, + "src/main/github/pr-mutation-review-invalidation.test.ts": 2422, + "src/main/github/pr-refresh-coordinator-active-burst-pacing.test.ts": 962, + "src/main/github/pr-refresh-coordinator-active-visible-priority.test.ts": 897, + "src/main/github/pr-refresh-coordinator-alias-coalescing.test.ts": 1341, + "src/main/github/pr-refresh-coordinator-completion-order.test.ts": 1083, + "src/main/github/pr-refresh-coordinator-error-cadence.test.ts": 992, + "src/main/github/pr-refresh-coordinator-foreground-pacing.test.ts": 777, + "src/main/github/pr-refresh-coordinator-rate-limit-budget.test.ts": 997, + "src/main/github/pr-refresh-coordinator-refresh-events.test.ts": 1193, + "src/main/github/pr-refresh-coordinator-visibility-policy.test.ts": 1798, + "src/main/github/pr-refresh-coordinator-visible-follow-up.test.ts": 1201, + "src/main/github/pr-refresh-error-classification.test.ts": 41, + "src/main/github/pr-refresh-queue-growth-bound.test.ts": 1418, + "src/main/github/pr-refresh-validation-backoff.test.ts": 61, + "src/main/github/pr-review-comment-lines.test.ts": 35, + "src/main/github/pr-start-point-compare-base.test.ts": 50, + "src/main/github/pr-start-point.test.ts": 86, "src/main/github/project-view-host-auth.test.ts": 102, - "src/main/github/project-view.test.ts": 482, - "src/main/github/project-view/mutations.test.ts": 63, - "src/main/github/project-view/project-field-mutations.test.ts": 51, - "src/main/github/project-view/project-view-config.test.ts": 427, - "src/main/github/project-view/project-view-table.test.ts": 461, - "src/main/github/project-view/repository-field-options.test.ts": 51, - "src/main/github/rate-limit.test.ts": 88, - "src/main/github/review-head-remote.test.ts": 60, - "src/main/github/stacked-pr-creation.test.ts": 437, - "src/main/github/work-item-details-concurrency.test.ts": 71, - "src/main/github/work-item-details-config-change.test.ts": 426, - "src/main/github/work-item-details-enterprise-host.test.ts": 450, - "src/main/github/work-item-details-file-viewed.test.ts": 468, - "src/main/github/work-item-details-issue-source.test.ts": 72, - "src/main/github/work-item-details-pr-files.test.ts": 418, - "src/main/github/work-item-details.test.ts": 472, - "src/main/github/work-item-search-fallback-environment.test.ts": 603, - "src/main/github/work-item-search-freshness.test.ts": 1299, - "src/main/github/work-item-search-isolation.test.ts": 679, - "src/main/github/work-item-search-pagination.test.ts": 501, - "src/main/github/work-item-search-semantics.test.ts": 638, - "src/main/gitlab/client-mr-auth-rate-limit.test.ts": 411, - "src/main/gitlab/client-mr-branch-lookup.test.ts": 473, - "src/main/gitlab/client-mr-job-ci.test.ts": 408, - "src/main/gitlab/client-mr-listing.test.ts": 409, - "src/main/gitlab/client-mr-review-actions.test.ts": 387, - "src/main/gitlab/client-work-items.test.ts": 451, - "src/main/gitlab/client.test.ts": 456, - "src/main/gitlab/gitlab-known-host-probe-wsl-fallback.test.ts": 375, - "src/main/gitlab/gitlab-known-host-probe.test.ts": 383, - "src/main/gitlab/gitlab-known-host-retirement.test.ts": 728, - "src/main/gitlab/gl-utils.test.ts": 452, - "src/main/gitlab/issues.test.ts": 490, - "src/main/gitlab/mappers-workitem.test.ts": 45, - "src/main/gitlab/mappers.test.ts": 71, - "src/main/gitlab/merge-request-creation.test.ts": 446, - "src/main/gitlab/mr-head-tracking-ref.test.ts": 61, - "src/main/gitlab/project-ref-parser.test.ts": 60, - "src/main/gitlab/work-item-details.test.ts": 100, - "src/main/grok-accounts/status.test.ts": 69, - "src/main/grok/grok-hook-config-file.test.ts": 153, - "src/main/grok/grok-hook-owners.test.ts": 61, - "src/main/grok/grok-hook-remnant-removal.test.ts": 89, + "src/main/github/project-view.test.ts": 529, + "src/main/github/project-view/mutations.test.ts": 48, + "src/main/github/project-view/project-field-mutations.test.ts": 35, + "src/main/github/project-view/project-view-config.test.ts": 566, + "src/main/github/project-view/project-view-table.test.ts": 560, + "src/main/github/project-view/repository-field-options.test.ts": 39, + "src/main/github/rate-limit.test.ts": 62, + "src/main/github/review-head-remote.test.ts": 41, + "src/main/github/stacked-pr-creation.test.ts": 509, + "src/main/github/work-item-details-concurrency.test.ts": 81, + "src/main/github/work-item-details-config-change.test.ts": 765, + "src/main/github/work-item-details-enterprise-host.test.ts": 571, + "src/main/github/work-item-details-file-viewed.test.ts": 542, + "src/main/github/work-item-details-issue-source.test.ts": 78, + "src/main/github/work-item-details-pr-files.test.ts": 627, + "src/main/github/work-item-details.test.ts": 516, + "src/main/github/work-item-search-fallback-environment.test.ts": 606, + "src/main/github/work-item-search-freshness.test.ts": 1165, + "src/main/github/work-item-search-isolation.test.ts": 636, + "src/main/github/work-item-search-pagination.test.ts": 646, + "src/main/github/work-item-search-semantics.test.ts": 954, + "src/main/gitlab/client-mr-auth-rate-limit.test.ts": 529, + "src/main/gitlab/client-mr-branch-lookup.test.ts": 631, + "src/main/gitlab/client-mr-job-ci.test.ts": 500, + "src/main/gitlab/client-mr-listing.test.ts": 542, + "src/main/gitlab/client-mr-review-actions.test.ts": 545, + "src/main/gitlab/client-work-items.test.ts": 544, + "src/main/gitlab/client.test.ts": 536, + "src/main/gitlab/gitlab-known-host-probe-wsl-fallback.test.ts": 608, + "src/main/gitlab/gitlab-known-host-probe.test.ts": 579, + "src/main/gitlab/gitlab-known-host-retirement.test.ts": 648, + "src/main/gitlab/gl-utils.test.ts": 589, + "src/main/gitlab/issues.test.ts": 616, + "src/main/gitlab/mappers-workitem.test.ts": 34, + "src/main/gitlab/mappers.test.ts": 62, + "src/main/gitlab/merge-request-creation.test.ts": 586, + "src/main/gitlab/mr-head-tracking-ref.test.ts": 42, + "src/main/gitlab/project-ref-parser.test.ts": 42, + "src/main/gitlab/work-item-details.test.ts": 93, + "src/main/grok-accounts/status.test.ts": 35, + "src/main/grok/grok-hook-config-file.test.ts": 160, + "src/main/grok/grok-hook-owners.test.ts": 41, + "src/main/grok/grok-hook-remnant-removal.test.ts": 110, "src/main/grok/grok-hook-stdin-no-eof.test.ts": 465, - "src/main/grok/hook-service.test.ts": 154, - "src/main/grok/windows-grok-hook-script.test.ts": 87, - "src/main/grok/windows-hook-launcher-chain.test.ts": 104, - "src/main/hang-watchdog/hang-detection-marker.test.ts": 71, - "src/main/hang-watchdog/hang-watchdog-detection-loop.test.ts": 64, - "src/main/hang-watchdog/hang-watchdog-worker-path.test.ts": 53, - "src/main/hang-watchdog/main-thread-hang-telemetry.test.ts": 299, - "src/main/hang-watchdog/main-thread-hang-watchdog-entry.test.ts": 107, - "src/main/hang-watchdog/main-thread-hang-watchdog.test.ts": 134, - "src/main/hang-watchdog/previous-hang-detection.test.ts": 118, - "src/main/hermes/hermes-config-comment-boundaries.test.ts": 513, - "src/main/hermes/hermes-config-document.test.ts": 197, - "src/main/hermes/hermes-config-flow-insertion.test.ts": 417, - "src/main/hermes/hermes-config-preservation.test.ts": 197, - "src/main/hermes/hermes-config-remote-adapter.test.ts": 186, - "src/main/hermes/hermes-config-source-edits.test.ts": 275, - "src/main/hermes/hook-service.test.ts": 307, - "src/main/hook-archive-timeout-observation.test.ts": 1099, - "src/main/hook-termination-real-process.test.ts": 8247, - "src/main/hooks-archive-exit-observation.test.ts": 277, - "src/main/hooks-effective-hook-resolution.test.ts": 471, - "src/main/hooks-issue-command.test.ts": 500, - "src/main/hooks-orca-yaml-parsing.test.ts": 485, - "src/main/hooks-runner.test.ts": 274, + "src/main/grok/hook-service.test.ts": 199, + "src/main/grok/windows-grok-hook-script.test.ts": 63, + "src/main/grok/windows-hook-launcher-chain.test.ts": 105, + "src/main/hang-watchdog/hang-detection-marker.test.ts": 41, + "src/main/hang-watchdog/hang-watchdog-detection-loop.test.ts": 48, + "src/main/hang-watchdog/hang-watchdog-worker-path.test.ts": 38, + "src/main/hang-watchdog/main-thread-hang-telemetry.test.ts": 277, + "src/main/hang-watchdog/main-thread-hang-watchdog-entry.test.ts": 68, + "src/main/hang-watchdog/main-thread-hang-watchdog.test.ts": 151, + "src/main/hang-watchdog/previous-hang-detection.test.ts": 80, + "src/main/hermes/hermes-config-comment-boundaries.test.ts": 545, + "src/main/hermes/hermes-config-document.test.ts": 218, + "src/main/hermes/hermes-config-flow-insertion.test.ts": 542, + "src/main/hermes/hermes-config-preservation.test.ts": 218, + "src/main/hermes/hermes-config-remote-adapter.test.ts": 192, + "src/main/hermes/hermes-config-source-edits.test.ts": 164, + "src/main/hermes/hook-service.test.ts": 305, + "src/main/hook-archive-timeout-observation.test.ts": 1048, + "src/main/hook-termination-real-process.test.ts": 3098, + "src/main/hooks-archive-exit-observation.test.ts": 244, + "src/main/hooks-effective-hook-resolution.test.ts": 456, + "src/main/hooks-issue-command.test.ts": 512, + "src/main/hooks-orca-yaml-parsing.test.ts": 414, + "src/main/hooks-runner.test.ts": 253, "src/main/hooks-setup-runner-script.test.ts": 238, - "src/main/hooks.test.ts": 493, - "src/main/host-tree-removal-asar.electron.test.ts": 259, - "src/main/host/deferred-secret-protection-report.test.ts": 99, - "src/main/host/electron-secret-store.test.ts": 91, - "src/main/host/secret-protection-report.test.ts": 79, - "src/main/host/structured-attention-localization.test.ts": 333, - "src/main/i18n/main-i18n-lazy-locale.test.ts": 278, - "src/main/ipc/agent-hooks.test.ts": 733, - "src/main/ipc/agent-pane-authority-ownership.test.ts": 56, - "src/main/ipc/agent-pane-retirement-ipc.test.ts": 62, - "src/main/ipc/agent-session-attachment-upload.test.ts": 7519, - "src/main/ipc/ai-vault-host-leg-cache-retention.test.ts": 553, - "src/main/ipc/ai-vault-scan-coalescing.test.ts": 1267, - "src/main/ipc/ai-vault-search-all-hosts-real-index.test.ts": 1776, - "src/main/ipc/ai-vault-search-all-hosts.test.ts": 260, - "src/main/ipc/ai-vault-search-merged-cursor.test.ts": 223, - "src/main/ipc/ai-vault-search.test.ts": 1049, - "src/main/ipc/ai-vault-unresolved-owner.test.ts": 589, - "src/main/ipc/ai-vault.test.ts": 847, - "src/main/ipc/app.test.ts": 585, - "src/main/ipc/automations-external-scope.test.ts": 481, - "src/main/ipc/bounded-warning-dedupe.test.ts": 45, - "src/main/ipc/browser-client-page-metadata-ipc.test.ts": 222, - "src/main/ipc/browser-preview-tool-authorization.test.ts": 669, - "src/main/ipc/browser-session-profile-ipc.test.ts": 342, - "src/main/ipc/browser-tab-registration-wait.test.ts": 259, - "src/main/ipc/browser.test.ts": 731, - "src/main/ipc/cli-appimage-stale-registration.test.ts": 272, - "src/main/ipc/cli.test.ts": 145, - "src/main/ipc/codex-config-sync.test.ts": 86, - "src/main/ipc/command-path-resolver.test.ts": 84, - "src/main/ipc/computer-use-permissions.test.ts": 62, - "src/main/ipc/crash-reporting-renderer-breadcrumbs.test.ts": 201, - "src/main/ipc/crash-reporting-renderer-error-report-attribution.test.ts": 95, - "src/main/ipc/crash-reporting-replay-guard-wedge-burst.test.ts": 86, - "src/main/ipc/crash-reporting.test.ts": 194, - "src/main/ipc/created-worktree-reconciliation.test.ts": 142, - "src/main/ipc/created-worktree-root-prune.test.ts": 404, - "src/main/ipc/credential-change-rate-limit-refresh.test.ts": 129, - "src/main/ipc/dashboard-payload-validation.test.ts": 200, - "src/main/ipc/dashboard-popout.test.ts": 134, - "src/main/ipc/deferred-emoji-shortcode-dataset.test.ts": 105, - "src/main/ipc/developer-permissions.test.ts": 262, - "src/main/ipc/diagnostics.test.ts": 104, - "src/main/ipc/doc-preview-grant-ipc.test.ts": 77, - "src/main/ipc/dropped-path-resolution.test.ts": 74, - "src/main/ipc/emulator-frame-stream-socket-lifetime.test.ts": 163, - "src/main/ipc/emulator-renderer-lifetime.test.ts": 117, - "src/main/ipc/emulator-stream-listener-cleanup.test.ts": 66, - "src/main/ipc/ephemeral-vm-provision-cancel.test.ts": 5904, - "src/main/ipc/ephemeral-vm-provisioned-root-ref.test.ts": 3154, - "src/main/ipc/ephemeral-vm-runtime-handler-cleanup.test.ts": 1695, - "src/main/ipc/ephemeral-vm.test.ts": 938, - "src/main/ipc/feedback-image-attachments.test.ts": 95, - "src/main/ipc/feedback-request-ownership.test.ts": 72, - "src/main/ipc/feedback.test.ts": 140, - "src/main/ipc/filesystem-allowed-roots.test.ts": 577, - "src/main/ipc/filesystem-auth.test.ts": 686, - "src/main/ipc/filesystem-branch-compare-diff.test.ts": 1287, - "src/main/ipc/filesystem-canonical-owner.test.ts": 135, - "src/main/ipc/filesystem-commit-message-generation.test.ts": 1529, - "src/main/ipc/filesystem-commit-message-model-discovery.test.ts": 2164, - "src/main/ipc/filesystem-conflict-operation-routing.test.ts": 1720, - "src/main/ipc/filesystem-content-search-cancellation.test.ts": 1714, - "src/main/ipc/filesystem-document-folder-writes.test.ts": 1153, - "src/main/ipc/filesystem-download-lifetime.test.ts": 722, - "src/main/ipc/filesystem-download-transfers.test.ts": 1825, - "src/main/ipc/filesystem-git-commit-dispatch.test.ts": 1766, - "src/main/ipc/filesystem-git-status-staging.test.ts": 1611, - "src/main/ipc/filesystem-import-local-rollback.test.ts": 87, - "src/main/ipc/filesystem-import-ssh-ops.test.ts": 1162, - "src/main/ipc/filesystem-import-ssh-path-safety.test.ts": 101, - "src/main/ipc/filesystem-import-ssh.test.ts": 1053, - "src/main/ipc/filesystem-import.test.ts": 2156, - "src/main/ipc/filesystem-list-files-handler-name-filter.test.ts": 1379, - "src/main/ipc/filesystem-list-files-name-filter.test.ts": 426, - "src/main/ipc/filesystem-list-files.test.ts": 778, - "src/main/ipc/filesystem-markdown-document-listing.test.ts": 1718, - "src/main/ipc/filesystem-mutations-runtime-upload.test.ts": 386, - "src/main/ipc/filesystem-mutations.test.ts": 1142, - "src/main/ipc/filesystem-path-containment-remote-enoent.test.ts": 46, - "src/main/ipc/filesystem-path-containment-unicode-normalization.test.ts": 492, - "src/main/ipc/filesystem-pull-request-linked-issue-lifetime.test.ts": 2316, - "src/main/ipc/filesystem-quick-open-options.integration.test.ts": 1700, - "src/main/ipc/filesystem-runtime-upload-staging.test.ts": 79, - "src/main/ipc/filesystem-search-file-paths.test.ts": 346, - "src/main/ipc/filesystem-search-rg-timeout.test.ts": 1670, - "src/main/ipc/filesystem-symlink-directory-entries.test.ts": 44, - "src/main/ipc/filesystem-watcher-canonical-root-paths.test.ts": 509, - "src/main/ipc/filesystem-watcher-document-lifetime.test.ts": 381, - "src/main/ipc/filesystem-watcher-dormant-rearm.test.ts": 384, - "src/main/ipc/filesystem-watcher-event-batch.test.ts": 44, - "src/main/ipc/filesystem-watcher-ignore-real.test.ts": 424, - "src/main/ipc/filesystem-watcher-ignore.test.ts": 67, - "src/main/ipc/filesystem-watcher-large-batch.test.ts": 173, - "src/main/ipc/filesystem-watcher-local-events.test.ts": 406, - "src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts": 1181, - "src/main/ipc/filesystem-watcher-native-capacity.test.ts": 174, - "src/main/ipc/filesystem-watcher-real.test.ts": 546, - "src/main/ipc/filesystem-watcher-remote-batch.test.ts": 198, - "src/main/ipc/filesystem-watcher-remote-cancellation.test.ts": 474, - "src/main/ipc/filesystem-watcher-remote-capacity.test.ts": 195, - "src/main/ipc/filesystem-watcher-remote-rearm.test.ts": 160, - "src/main/ipc/filesystem-watcher-removal-deadline.test.ts": 396, - "src/main/ipc/filesystem-watcher-terminal-resync.test.ts": 167, - "src/main/ipc/filesystem-watcher-unwatchable-roots.test.ts": 189, - "src/main/ipc/filesystem-watcher-wsl.test.ts": 95, - "src/main/ipc/filesystem-watcher.test.ts": 2015, - "src/main/ipc/filesystem.test.ts": 2109, - "src/main/ipc/filesystem/filesystem-source-control-ai-targets.test.ts": 50, - "src/main/ipc/filesystem/local-file-range-read.test.ts": 64, - "src/main/ipc/filesystem/local-regular-file-read.test.ts": 83, - "src/main/ipc/floating-workspace-directory.test.ts": 59, - "src/main/ipc/folder-repo-git-upgrade-lifetime.test.ts": 100, - "src/main/ipc/folder-repo-git-upgrade.test.ts": 2130, - "src/main/ipc/freebuff-detection.test.ts": 42, - "src/main/ipc/git-status-upstream-ref-watch-request.test.ts": 321, - "src/main/ipc/github-issue-source-preference.test.ts": 602, - "src/main/ipc/github-pr-refresh-routing.test.ts": 729, - "src/main/ipc/github-repo-access-guards.test.ts": 690, - "src/main/ipc/github-ssh-connection-routing.test.ts": 658, - "src/main/ipc/github-star-telemetry.test.ts": 592, - "src/main/ipc/github-work-item-args.test.ts": 56, - "src/main/ipc/github-wsl-runtime-routing.test.ts": 680, - "src/main/ipc/gitlab-repo-access.test.ts": 105, - "src/main/ipc/gitlab.test.ts": 203, - "src/main/ipc/hooks/worktree-hook-execution-host.test.ts": 783, - "src/main/ipc/hosted-review.test.ts": 668, - "src/main/ipc/jira-cancellable-requests.test.ts": 58, - "src/main/ipc/keybindings.test.ts": 57, - "src/main/ipc/linear.test.ts": 110, - "src/main/ipc/local-file-access-resolution-network-path.test.ts": 176, - "src/main/ipc/local-file-access-resolution.test.ts": 435, - "src/main/ipc/local-log-tail-lifetime.test.ts": 131, - "src/main/ipc/local-log-tail.test.ts": 65, - "src/main/ipc/local-network-connection-test.test.ts": 323, - "src/main/ipc/local-worktree-runtime-options.test.ts": 105, - "src/main/ipc/macos-keyboard-layout-change-notifications.test.ts": 66, - "src/main/ipc/macos-keyboard-layout-snapshot.test.ts": 47, - "src/main/ipc/managed-host-status-publication.test.ts": 1003, - "src/main/ipc/markdown-documents-remote-paths.test.ts": 180, - "src/main/ipc/markdown-documents-ripgrep-real.test.ts": 190, - "src/main/ipc/markdown-documents-ripgrep.test.ts": 250, - "src/main/ipc/markdown-documents.test.ts": 212, - "src/main/ipc/minimax-credentials.test.ts": 76, - "src/main/ipc/mobile.test.ts": 27111, - "src/main/ipc/native-chat-renderer-lifetime.test.ts": 910, - "src/main/ipc/native-chat-renderer-watcher-cleanup.test.ts": 1169, - "src/main/ipc/native-chat-subscribe-lifecycle.test.ts": 1282, - "src/main/ipc/native-chat.test.ts": 2590, - "src/main/ipc/notebook-project-link.test.ts": 365, - "src/main/ipc/notebook.test.ts": 507, - "src/main/ipc/notifications-custom-sound.test.ts": 328, - "src/main/ipc/notifications-delivery-gating.test.ts": 426, - "src/main/ipc/notifications-message-formatting.test.ts": 402, - "src/main/ipc/notifications-mobile-fanout.test.ts": 319, - "src/main/ipc/notifications-permission-onboarding.test.ts": 333, - "src/main/ipc/notifications-retention-lifecycle.test.ts": 506, - "src/main/ipc/notifications-subject-retirement.test.ts": 411, - "src/main/ipc/opencode-go-credentials.test.ts": 81, - "src/main/ipc/orca-profile-auth-handlers.test.ts": 473, - "src/main/ipc/orca-profile-org-members-handlers.test.ts": 61, - "src/main/ipc/orca-profiles-switch-persistence.test.ts": 2035, - "src/main/ipc/orca-profiles.test.ts": 706, - "src/main/ipc/orcad-delta-move-handlers.test.ts": 750, - "src/main/ipc/orcad-runtime-conversion-handlers.test.ts": 650, - "src/main/ipc/orcad-runtime-lifecycle-handlers.test.ts": 47, - "src/main/ipc/orcad-runtime-maintenance-handlers.test.ts": 826, - "src/main/ipc/orcad-ssh-provisioning-handlers.test.ts": 51, - "src/main/ipc/parcel-watcher-child-launch.test.ts": 66, - "src/main/ipc/parcel-watcher-child-registry.test.ts": 98, - "src/main/ipc/parcel-watcher-crash-fuse.test.ts": 48, - "src/main/ipc/parcel-watcher-disconnect-termination.test.ts": 142, - "src/main/ipc/parcel-watcher-entry-path.test.ts": 61, - "src/main/ipc/parcel-watcher-event-cancellation.test.ts": 64, - "src/main/ipc/parcel-watcher-event-delivery.test.ts": 119, - "src/main/ipc/parcel-watcher-module-loader.test.ts": 51, - "src/main/ipc/parcel-watcher-native-error-retirement.test.ts": 106, - "src/main/ipc/parcel-watcher-owned-children.test.ts": 52, - "src/main/ipc/parcel-watcher-process-dispose.test.ts": 55, - "src/main/ipc/parcel-watcher-process-entry.test.ts": 215, - "src/main/ipc/parcel-watcher-process.test.ts": 562, - "src/main/ipc/parcel-watcher-root-path-rewrite.test.ts": 114, - "src/main/ipc/parcel-watcher-shallow-subscription.test.ts": 146, - "src/main/ipc/parcel-watcher-supervisor-capacity-wait.test.ts": 173, - "src/main/ipc/parcel-watcher-supervisor-capacity.test.ts": 232, - "src/main/ipc/parcel-watcher-supervisor-disposal.test.ts": 90, - "src/main/ipc/parcel-watcher-unsubscribe-timeout.test.ts": 112, - "src/main/ipc/pet-bundle.test.ts": 57, - "src/main/ipc/pet.test.ts": 319, - "src/main/ipc/plugin-marketplaces.test.ts": 275, - "src/main/ipc/plugins.test.ts": 437, - "src/main/ipc/preflight-agent-detection-no-subprocess.test.ts": 688, - "src/main/ipc/preflight-agent-detection.test.ts": 725, - "src/main/ipc/preflight-agent-refresh.test.ts": 717, - "src/main/ipc/preflight-command-exec.test.ts": 89, - "src/main/ipc/preflight-host-cli-status.test.ts": 731, - "src/main/ipc/preflight-provider-command-selection.test.ts": 352, - "src/main/ipc/preflight-remote-ssh.test.ts": 631, - "src/main/ipc/preflight-runnable-local-cli.test.ts": 9174, - "src/main/ipc/preflight-windows-path-refresh.repro.test.ts": 77, + "src/main/hooks.test.ts": 484, + "src/main/host-tree-removal-asar.electron.test.ts": 197, + "src/main/host/deferred-secret-protection-report.test.ts": 67, + "src/main/host/electron-http-client.test.ts": 489, + "src/main/host/electron-secret-store.test.ts": 46, + "src/main/host/macos-keychain-reachability.test.ts": 65, + "src/main/host/safe-storage-availability.test.ts": 30, + "src/main/host/safe-storage-import-ratchet.test.ts": 128, + "src/main/host/secret-protection-report.test.ts": 44, + "src/main/host/serve-keychain-guard.test.ts": 48, + "src/main/host/structured-attention-localization.test.ts": 403, + "src/main/i18n/main-i18n-lazy-locale.test.ts": 282, + "src/main/integration-state-file-cache.test.ts": 65, + "src/main/integration-token-file-cache.test.ts": 128, + "src/main/ipc/agent-hooks.test.ts": 801, + "src/main/ipc/agent-pane-authority-ownership.test.ts": 36, + "src/main/ipc/agent-pane-retirement-ipc.test.ts": 42, + "src/main/ipc/agent-session-attachment-upload.test.ts": 6808, + "src/main/ipc/ai-vault-host-leg-cache-retention.test.ts": 1790, + "src/main/ipc/ai-vault-resume.test.ts": 1185, + "src/main/ipc/ai-vault-scan-coalescing.test.ts": 2026, + "src/main/ipc/ai-vault-search-all-hosts-real-index.test.ts": 1534, + "src/main/ipc/ai-vault-search-all-hosts.test.ts": 232, + "src/main/ipc/ai-vault-search-merged-cursor.test.ts": 205, + "src/main/ipc/ai-vault-search.test.ts": 1285, + "src/main/ipc/ai-vault-unresolved-owner.test.ts": 1330, + "src/main/ipc/ai-vault.test.ts": 1323, + "src/main/ipc/app.test.ts": 466, + "src/main/ipc/automations-external-scope.test.ts": 403, + "src/main/ipc/bounded-warning-dedupe.test.ts": 36, + "src/main/ipc/browser-annotation-viewport-ipc.test.ts": 195, + "src/main/ipc/browser-client-page-metadata-ipc.test.ts": 265, + "src/main/ipc/browser-preview-tool-authorization.test.ts": 862, + "src/main/ipc/browser-session-profile-ipc.test.ts": 320, + "src/main/ipc/browser-tab-registration-wait.test.ts": 262, + "src/main/ipc/browser.test.ts": 749, + "src/main/ipc/cli-appimage-stale-registration.test.ts": 283, + "src/main/ipc/cli.test.ts": 179, + "src/main/ipc/codex-config-sync.test.ts": 75, + "src/main/ipc/command-path-resolver.test.ts": 60, + "src/main/ipc/computer-use-permissions.test.ts": 30, + "src/main/ipc/crash-reporting-renderer-breadcrumbs.test.ts": 136, + "src/main/ipc/crash-reporting-renderer-error-report-attribution.test.ts": 63, + "src/main/ipc/crash-reporting-replay-guard-wedge-burst.test.ts": 49, + "src/main/ipc/crash-reporting.test.ts": 134, + "src/main/ipc/created-worktree-reconciliation.test.ts": 113, + "src/main/ipc/created-worktree-root-prune.test.ts": 498, + "src/main/ipc/credential-change-rate-limit-refresh.test.ts": 108, + "src/main/ipc/dashboard-payload-validation.test.ts": 149, + "src/main/ipc/dashboard-popout.test.ts": 89, + "src/main/ipc/deferred-emoji-shortcode-dataset.test.ts": 84, + "src/main/ipc/developer-permissions.test.ts": 208, + "src/main/ipc/diagnostics.test.ts": 71, + "src/main/ipc/doc-preview-grant-ipc.test.ts": 63, + "src/main/ipc/dropped-path-resolution.test.ts": 62, + "src/main/ipc/emulator-frame-stream-socket-lifetime.test.ts": 164, + "src/main/ipc/emulator-renderer-lifetime.test.ts": 112, + "src/main/ipc/emulator-stream-listener-cleanup.test.ts": 56, + "src/main/ipc/ephemeral-vm-managed-ssh.test.ts": 552, + "src/main/ipc/ephemeral-vm-provision-cancel.test.ts": 4199, + "src/main/ipc/ephemeral-vm-provisioned-root-ref.test.ts": 2965, + "src/main/ipc/ephemeral-vm-runtime-handler-cleanup.test.ts": 1376, + "src/main/ipc/ephemeral-vm.test.ts": 866, + "src/main/ipc/feedback-image-attachments.test.ts": 52, + "src/main/ipc/feedback-request-ownership.test.ts": 56, + "src/main/ipc/feedback.test.ts": 142, + "src/main/ipc/filesystem-allowed-roots.test.ts": 730, + "src/main/ipc/filesystem-auth.test.ts": 599, + "src/main/ipc/filesystem-branch-compare-diff.test.ts": 1385, + "src/main/ipc/filesystem-canonical-owner.test.ts": 340, + "src/main/ipc/filesystem-commit-message-generation.test.ts": 1421, + "src/main/ipc/filesystem-commit-message-model-discovery.test.ts": 1485, + "src/main/ipc/filesystem-conflict-operation-routing.test.ts": 1434, + "src/main/ipc/filesystem-content-search-cancellation.test.ts": 1939, + "src/main/ipc/filesystem-document-folder-writes.test.ts": 1256, + "src/main/ipc/filesystem-download-lifetime.test.ts": 713, + "src/main/ipc/filesystem-download-transfers.test.ts": 1450, + "src/main/ipc/filesystem-git-commit-dispatch.test.ts": 1454, + "src/main/ipc/filesystem-git-status-staging.test.ts": 1645, + "src/main/ipc/filesystem-import-local-rollback.test.ts": 86, + "src/main/ipc/filesystem-import.test.ts": 1213, + "src/main/ipc/filesystem-list-files-handler-name-filter.test.ts": 1554, + "src/main/ipc/filesystem-list-files-name-filter.test.ts": 564, + "src/main/ipc/filesystem-list-files.test.ts": 742, + "src/main/ipc/filesystem-markdown-document-listing.test.ts": 1217, + "src/main/ipc/filesystem-mutations-runtime-upload.test.ts": 605, + "src/main/ipc/filesystem-mutations.test.ts": 1536, + "src/main/ipc/filesystem-path-containment-remote-enoent.test.ts": 40, + "src/main/ipc/filesystem-path-containment-unicode-normalization.test.ts": 616, + "src/main/ipc/filesystem-pull-request-linked-issue-lifetime.test.ts": 2341, + "src/main/ipc/filesystem-quick-open-options.integration.test.ts": 1280, + "src/main/ipc/filesystem-runtime-upload-staging.test.ts": 62, + "src/main/ipc/filesystem-search-file-paths.test.ts": 361, + "src/main/ipc/filesystem-search-rg-timeout.test.ts": 1957, + "src/main/ipc/filesystem-symlink-directory-entries.test.ts": 33, + "src/main/ipc/filesystem-watcher-canonical-root-paths.test.ts": 456, + "src/main/ipc/filesystem-watcher-document-lifetime.test.ts": 388, + "src/main/ipc/filesystem-watcher-dormant-rearm.test.ts": 204, + "src/main/ipc/filesystem-watcher-event-batch.test.ts": 34, + "src/main/ipc/filesystem-watcher-ignore-real.test.ts": 441, + "src/main/ipc/filesystem-watcher-ignore.test.ts": 45, + "src/main/ipc/filesystem-watcher-large-batch.test.ts": 172, + "src/main/ipc/filesystem-watcher-local-events.test.ts": 375, + "src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts": 1005, + "src/main/ipc/filesystem-watcher-native-capacity.test.ts": 186, + "src/main/ipc/filesystem-watcher-real.test.ts": 762, + "src/main/ipc/filesystem-watcher-remote-batch.test.ts": 202, + "src/main/ipc/filesystem-watcher-remote-cancellation.test.ts": 501, + "src/main/ipc/filesystem-watcher-remote-capacity.test.ts": 330, + "src/main/ipc/filesystem-watcher-remote-rearm.test.ts": 168, + "src/main/ipc/filesystem-watcher-removal-deadline.test.ts": 664, + "src/main/ipc/filesystem-watcher-terminal-resync.test.ts": 195, + "src/main/ipc/filesystem-watcher-unwatchable-roots.test.ts": 174, + "src/main/ipc/filesystem-watcher-wsl.test.ts": 107, + "src/main/ipc/filesystem-watcher.test.ts": 2082, + "src/main/ipc/filesystem.test.ts": 1823, + "src/main/ipc/filesystem/filesystem-source-control-ai-targets.test.ts": 45, + "src/main/ipc/filesystem/local-file-range-read.test.ts": 38, + "src/main/ipc/filesystem/local-regular-file-read.test.ts": 55, + "src/main/ipc/floating-workspace-directory.test.ts": 49, + "src/main/ipc/folder-repo-git-upgrade-lifetime.test.ts": 108, + "src/main/ipc/folder-repo-git-upgrade.test.ts": 2177, + "src/main/ipc/freebuff-detection.test.ts": 33, + "src/main/ipc/git-status-upstream-ref-watch-request.test.ts": 368, + "src/main/ipc/github-issue-source-preference.test.ts": 585, + "src/main/ipc/github-pr-refresh-routing.test.ts": 774, + "src/main/ipc/github-repo-access-guards.test.ts": 705, + "src/main/ipc/github-ssh-connection-routing.test.ts": 664, + "src/main/ipc/github-star-telemetry.test.ts": 551, + "src/main/ipc/github-work-item-args.test.ts": 38, + "src/main/ipc/github-wsl-runtime-routing.test.ts": 767, + "src/main/ipc/gitlab-repo-access.test.ts": 276, + "src/main/ipc/gitlab.test.ts": 362, + "src/main/ipc/hooks/worktree-hook-execution-host.test.ts": 758, + "src/main/ipc/hosted-review.test.ts": 617, + "src/main/ipc/jira-cancellable-requests.test.ts": 34, + "src/main/ipc/jira-ipc-params.test.ts": 53, + "src/main/ipc/jira-issue-summary-generation.test.ts": 232, + "src/main/ipc/jira-project-metadata-registration.test.ts": 214, + "src/main/ipc/keybindings.test.ts": 49, + "src/main/ipc/linear.test.ts": 91, + "src/main/ipc/local-file-access-resolution-network-path.test.ts": 372, + "src/main/ipc/local-file-access-resolution.test.ts": 682, + "src/main/ipc/local-log-tail-lifetime.test.ts": 150, + "src/main/ipc/local-log-tail.test.ts": 38, + "src/main/ipc/local-network-connection-test.test.ts": 291, + "src/main/ipc/local-worktree-runtime-options.test.ts": 266, + "src/main/ipc/macos-keyboard-layout-change-notifications.test.ts": 51, + "src/main/ipc/macos-keyboard-layout-snapshot.test.ts": 33, + "src/main/ipc/managed-host-status-publication.test.ts": 731, + "src/main/ipc/markdown-documents-remote-paths.test.ts": 191, + "src/main/ipc/markdown-documents-ripgrep-real.test.ts": 264, + "src/main/ipc/markdown-documents-ripgrep.test.ts": 256, + "src/main/ipc/markdown-documents.test.ts": 189, + "src/main/ipc/minimax-credentials.test.ts": 79, + "src/main/ipc/mobile.test.ts": 26150, + "src/main/ipc/native-chat-renderer-lifetime.test.ts": 924, + "src/main/ipc/native-chat-renderer-watcher-cleanup.test.ts": 1312, + "src/main/ipc/native-chat-subscribe-lifecycle.test.ts": 840, + "src/main/ipc/native-chat.test.ts": 2682, + "src/main/ipc/notebook-project-link.test.ts": 875, + "src/main/ipc/notebook.test.ts": 606, + "src/main/ipc/notifications-custom-sound.test.ts": 375, + "src/main/ipc/notifications-delivery-gating.test.ts": 453, + "src/main/ipc/notifications-macos-removal.test.ts": 338, + "src/main/ipc/notifications-message-formatting.test.ts": 331, + "src/main/ipc/notifications-mobile-fanout.test.ts": 270, + "src/main/ipc/notifications-permission-onboarding.test.ts": 457, + "src/main/ipc/notifications-retention-lifecycle.test.ts": 415, + "src/main/ipc/notifications-subject-retirement.test.ts": 285, + "src/main/ipc/notifications-workspace-click-binding.test.ts": 259, + "src/main/ipc/opencode-go-credentials.test.ts": 63, + "src/main/ipc/orca-profile-auth-handlers.test.ts": 498, + "src/main/ipc/orca-profile-org-members-handlers.test.ts": 47, + "src/main/ipc/orca-profiles-switch-persistence.test.ts": 1882, + "src/main/ipc/orca-profiles.test.ts": 595, + "src/main/ipc/orcad-delta-move-handlers.test.ts": 637, + "src/main/ipc/orcad-runtime-conversion-handlers.test.ts": 886, + "src/main/ipc/orcad-runtime-lifecycle-handlers.test.ts": 43, + "src/main/ipc/orcad-runtime-maintenance-handlers.test.ts": 856, + "src/main/ipc/orcad-ssh-provisioning-handlers.test.ts": 33, + "src/main/ipc/parcel-watcher-child-launch.test.ts": 51, + "src/main/ipc/parcel-watcher-child-registry.test.ts": 88, + "src/main/ipc/parcel-watcher-crash-fuse.test.ts": 37, + "src/main/ipc/parcel-watcher-disconnect-termination.test.ts": 146, + "src/main/ipc/parcel-watcher-entry-path.test.ts": 41, + "src/main/ipc/parcel-watcher-event-cancellation.test.ts": 45, + "src/main/ipc/parcel-watcher-event-delivery.test.ts": 112, + "src/main/ipc/parcel-watcher-module-loader.test.ts": 32, + "src/main/ipc/parcel-watcher-native-error-retirement.test.ts": 100, + "src/main/ipc/parcel-watcher-owned-children.test.ts": 50, + "src/main/ipc/parcel-watcher-process-dispose.test.ts": 36, + "src/main/ipc/parcel-watcher-process-entry.test.ts": 179, + "src/main/ipc/parcel-watcher-process.test.ts": 558, + "src/main/ipc/parcel-watcher-root-path-rewrite.test.ts": 109, + "src/main/ipc/parcel-watcher-shallow-subscription.test.ts": 113, + "src/main/ipc/parcel-watcher-supervisor-capacity-wait.test.ts": 177, + "src/main/ipc/parcel-watcher-supervisor-capacity.test.ts": 247, + "src/main/ipc/parcel-watcher-supervisor-disposal.test.ts": 91, + "src/main/ipc/parcel-watcher-unsubscribe-timeout.test.ts": 118, + "src/main/ipc/pet-bundle.test.ts": 52, + "src/main/ipc/pet.test.ts": 328, + "src/main/ipc/plugin-marketplaces.test.ts": 250, + "src/main/ipc/plugins.test.ts": 430, + "src/main/ipc/preflight-agent-detection-no-subprocess.test.ts": 598, + "src/main/ipc/preflight-agent-detection.test.ts": 554, + "src/main/ipc/preflight-agent-refresh.test.ts": 672, + "src/main/ipc/preflight-command-exec.test.ts": 77, + "src/main/ipc/preflight-host-cli-status.test.ts": 781, + "src/main/ipc/preflight-provider-command-selection.test.ts": 376, + "src/main/ipc/preflight-remote-ssh.test.ts": 635, + "src/main/ipc/preflight-runnable-local-cli.test.ts": 9234, + "src/main/ipc/preflight-windows-path-refresh.repro.test.ts": 105, "src/main/ipc/preflight-wsl-agent-detection.test.ts": 118, - "src/main/ipc/preflight-wsl-command.test.ts": 64, - "src/main/ipc/pty-activation-inventory-scope.test.ts": 1905, - "src/main/ipc/pty-agent-launch-pane-attach.test.ts": 2906, - "src/main/ipc/pty-applied-size-reporting.test.ts": 2036, - "src/main/ipc/pty-buffer-snapshot-dispatch.test.ts": 2028, - "src/main/ipc/pty-codex-account-attribution.test.ts": 1676, - "src/main/ipc/pty-controller-owner-recovery.test.ts": 1924, - "src/main/ipc/pty-controller-ownership-routing.test.ts": 2013, - "src/main/ipc/pty-controller-process-inventory.test.ts": 2060, - "src/main/ipc/pty-controller-spawn-admission.test.ts": 4498, - "src/main/ipc/pty-cumulative-ack-accounting.test.ts": 2167, - "src/main/ipc/pty-daemon-controller-teardown.test.ts": 2056, - "src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts": 2293, - "src/main/ipc/pty-daemon-spawn-codex-auth.test.ts": 2131, - "src/main/ipc/pty-daemon-spawn-session-identity.test.ts": 2097, - "src/main/ipc/pty-daemon-spawn-wsl-runtime.test.ts": 2152, - "src/main/ipc/pty-daemon-ssh-lease-lifecycle.test.ts": 1871, - "src/main/ipc/pty-dead-owner-respawn.test.ts": 2153, - "src/main/ipc/pty-delivery-health-heal.test.ts": 2042, - "src/main/ipc/pty-dispatcher-handshake-osc-answers.test.ts": 2140, - "src/main/ipc/pty-encoding.test.ts": 43, - "src/main/ipc/pty-global-renderer-credit.test.ts": 2232, - "src/main/ipc/pty-hidden-at-spawn-mark.test.ts": 5138, - "src/main/ipc/pty-hidden-delivery-gate.test.ts": 59, - "src/main/ipc/pty-ipc-hidden-delivery-gate.test.ts": 2060, - "src/main/ipc/pty-ipc-producer-flow-control.test.ts": 1883, - "src/main/ipc/pty-listener-teardown-and-orphans.test.ts": 4358, - "src/main/ipc/pty-login-shell-startup-commands.test.ts": 2204, - "src/main/ipc/pty-management.test.ts": 138, - "src/main/ipc/pty-output-batching-drain.test.ts": 1870, - "src/main/ipc/pty-output-drain-rounds.test.ts": 1637, - "src/main/ipc/pty-pane-claim-arbitration.test.ts": 7990, - "src/main/ipc/pty-pane-materialization-race.test.ts": 2686, - "src/main/ipc/pty-pane-reservation-settlement.test.ts": 2440, - "src/main/ipc/pty-pane-restart-replace.test.ts": 2428, - "src/main/ipc/pty-pending-data-drain-queue-differential.test.ts": 1638, - "src/main/ipc/pty-pending-data-drain-queue.test.ts": 65, - "src/main/ipc/pty-pending-data-drain-scheduler-differential.test.ts": 93, - "src/main/ipc/pty-pending-output-cap.test.ts": 1952, - "src/main/ipc/pty-persisted-incarnation-repair.test.ts": 1941, - "src/main/ipc/pty-producer-flow-control.test.ts": 60, - "src/main/ipc/pty-renderer-inflight-credit.test.ts": 2177, - "src/main/ipc/pty-renderer-lifecycle-delivery-reset.test.ts": 2053, - "src/main/ipc/pty-renderer-liveness-guard.test.ts": 2073, - "src/main/ipc/pty-renderer-send-failure-recovery.test.ts": 2046, - "src/main/ipc/pty-reset-input-modes.test.ts": 1487, - "src/main/ipc/pty-restore-record-seeding.test.ts": 4523, - "src/main/ipc/pty-restored-appimage-cli-shim-refresh.test.ts": 2019, - "src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts": 4718, - "src/main/ipc/pty-runtime-kill-and-exit.test.ts": 2264, - "src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts": 1912, - "src/main/ipc/pty-serializer-settlement-mapping.test.ts": 5457, - "src/main/ipc/pty-session-liveness-and-ownership.test.ts": 2154, - "src/main/ipc/pty-spawn-codex-home-unavailable.test.ts": 1758, - "src/main/ipc/pty-spawn-cwd-fallback.test.ts": 2244, - "src/main/ipc/pty-spawn-env-agent-overlays.test.ts": 2493, - "src/main/ipc/pty-spawn-env-codex-home-routing.test.ts": 2025, - "src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts": 2070, - "src/main/ipc/pty-spawn-env-terminal-basics.test.ts": 2141, - "src/main/ipc/pty-spawn-pi-owner-env.test.ts": 2128, - "src/main/ipc/pty-spawn-placement-threading.test.ts": 2049, - "src/main/ipc/pty-spawn-runtime-handle-binding.test.ts": 5122, - "src/main/ipc/pty-spawn-timing.test.ts": 57, - "src/main/ipc/pty-ssh-stop-verdict.test.ts": 2449, - "src/main/ipc/pty-ssh-undelivered-kill.test.ts": 2026, - "src/main/ipc/pty-startup-barrier-and-listing.test.ts": 2108, - "src/main/ipc/pty-startup-swap-window-presence.test.ts": 1988, - "src/main/ipc/pty-windows-shell-selection.test.ts": 2490, - "src/main/ipc/pty-write-ipc-validation.test.ts": 2150, - "src/main/ipc/pty-wsl-cwd-validation.test.ts": 2113, - "src/main/ipc/pty/configured-terminal-shell-args.test.ts": 54, - "src/main/ipc/pty/daemon-late-exit.test.ts": 4940, - "src/main/ipc/pty/delivery/attached-pty-size.test.ts": 53, - "src/main/ipc/pty/delivery/interactive-typing-burst.test.ts": 51, - "src/main/ipc/pty/host-env/codex-hook-reconcile-spawn.test.ts": 291, - "src/main/ipc/pty/host-env/omp-launch-environment.test.ts": 75, - "src/main/ipc/pty/host-env/opencode-hook-installation.test.ts": 669, - "src/main/ipc/pty/ipc/claude-old-terminal.test.ts": 48, - "src/main/ipc/pty/ipc/codex-shared-server.test.ts": 76, - "src/main/ipc/pty/ipc/leaf-move.test.ts": 628, - "src/main/ipc/pty/ipc/spawn-commit-run-facts.test.ts": 819, - "src/main/ipc/pty/ipc/spawn-commit-ssh-lease-cardinality.test.ts": 1887, - "src/main/ipc/pty/ipc/spawn-options-codex-isolation.test.ts": 880, - "src/main/ipc/pty/ipc/spawn-options-terminal-shell-args.test.ts": 1183, - "src/main/ipc/pty/ipc/spawn-push-target-materialization-real-git.test.ts": 1122, - "src/main/ipc/pty/ipc/spawn-push-target-materialization.test.ts": 77, - "src/main/ipc/pty/ipc/spawn-reattach-size-cache.test.ts": 1409, - "src/main/ipc/pty/ipc/terminal-view-attributes-ipc.test.ts": 400, - "src/main/ipc/pty/ipc/write-input-acceptance.test.ts": 77, - "src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts": 69, - "src/main/ipc/pty/ipc/write-input-user-input.test.ts": 69, - "src/main/ipc/pty/managed-data-account-spawn-environment.test.ts": 770, - "src/main/ipc/pty/opencode-legacy-spawn-deletions.test.ts": 790, - "src/main/ipc/pty/pane/agent-launch-pane-evidence.test.ts": 721, - "src/main/ipc/pty/pane/join-pane-spawn.test.ts": 924, - "src/main/ipc/pty/pane/launch-authority.test.ts": 71, - "src/main/ipc/pty/pane/pane-owner-replacement.test.ts": 1478, - "src/main/ipc/pty/pane/stable-pane-absence-death-certificate.test.ts": 5118, - "src/main/ipc/pty/pane/stable-pane-relay-absence-respawn.test.ts": 814, - "src/main/ipc/pty/provider/registry-color-query-reply-colors.test.ts": 398, - "src/main/ipc/pty/provider/registry-host-routing.test.ts": 417, - "src/main/ipc/pty/register-headless-runtime.test.ts": 43, - "src/main/ipc/pty/register-without-renderer.test.ts": 5362, - "src/main/ipc/pty/runtime/queried-host-kinds.test.ts": 512, - "src/main/ipc/pty/runtime/spawn-commit-pty-size.test.ts": 1018, - "src/main/ipc/pty/runtime/spawn-commit-run-facts.test.ts": 833, - "src/main/ipc/pty/runtime/spawn-preflight-requested-shell.test.ts": 2118, - "src/main/ipc/pty/spawn-options-agent-workspace-trust.test.ts": 1046, - "src/main/ipc/qoder-desktop-execution-host.test.ts": 1898, - "src/main/ipc/qoder-independent-renderer-spawn-review.test.ts": 1589, - "src/main/ipc/rate-limits.test.ts": 52, - "src/main/ipc/readdir-error-diagnostics.test.ts": 41, - "src/main/ipc/register-core-handlers/register-core-handlers.test.ts": 2495, - "src/main/ipc/registered-worktree-root-ownership.test.ts": 438, - "src/main/ipc/registered-worktree-root-refresh.test.ts": 184, - "src/main/ipc/registered-worktree-root-wsl-runtime.test.ts": 171, - "src/main/ipc/registered-worktree-roots-scoped-invalidation.test.ts": 89, - "src/main/ipc/remote-watcher-event-batch.test.ts": 77, - "src/main/ipc/renderer-lifetime-abort.test.ts": 58, - "src/main/ipc/renderer-shutdown-checkpoint.test.ts": 457, - "src/main/ipc/renderer-terminal-serializer-readiness.test.ts": 56, - "src/main/ipc/repos-add-linked-worktree.test.ts": 1324, - "src/main/ipc/repos-create.test.ts": 1219, - "src/main/ipc/repos-execution-host-catalog.test.ts": 1173, - "src/main/ipc/repos-local-add-and-project-setup.test.ts": 1476, - "src/main/ipc/repos-local-clone-lifecycle.test.ts": 1644, - "src/main/ipc/repos-nested-import.test.ts": 1393, - "src/main/ipc/repos-nested-scan.test.ts": 1295, - "src/main/ipc/repos-picker.test.ts": 1109, - "src/main/ipc/repos-remote-base-ref-queries.test.ts": 1223, - "src/main/ipc/repos-remote-client-events.test.ts": 1788, - "src/main/ipc/repos-remote-git-username.test.ts": 1104, - "src/main/ipc/repos-sparse-presets.test.ts": 1196, - "src/main/ipc/repos/folder-workspace-attachment-handlers.test.ts": 1525, - "src/main/ipc/repos/repo-creation-git-availability.test.ts": 310, - "src/main/ipc/runtime-agent-launch-capability.test.ts": 3143, - "src/main/ipc/runtime-environment-browser-client-host-handler.test.ts": 1143, - "src/main/ipc/runtime-environment-capability-evidence.test.ts": 46, - "src/main/ipc/runtime-environment-cli-removal-routing.test.ts": 1153, - "src/main/ipc/runtime-environment-diagnostics-broadcast.test.ts": 43, - "src/main/ipc/runtime-environment-federated-read-routing.test.ts": 1096, - "src/main/ipc/runtime-environment-federated-read-transport.bench.test.ts": 328, - "src/main/ipc/runtime-environment-host-key.test.ts": 181, - "src/main/ipc/runtime-environment-managed-tunnel.test.ts": 483, - "src/main/ipc/runtime-environment-preference.test.ts": 401, - "src/main/ipc/runtime-environment-removal-cleanup.test.ts": 53, - "src/main/ipc/runtime-environment-removal-storage.test.ts": 1576, - "src/main/ipc/runtime-environment-request-connections.test.ts": 2790, - "src/main/ipc/runtime-environment-revision-guard.test.ts": 43, - "src/main/ipc/runtime-environment-session-reconcile.test.ts": 977, - "src/main/ipc/runtime-environment-status-connection.test.ts": 1592, - "src/main/ipc/runtime-environment-status-recovery.test.ts": 1038, - "src/main/ipc/runtime-environment-status-retirement.test.ts": 1009, - "src/main/ipc/runtime-environment-subscription-identity.test.ts": 1066, - "src/main/ipc/runtime-environment-support-routing.test.ts": 1306, - "src/main/ipc/runtime-environment-transport-generation.test.ts": 42, - "src/main/ipc/runtime-environment-transport-routing-tailscale-hint.test.ts": 2342, - "src/main/ipc/runtime-environments-call-routing.test.ts": 2632, - "src/main/ipc/runtime-environments-capability-cache.test.ts": 1740, - "src/main/ipc/runtime-environments-pairing.test.ts": 2250, - "src/main/ipc/runtime-environments-status-diagnostics.test.ts": 1491, - "src/main/ipc/runtime-environments-subscription-lifecycle.test.ts": 2695, - "src/main/ipc/runtime-environments-subscription-routing.test.ts": 1823, - "src/main/ipc/runtime-environments-subscription-teardown.test.ts": 1629, - "src/main/ipc/runtime-import-limits.test.ts": 46, - "src/main/ipc/runtime-ssh-access-handlers.test.ts": 191, - "src/main/ipc/runtime-subscribe-lifecycle.test.ts": 2991, - "src/main/ipc/runtime-upload-file-stream.test.ts": 140, - "src/main/ipc/runtime-upload-slice-boundaries.test.ts": 373, - "src/main/ipc/runtime-upload-temp-sweep.test.ts": 1055, - "src/main/ipc/runtime-watcher-disposal-owners.test.ts": 51, - "src/main/ipc/runtime-watcher-pending-assignment.test.ts": 294, - "src/main/ipc/runtime-watcher-pool-shutdown.test.ts": 91, - "src/main/ipc/runtime-watcher-process-pool.test.ts": 146, - "src/main/ipc/runtime.test.ts": 3357, - "src/main/ipc/sender-scoped-request-cancellation.test.ts": 96, - "src/main/ipc/session-close-terminal-surface.test.ts": 346, - "src/main/ipc/session.test.ts": 431, - "src/main/ipc/settings.test.ts": 1109, - "src/main/ipc/shallow-watch-delivery-probe-lifetime.test.ts": 92, - "src/main/ipc/shallow-watch-delivery-probe.test.ts": 104, - "src/main/ipc/shell.test.ts": 168, - "src/main/ipc/skill-cloud-install-ipc-schemas.test.ts": 269, - "src/main/ipc/skill-cloud-ipc-handlers.test.ts": 280, - "src/main/ipc/skill-install-management-ipc-handlers.test.ts": 274, - "src/main/ipc/skill-install-progress-ipc.test.ts": 50, - "src/main/ipc/skill-ipc-main-window.test.ts": 56, - "src/main/ipc/skills.test.ts": 1180, - "src/main/ipc/source-control-ai-linked-issue.test.ts": 89, - "src/main/ipc/sparse-preset-attribution.test.ts": 43, - "src/main/ipc/speech-worker-exit-lifetime.test.ts": 529, - "src/main/ipc/speech.test.ts": 80, - "src/main/ipc/ssh-app-shutdown.test.ts": 4272, - "src/main/ipc/ssh-browse.test.ts": 706, - "src/main/ipc/ssh-connect-recheck-cancellation.test.ts": 4164, - "src/main/ipc/ssh-disconnect-cancellation.test.ts": 5166, - "src/main/ipc/ssh-handler-reregistration.test.ts": 4043, - "src/main/ipc/ssh-host-server-decision-failure.test.ts": 385, - "src/main/ipc/ssh-host-server-on-connect-wiring.test.ts": 2374, - "src/main/ipc/ssh-host-sleep-reconnect.test.ts": 424, - "src/main/ipc/ssh-managed-server-unlink-broadcast.test.ts": 487, - "src/main/ipc/ssh-passphrase.test.ts": 104, - "src/main/ipc/ssh-state-broadcast-fanout.test.ts": 4164, - "src/main/ipc/ssh-target-crud-handlers-ownership.test.ts": 560, - "src/main/ipc/ssh-target-registry.test.ts": 6430, - "src/main/ipc/ssh-test-connection-probe-registry.test.ts": 46, - "src/main/ipc/ssh-test-probe-presence.test.ts": 63, - "src/main/ipc/ssh.test.ts": 4177, - "src/main/ipc/telemetry.test.ts": 297, - "src/main/ipc/terminal-git-credential-guard.test.ts": 103, - "src/main/ipc/terminal-preview-output-stream.test.ts": 57, - "src/main/ipc/terminal-preview.test.ts": 237, - "src/main/ipc/terminal-render-desync-evidence.test.ts": 88, - "src/main/ipc/terminal-startup-color-query-replies.test.ts": 322, - "src/main/ipc/tui-agent-detection-commands.test.ts": 60, - "src/main/ipc/ui.test.ts": 86, - "src/main/ipc/usage-provider-handlers-readiness.test.ts": 597, - "src/main/ipc/usage-provider-handlers.test.ts": 59, - "src/main/ipc/watched-worktree-catalog-notification.test.ts": 74, - "src/main/ipc/watcher-event-root-path-rewrite.test.ts": 56, - "src/main/ipc/watcher-removal-gate.test.ts": 52, - "src/main/ipc/workspace-cleanup-activity.test.ts": 89, - "src/main/ipc/workspace-cleanup-broad-scan.test.ts": 686, - "src/main/ipc/workspace-cleanup-execution-host-routing.test.ts": 595, - "src/main/ipc/workspace-cleanup-local-git-routing.test.ts": 1167, - "src/main/ipc/workspace-cleanup-snapshot-ipc.test.ts": 105, - "src/main/ipc/workspace-cleanup.test.ts": 2928, - "src/main/ipc/workspace-create-error-classifier.test.ts": 52, - "src/main/ipc/workspace-ports.test.ts": 92, - "src/main/ipc/workspace-space.test.ts": 66, - "src/main/ipc/worktree-base-directory-change-collector.test.ts": 54, - "src/main/ipc/worktree-base-directory-event-filter.test.ts": 72, - "src/main/ipc/worktree-base-directory-poller-marker-fanout.test.ts": 204, - "src/main/ipc/worktree-base-directory-poller.test.ts": 2668, - "src/main/ipc/worktree-base-directory-watch-targets.test.ts": 437, - "src/main/ipc/worktree-base-directory-watcher.test.ts": 293, - "src/main/ipc/worktree-create-lineage.test.ts": 958, - "src/main/ipc/worktree-folder-rename-target.test.ts": 96, - "src/main/ipc/worktree-git-common-polling.test.ts": 1046, - "src/main/ipc/worktree-git-common-watch-reconciliation.test.ts": 112, - "src/main/ipc/worktree-git-common-watch.test.ts": 2683, - "src/main/ipc/worktree-git-status-ref-watch.test.ts": 76, - "src/main/ipc/worktree-head-identity-reader-concurrency.test.ts": 77, - "src/main/ipc/worktree-head-identity-reader-incremental.test.ts": 113, - "src/main/ipc/worktree-head-identity-reader.test.ts": 85, - "src/main/ipc/worktree-head-identity-refresh.test.ts": 87, - "src/main/ipc/worktree-head-identity-retirement.test.ts": 79, - "src/main/ipc/worktree-include-copy-budget.test.ts": 129, - "src/main/ipc/worktree-logic-created-agent.test.ts": 97, - "src/main/ipc/worktree-logic-wsl.test.ts": 104, - "src/main/ipc/worktree-logic.test.ts": 143, - "src/main/ipc/worktree-metadata-merge.test.ts": 52, - "src/main/ipc/worktree-path-deduplication.test.ts": 77, - "src/main/ipc/worktree-push-target-cleanup.test.ts": 454, - "src/main/ipc/worktree-push-target-reconciliation-real-git.test.ts": 789, - "src/main/ipc/worktree-push-target-reconciliation.test.ts": 466, - "src/main/ipc/worktree-push-target-refspec-migration.test.ts": 188, - "src/main/ipc/worktree-push-target-refspec-real-git.test.ts": 1650, - "src/main/ipc/worktree-push-target-remote-scan.test.ts": 440, - "src/main/ipc/worktree-push-target-setup.test.ts": 464, - "src/main/ipc/worktree-remote-push-target-materialization.test.ts": 772, - "src/main/ipc/worktree-symlink-reconciliation.real.test.ts": 465, - "src/main/ipc/worktree-symlinks.test.ts": 155, - "src/main/ipc/worktree-watcher-removal-binding.test.ts": 51, - "src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts": 1670, - "src/main/ipc/worktrees-background-removal.test.ts": 1866, - "src/main/ipc/worktrees-canonical-ssh-listing.test.ts": 1212, - "src/main/ipc/worktrees-create-event-timing.test.ts": 1165, - "src/main/ipc/worktrees-create-metadata-persistence.test.ts": 1531, - "src/main/ipc/worktrees-delete-pty-teardown.test.ts": 1507, + "src/main/ipc/preflight-wsl-command.test.ts": 51, + "src/main/ipc/pty-activation-inventory-scope.test.ts": 2045, + "src/main/ipc/pty-agent-launch-pane-attach.test.ts": 3145, + "src/main/ipc/pty-agent-pane-launch.test.ts": 1883, + "src/main/ipc/pty-applied-size-reporting.test.ts": 2339, + "src/main/ipc/pty-buffer-snapshot-dispatch.test.ts": 1820, + "src/main/ipc/pty-codex-account-attribution.test.ts": 1982, + "src/main/ipc/pty-controller-owner-recovery.test.ts": 2124, + "src/main/ipc/pty-controller-ownership-routing.test.ts": 2014, + "src/main/ipc/pty-controller-process-inventory.test.ts": 1998, + "src/main/ipc/pty-controller-spawn-admission.test.ts": 4679, + "src/main/ipc/pty-cumulative-ack-accounting.test.ts": 2200, + "src/main/ipc/pty-daemon-controller-teardown.test.ts": 1944, + "src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts": 2206, + "src/main/ipc/pty-daemon-spawn-codex-auth.test.ts": 2270, + "src/main/ipc/pty-daemon-spawn-session-identity.test.ts": 1909, + "src/main/ipc/pty-daemon-spawn-wsl-runtime.test.ts": 2030, + "src/main/ipc/pty-daemon-ssh-lease-lifecycle.test.ts": 1727, + "src/main/ipc/pty-dead-owner-respawn.test.ts": 1858, + "src/main/ipc/pty-delivery-health-heal.test.ts": 1926, + "src/main/ipc/pty-dispatcher-handshake-osc-answers.test.ts": 2029, + "src/main/ipc/pty-encoding.test.ts": 37, + "src/main/ipc/pty-global-renderer-credit.test.ts": 2307, + "src/main/ipc/pty-hidden-at-spawn-mark.test.ts": 4605, + "src/main/ipc/pty-hidden-delivery-gate.test.ts": 35, + "src/main/ipc/pty-ipc-hidden-delivery-gate.test.ts": 2345, + "src/main/ipc/pty-ipc-producer-flow-control.test.ts": 1606, + "src/main/ipc/pty-launch-parity.test.ts": 6066, + "src/main/ipc/pty-launch-race-parity.test.ts": 2327, + "src/main/ipc/pty-listener-teardown-and-orphans.test.ts": 4257, + "src/main/ipc/pty-login-shell-startup-commands.test.ts": 2112, + "src/main/ipc/pty-management.test.ts": 122, + "src/main/ipc/pty-output-batching-drain.test.ts": 2085, + "src/main/ipc/pty-output-drain-rounds.test.ts": 2064, + "src/main/ipc/pty-pane-claim-arbitration.test.ts": 5856, + "src/main/ipc/pty-pane-materialization-race.test.ts": 2733, + "src/main/ipc/pty-pane-reservation-settlement.test.ts": 2331, + "src/main/ipc/pty-pane-restart-replace.test.ts": 2178, + "src/main/ipc/pty-pending-data-drain-queue-differential.test.ts": 1691, + "src/main/ipc/pty-pending-data-drain-queue.test.ts": 48, + "src/main/ipc/pty-pending-data-drain-scheduler-differential.test.ts": 74, + "src/main/ipc/pty-pending-output-cap.test.ts": 1806, + "src/main/ipc/pty-persisted-incarnation-repair.test.ts": 1916, + "src/main/ipc/pty-producer-flow-control.test.ts": 45, + "src/main/ipc/pty-renderer-inflight-credit.test.ts": 2088, + "src/main/ipc/pty-renderer-lifecycle-delivery-reset.test.ts": 2291, + "src/main/ipc/pty-renderer-liveness-guard.test.ts": 2009, + "src/main/ipc/pty-renderer-send-failure-recovery.test.ts": 2032, + "src/main/ipc/pty-reset-input-modes.test.ts": 1920, + "src/main/ipc/pty-restore-record-seeding.test.ts": 4510, + "src/main/ipc/pty-restored-appimage-cli-shim-refresh.test.ts": 2141, + "src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts": 4797, + "src/main/ipc/pty-runtime-kill-and-exit.test.ts": 2167, + "src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts": 2004, + "src/main/ipc/pty-serializer-settlement-mapping.test.ts": 3692, + "src/main/ipc/pty-session-liveness-and-ownership.test.ts": 1826, + "src/main/ipc/pty-spawn-codex-home-unavailable.test.ts": 2268, + "src/main/ipc/pty-spawn-cwd-fallback.test.ts": 2106, + "src/main/ipc/pty-spawn-env-agent-overlays.test.ts": 2292, + "src/main/ipc/pty-spawn-env-codex-home-routing.test.ts": 2050, + "src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts": 2201, + "src/main/ipc/pty-spawn-env-terminal-basics.test.ts": 2276, + "src/main/ipc/pty-spawn-pi-owner-env.test.ts": 1543, + "src/main/ipc/pty-spawn-placement-threading.test.ts": 1876, + "src/main/ipc/pty-spawn-runtime-handle-binding.test.ts": 7101, + "src/main/ipc/pty-spawn-timing.test.ts": 44, + "src/main/ipc/pty-ssh-stop-verdict.test.ts": 1949, + "src/main/ipc/pty-startup-barrier-and-listing.test.ts": 2134, + "src/main/ipc/pty-startup-swap-window-presence.test.ts": 1999, + "src/main/ipc/pty-windows-shell-selection.test.ts": 2114, + "src/main/ipc/pty-write-ipc-validation.test.ts": 2198, + "src/main/ipc/pty-wsl-cwd-validation.test.ts": 2138, + "src/main/ipc/pty/configured-terminal-shell-args.test.ts": 37, + "src/main/ipc/pty/daemon-late-exit.test.ts": 5049, + "src/main/ipc/pty/delivery/attached-pty-size.test.ts": 43, + "src/main/ipc/pty/delivery/interactive-typing-burst.test.ts": 38, + "src/main/ipc/pty/host-env/codex-hook-reconcile-spawn.test.ts": 424, + "src/main/ipc/pty/host-env/codex-terminal-clipboard.test.ts": 43, + "src/main/ipc/pty/host-env/omp-launch-environment.test.ts": 103, + "src/main/ipc/pty/host-env/opencode-hook-installation.test.ts": 674, + "src/main/ipc/pty/host-env/spawn-cwd-wsl-move.test.ts": 357, + "src/main/ipc/pty/ipc/claude-old-terminal.test.ts": 41, + "src/main/ipc/pty/ipc/codex-shared-server.test.ts": 86, + "src/main/ipc/pty/ipc/leaf-move.test.ts": 640, + "src/main/ipc/pty/ipc/spawn-commit-run-facts.test.ts": 1163, + "src/main/ipc/pty/ipc/spawn-options-codex-isolation.test.ts": 1015, + "src/main/ipc/pty/ipc/spawn-options-spawn-intent.test.ts": 979, + "src/main/ipc/pty/ipc/spawn-options-terminal-shell-args.test.ts": 972, + "src/main/ipc/pty/ipc/spawn-push-target-materialization-real-git.test.ts": 1079, + "src/main/ipc/pty/ipc/spawn-push-target-materialization.test.ts": 49, + "src/main/ipc/pty/ipc/spawn-reattach-size-cache.test.ts": 1063, + "src/main/ipc/pty/ipc/terminal-view-attributes-ipc.test.ts": 430, + "src/main/ipc/pty/ipc/write-input-acceptance.test.ts": 56, + "src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts": 58, + "src/main/ipc/pty/ipc/write-input-user-input.test.ts": 68, + "src/main/ipc/pty/managed-data-account-spawn-environment.test.ts": 1169, + "src/main/ipc/pty/opencode-legacy-spawn-deletions.test.ts": 1041, + "src/main/ipc/pty/pane/join-pane-spawn.test.ts": 1000, + "src/main/ipc/pty/pane/launch-authority.test.ts": 30, + "src/main/ipc/pty/pane/pane-owner-replacement.test.ts": 1557, + "src/main/ipc/pty/pane/stable-pane-absence-death-certificate.test.ts": 4590, + "src/main/ipc/pty/pane/stable-pane-absence-respawn.test.ts": 1106, + "src/main/ipc/pty/provider/registry-color-query-reply-colors.test.ts": 554, + "src/main/ipc/pty/provider/registry-host-routing.test.ts": 1006, + "src/main/ipc/pty/register-headless-runtime.test.ts": 35, + "src/main/ipc/pty/register-without-renderer.test.ts": 4896, + "src/main/ipc/pty/runtime/queried-host-kinds.test.ts": 447, + "src/main/ipc/pty/runtime/spawn-commit-pty-size.test.ts": 792, + "src/main/ipc/pty/runtime/spawn-commit-run-facts.test.ts": 1071, + "src/main/ipc/pty/runtime/spawn-preflight-requested-shell.test.ts": 2152, + "src/main/ipc/pty/spawn-options-agent-workspace-trust.test.ts": 1000, + "src/main/ipc/qoder-desktop-execution-host.test.ts": 1987, + "src/main/ipc/qoder-independent-renderer-spawn-review.test.ts": 1923, + "src/main/ipc/rate-limits.test.ts": 31, + "src/main/ipc/readdir-error-diagnostics.test.ts": 39, + "src/main/ipc/register-core-handlers/register-core-handlers.test.ts": 758, + "src/main/ipc/registered-worktree-root-ownership.test.ts": 659, + "src/main/ipc/registered-worktree-root-refresh.test.ts": 321, + "src/main/ipc/registered-worktree-root-wsl-runtime.test.ts": 192, + "src/main/ipc/registered-worktree-roots-scoped-invalidation.test.ts": 239, + "src/main/ipc/remote-watcher-event-batch.test.ts": 56, + "src/main/ipc/renderer-lifetime-abort.test.ts": 47, + "src/main/ipc/renderer-shutdown-checkpoint.test.ts": 433, + "src/main/ipc/renderer-terminal-serializer-readiness.test.ts": 40, + "src/main/ipc/repos-add-linked-worktree.test.ts": 1287, + "src/main/ipc/repos-create.test.ts": 1160, + "src/main/ipc/repos-execution-host-catalog.test.ts": 1289, + "src/main/ipc/repos-local-add-and-project-setup.test.ts": 1353, + "src/main/ipc/repos-local-clone-lifecycle.test.ts": 1536, + "src/main/ipc/repos-nested-import.test.ts": 1432, + "src/main/ipc/repos-nested-scan.test.ts": 1111, + "src/main/ipc/repos-picker.test.ts": 1058, + "src/main/ipc/repos-remote-base-ref-queries.test.ts": 1422, + "src/main/ipc/repos-remote-client-events.test.ts": 1818, + "src/main/ipc/repos-remote-git-username.test.ts": 1278, + "src/main/ipc/repos-sparse-presets.test.ts": 1309, + "src/main/ipc/repos/folder-workspace-attachment-handlers.test.ts": 1735, + "src/main/ipc/repos/project-host-setup-wsl-move-fence.test.ts": 2051, + "src/main/ipc/repos/repo-creation-git-availability.test.ts": 272, + "src/main/ipc/repos/repo-ipc-wsl-move-fence.test.ts": 1840, + "src/main/ipc/runtime-agent-launch-capability.test.ts": 2779, + "src/main/ipc/runtime-environment-browser-client-host-handler.test.ts": 762, + "src/main/ipc/runtime-environment-capability-evidence.test.ts": 38, + "src/main/ipc/runtime-environment-cli-removal-routing.test.ts": 806, + "src/main/ipc/runtime-environment-diagnostics-broadcast.test.ts": 32, + "src/main/ipc/runtime-environment-federated-read-routing.test.ts": 730, + "src/main/ipc/runtime-environment-federated-read-transport.bench.test.ts": 310, + "src/main/ipc/runtime-environment-host-key.test.ts": 257, + "src/main/ipc/runtime-environment-managed-tunnel.test.ts": 518, + "src/main/ipc/runtime-environment-mobile-relay-hosts.test.ts": 479, + "src/main/ipc/runtime-environment-preference.test.ts": 456, + "src/main/ipc/runtime-environment-removal-cleanup.test.ts": 215, + "src/main/ipc/runtime-environment-removal-storage.test.ts": 1294, + "src/main/ipc/runtime-environment-request-connections.test.ts": 2310, + "src/main/ipc/runtime-environment-revision-guard.test.ts": 35, + "src/main/ipc/runtime-environment-session-reconcile.test.ts": 750, + "src/main/ipc/runtime-environment-status-connection.test.ts": 1202, + "src/main/ipc/runtime-environment-status-recovery.test.ts": 855, + "src/main/ipc/runtime-environment-status-retirement.test.ts": 905, + "src/main/ipc/runtime-environment-subscription-identity.test.ts": 931, + "src/main/ipc/runtime-environment-subscription-layout-recording.test.ts": 851, + "src/main/ipc/runtime-environment-support-routing.test.ts": 843, + "src/main/ipc/runtime-environment-transport-generation.test.ts": 31, + "src/main/ipc/runtime-environment-transport-routing-tailscale-hint.test.ts": 2039, + "src/main/ipc/runtime-environment-unreachable-hint.test.ts": 225, + "src/main/ipc/runtime-environments-call-routing.test.ts": 2173, + "src/main/ipc/runtime-environments-capability-cache.test.ts": 1268, + "src/main/ipc/runtime-environments-pairing.test.ts": 1923, + "src/main/ipc/runtime-environments-status-diagnostics.test.ts": 1104, + "src/main/ipc/runtime-environments-subscription-lifecycle.test.ts": 1561, + "src/main/ipc/runtime-environments-subscription-routing.test.ts": 1154, + "src/main/ipc/runtime-environments-subscription-teardown.test.ts": 1024, + "src/main/ipc/runtime-import-limits.test.ts": 37, + "src/main/ipc/runtime-ssh-access-handlers.test.ts": 235, + "src/main/ipc/runtime-subscribe-lifecycle.test.ts": 3297, + "src/main/ipc/runtime-upload-file-stream.test.ts": 154, + "src/main/ipc/runtime-upload-slice-boundaries.test.ts": 402, + "src/main/ipc/runtime-upload-temp-sweep.test.ts": 1037, + "src/main/ipc/runtime-watcher-disposal-owners.test.ts": 46, + "src/main/ipc/runtime-watcher-pending-assignment.test.ts": 202, + "src/main/ipc/runtime-watcher-pool-shutdown.test.ts": 73, + "src/main/ipc/runtime-watcher-process-pool.test.ts": 132, + "src/main/ipc/runtime.test.ts": 2838, + "src/main/ipc/sender-scoped-request-cancellation.test.ts": 54, + "src/main/ipc/session-close-terminal-surface.test.ts": 397, + "src/main/ipc/session.test.ts": 470, + "src/main/ipc/settings-delayed-response.test.ts": 2558, + "src/main/ipc/settings.test.ts": 1101, + "src/main/ipc/shallow-watch-delivery-probe-lifetime.test.ts": 103, + "src/main/ipc/shallow-watch-delivery-probe.test.ts": 65, + "src/main/ipc/shell-wsl-owner.test.ts": 131, + "src/main/ipc/shell.test.ts": 253, + "src/main/ipc/skill-cloud-install-ipc-schemas.test.ts": 244, + "src/main/ipc/skill-cloud-ipc-handlers.test.ts": 285, + "src/main/ipc/skill-install-management-ipc-handlers.test.ts": 257, + "src/main/ipc/skill-install-progress-ipc.test.ts": 43, + "src/main/ipc/skill-ipc-main-window.test.ts": 42, + "src/main/ipc/skills.test.ts": 980, + "src/main/ipc/source-control-ai-linked-issue.test.ts": 46, + "src/main/ipc/sparse-preset-attribution.test.ts": 41, + "src/main/ipc/speech-worker-exit-lifetime.test.ts": 662, + "src/main/ipc/speech.test.ts": 63, + "src/main/ipc/ssh-app-shutdown.test.ts": 1656, + "src/main/ipc/ssh-browse.test.ts": 772, + "src/main/ipc/ssh-connect-decision-cancellation.test.ts": 1348, + "src/main/ipc/ssh-credential-cancel.test.ts": 1371, + "src/main/ipc/ssh-disconnect-cancellation.test.ts": 1441, + "src/main/ipc/ssh-handler-reregistration.test.ts": 1334, + "src/main/ipc/ssh-host-server-decision-failure.test.ts": 107, + "src/main/ipc/ssh-host-server-on-connect-wiring.test.ts": 2114, + "src/main/ipc/ssh-host-sleep-reconnect.test.ts": 37, + "src/main/ipc/ssh-managed-host-port-forward-lifecycle.test.ts": 516, + "src/main/ipc/ssh-managed-server-unlink-broadcast.test.ts": 288, + "src/main/ipc/ssh-passphrase.test.ts": 251, + "src/main/ipc/ssh-port-forward-restore.test.ts": 95, + "src/main/ipc/ssh-state-broadcast-fanout.test.ts": 1828, + "src/main/ipc/ssh-target-crud-handlers-ownership.test.ts": 1478, + "src/main/ipc/ssh-target-registry.test.ts": 3921, + "src/main/ipc/ssh-test-connection-probe-registry.test.ts": 38, + "src/main/ipc/ssh-test-probe-presence.test.ts": 45, + "src/main/ipc/ssh.test.ts": 2105, + "src/main/ipc/telemetry.test.ts": 320, + "src/main/ipc/terminal-git-credential-guard.test.ts": 87, + "src/main/ipc/terminal-preview-output-stream.test.ts": 54, + "src/main/ipc/terminal-preview.test.ts": 228, + "src/main/ipc/terminal-render-desync-evidence.test.ts": 70, + "src/main/ipc/terminal-startup-color-query-replies.test.ts": 316, + "src/main/ipc/tui-agent-detection-commands.test.ts": 44, + "src/main/ipc/ui.test.ts": 198, + "src/main/ipc/usage-provider-handlers-readiness.test.ts": 372, + "src/main/ipc/usage-provider-handlers.test.ts": 35, + "src/main/ipc/watched-worktree-catalog-notification.test.ts": 48, + "src/main/ipc/watcher-event-root-path-rewrite.test.ts": 48, + "src/main/ipc/watcher-removal-gate.test.ts": 37, + "src/main/ipc/workspace-cleanup-activity.test.ts": 59, + "src/main/ipc/workspace-cleanup-broad-scan.test.ts": 629, + "src/main/ipc/workspace-cleanup-execution-host-routing.test.ts": 614, + "src/main/ipc/workspace-cleanup-local-git-routing.test.ts": 640, + "src/main/ipc/workspace-cleanup-snapshot-ipc.test.ts": 77, + "src/main/ipc/workspace-cleanup.test.ts": 2673, + "src/main/ipc/workspace-create-error-classifier.test.ts": 45, + "src/main/ipc/workspace-ports.test.ts": 69, + "src/main/ipc/workspace-space.test.ts": 47, + "src/main/ipc/worktree-base-directory-change-collector.test.ts": 48, + "src/main/ipc/worktree-base-directory-event-filter.test.ts": 48, + "src/main/ipc/worktree-base-directory-notifications.test.ts": 72, + "src/main/ipc/worktree-base-directory-poller-marker-fanout.test.ts": 171, + "src/main/ipc/worktree-base-directory-poller.test.ts": 2613, + "src/main/ipc/worktree-base-directory-watch-targets.test.ts": 593, + "src/main/ipc/worktree-base-directory-watcher.test.ts": 411, + "src/main/ipc/worktree-change-invalidators.test.ts": 28, + "src/main/ipc/worktree-create-lineage.test.ts": 966, + "src/main/ipc/worktree-folder-rename-target.test.ts": 76, + "src/main/ipc/worktree-git-common-polling.test.ts": 1028, + "src/main/ipc/worktree-git-common-watch-reconciliation.test.ts": 99, + "src/main/ipc/worktree-git-common-watch.test.ts": 2624, + "src/main/ipc/worktree-git-status-attribution-real-git.test.ts": 1802, + "src/main/ipc/worktree-git-status-ref-watch.test.ts": 55, + "src/main/ipc/worktree-head-identity-cached-paths.test.ts": 46, + "src/main/ipc/worktree-head-identity-reader-concurrency.test.ts": 64, + "src/main/ipc/worktree-head-identity-reader-incremental.test.ts": 157, + "src/main/ipc/worktree-head-identity-reader.test.ts": 65, + "src/main/ipc/worktree-head-identity-refresh.test.ts": 68, + "src/main/ipc/worktree-head-identity-retirement.test.ts": 80, + "src/main/ipc/worktree-include-copy-budget.test.ts": 122, + "src/main/ipc/worktree-logic-created-agent.test.ts": 102, + "src/main/ipc/worktree-logic-wsl.test.ts": 95, + "src/main/ipc/worktree-logic.test.ts": 203, + "src/main/ipc/worktree-metadata-merge.test.ts": 42, + "src/main/ipc/worktree-path-deduplication.test.ts": 63, + "src/main/ipc/worktree-push-target-cleanup.test.ts": 639, + "src/main/ipc/worktree-push-target-reconciliation-real-git.test.ts": 950, + "src/main/ipc/worktree-push-target-reconciliation.test.ts": 600, + "src/main/ipc/worktree-push-target-refspec-migration.test.ts": 212, + "src/main/ipc/worktree-push-target-refspec-real-git.test.ts": 1849, + "src/main/ipc/worktree-push-target-remote-scan.test.ts": 570, + "src/main/ipc/worktree-push-target-setup.test.ts": 568, + "src/main/ipc/worktree-remote-push-target-materialization.test.ts": 890, + "src/main/ipc/worktree-symlink-reconciliation.real.test.ts": 369, + "src/main/ipc/worktree-symlinks.test.ts": 147, + "src/main/ipc/worktree-watcher-removal-binding.test.ts": 28, + "src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts": 1874, + "src/main/ipc/worktrees-automation-create-terminals.test.ts": 1366, + "src/main/ipc/worktrees-background-removal.test.ts": 2079, + "src/main/ipc/worktrees-canonical-ssh-listing.test.ts": 1171, + "src/main/ipc/worktrees-create-event-timing.test.ts": 1341, + "src/main/ipc/worktrees-create-metadata-persistence.test.ts": 1185, + "src/main/ipc/worktrees-delete-pty-teardown.test.ts": 1295, "src/main/ipc/worktrees-detected-scan-cache.test.ts": 2006, - "src/main/ipc/worktrees-discovery-metadata-backfill.test.ts": 1324, - "src/main/ipc/worktrees-existing-branch-checkout.test.ts": 1158, - "src/main/ipc/worktrees-failed-removal.test.ts": 1242, - "src/main/ipc/worktrees-forget-local.test.ts": 1316, - "src/main/ipc/worktrees-issue-command-overrides.test.ts": 1354, - "src/main/ipc/worktrees-lineage-hydration.test.ts": 1297, - "src/main/ipc/worktrees-listing-fallback-rows.test.ts": 1487, - "src/main/ipc/worktrees-local-base-ref-resolution.test.ts": 1454, - "src/main/ipc/worktrees-local-create-flow.test.ts": 1392, - "src/main/ipc/worktrees-mutation-scan-generation-order.test.ts": 1353, - "src/main/ipc/worktrees-orphan-directory-cleanup.test.ts": 1334, - "src/main/ipc/worktrees-removal-recovery.test.ts": 1321, - "src/main/ipc/worktrees-remove-archive-hooks.test.ts": 1873, - "src/main/ipc/worktrees-remove-preflight.test.ts": 1662, - "src/main/ipc/worktrees-setup-launch-sparse-checkout.test.ts": 1390, - "src/main/ipc/worktrees-ssh-pr-head-fetch.test.ts": 1459, - "src/main/ipc/worktrees-ssh-repo-owner-resolution.test.ts": 1671, - "src/main/ipc/worktrees-windows.test.ts": 1489, - "src/main/ipc/worktrees-wsl-runtime-routing.test.ts": 1166, - "src/main/ipc/worktrees/listing/detected-provider-listing-catalog-version.test.ts": 337, - "src/main/ipc/worktrees/listing/detected-provider-listing-meta-index.test.ts": 1057, - "src/main/ipc/worktrees/listing/detected-provider-listing-overtaken-scan.test.ts": 390, - "src/main/ipc/worktrees/listing/detected-scan-failure-authority.test.ts": 1030, - "src/main/ipc/worktrees/listing/detected-worktree-classification.test.ts": 161, - "src/main/ipc/worktrees/listing/detected-worktree-scan-hygiene-gate.test.ts": 93, - "src/main/ipc/worktrees/listing/detected-worktree-scan-listing-distro.test.ts": 559, - "src/main/ipc/worktrees/listing/detected-worktree-scan-superseded.test.ts": 68, - "src/main/ipc/worktrees/listing/register-sparse-checkout-cache-invalidation.test.ts": 197, - "src/main/ipc/zcode-plan-credentials.test.ts": 58, - "src/main/issue-command-ignore.test.ts": 620, - "src/main/jcode/daemon-prewarm.test.ts": 54, - "src/main/jcode/hook-command-quoting.test.ts": 54, - "src/main/jcode/hook-config.test.ts": 80, - "src/main/jcode/hook-gate-script.test.ts": 118, - "src/main/jcode/hook-service.test.ts": 114, - "src/main/jira/adf-markdown.test.ts": 57, - "src/main/jira/attachment-image-cache-generation.test.ts": 63, - "src/main/jira/attachment-image-cache.test.ts": 76, - "src/main/jira/attachment-images.test.ts": 80, - "src/main/jira/client.test.ts": 285, - "src/main/jira/issues.test.ts": 170, - "src/main/jira/jira-issue-mutations.test.ts": 100, - "src/main/jira/jira-issue-summary-timeout.test.ts": 80, - "src/main/jira/jira-search-abort.test.ts": 94, - "src/main/jsonl-rpc/agent-connection.test.ts": 224, - "src/main/jsonl-rpc/peer.test.ts": 223, - "src/main/jsonl-rpc/timeline-final-tail.test.ts": 523, - "src/main/jsonl-rpc/timeline-lane.test.ts": 471, - "src/main/keybindings/keybinding-file.test.ts": 185, - "src/main/keybindings/keybinding-service.test.ts": 138, - "src/main/kimi/hook-service-config-delete-race.test.ts": 90, - "src/main/kimi/hook-service-mode-read-failure.test.ts": 86, - "src/main/kimi/hook-service.test.ts": 93, - "src/main/kimi/kimi-hook-config-toml.test.ts": 109, - "src/main/kimi/kimi-runtime-home.test.ts": 66, - "src/main/kiro/hook-service.test.ts": 152, - "src/main/lib/html-to-pdf.test.ts": 79, - "src/main/lib/unread-response-body.test.ts": 60, - "src/main/line-editor-ready-output-scanner.test.ts": 49, - "src/main/linear/client.test.ts": 174, - "src/main/linear/issue-context-client.test.ts": 86, - "src/main/linear/issue-context-current.test.ts": 58, - "src/main/linear/issue-context-errors.test.ts": 45, - "src/main/linear/issue-context-includes.test.ts": 92, - "src/main/linear/issue-context-inline-media.test.ts": 85, - "src/main/linear/issue-context-relations.test.ts": 58, - "src/main/linear/issue-context.test.ts": 77, - "src/main/linear/issue-list-filter.test.ts": 55, - "src/main/linear/issue-relation-write.test.ts": 74, - "src/main/linear/issue-search-summary.test.ts": 45, - "src/main/linear/issues.test.ts": 133, - "src/main/linear/linear-description-images.test.ts": 163, - "src/main/linear/linear-issue-detail-deadline.test.ts": 225, - "src/main/linear/linear-workspace-registry.test.ts": 52, - "src/main/linear/mappers.test.ts": 43, - "src/main/linear/mcp-issue-list-pagination.test.ts": 100, - "src/main/linear/mcp-issue-list.test.ts": 142, - "src/main/linear/projects.test.ts": 394, - "src/main/linear/teams.test.ts": 85, - "src/main/linux-lid-sleep-assertion.test.ts": 95, - "src/main/linux-package-install-command.test.ts": 104, - "src/main/linux-package-install-diagnostic.test.ts": 71, - "src/main/linux-package-update-recovery.test.ts": 226, + "src/main/ipc/worktrees-discovery-metadata-backfill.test.ts": 1239, + "src/main/ipc/worktrees-existing-branch-checkout.test.ts": 1381, + "src/main/ipc/worktrees-failed-removal.test.ts": 1468, + "src/main/ipc/worktrees-forget-local.test.ts": 1309, + "src/main/ipc/worktrees-issue-command-overrides.test.ts": 1268, + "src/main/ipc/worktrees-lineage-hydration.test.ts": 1042, + "src/main/ipc/worktrees-listing-fallback-rows.test.ts": 1370, + "src/main/ipc/worktrees-local-base-ref-resolution.test.ts": 1335, + "src/main/ipc/worktrees-local-create-flow.test.ts": 1294, + "src/main/ipc/worktrees-mutation-scan-generation-order.test.ts": 1257, + "src/main/ipc/worktrees-orphan-directory-cleanup.test.ts": 1246, + "src/main/ipc/worktrees-removal-recovery.test.ts": 1450, + "src/main/ipc/worktrees-remove-archive-hooks.test.ts": 1437, + "src/main/ipc/worktrees-remove-preflight.test.ts": 1681, + "src/main/ipc/worktrees-setup-launch-sparse-checkout.test.ts": 1334, + "src/main/ipc/worktrees-ssh-pr-head-fetch.test.ts": 1358, + "src/main/ipc/worktrees-ssh-repo-owner-resolution.test.ts": 1715, + "src/main/ipc/worktrees-windows.test.ts": 1397, + "src/main/ipc/worktrees-wsl-move-fence.test.ts": 1516, + "src/main/ipc/worktrees-wsl-runtime-routing.test.ts": 1489, + "src/main/ipc/worktrees/listing/detected-provider-listing-catalog-version.test.ts": 721, + "src/main/ipc/worktrees/listing/detected-provider-listing-meta-index.test.ts": 1062, + "src/main/ipc/worktrees/listing/detected-provider-listing-overtaken-scan.test.ts": 641, + "src/main/ipc/worktrees/listing/detected-scan-failure-authority.test.ts": 1132, + "src/main/ipc/worktrees/listing/detected-worktree-classification.test.ts": 173, + "src/main/ipc/worktrees/listing/detected-worktree-scan-hygiene-gate.test.ts": 80, + "src/main/ipc/worktrees/listing/detected-worktree-scan-listing-distro.test.ts": 797, + "src/main/ipc/worktrees/listing/detected-worktree-scan-superseded.test.ts": 47, + "src/main/ipc/worktrees/listing/register-sparse-checkout-cache-invalidation.test.ts": 171, + "src/main/ipc/wsl-guest-windows-drive-path.test.ts": 56, + "src/main/ipc/wsl-host-gate-off.test.ts": 1108, + "src/main/ipc/zcode-plan-credentials.test.ts": 49, + "src/main/issue-command-ignore.test.ts": 508, + "src/main/jcode/daemon-prewarm.test.ts": 63, + "src/main/jcode/hook-command-quoting.test.ts": 31, + "src/main/jcode/hook-config.test.ts": 74, + "src/main/jcode/hook-gate-script.test.ts": 139, + "src/main/jcode/hook-service.test.ts": 113, + "src/main/jira/adf-link-mapping.test.ts": 88, + "src/main/jira/adf-markdown.test.ts": 52, + "src/main/jira/attachment-image-cache-generation.test.ts": 42, + "src/main/jira/attachment-image-cache.test.ts": 56, + "src/main/jira/attachment-image-read.test.ts": 1076, + "src/main/jira/attachment-image-references.test.ts": 74, + "src/main/jira/attachment-images.test.ts": 79, + "src/main/jira/attachment-legacy-cancellation.test.ts": 116, + "src/main/jira/authenticated-request-image-limit.test.ts": 31, + "src/main/jira/client-scoped-token.test.ts": 419, + "src/main/jira/client.test.ts": 342, + "src/main/jira/issues.test.ts": 186, + "src/main/jira/jira-agent-read.test.ts": 97, + "src/main/jira/jira-agile-field-search.test.ts": 89, + "src/main/jira/jira-agile-fields.test.ts": 56, + "src/main/jira/jira-board-column-names.test.ts": 37, + "src/main/jira/jira-cloud-planning-composition.test.ts": 70, + "src/main/jira/jira-issue-mutations.test.ts": 93, + "src/main/jira/jira-issue-summary-timeout.test.ts": 84, + "src/main/jira/jira-preset-filters.test.ts": 105, + "src/main/jira/jira-search-abort.test.ts": 86, + "src/main/jira/jira-transition-required-fields.test.ts": 70, + "src/main/jsonl-rpc/agent-connection.test.ts": 262, + "src/main/jsonl-rpc/peer.test.ts": 207, + "src/main/jsonl-rpc/timeline-final-tail.test.ts": 643, + "src/main/jsonl-rpc/timeline-lane.test.ts": 740, + "src/main/keybindings/keybinding-file.test.ts": 182, + "src/main/keybindings/keybinding-service.test.ts": 169, + "src/main/kimi/hook-service-config-delete-race.test.ts": 66, + "src/main/kimi/hook-service-mode-read-failure.test.ts": 83, + "src/main/kimi/hook-service.test.ts": 109, + "src/main/kimi/kimi-hook-config-toml.test.ts": 138, + "src/main/kimi/kimi-runtime-home.test.ts": 58, + "src/main/kiro/hook-service.test.ts": 156, + "src/main/lib/html-to-pdf.test.ts": 92, + "src/main/lib/unread-response-body.test.ts": 45, + "src/main/line-editor-ready-output-scanner.test.ts": 30, + "src/main/linear/client.test.ts": 291, + "src/main/linear/issue-context-client.test.ts": 74, + "src/main/linear/issue-context-current.test.ts": 31, + "src/main/linear/issue-context-errors.test.ts": 32, + "src/main/linear/issue-context-includes.test.ts": 74, + "src/main/linear/issue-context-inline-media.test.ts": 69, + "src/main/linear/issue-context-relations.test.ts": 46, + "src/main/linear/issue-context.test.ts": 57, + "src/main/linear/issue-list-filter.test.ts": 39, + "src/main/linear/issue-relation-write.test.ts": 56, + "src/main/linear/issue-search-summary.test.ts": 24, + "src/main/linear/issues.test.ts": 137, + "src/main/linear/linear-description-images.test.ts": 155, + "src/main/linear/linear-issue-detail-deadline.test.ts": 201, + "src/main/linear/linear-workspace-registry.test.ts": 48, + "src/main/linear/mappers.test.ts": 34, + "src/main/linear/mcp-issue-list-pagination.test.ts": 96, + "src/main/linear/mcp-issue-list.test.ts": 112, + "src/main/linear/projects.test.ts": 382, + "src/main/linear/teams.test.ts": 66, + "src/main/linux-lid-sleep-assertion.test.ts": 83, + "src/main/linux-package-install-command.test.ts": 83, + "src/main/linux-package-install-diagnostic.test.ts": 54, + "src/main/linux-package-update-recovery.test.ts": 214, "src/main/linux-update-package-type.test.ts": 140, - "src/main/local-builds/local-build-candidate.test.ts": 178, - "src/main/local-builds/local-build-compatibility-contract.test.ts": 295, - "src/main/local-builds/local-build-feed-server.test.ts": 88, - "src/main/local-downloaded-folder-promotion.test.ts": 163, - "src/main/local-worktree-filesystem-wsl-banner.wsl.test.ts": 81, - "src/main/local-worktree-filesystem.test.ts": 368, - "src/main/local-worktree-metadata-prune-gate.test.ts": 56, - "src/main/local-worktree-path-presence.test.ts": 70, - "src/main/local-worktree-removal-recovery-live.test.ts": 385, - "src/main/local-worktree-removal-recovery.test.ts": 131, - "src/main/localhost-worktree-label-proxy.test.ts": 126, - "src/main/macos-full-disk-access-status.test.ts": 49, - "src/main/macos-keychain/generic-password.test.ts": 57, - "src/main/macos-press-and-hold-default.defaults-domain.test.ts": 68, - "src/main/macos-press-and-hold-default.test.ts": 95, - "src/main/macos-system-sleep-assertion.test.ts": 71, - "src/main/macos-tcc-prompt-notice.test.ts": 96, - "src/main/macos-tcc-prompt-watch.test.ts": 94, - "src/main/macos-tcc-reset.test.ts": 67, - "src/main/macos-update-running-instances.integration.test.ts": 47, - "src/main/macos-update-running-instances.test.ts": 69, - "src/main/managed-data-accounts/launch-environment.test.ts": 342, - "src/main/managed-data-accounts/original-environment.test.ts": 316, - "src/main/managed-data-accounts/profile-removal-recovery.test.ts": 629, - "src/main/managed-data-accounts/service-enrollment-persistence.test.ts": 397, - "src/main/managed-data-accounts/service.test.ts": 1418, - "src/main/media/media-preview-protocol.test.ts": 82, - "src/main/media/media-range-response.test.ts": 78, - "src/main/memory/collector-windows-sweep.test.ts": 699, - "src/main/memory/collector.test.ts": 815, - "src/main/memory/host-memory.test.ts": 78, - "src/main/memory/hydrate-local-pty-registry-folder-workspaces.test.ts": 129, - "src/main/memory/hydrate-local-pty-registry.test.ts": 757, - "src/main/memory/memory-snapshot-buckets.test.ts": 104, - "src/main/memory/process-memory-metric.test.ts": 47, - "src/main/memory/verified-local-folder-workspaces.test.ts": 63, - "src/main/memory/windows-process-sample-parsing.test.ts": 105, - "src/main/menu/gpu-acceleration-about-panel.test.ts": 53, - "src/main/menu/register-app-menu.test.ts": 176, - "src/main/mimo/hook-service.test.ts": 120, - "src/main/minimax/minimax-api-key-store.test.ts": 107, - "src/main/minimax/minimax-cookie-store.test.ts": 113, - "src/main/muse-usage/scanner.test.ts": 218, - "src/main/muse-usage/snapshot-rollups.test.ts": 62, - "src/main/muse-usage/store-snapshot-filtering.test.ts": 705, - "src/main/muse/hook-config-json.test.ts": 81, - "src/main/muse/hook-service.test.ts": 135, - "src/main/muse/hook-settings.test.ts": 107, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-discovery-freshness.test.ts": 119, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-entry.test.ts": 52, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-learned-default.test.ts": 342, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-prewarm.test.ts": 325, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-probe-runner.test.ts": 1592, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-saved-only-read.test.ts": 87, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-service.test.ts": 299, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-store.test.ts": 609, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-unavailable.test.ts": 103, - "src/main/native-chat/agent-model-catalog/agent-project-model-override.test.ts": 203, - "src/main/native-chat/agent-session-attachments/agent-session-attachment-claims.test.ts": 637, - "src/main/native-chat/agent-session-attachments/agent-session-attachment-store.test.ts": 129, - "src/main/native-chat/agent-session-attachments/agent-session-attachment-sweep.test.ts": 664, - "src/main/native-chat/agent-session-journal/command-receipt-schema.test.ts": 1186, - "src/main/native-chat/agent-session-journal/command-receipt-table.test.ts": 994, - "src/main/native-chat/agent-session-journal/command-receipt-transaction.test.ts": 595, - "src/main/native-chat/agent-session-journal/journal-approval-subject-kinds.test.ts": 215, - "src/main/native-chat/agent-session-journal/journal-atomic-terminal-settlement.test.ts": 1761, - "src/main/native-chat/agent-session-journal/journal-closed-sets.test.ts": 242, - "src/main/native-chat/agent-session-journal/journal-crash-boundary.test.ts": 626, - "src/main/native-chat/agent-session-journal/journal-cursor.test.ts": 58, - "src/main/native-chat/agent-session-journal/journal-damage.test.ts": 696, - "src/main/native-chat/agent-session-journal/journal-database.test.ts": 465, - "src/main/native-chat/agent-session-journal/journal-dispatch-answered-turn.test.ts": 366, - "src/main/native-chat/agent-session-journal/journal-dispatch-observation.test.ts": 57, - "src/main/native-chat/agent-session-journal/journal-dispatch-reason-bound.test.ts": 626, - "src/main/native-chat/agent-session-journal/journal-dispatch-settlement.test.ts": 261, - "src/main/native-chat/agent-session-journal/journal-epoch-replacement.test.ts": 596, - "src/main/native-chat/agent-session-journal/journal-host-database.test.ts": 889, - "src/main/native-chat/agent-session-journal/journal-item-identity.test.ts": 76, - "src/main/native-chat/agent-session-journal/journal-legacy-import-source-bound.test.ts": 281, - "src/main/native-chat/agent-session-journal/journal-legacy-import-tool-pairing.test.ts": 479, - "src/main/native-chat/agent-session-journal/journal-legacy-import.test.ts": 1038, - "src/main/native-chat/agent-session-journal/journal-lifecycle-batch-appender.test.ts": 781, - "src/main/native-chat/agent-session-journal/journal-lifecycle-batch-partition.test.ts": 13771, - "src/main/native-chat/agent-session-journal/journal-open-failure.test.ts": 384, - "src/main/native-chat/agent-session-journal/journal-payload-bounds-retention.test.ts": 337, - "src/main/native-chat/agent-session-journal/journal-producer-inheritance.test.ts": 561, - "src/main/native-chat/agent-session-journal/journal-queued-rejection-batch.test.ts": 513, - "src/main/native-chat/agent-session-journal/journal-reducer-producer-linkage.test.ts": 343, - "src/main/native-chat/agent-session-journal/journal-reducer-rejection-fact.test.ts": 339, - "src/main/native-chat/agent-session-journal/journal-reducer.test.ts": 439, - "src/main/native-chat/agent-session-journal/journal-rejected-message-placement.test.ts": 335, - "src/main/native-chat/agent-session-journal/journal-restart-reconciliation.test.ts": 846, - "src/main/native-chat/agent-session-journal/journal-row-context-usage.test.ts": 690, - "src/main/native-chat/agent-session-journal/journal-row-schema-version.test.ts": 524, - "src/main/native-chat/agent-session-journal/journal-row-schema.test.ts": 461, - "src/main/native-chat/agent-session-journal/journal-row-writer.test.ts": 605, - "src/main/native-chat/agent-session-journal/journal-stop-and-resume-rows.test.ts": 548, + "src/main/local-builds/local-build-candidate.test.ts": 152, + "src/main/local-builds/local-build-compatibility-contract.test.ts": 257, + "src/main/local-builds/local-build-feed-server.test.ts": 67, + "src/main/local-worktree-filesystem-wsl-banner.wsl.test.ts": 68, + "src/main/local-worktree-filesystem.test.ts": 367, + "src/main/local-worktree-metadata-prune-gate.test.ts": 42, + "src/main/local-worktree-path-presence.test.ts": 54, + "src/main/local-worktree-removal-recovery-live.test.ts": 310, + "src/main/local-worktree-removal-recovery.test.ts": 121, + "src/main/localhost-worktree-label-proxy.test.ts": 109, + "src/main/macos-full-disk-access-status.test.ts": 44, + "src/main/macos-keychain/generic-password.test.ts": 35, + "src/main/macos-press-and-hold-default.defaults-domain.test.ts": 59, + "src/main/macos-press-and-hold-default.test.ts": 114, + "src/main/macos-system-sleep-assertion.test.ts": 57, + "src/main/macos-tcc-prompt-notice.test.ts": 62, + "src/main/macos-tcc-prompt-watch.test.ts": 63, + "src/main/macos-tcc-reset.test.ts": 38, + "src/main/macos-update-running-instances.integration.test.ts": 53, + "src/main/macos-update-running-instances.test.ts": 45, + "src/main/managed-data-accounts/launch-environment.test.ts": 346, + "src/main/managed-data-accounts/original-environment.test.ts": 298, + "src/main/managed-data-accounts/profile-removal-recovery.test.ts": 648, + "src/main/managed-data-accounts/service-enrollment-persistence.test.ts": 1001, + "src/main/managed-data-accounts/service.test.ts": 1235, + "src/main/media/media-preview-protocol.test.ts": 90, + "src/main/media/media-range-response.test.ts": 72, + "src/main/memory/collector-windows-sweep.test.ts": 940, + "src/main/memory/collector.test.ts": 873, + "src/main/memory/host-memory.test.ts": 73, + "src/main/memory/hydrate-local-pty-registry-folder-workspaces.test.ts": 327, + "src/main/memory/hydrate-local-pty-registry.test.ts": 712, + "src/main/memory/memory-snapshot-buckets.test.ts": 89, + "src/main/memory/process-memory-metric.test.ts": 42, + "src/main/memory/verified-local-folder-workspaces.test.ts": 54, + "src/main/memory/windows-process-sample-parsing.test.ts": 104, + "src/main/menu/gpu-acceleration-about-panel.test.ts": 38, + "src/main/menu/register-app-menu.test.ts": 156, + "src/main/mimo/hook-service.test.ts": 106, + "src/main/minimax/minimax-api-key-store.test.ts": 120, + "src/main/minimax/minimax-cookie-store.test.ts": 114, + "src/main/muse-usage/scanner.test.ts": 202, + "src/main/muse-usage/snapshot-rollups.test.ts": 35, + "src/main/muse-usage/store-snapshot-filtering.test.ts": 821, + "src/main/muse/hook-config-json.test.ts": 64, + "src/main/muse/hook-service.test.ts": 115, + "src/main/muse/hook-settings.test.ts": 77, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-discovery-freshness.test.ts": 117, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-entry.test.ts": 27, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-learned-default.test.ts": 320, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-prewarm.test.ts": 332, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-probe-runner.test.ts": 1582, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-saved-only-read.test.ts": 72, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-service.test.ts": 296, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-store.test.ts": 598, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-unavailable.test.ts": 95, + "src/main/native-chat/agent-model-catalog/agent-project-model-override.test.ts": 152, + "src/main/native-chat/agent-session-attachments/agent-session-attachment-claims.test.ts": 698, + "src/main/native-chat/agent-session-attachments/agent-session-attachment-store.test.ts": 126, + "src/main/native-chat/agent-session-attachments/agent-session-attachment-sweep.test.ts": 647, + "src/main/native-chat/agent-session-journal/command-receipt-schema.test.ts": 1268, + "src/main/native-chat/agent-session-journal/command-receipt-table.test.ts": 1511, + "src/main/native-chat/agent-session-journal/command-receipt-transaction.test.ts": 636, + "src/main/native-chat/agent-session-journal/journal-approval-subject-kinds.test.ts": 245, + "src/main/native-chat/agent-session-journal/journal-atomic-terminal-settlement.test.ts": 1440, + "src/main/native-chat/agent-session-journal/journal-closed-sets.test.ts": 270, + "src/main/native-chat/agent-session-journal/journal-crash-boundary.test.ts": 762, + "src/main/native-chat/agent-session-journal/journal-cursor.test.ts": 49, + "src/main/native-chat/agent-session-journal/journal-damage.test.ts": 818, + "src/main/native-chat/agent-session-journal/journal-database.test.ts": 545, + "src/main/native-chat/agent-session-journal/journal-dispatch-answered-turn.test.ts": 370, + "src/main/native-chat/agent-session-journal/journal-dispatch-legacy-reason.test.ts": 352, + "src/main/native-chat/agent-session-journal/journal-dispatch-observation.test.ts": 39, + "src/main/native-chat/agent-session-journal/journal-dispatch-reason-bound.test.ts": 542, + "src/main/native-chat/agent-session-journal/journal-dispatch-settlement.test.ts": 307, + "src/main/native-chat/agent-session-journal/journal-epoch-replacement.test.ts": 645, + "src/main/native-chat/agent-session-journal/journal-host-database.test.ts": 900, + "src/main/native-chat/agent-session-journal/journal-item-appender.test.ts": 331, + "src/main/native-chat/agent-session-journal/journal-item-identity.test.ts": 66, + "src/main/native-chat/agent-session-journal/journal-item-provenance.test.ts": 561, + "src/main/native-chat/agent-session-journal/journal-legacy-import-source-bound.test.ts": 693, + "src/main/native-chat/agent-session-journal/journal-legacy-import-tool-pairing.test.ts": 775, + "src/main/native-chat/agent-session-journal/journal-legacy-import.test.ts": 1244, + "src/main/native-chat/agent-session-journal/journal-lifecycle-batch-appender.test.ts": 675, + "src/main/native-chat/agent-session-journal/journal-lifecycle-batch-partition.test.ts": 1177, + "src/main/native-chat/agent-session-journal/journal-open-failure.test.ts": 481, + "src/main/native-chat/agent-session-journal/journal-payload-bounds-retention.test.ts": 345, + "src/main/native-chat/agent-session-journal/journal-pi-context-import.test.ts": 808, + "src/main/native-chat/agent-session-journal/journal-producer-inheritance.test.ts": 512, + "src/main/native-chat/agent-session-journal/journal-queued-rejection-batch.test.ts": 640, + "src/main/native-chat/agent-session-journal/journal-reducer-producer-linkage.test.ts": 390, + "src/main/native-chat/agent-session-journal/journal-reducer-rejection-fact.test.ts": 432, + "src/main/native-chat/agent-session-journal/journal-reducer.test.ts": 406, + "src/main/native-chat/agent-session-journal/journal-rejected-message-placement.test.ts": 377, + "src/main/native-chat/agent-session-journal/journal-restart-reconciliation.test.ts": 811, + "src/main/native-chat/agent-session-journal/journal-row-context-usage.test.ts": 601, + "src/main/native-chat/agent-session-journal/journal-row-schema-version.test.ts": 572, + "src/main/native-chat/agent-session-journal/journal-row-schema.test.ts": 435, + "src/main/native-chat/agent-session-journal/journal-row-writer.test.ts": 607, + "src/main/native-chat/agent-session-journal/journal-stop-and-resume-rows.test.ts": 514, "src/main/native-chat/agent-session-journal/journal-stop-turn-end.test.ts": 349, - "src/main/native-chat/agent-session-journal/journal-store-schema.test.ts": 667, - "src/main/native-chat/agent-session-journal/journal-store.test.ts": 753, - "src/main/native-chat/agent-session-journal/journal-streaming-replay.test.ts": 1439, - "src/main/native-chat/agent-session-journal/journal-subagent-liveness.test.ts": 658, - "src/main/native-chat/agent-session-journal/journal-submission-positions.test.ts": 588, - "src/main/native-chat/agent-session-journal/journal-submission-queued-link.test.ts": 547, - "src/main/native-chat/agent-session-journal/journal-terminal-settlement-validation.test.ts": 749, - "src/main/native-chat/agent-session-journal/journal-turn-scope.test.ts": 407, - "src/main/native-chat/agent-session-journal/journal-unknown-content.test.ts": 753, - "src/main/native-chat/agent-session-journal/journal-unknown-row-kind.test.ts": 652, - "src/main/native-chat/agent-session-journal/journal-unsent-send-hold.test.ts": 742, - "src/main/native-chat/agent-session-journal/journal-write-queue.test.ts": 425, - "src/main/native-chat/agent-session-journal/queued-message-bookkeeping-failure.test.ts": 663, - "src/main/native-chat/agent-session-journal/queued-message-context-clear.test.ts": 62, - "src/main/native-chat/agent-session-journal/queued-message-delivered-echo.test.ts": 638, - "src/main/native-chat/agent-session-journal/queued-message-edit-leases.test.ts": 303, - "src/main/native-chat/agent-session-journal/queued-message-pause.test.ts": 808, - "src/main/native-chat/agent-session-journal/queued-message-store.test.ts": 1498, - "src/main/native-chat/agent-session-journal/queued-message-text-update.test.ts": 346, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-attribution.test.ts": 577, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-earlier-turn-end.test.ts": 460, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-held-rows.test.ts": 483, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-items.test.ts": 840, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-order.test.ts": 704, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-refusal.test.ts": 802, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-restart.test.ts": 484, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-rows.test.ts": 857, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-session.test.ts": 773, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-stop.test.ts": 1106, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-streams.test.ts": 577, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-tool-ends.test.ts": 642, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-turns.test.ts": 502, - "src/main/native-chat/agent-session-timeline/provider-timeline-identity.test.ts": 64, - "src/main/native-chat/agent-session-wire/agent-session-conversation-outline.test.ts": 468, - "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer-overflow-prefix.test.ts": 101, - "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer-protected.test.ts": 79, - "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.test.ts": 74, - "src/main/native-chat/agent-session-wire/agent-session-empty-delta-retention.test.ts": 440, - "src/main/native-chat/agent-session-wire/agent-session-history-byte-accounting.test.ts": 917, - "src/main/native-chat/agent-session-wire/agent-session-history-conversation-window.test.ts": 1364, - "src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts": 1062, - "src/main/native-chat/agent-session-wire/agent-session-history-page-grouping-parity.test.ts": 229, - "src/main/native-chat/agent-session-wire/agent-session-history-page-scaling.test.ts": 78, - "src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts": 1739, - "src/main/native-chat/agent-session-wire/agent-session-stop-note-projection.test.ts": 853, - "src/main/native-chat/agent-session-wire/provider-frame-activity.test.ts": 51, - "src/main/native-chat/agent-session-wire/provider-frame-disposition.test.ts": 91, - "src/main/native-chat/agent-session-wire/provider-turn-activity-routing.test.ts": 464, - "src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts": 2781, - "src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-options.test.ts": 1351, - "src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-runtime-stamp.test.ts": 891, - "src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router-registry.test.ts": 355, - "src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.test.ts": 391, - "src/main/native-chat/agent-session-wire/structured-agent-session-adapter.test.ts": 233, - "src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts": 1223, - "src/main/native-chat/agent-session-wire/structured-agent-session-append-delivery.test.ts": 1257, - "src/main/native-chat/agent-session-wire/structured-agent-session-at-rest-commands.test.ts": 1891, - "src/main/native-chat/agent-session-wire/structured-agent-session-attach-reconciliation.test.ts": 624, - "src/main/native-chat/agent-session-wire/structured-agent-session-attachment-admission.test.ts": 1373, - "src/main/native-chat/agent-session-wire/structured-agent-session-background-task-channel.test.ts": 349, - "src/main/native-chat/agent-session-wire/structured-agent-session-child-work-selection.test.ts": 987, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-compact-stop.test.ts": 1595, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-echo-working.test.ts": 1289, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-hung-start-stop.test.ts": 1404, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-opening-send-hold.test.ts": 4766, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-option-queue.test.ts": 1212, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-queued-stop.test.ts": 4570, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-root-exit.test.ts": 1253, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-ends-session.test.ts": 3995, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-exit-ends-record.test.ts": 3545, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-note-end.test.ts": 1652, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-turn-end.test.ts": 4314, - "src/main/native-chat/agent-session-wire/structured-agent-session-clear-queued-command.test.ts": 1453, - "src/main/native-chat/agent-session-wire/structured-agent-session-clear-rewind.test.ts": 2367, - "src/main/native-chat/agent-session-wire/structured-agent-session-close-aborts-start.test.ts": 941, - "src/main/native-chat/agent-session-wire/structured-agent-session-close-verdict.test.ts": 1320, - "src/main/native-chat/agent-session-wire/structured-agent-session-codex-stop-note-end.test.ts": 1347, - "src/main/native-chat/agent-session-wire/structured-agent-session-codex-stop-row.test.ts": 3905, - "src/main/native-chat/agent-session-wire/structured-agent-session-command-publication.test.ts": 1042, - "src/main/native-chat/agent-session-wire/structured-agent-session-command-readers.test.ts": 459, - "src/main/native-chat/agent-session-wire/structured-agent-session-conversation-close.test.ts": 1873, - "src/main/native-chat/agent-session-wire/structured-agent-session-conversation-stop.test.ts": 3529, - "src/main/native-chat/agent-session-wire/structured-agent-session-conversations.test.ts": 746, - "src/main/native-chat/agent-session-wire/structured-agent-session-crash-mid-start.test.ts": 1635, - "src/main/native-chat/agent-session-wire/structured-agent-session-crash-turn-end.test.ts": 992, - "src/main/native-chat/agent-session-wire/structured-agent-session-create-adoption.test.ts": 1433, - "src/main/native-chat/agent-session-wire/structured-agent-session-create.test.ts": 1584, - "src/main/native-chat/agent-session-wire/structured-agent-session-dead-generation-settlement.test.ts": 843, - "src/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.test.ts": 201, - "src/main/native-chat/agent-session-wire/structured-agent-session-drivability.test.ts": 985, - "src/main/native-chat/agent-session-wire/structured-agent-session-event-sink.test.ts": 876, - "src/main/native-chat/agent-session-wire/structured-agent-session-eviction.test.ts": 434, - "src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-owner-verdict.test.ts": 1316, - "src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-sink-release.test.ts": 1186, - "src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts": 223, - "src/main/native-chat/agent-session-wire/structured-agent-session-final-tail-reservation.test.ts": 40, - "src/main/native-chat/agent-session-wire/structured-agent-session-forget-status.test.ts": 1449, - "src/main/native-chat/agent-session-wire/structured-agent-session-grouped-prompt.test.ts": 1196, - "src/main/native-chat/agent-session-wire/structured-agent-session-held-child-lease-renewal.test.ts": 580, - "src/main/native-chat/agent-session-wire/structured-agent-session-host-held-child-renewal.test.ts": 915, - "src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.test.ts": 699, - "src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.test.ts": 473, - "src/main/native-chat/agent-session-wire/structured-agent-session-host-test-abandon.test.ts": 1168, - "src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts": 1737, - "src/main/native-chat/agent-session-wire/structured-agent-session-idle-sweep.test.ts": 2591, - "src/main/native-chat/agent-session-wire/structured-agent-session-interrupted-continuation.test.ts": 3775, - "src/main/native-chat/agent-session-wire/structured-agent-session-journal-corruption.test.ts": 957, - "src/main/native-chat/agent-session-wire/structured-agent-session-late-proof-tool-ends.test.ts": 486, - "src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts": 1481, - "src/main/native-chat/agent-session-wire/structured-agent-session-launch-env.test.ts": 355, - "src/main/native-chat/agent-session-wire/structured-agent-session-launch-send-after-create.test.ts": 1218, - "src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer.test.ts": 706, - "src/main/native-chat/agent-session-wire/structured-agent-session-legacy-handoff-record.test.ts": 1500, - "src/main/native-chat/agent-session-wire/structured-agent-session-live-turn-window.test.ts": 1117, - "src/main/native-chat/agent-session-wire/structured-agent-session-logger.test.ts": 190, - "src/main/native-chat/agent-session-wire/structured-agent-session-main-agent-working-agreement.test.ts": 1078, - "src/main/native-chat/agent-session-wire/structured-agent-session-mutation-context.test.ts": 1098, - "src/main/native-chat/agent-session-wire/structured-agent-session-newer-content-host.test.ts": 1244, - "src/main/native-chat/agent-session-wire/structured-agent-session-operation-settlement.test.ts": 1201, - "src/main/native-chat/agent-session-wire/structured-agent-session-operation-start-refusal.test.ts": 519, - "src/main/native-chat/agent-session-wire/structured-agent-session-option-settlement.test.ts": 1248, - "src/main/native-chat/agent-session-wire/structured-agent-session-options-read.test.ts": 365, - "src/main/native-chat/agent-session-wire/structured-agent-session-orca-stop-row-readers.test.ts": 198, - "src/main/native-chat/agent-session-wire/structured-agent-session-owed-work-release.test.ts": 3318, - "src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts": 917, - "src/main/native-chat/agent-session-wire/structured-agent-session-processless-reservation.test.ts": 1125, - "src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel-repeat.test.ts": 782, - "src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel.test.ts": 914, - "src/main/native-chat/agent-session-wire/structured-agent-session-provider-child-record.test.ts": 3062, - "src/main/native-chat/agent-session-wire/structured-agent-session-provider-restore.test.ts": 1138, - "src/main/native-chat/agent-session-wire/structured-agent-session-provider-started-catalog.test.ts": 758, - "src/main/native-chat/agent-session-wire/structured-agent-session-provider-started.test.ts": 1956, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-card-holds.test.ts": 5719, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-command.test.ts": 1912, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-compact.test.ts": 5019, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-edit.test.ts": 1908, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-gate.test.ts": 1540, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.test.ts": 5700, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-newer-orca.test.ts": 1096, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-pause-lift.test.ts": 6327, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-published-bytes.test.ts": 957, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-quit.test.ts": 1221, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-repeat-press.test.ts": 1544, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-stop-row.test.ts": 4507, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-withdrawn-draft.test.ts": 1569, - "src/main/native-chat/agent-session-wire/structured-agent-session-read-restore.test.ts": 628, - "src/main/native-chat/agent-session-wire/structured-agent-session-readable-restorer.test.ts": 543, - "src/main/native-chat/agent-session-wire/structured-agent-session-reasoning-sweep.test.ts": 461, - "src/main/native-chat/agent-session-wire/structured-agent-session-recovered-turn-clock.test.ts": 1364, - "src/main/native-chat/agent-session-wire/structured-agent-session-recovery-exits.test.ts": 1017, - "src/main/native-chat/agent-session-wire/structured-agent-session-recovery-resolution.test.ts": 4093, - "src/main/native-chat/agent-session-wire/structured-agent-session-refusal-details.test.ts": 360, - "src/main/native-chat/agent-session-wire/structured-agent-session-refusal-retry.test.ts": 1144, - "src/main/native-chat/agent-session-wire/structured-agent-session-registered-definition.test.ts": 314, - "src/main/native-chat/agent-session-wire/structured-agent-session-registry.test.ts": 47, - "src/main/native-chat/agent-session-wire/structured-agent-session-repeat-press.test.ts": 999, - "src/main/native-chat/agent-session-wire/structured-agent-session-repeated-stop.test.ts": 1072, - "src/main/native-chat/agent-session-wire/structured-agent-session-replaced-server-turn.test.ts": 1207, - "src/main/native-chat/agent-session-wire/structured-agent-session-resend-answer.test.ts": 2268, - "src/main/native-chat/agent-session-wire/structured-agent-session-resend-ledger-refusal.test.ts": 1204, - "src/main/native-chat/agent-session-wire/structured-agent-session-reservation-directory.test.ts": 235, - "src/main/native-chat/agent-session-wire/structured-agent-session-resolved-append.test.ts": 576, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-audience.test.ts": 1011, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-cap.test.ts": 686, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-child-work.test.ts": 1345, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-context-clear.test.ts": 399, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation-wait.test.ts": 242, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.test.ts": 518, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-failure-filing.test.ts": 1326, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-after-reattach.test.ts": 1335, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-endings.test.ts": 1285, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-newer-orca.test.ts": 1212, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-withdrawal.test.ts": 2691, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-ownership.test.ts": 2611, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-provider-output.test.ts": 2206, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-reconcile.test.ts": 269, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-recovery-output.test.ts": 1503, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-restore-gate.test.ts": 67, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-restore.test.ts": 649, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-progress.test.ts": 1297, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume.test.ts": 632, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-status-publication.test.ts": 1232, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-stream-duration.test.ts": 1339, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-teardown.test.ts": 1379, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-undrivable.test.ts": 959, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.test.ts": 434, - "src/main/native-chat/agent-session-wire/structured-agent-session-reveal.test.ts": 427, - "src/main/native-chat/agent-session-wire/structured-agent-session-rewind-at-rest.test.ts": 1079, - "src/main/native-chat/agent-session-wire/structured-agent-session-rewind-live-send.test.ts": 1097, - "src/main/native-chat/agent-session-wire/structured-agent-session-rewind.test.ts": 2657, - "src/main/native-chat/agent-session-wire/structured-agent-session-send-block.test.ts": 280, - "src/main/native-chat/agent-session-wire/structured-agent-session-send-idempotency.test.ts": 796, - "src/main/native-chat/agent-session-wire/structured-agent-session-send-open-stale-turn.test.ts": 1279, - "src/main/native-chat/agent-session-wire/structured-agent-session-send-preparation.test.ts": 3251, - "src/main/native-chat/agent-session-wire/structured-agent-session-send-restarts-failed-start.test.ts": 2021, - "src/main/native-chat/agent-session-wire/structured-agent-session-send-settlement.test.ts": 337, - "src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts": 2285, - "src/main/native-chat/agent-session-wire/structured-agent-session-settled-attach-retry.test.ts": 1282, - "src/main/native-chat/agent-session-wire/structured-agent-session-shown-work.test.ts": 478, - "src/main/native-chat/agent-session-wire/structured-agent-session-shutdown-cut-row.test.ts": 1364, - "src/main/native-chat/agent-session-wire/structured-agent-session-signed-out-child.test.ts": 1452, - "src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-open.test.ts": 1043, - "src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.test.ts": 1011, - "src/main/native-chat/agent-session-wire/structured-agent-session-start-failure-writer.test.ts": 1029, - "src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt-host.test.ts": 3748, - "src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt-registrations.test.ts": 2188, - "src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt.test.ts": 64, - "src/main/native-chat/agent-session-wire/structured-agent-session-startup-failure-exit.test.ts": 392, - "src/main/native-chat/agent-session-wire/structured-agent-session-startup-reconcile-failure.test.ts": 1817, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child-work.test.ts": 570, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child.test.ts": 454, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-children.test.ts": 643, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-clock.test.ts": 787, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-conversation-name.test.ts": 595, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-launch-directory.test.ts": 649, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-stopping.test.ts": 524, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.test.ts": 904, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-first-input-identity.test.ts": 470, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-ownership.test.ts": 251, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-reentry.test.ts": 1002, - "src/main/native-chat/agent-session-wire/structured-agent-session-steer-while-stopping.test.ts": 1701, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-aborts-start.test.ts": 890, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event-binding.test.ts": 2907, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event-entries.test.ts": 5698, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event-restart.test.ts": 1979, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event.test.ts": 2801, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-never-waits.test.ts": 1766, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-note-opened-turn.test.ts": 701, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-note-withdrawn-send.test.ts": 513, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-settle-binding.test.ts": 1319, - "src/main/native-chat/agent-session-wire/structured-agent-session-stopping-journal.test.ts": 457, - "src/main/native-chat/agent-session-wire/structured-agent-session-stopping.test.ts": 3973, - "src/main/native-chat/agent-session-wire/structured-agent-session-subagent-recency.test.ts": 1318, - "src/main/native-chat/agent-session-wire/structured-agent-session-subscriber-background-tasks.test.ts": 643, - "src/main/native-chat/agent-session-wire/structured-agent-session-subscriber-journal-retention.test.ts": 5915, - "src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts": 1011, - "src/main/native-chat/agent-session-wire/structured-agent-session-surface-lifetime.test.ts": 2430, - "src/main/native-chat/agent-session-wire/structured-agent-session-task-queue.test.ts": 98, - "src/main/native-chat/agent-session-wire/structured-agent-session-thread-goal.test.ts": 847, - "src/main/native-chat/agent-session-wire/structured-agent-session-transition.test.ts": 829, - "src/main/native-chat/agent-session-wire/structured-agent-session-turn-completion-feed.test.ts": 328, - "src/main/native-chat/agent-session-wire/structured-agent-session-turns.test.ts": 1030, - "src/main/native-chat/agent-session-wire/structured-agent-session-unanswered-dispatch-release.test.ts": 276, - "src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.ts": 480, - "src/main/native-chat/agent-session-wire/structured-agent-session-unopened-send-withdrawal.test.ts": 195, - "src/main/native-chat/agent-session-wire/structured-agent-session-unopened-tab.test.ts": 968, - "src/main/native-chat/agent-session-wire/structured-agent-session-unrun-send-hold.test.ts": 1496, - "src/main/native-chat/agent-session-wire/structured-agent-session-unsent-send-hold.test.ts": 5952, - "src/main/native-chat/agent-session-wire/structured-agent-session-view-start-after-failed-start.test.ts": 1354, - "src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts": 1592, - "src/main/native-chat/agent-session-wire/structured-agent-session-wire-admission.test.ts": 922, - "src/main/native-chat/agent-session-wire/structured-conversation-command-admission.test.ts": 326, - "src/main/native-chat/agent-session-wire/structured-conversation-command-child-work.test.ts": 939, - "src/main/native-chat/agent-session-wire/structured-conversation-command-outcome.test.ts": 196, - "src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts": 1502, - "src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts": 2102, - "src/main/native-chat/agent-session-wire/structured-rewind-journal-body.test.ts": 273, - "src/main/native-chat/agent-session-wire/structured-rewind-retained-host-rows.test.ts": 234, - "src/main/native-chat/agent-session-wire/unhandled-provider-frame.test.ts": 332, - "src/main/native-chat/host-readable-transcript-path-fs-gate.test.ts": 359, - "src/main/native-chat/host-readable-transcript-path.test.ts": 160, - "src/main/native-chat/native-chat-file-provenance.test.ts": 575, - "src/main/native-chat/native-chat-inline-visuals-setting.test.ts": 536, - "src/main/native-chat/native-chat-visual-file-read.test.ts": 85, - "src/main/native-chat/native-chat-visuals-delivery.test.ts": 343, - "src/main/native-chat/native-chat-visuals-sweep.test.ts": 78, - "src/main/native-chat/session-file-resolver-claude-roots.test.ts": 142, - "src/main/native-chat/session-file-resolver-codex-roots.test.ts": 136, - "src/main/native-chat/session-file-resolver-wsl-scan-gate.test.ts": 141, - "src/main/native-chat/session-file-resolver-wsl.test.ts": 139, - "src/main/native-chat/session-file-resolver.test.ts": 217, - "src/main/native-chat/ssh-transcript-path.test.ts": 40, - "src/main/native-chat/structured-agent-command-resolution.test.ts": 86, - "src/main/native-chat/structured-agent-configured-args.test.ts": 48, - "src/main/native-chat/structured-agent-session-create-support.test.ts": 54, - "src/main/native-chat/structured-agent-session-history-adoption.test.ts": 279, - "src/main/native-chat/structured-chat-naming-command-first.test.ts": 1258, - "src/main/native-chat/structured-chat-naming-status-hook.test.ts": 2387, - "src/main/native-chat/structured-chat-naming.test.ts": 225, - "src/main/native-chat/subagent-entry-id-bounds.test.ts": 51, - "src/main/native-chat/subagent-roster-retention.test.ts": 88, - "src/main/native-chat/transcript-fallback-id.test.ts": 52, - "src/main/native-chat/transcript-file-version.test.ts": 67, - "src/main/native-chat/transcript-interruption-message.test.ts": 100, - "src/main/native-chat/transcript-line-decoders-claude-command-output.test.ts": 96, - "src/main/native-chat/transcript-line-decoders-claude-control-bytes.test.ts": 104, - "src/main/native-chat/transcript-line-decoders-claude-image-companion.test.ts": 102, - "src/main/native-chat/transcript-line-decoders-claude-pasted-content.test.ts": 108, + "src/main/native-chat/agent-session-journal/journal-store-schema.test.ts": 590, + "src/main/native-chat/agent-session-journal/journal-store.test.ts": 888, + "src/main/native-chat/agent-session-journal/journal-streaming-replay.test.ts": 1277, + "src/main/native-chat/agent-session-journal/journal-subagent-item.test.ts": 566, + "src/main/native-chat/agent-session-journal/journal-subagent-liveness.test.ts": 636, + "src/main/native-chat/agent-session-journal/journal-submission-positions.test.ts": 591, + "src/main/native-chat/agent-session-journal/journal-submission-queued-link.test.ts": 591, + "src/main/native-chat/agent-session-journal/journal-terminal-settlement-validation.test.ts": 1021, + "src/main/native-chat/agent-session-journal/journal-turn-scope.test.ts": 413, + "src/main/native-chat/agent-session-journal/journal-unknown-content.test.ts": 849, + "src/main/native-chat/agent-session-journal/journal-unknown-row-kind.test.ts": 637, + "src/main/native-chat/agent-session-journal/journal-unsent-send-hold.test.ts": 974, + "src/main/native-chat/agent-session-journal/journal-write-queue.test.ts": 687, + "src/main/native-chat/agent-session-journal/queued-message-bookkeeping-failure.test.ts": 676, + "src/main/native-chat/agent-session-journal/queued-message-context-clear.test.ts": 58, + "src/main/native-chat/agent-session-journal/queued-message-delivered-echo.test.ts": 757, + "src/main/native-chat/agent-session-journal/queued-message-edit-leases.test.ts": 317, + "src/main/native-chat/agent-session-journal/queued-message-pause.test.ts": 881, + "src/main/native-chat/agent-session-journal/queued-message-store.test.ts": 1156, + "src/main/native-chat/agent-session-journal/queued-message-text-update.test.ts": 325, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-attribution.test.ts": 802, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-earlier-turn-end.test.ts": 594, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-held-rows.test.ts": 686, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-items.test.ts": 675, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-order.test.ts": 879, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-refusal.test.ts": 933, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-restart.test.ts": 532, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-rows.test.ts": 1147, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-session.test.ts": 761, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-stop.test.ts": 1711, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-streams.test.ts": 568, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-tool-ends.test.ts": 758, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-turns.test.ts": 657, + "src/main/native-chat/agent-session-timeline/provider-timeline-identity.test.ts": 35, + "src/main/native-chat/agent-session-wire/agent-session-bounded-frames.test.ts": 3078, + "src/main/native-chat/agent-session-wire/agent-session-conversation-outline.test.ts": 512, + "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer-overflow-prefix.test.ts": 104, + "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer-protected.test.ts": 69, + "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.test.ts": 44, + "src/main/native-chat/agent-session-wire/agent-session-empty-delta-retention.test.ts": 420, + "src/main/native-chat/agent-session-wire/agent-session-history-byte-accounting.test.ts": 689, + "src/main/native-chat/agent-session-wire/agent-session-history-conversation-window.test.ts": 1608, + "src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts": 954, + "src/main/native-chat/agent-session-wire/agent-session-history-page-grouping-parity.test.ts": 210, + "src/main/native-chat/agent-session-wire/agent-session-history-page-scaling.test.ts": 70, + "src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts": 1878, + "src/main/native-chat/agent-session-wire/agent-session-stop-note-projection.test.ts": 1211, + "src/main/native-chat/agent-session-wire/hook-failure-provider-frame.test.ts": 252, + "src/main/native-chat/agent-session-wire/json-string-shortening.test.ts": 58, + "src/main/native-chat/agent-session-wire/provider-frame-activity.test.ts": 41, + "src/main/native-chat/agent-session-wire/provider-frame-disposition.test.ts": 86, + "src/main/native-chat/agent-session-wire/provider-turn-activity-routing.test.ts": 361, + "src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts": 3549, + "src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-options.test.ts": 1536, + "src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-runtime-stamp.test.ts": 1084, + "src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router-registry.test.ts": 378, + "src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.test.ts": 359, + "src/main/native-chat/agent-session-wire/structured-agent-session-adapter.test.ts": 261, + "src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts": 2552, + "src/main/native-chat/agent-session-wire/structured-agent-session-append-delivery.test.ts": 1472, + "src/main/native-chat/agent-session-wire/structured-agent-session-at-rest-commands.test.ts": 2212, + "src/main/native-chat/agent-session-wire/structured-agent-session-attach-reconciliation.test.ts": 703, + "src/main/native-chat/agent-session-wire/structured-agent-session-attachment-admission.test.ts": 1723, + "src/main/native-chat/agent-session-wire/structured-agent-session-background-stop-unreadable.test.ts": 1548, + "src/main/native-chat/agent-session-wire/structured-agent-session-background-task-channel.test.ts": 499, + "src/main/native-chat/agent-session-wire/structured-agent-session-child-work-selection.test.ts": 1063, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-compact-stop.test.ts": 1758, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-echo-working.test.ts": 1791, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-hung-start-stop.test.ts": 1955, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-opening-send-hold.test.ts": 5597, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-option-queue.test.ts": 1723, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-queued-stop.test.ts": 2143, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-root-exit.test.ts": 891, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-ends-session.test.ts": 3972, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-exit-ends-record.test.ts": 3799, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-note-end.test.ts": 2032, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-turn-end.test.ts": 1804, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-subagent-row.test.ts": 3710, + "src/main/native-chat/agent-session-wire/structured-agent-session-clear-queued-command.test.ts": 1689, + "src/main/native-chat/agent-session-wire/structured-agent-session-clear-rewind.test.ts": 2692, + "src/main/native-chat/agent-session-wire/structured-agent-session-close-aborts-start.test.ts": 1480, + "src/main/native-chat/agent-session-wire/structured-agent-session-close-verdict.test.ts": 1703, + "src/main/native-chat/agent-session-wire/structured-agent-session-codex-stop-note-end.test.ts": 2393, + "src/main/native-chat/agent-session-wire/structured-agent-session-codex-stop-row.test.ts": 4109, + "src/main/native-chat/agent-session-wire/structured-agent-session-codex-version-recovery.test.ts": 1626, + "src/main/native-chat/agent-session-wire/structured-agent-session-command-publication.test.ts": 883, + "src/main/native-chat/agent-session-wire/structured-agent-session-command-readers.test.ts": 497, + "src/main/native-chat/agent-session-wire/structured-agent-session-conversation-close.test.ts": 2355, + "src/main/native-chat/agent-session-wire/structured-agent-session-conversation-stop.test.ts": 4235, + "src/main/native-chat/agent-session-wire/structured-agent-session-conversations.test.ts": 799, + "src/main/native-chat/agent-session-wire/structured-agent-session-crash-mid-start.test.ts": 1852, + "src/main/native-chat/agent-session-wire/structured-agent-session-crash-turn-end.test.ts": 1629, + "src/main/native-chat/agent-session-wire/structured-agent-session-create-adoption.test.ts": 1495, + "src/main/native-chat/agent-session-wire/structured-agent-session-create.test.ts": 2354, + "src/main/native-chat/agent-session-wire/structured-agent-session-dead-generation-settlement.test.ts": 983, + "src/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.test.ts": 238, + "src/main/native-chat/agent-session-wire/structured-agent-session-drivability.test.ts": 1303, + "src/main/native-chat/agent-session-wire/structured-agent-session-event-sink.test.ts": 798, + "src/main/native-chat/agent-session-wire/structured-agent-session-eviction.test.ts": 459, + "src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-owner-verdict.test.ts": 1604, + "src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-sink-release.test.ts": 1418, + "src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts": 312, + "src/main/native-chat/agent-session-wire/structured-agent-session-final-tail-reservation.test.ts": 35, + "src/main/native-chat/agent-session-wire/structured-agent-session-forget-status.test.ts": 1131, + "src/main/native-chat/agent-session-wire/structured-agent-session-grouped-prompt.test.ts": 1275, + "src/main/native-chat/agent-session-wire/structured-agent-session-held-child-lease-renewal.test.ts": 798, + "src/main/native-chat/agent-session-wire/structured-agent-session-host-held-child-renewal.test.ts": 1317, + "src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.test.ts": 588, + "src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.test.ts": 589, + "src/main/native-chat/agent-session-wire/structured-agent-session-host-test-abandon.test.ts": 1605, + "src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts": 2537, + "src/main/native-chat/agent-session-wire/structured-agent-session-idle-sweep.test.ts": 2933, + "src/main/native-chat/agent-session-wire/structured-agent-session-interrupted-continuation.test.ts": 1805, + "src/main/native-chat/agent-session-wire/structured-agent-session-journal-corruption.test.ts": 1402, + "src/main/native-chat/agent-session-wire/structured-agent-session-late-proof-tool-ends.test.ts": 707, + "src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts": 1845, + "src/main/native-chat/agent-session-wire/structured-agent-session-launch-env.test.ts": 332, + "src/main/native-chat/agent-session-wire/structured-agent-session-launch-permission.test.ts": 313, + "src/main/native-chat/agent-session-wire/structured-agent-session-launch-send-after-create.test.ts": 1249, + "src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer.test.ts": 774, + "src/main/native-chat/agent-session-wire/structured-agent-session-legacy-handoff-record.test.ts": 1676, + "src/main/native-chat/agent-session-wire/structured-agent-session-live-turn-window.test.ts": 1952, + "src/main/native-chat/agent-session-wire/structured-agent-session-logger.test.ts": 263, + "src/main/native-chat/agent-session-wire/structured-agent-session-main-agent-working-agreement.test.ts": 1594, + "src/main/native-chat/agent-session-wire/structured-agent-session-mutation-context.test.ts": 1320, + "src/main/native-chat/agent-session-wire/structured-agent-session-newer-content-host.test.ts": 1448, + "src/main/native-chat/agent-session-wire/structured-agent-session-operation-settlement.test.ts": 1290, + "src/main/native-chat/agent-session-wire/structured-agent-session-operation-start-refusal.test.ts": 909, + "src/main/native-chat/agent-session-wire/structured-agent-session-option-settlement.test.ts": 1571, + "src/main/native-chat/agent-session-wire/structured-agent-session-options-read.test.ts": 517, + "src/main/native-chat/agent-session-wire/structured-agent-session-orca-stop-row-readers.test.ts": 284, + "src/main/native-chat/agent-session-wire/structured-agent-session-outgrown-child.test.ts": 1833, + "src/main/native-chat/agent-session-wire/structured-agent-session-owed-work-release.test.ts": 3411, + "src/main/native-chat/agent-session-wire/structured-agent-session-permission-mode.test.ts": 2186, + "src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts": 1412, + "src/main/native-chat/agent-session-wire/structured-agent-session-processless-reservation.test.ts": 1227, + "src/main/native-chat/agent-session-wire/structured-agent-session-prompt-answer-receipts.test.ts": 2036, + "src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel-repeat.test.ts": 1049, + "src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel.test.ts": 1033, + "src/main/native-chat/agent-session-wire/structured-agent-session-provider-child-record.test.ts": 3521, + "src/main/native-chat/agent-session-wire/structured-agent-session-provider-restore.test.ts": 1247, + "src/main/native-chat/agent-session-wire/structured-agent-session-provider-started-catalog.test.ts": 1659, + "src/main/native-chat/agent-session-wire/structured-agent-session-provider-started.test.ts": 1846, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-action-receipts.test.ts": 3551, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-card-holds.test.ts": 3891, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-command.test.ts": 2383, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-compact.test.ts": 4502, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-edit.test.ts": 2454, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-gate.test.ts": 1796, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.test.ts": 6375, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-newer-orca.test.ts": 1559, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-pause-lift.test.ts": 6142, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-published-bytes.test.ts": 1468, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-quit.test.ts": 1572, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-repeat-press.test.ts": 1854, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-stop-row.test.ts": 2829, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-withdrawn-draft.test.ts": 1837, + "src/main/native-chat/agent-session-wire/structured-agent-session-read-restore.test.ts": 589, + "src/main/native-chat/agent-session-wire/structured-agent-session-readable-restorer.test.ts": 575, + "src/main/native-chat/agent-session-wire/structured-agent-session-reasoning-sweep.test.ts": 690, + "src/main/native-chat/agent-session-wire/structured-agent-session-recovered-turn-clock.test.ts": 1211, + "src/main/native-chat/agent-session-wire/structured-agent-session-recovery-exits.test.ts": 1336, + "src/main/native-chat/agent-session-wire/structured-agent-session-recovery-resolution.test.ts": 4135, + "src/main/native-chat/agent-session-wire/structured-agent-session-refusal-details.test.ts": 519, + "src/main/native-chat/agent-session-wire/structured-agent-session-refusal-retry.test.ts": 1373, + "src/main/native-chat/agent-session-wire/structured-agent-session-registered-definition.test.ts": 347, + "src/main/native-chat/agent-session-wire/structured-agent-session-registry.test.ts": 24, + "src/main/native-chat/agent-session-wire/structured-agent-session-repeat-press.test.ts": 1697, + "src/main/native-chat/agent-session-wire/structured-agent-session-repeated-stop.test.ts": 1280, + "src/main/native-chat/agent-session-wire/structured-agent-session-replaced-server-turn.test.ts": 1672, + "src/main/native-chat/agent-session-wire/structured-agent-session-resend-answer.test.ts": 2850, + "src/main/native-chat/agent-session-wire/structured-agent-session-resend-ledger-refusal.test.ts": 1528, + "src/main/native-chat/agent-session-wire/structured-agent-session-reservation-directory.test.ts": 305, + "src/main/native-chat/agent-session-wire/structured-agent-session-resolved-append.test.ts": 593, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-audience.test.ts": 1607, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-cap.test.ts": 787, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-child-work.test.ts": 1341, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-context-clear.test.ts": 469, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation-wait.test.ts": 349, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.test.ts": 541, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-failure-filing.test.ts": 1603, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-after-reattach.test.ts": 1318, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-endings.test.ts": 1421, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-newer-orca.test.ts": 1839, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-withdrawal.test.ts": 3680, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-ownership.test.ts": 3324, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-provider-output.test.ts": 2721, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-reconcile.test.ts": 260, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-recovery-output.test.ts": 1996, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-restore-gate.test.ts": 33, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-restore.test.ts": 614, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-progress.test.ts": 1877, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume.test.ts": 639, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-status-publication.test.ts": 1485, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-stream-duration.test.ts": 1813, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-teardown.test.ts": 1630, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-undrivable.test.ts": 1222, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.test.ts": 552, + "src/main/native-chat/agent-session-wire/structured-agent-session-reveal.test.ts": 568, + "src/main/native-chat/agent-session-wire/structured-agent-session-rewind-at-rest.test.ts": 1360, + "src/main/native-chat/agent-session-wire/structured-agent-session-rewind-live-send.test.ts": 1724, + "src/main/native-chat/agent-session-wire/structured-agent-session-rewind.test.ts": 2365, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-block.test.ts": 387, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-idempotency.test.ts": 627, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-open-stale-turn.test.ts": 1669, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-options.test.ts": 2572, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-preparation.test.ts": 3260, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-restarts-failed-start.test.ts": 1922, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-settlement.test.ts": 328, + "src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts": 2632, + "src/main/native-chat/agent-session-wire/structured-agent-session-settled-attach-retry.test.ts": 1619, + "src/main/native-chat/agent-session-wire/structured-agent-session-shown-work.test.ts": 463, + "src/main/native-chat/agent-session-wire/structured-agent-session-shutdown-cut-row.test.ts": 1597, + "src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-open.test.ts": 1539, + "src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.test.ts": 1202, + "src/main/native-chat/agent-session-wire/structured-agent-session-start-failure-writer.test.ts": 1574, + "src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt-host.test.ts": 2563, + "src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt-registrations.test.ts": 2613, + "src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt.test.ts": 68, + "src/main/native-chat/agent-session-wire/structured-agent-session-startup-failure-exit.test.ts": 461, + "src/main/native-chat/agent-session-wire/structured-agent-session-startup-reconcile-failure.test.ts": 2288, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child-work.test.ts": 617, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child.test.ts": 661, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-children.test.ts": 833, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-clock.test.ts": 633, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-conversation-name.test.ts": 690, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-launch-directory.test.ts": 669, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-stopping.test.ts": 725, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.test.ts": 1201, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-first-input-identity.test.ts": 703, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-ownership.test.ts": 4923, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-reentry.test.ts": 1131, + "src/main/native-chat/agent-session-wire/structured-agent-session-steer-while-stopping.test.ts": 2161, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-aborts-start.test.ts": 1542, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event-binding.test.ts": 2987, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event-entries.test.ts": 5972, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event-restart.test.ts": 2339, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event.test.ts": 3184, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-note-opened-turn.test.ts": 718, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-note-withdrawn-send.test.ts": 788, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-receipts.test.ts": 1967, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-settle-binding.test.ts": 1732, + "src/main/native-chat/agent-session-wire/structured-agent-session-stopped-history.test.ts": 1593, + "src/main/native-chat/agent-session-wire/structured-agent-session-stopping-journal.test.ts": 649, + "src/main/native-chat/agent-session-wire/structured-agent-session-stopping.test.ts": 4697, + "src/main/native-chat/agent-session-wire/structured-agent-session-subagent-recency.test.ts": 1194, + "src/main/native-chat/agent-session-wire/structured-agent-session-subagent-rows.test.ts": 711, + "src/main/native-chat/agent-session-wire/structured-agent-session-subscriber-background-tasks.test.ts": 686, + "src/main/native-chat/agent-session-wire/structured-agent-session-subscriber-journal-retention.test.ts": 5985, + "src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts": 993, + "src/main/native-chat/agent-session-wire/structured-agent-session-surface-lifetime.test.ts": 2991, + "src/main/native-chat/agent-session-wire/structured-agent-session-task-queue.test.ts": 90, + "src/main/native-chat/agent-session-wire/structured-agent-session-thread-goal.test.ts": 757, + "src/main/native-chat/agent-session-wire/structured-agent-session-transition.test.ts": 1084, + "src/main/native-chat/agent-session-wire/structured-agent-session-turn-completion-feed.test.ts": 344, + "src/main/native-chat/agent-session-wire/structured-agent-session-turns.test.ts": 877, + "src/main/native-chat/agent-session-wire/structured-agent-session-unanswered-dispatch-release.test.ts": 278, + "src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.ts": 494, + "src/main/native-chat/agent-session-wire/structured-agent-session-unnamed-stop-races.test.ts": 7136, + "src/main/native-chat/agent-session-wire/structured-agent-session-unopened-send-withdrawal.test.ts": 259, + "src/main/native-chat/agent-session-wire/structured-agent-session-unopened-tab.test.ts": 1285, + "src/main/native-chat/agent-session-wire/structured-agent-session-unrun-send-hold.test.ts": 1501, + "src/main/native-chat/agent-session-wire/structured-agent-session-unsent-send-hold.test.ts": 6554, + "src/main/native-chat/agent-session-wire/structured-agent-session-view-start-after-failed-start.test.ts": 1637, + "src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts": 1607, + "src/main/native-chat/agent-session-wire/structured-agent-session-wire-admission.test.ts": 1083, + "src/main/native-chat/agent-session-wire/structured-conversation-command-admission.test.ts": 319, + "src/main/native-chat/agent-session-wire/structured-conversation-command-child-work.test.ts": 1226, + "src/main/native-chat/agent-session-wire/structured-conversation-command-outcome.test.ts": 294, + "src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts": 2238, + "src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts": 1915, + "src/main/native-chat/agent-session-wire/structured-rewind-journal-body.test.ts": 258, + "src/main/native-chat/agent-session-wire/structured-rewind-retained-host-rows.test.ts": 342, + "src/main/native-chat/agent-session-wire/unhandled-provider-frame.test.ts": 276, + "src/main/native-chat/host-readable-transcript-path-fs-gate.test.ts": 385, + "src/main/native-chat/host-readable-transcript-path.test.ts": 114, + "src/main/native-chat/native-chat-file-provenance.test.ts": 472, + "src/main/native-chat/native-chat-inline-visuals-setting.test.ts": 760, + "src/main/native-chat/native-chat-visual-file-read.test.ts": 99, + "src/main/native-chat/native-chat-visuals-delivery.test.ts": 634, + "src/main/native-chat/native-chat-visuals-sweep.test.ts": 170, + "src/main/native-chat/session-file-resolver-claude-roots.test.ts": 154, + "src/main/native-chat/session-file-resolver-codex-roots.test.ts": 135, + "src/main/native-chat/session-file-resolver-wsl-scan-gate.test.ts": 120, + "src/main/native-chat/session-file-resolver-wsl.test.ts": 213, + "src/main/native-chat/session-file-resolver.test.ts": 171, + "src/main/native-chat/ssh-transcript-path.test.ts": 41, + "src/main/native-chat/structured-agent-cli-conversation-adoption.test.ts": 1893, + "src/main/native-chat/structured-agent-command-guest-path.test.ts": 54, + "src/main/native-chat/structured-agent-command-resolution.test.ts": 79, + "src/main/native-chat/structured-agent-configured-args.test.ts": 58, + "src/main/native-chat/structured-agent-session-create-support.test.ts": 41, + "src/main/native-chat/structured-agent-session-history-adoption.test.ts": 316, + "src/main/native-chat/structured-chat-naming-command-first.test.ts": 1484, + "src/main/native-chat/structured-chat-naming-status-hook.test.ts": 2515, + "src/main/native-chat/structured-chat-naming.test.ts": 294, + "src/main/native-chat/subagent-entry-id-bounds.test.ts": 26, + "src/main/native-chat/transcript-fallback-id.test.ts": 30, + "src/main/native-chat/transcript-file-version.test.ts": 72, + "src/main/native-chat/transcript-interruption-message.test.ts": 78, + "src/main/native-chat/transcript-line-decoders-claude-command-output.test.ts": 93, + "src/main/native-chat/transcript-line-decoders-claude-control-bytes.test.ts": 74, + "src/main/native-chat/transcript-line-decoders-claude-image-companion.test.ts": 103, + "src/main/native-chat/transcript-line-decoders-claude-pasted-content.test.ts": 103, "src/main/native-chat/transcript-line-decoders-codex-skill-context.test.ts": 97, - "src/main/native-chat/transcript-line-decoders.grok.test.ts": 132, - "src/main/native-chat/transcript-line-decoders.omp.test.ts": 142, - "src/main/native-chat/transcript-opencode-database.test.ts": 107, - "src/main/native-chat/transcript-opencode-read-bounds.test.ts": 589, - "src/main/native-chat/transcript-opencode-semantic-fidelity.test.ts": 2910, - "src/main/native-chat/transcript-opencode-sqlite-query.test.ts": 594, - "src/main/native-chat/transcript-opencode-subscribe.test.ts": 5611, - "src/main/native-chat/transcript-opencode.test.ts": 405, - "src/main/native-chat/transcript-opencode2-sqlite.test.ts": 10052, - "src/main/native-chat/transcript-read-cache-inflight.test.ts": 1054, - "src/main/native-chat/transcript-read-cache-refusal-recovery.test.ts": 580, - "src/main/native-chat/transcript-read-cache-wsl-stall.test.ts": 758, - "src/main/native-chat/transcript-read-cache.test.ts": 652, - "src/main/native-chat/transcript-reader-codex-history-mode.test.ts": 636, - "src/main/native-chat/transcript-reader-wsl-stall.test.ts": 637, - "src/main/native-chat/transcript-reader.test.ts": 607, - "src/main/native-chat/transcript-record-copy-counts.test.ts": 487, - "src/main/native-chat/transcript-stream-lines.test.ts": 103, - "src/main/native-chat/transcript-tail-reader-cancellation.test.ts": 701, - "src/main/native-chat/transcript-tail-reader-wsl-gate.test.ts": 611, - "src/main/native-chat/transcript-tool-result-identity.test.ts": 537, - "src/main/native-chat/transcript-turn-lifecycle.test.ts": 105, - "src/main/native-chat/transcript-watch-engine-wsl-lifecycle.test.ts": 730, - "src/main/native-chat/transcript-watch-error.test.ts": 2634, - "src/main/native-chat/transcript-watch-inplace-drain-race.test.ts": 960, - "src/main/native-chat/transcript-watch-liveness.test.ts": 1270, - "src/main/native-chat/transcript-watch-resolve-poll.test.ts": 784, - "src/main/native-chat/transcript-watch-unflushed-settle.test.ts": 820, - "src/main/native-chat/transcript-watch-unsubscribe-race.test.ts": 624, - "src/main/native-chat/transcript-watch-wsl-exact-path.test.ts": 594, - "src/main/native-chat/transcript-watch-wsl-stall.test.ts": 693, - "src/main/native-chat/transcript-watch.test.ts": 2600, - "src/main/native-chat/transcript-window-tool-attribution.test.ts": 616, - "src/main/native-chat/wsl-session-path-scan.test.ts": 107, - "src/main/native-chat/wsl-transcript-fs-access.test.ts": 161, - "src/main/native-chat/wsl-transcript-fs-gate.test.ts": 585, - "src/main/native-chat/wsl-transcript-fs-process-client.test.ts": 115, - "src/main/native-chat/wsl-transcript-fs-process-operations.test.ts": 55, - "src/main/native-chat/wsl-transcript-fs-route-quarantine.test.ts": 98, - "src/main/native-chat/wsl-transcript-fs-route.test.ts": 44, - "src/main/native-chat/wsl-transcript-running-observer.test.ts": 104, - "src/main/nested-worktree-removal-plan.test.ts": 87, - "src/main/network/electron-proxy-request-guard.test.ts": 381, - "src/main/network/macos-system-resolver-health.test.ts": 71, - "src/main/network/macos-tailscale-dns-diagnostic.test.ts": 58, - "src/main/network/macos-tailscale-dns-probe-admission.test.ts": 225, - "src/main/network/proxy-settings-session.test.ts": 883, - "src/main/network/proxy-settings.test.ts": 120, - "src/main/notebook/notebook-kernel.test.ts": 148, - "src/main/notebook/python-environments.test.ts": 126, - "src/main/notifications/announced-notification-registry.test.ts": 253, - "src/main/notifications/desktop-away-state.test.ts": 62, - "src/main/notifications/notification-delivery-service.test.ts": 87, - "src/main/observability/bundle.test.ts": 487, - "src/main/observability/diagnostic-upload-http.test.ts": 48, - "src/main/observability/instrumentation.test.ts": 235, - "src/main/observability/local-file-sink-memory.test.ts": 135, - "src/main/observability/local-file-sink.test.ts": 81, - "src/main/observability/redactor-environment-lines.test.ts": 217, - "src/main/observability/redactor.test.ts": 86, - "src/main/observability/tracer.test.ts": 91, - "src/main/opencode-usage/cache-read-reproduction.test.ts": 244, - "src/main/opencode-usage/cache-write-accounting.test.ts": 197, - "src/main/opencode-usage/opencode2-session-usage.test.ts": 906, - "src/main/opencode-usage/scanner-windows-data-directory.test.ts": 207, - "src/main/opencode-usage/scanner-wsl-gate.test.ts": 98, - "src/main/opencode-usage/scanner.test.ts": 349, - "src/main/opencode-usage/store.test.ts": 424, - "src/main/opencode/hook-plugin-background-child-completion.test.ts": 375, - "src/main/opencode/hook-plugin-child-attention.test.ts": 759, - "src/main/opencode/hook-plugin-dispose-host.test.ts": 404, - "src/main/opencode/hook-plugin-fail-open-ownership.test.ts": 553, - "src/main/opencode/hook-plugin-legacy-tui-ownership.test.ts": 2096, - "src/main/opencode/hook-plugin-lifecycle-delivery.test.ts": 1063, - "src/main/opencode/hook-plugin-message-part-throttle.test.ts": 266, - "src/main/opencode/hook-plugin-module-contract.test.ts": 682, - "src/main/opencode/hook-plugin-opencode2-session-lineage.test.ts": 1037, - "src/main/opencode/hook-plugin-opencode2-setup.test.ts": 2250, - "src/main/opencode/hook-plugin-opencode2-tui-ownership.test.ts": 1275, - "src/main/opencode/hook-plugin-run-process-session-start.test.ts": 183, - "src/main/opencode/hook-service-invalid-tui-config.test.ts": 111, - "src/main/opencode/hook-service-legacy-acl.test.ts": 251, - "src/main/opencode/hook-service-overlay-confinement.test.ts": 113, - "src/main/opencode/hook-service-startup-refresh.test.ts": 89, - "src/main/opencode/hook-service.test.ts": 350, - "src/main/opencode/legacy-shared-config-dir.test.ts": 54, - "src/main/opencode/opencode-acp-stored-messages.test.ts": 134, - "src/main/opencode/opencode-cli-version.test.ts": 53, - "src/main/opencode/opencode-client-sweep.test.ts": 119, - "src/main/opencode/opencode-credential-backend.test.ts": 79, - "src/main/opencode/opencode-data-directory.test.ts": 50, - "src/main/opencode/opencode-go-api-key-store.test.ts": 97, - "src/main/opencode/opencode-launch-capabilities.test.ts": 67, - "src/main/opencode/opencode-launch-model-config.test.ts": 217, - "src/main/opencode/opencode-launch-model-context-cleanup.test.ts": 4256, - "src/main/opencode/opencode-launch-model-context-supervision.test.ts": 2443, - "src/main/opencode/opencode-launch-model-context.test.ts": 201, - "src/main/opencode/opencode-model-availability.test.ts": 71, - "src/main/opencode/opencode-model-catalog-listing.test.ts": 190, - "src/main/opencode/opencode-model-startup-plan.test.ts": 263, - "src/main/opencode/opencode-model-version-policy.test.ts": 41, - "src/main/opencode/opencode-overlay-manifest.test.ts": 39, - "src/main/opencode/opencode-pty-launch.test.ts": 321, - "src/main/opencode/opencode-session-binder.test.ts": 118, - "src/main/opencode/opencode-startup-prompt-claims.test.ts": 59, - "src/main/opencode/opencode-startup-prompt-installer.test.ts": 181, - "src/main/opencode/opencode-startup-prompt-owner.test.ts": 81, - "src/main/opencode/opencode-startup-prompt-runtime-identity.test.ts": 4479, - "src/main/opencode/opencode-startup-prompt-source.test.ts": 4927, + "src/main/native-chat/transcript-line-decoders.grok.test.ts": 136, + "src/main/native-chat/transcript-line-decoders.omp.test.ts": 133, + "src/main/native-chat/transcript-opencode-database.test.ts": 123, + "src/main/native-chat/transcript-opencode-read-bounds.test.ts": 505, + "src/main/native-chat/transcript-opencode-semantic-fidelity.test.ts": 2041, + "src/main/native-chat/transcript-opencode-sqlite-query.test.ts": 927, + "src/main/native-chat/transcript-opencode-subscribe.test.ts": 5551, + "src/main/native-chat/transcript-opencode.test.ts": 386, + "src/main/native-chat/transcript-opencode2-sqlite.test.ts": 1767, + "src/main/native-chat/transcript-read-cache-inflight.test.ts": 964, + "src/main/native-chat/transcript-read-cache-refusal-recovery.test.ts": 608, + "src/main/native-chat/transcript-read-cache-wsl-stall.test.ts": 399, + "src/main/native-chat/transcript-read-cache.test.ts": 627, + "src/main/native-chat/transcript-reader-codex-history-mode.test.ts": 548, + "src/main/native-chat/transcript-reader-wsl-stall.test.ts": 506, + "src/main/native-chat/transcript-reader.test.ts": 551, + "src/main/native-chat/transcript-record-copy-counts.test.ts": 538, + "src/main/native-chat/transcript-stream-lines.test.ts": 77, + "src/main/native-chat/transcript-tail-reader-cancellation.test.ts": 699, + "src/main/native-chat/transcript-tail-reader-wsl-gate.test.ts": 512, + "src/main/native-chat/transcript-tool-result-identity.test.ts": 523, + "src/main/native-chat/transcript-turn-lifecycle.test.ts": 114, + "src/main/native-chat/transcript-watch-engine-wsl-lifecycle.test.ts": 570, + "src/main/native-chat/transcript-watch-error.test.ts": 2617, + "src/main/native-chat/transcript-watch-inplace-drain-race.test.ts": 939, + "src/main/native-chat/transcript-watch-liveness.test.ts": 1082, + "src/main/native-chat/transcript-watch-resolve-poll.test.ts": 478, + "src/main/native-chat/transcript-watch-unflushed-settle.test.ts": 537, + "src/main/native-chat/transcript-watch-unsubscribe-race.test.ts": 465, + "src/main/native-chat/transcript-watch-wsl-exact-path.test.ts": 462, + "src/main/native-chat/transcript-watch-wsl-stall.test.ts": 579, + "src/main/native-chat/transcript-watch.test.ts": 2407, + "src/main/native-chat/transcript-window-tool-attribution.test.ts": 489, + "src/main/native-chat/transcript-zcode.test.ts": 822, + "src/main/native-chat/wsl-session-path-scan.test.ts": 76, + "src/main/native-chat/wsl-transcript-fs-access.test.ts": 127, + "src/main/native-chat/wsl-transcript-fs-gate.test.ts": 580, + "src/main/native-chat/wsl-transcript-fs-process-client.test.ts": 81, + "src/main/native-chat/wsl-transcript-fs-process-operations.test.ts": 45, + "src/main/native-chat/wsl-transcript-fs-route-quarantine.test.ts": 55, + "src/main/native-chat/wsl-transcript-fs-route.test.ts": 35, + "src/main/native-chat/wsl-transcript-running-observer.test.ts": 94, + "src/main/nested-worktree-removal-plan.test.ts": 101, + "src/main/network/electron-proxy-request-guard.test.ts": 357, + "src/main/network/macos-system-resolver-health.test.ts": 48, + "src/main/network/macos-tailscale-dns-diagnostic.test.ts": 33, + "src/main/network/macos-tailscale-dns-probe-admission.test.ts": 246, + "src/main/network/proxy-settings-session.test.ts": 847, + "src/main/network/proxy-settings.test.ts": 128, + "src/main/node-runtime/appimage-bundled-node-runtime.test.ts": 53, + "src/main/node-runtime/pinned-runtime-materializer-node.test.ts": 136, + "src/main/node-runtime/pinned-runtime-materializer.test.ts": 3143, + "src/main/node-runtime/runtime-archive-download.test.ts": 97, + "src/main/notebook/notebook-kernel.test.ts": 134, + "src/main/notebook/python-environments.test.ts": 68, + "src/main/notifications/announced-notification-registry.test.ts": 276, + "src/main/notifications/desktop-away-state.test.ts": 32, + "src/main/notifications/notification-delivery-service.test.ts": 77, + "src/main/observability/bundle.test.ts": 452, + "src/main/observability/diagnostic-upload-http.test.ts": 36, + "src/main/observability/instrumentation.test.ts": 179, + "src/main/observability/local-file-sink-memory.test.ts": 153, + "src/main/observability/local-file-sink.test.ts": 63, + "src/main/observability/redactor-environment-lines.test.ts": 224, + "src/main/observability/redactor.test.ts": 84, + "src/main/observability/tracer.test.ts": 59, + "src/main/omp/omp-session-cwd-lookup.test.ts": 143, + "src/main/opencode-usage/cache-read-reproduction.test.ts": 170, + "src/main/opencode-usage/cache-write-accounting.test.ts": 219, + "src/main/opencode-usage/opencode2-session-usage.test.ts": 781, + "src/main/opencode-usage/scanner-windows-data-directory.test.ts": 211, + "src/main/opencode-usage/scanner-wsl-gate.test.ts": 61, + "src/main/opencode-usage/scanner.test.ts": 388, + "src/main/opencode-usage/store.test.ts": 435, + "src/main/opencode/hook-plugin-background-child-completion.test.ts": 338, + "src/main/opencode/hook-plugin-child-attention.test.ts": 694, + "src/main/opencode/hook-plugin-dispose-host.test.ts": 417, + "src/main/opencode/hook-plugin-fail-open-ownership.test.ts": 595, + "src/main/opencode/hook-plugin-legacy-tui-ownership.test.ts": 2178, + "src/main/opencode/hook-plugin-lifecycle-delivery.test.ts": 1062, + "src/main/opencode/hook-plugin-message-part-throttle.test.ts": 256, + "src/main/opencode/hook-plugin-module-contract.test.ts": 672, + "src/main/opencode/hook-plugin-opencode2-session-lineage.test.ts": 1083, + "src/main/opencode/hook-plugin-opencode2-setup.test.ts": 2532, + "src/main/opencode/hook-plugin-opencode2-tui-ownership.test.ts": 1667, + "src/main/opencode/hook-plugin-run-process-session-start.test.ts": 173, + "src/main/opencode/hook-service-invalid-tui-config.test.ts": 125, + "src/main/opencode/hook-service-legacy-acl.test.ts": 315, + "src/main/opencode/hook-service-overlay-confinement.test.ts": 80, + "src/main/opencode/hook-service-startup-refresh.test.ts": 114, + "src/main/opencode/hook-service.test.ts": 324, + "src/main/opencode/legacy-shared-config-dir.test.ts": 37, + "src/main/opencode/opencode-acp-stored-messages.test.ts": 135, + "src/main/opencode/opencode-cli-version.test.ts": 55, + "src/main/opencode/opencode-client-sweep.test.ts": 101, + "src/main/opencode/opencode-credential-backend.test.ts": 65, + "src/main/opencode/opencode-data-directory.test.ts": 36, + "src/main/opencode/opencode-go-api-key-store.test.ts": 121, + "src/main/opencode/opencode-launch-capabilities.test.ts": 98, + "src/main/opencode/opencode-launch-model-config.test.ts": 201, + "src/main/opencode/opencode-launch-model-context-cleanup.test.ts": 4232, + "src/main/opencode/opencode-launch-model-context-supervision.test.ts": 2461, + "src/main/opencode/opencode-launch-model-context.test.ts": 310, + "src/main/opencode/opencode-model-availability.test.ts": 48, + "src/main/opencode/opencode-model-catalog-listing.test.ts": 227, + "src/main/opencode/opencode-model-startup-plan.test.ts": 307, + "src/main/opencode/opencode-model-version-policy.test.ts": 29, + "src/main/opencode/opencode-overlay-manifest.test.ts": 33, + "src/main/opencode/opencode-pty-launch.test.ts": 314, + "src/main/opencode/opencode-session-binder.test.ts": 90, + "src/main/opencode/opencode-startup-prompt-claims.test.ts": 44, + "src/main/opencode/opencode-startup-prompt-installer.test.ts": 174, + "src/main/opencode/opencode-startup-prompt-owner.test.ts": 80, + "src/main/opencode/opencode-startup-prompt-runtime-identity.test.ts": 3956, + "src/main/opencode/opencode-startup-prompt-source.test.ts": 4775, "src/main/opencode/opencode-status-plugin-startup-refresh.test.ts": 52, - "src/main/opencode/opencode-structured-account-home.test.ts": 198, - "src/main/opencode/opencode-wsl-probe-environment.test.ts": 148, - "src/main/opencode/overlay-dir-gc-lifecycle.test.ts": 48, - "src/main/opencode/overlay-dir-gc.test.ts": 131, - "src/main/opencode/overlay-source-ownership.test.ts": 64, - "src/main/opencode/status-plugin-shared-server-capability.test.ts": 43, - "src/main/opencode2/status-plugin-setup-lifetime.test.ts": 98, - "src/main/orca-profiles/other-profile-workspace-catalog.test.ts": 385, - "src/main/orca-profiles/profile-active-transfer-worker.test.ts": 2466, - "src/main/orca-profiles/profile-active-transfer.test.ts": 1924, - "src/main/orca-profiles/profile-artifact-cloud-cleanup.test.ts": 640, - "src/main/orca-profiles/profile-cloud-auth-config.test.ts": 65, - "src/main/orca-profiles/profile-cloud-auth-status.test.ts": 72, - "src/main/orca-profiles/profile-cloud-client.test.ts": 148, - "src/main/orca-profiles/profile-cloud-dev-service.test.ts": 511, - "src/main/orca-profiles/profile-cloud-org-members-client.test.ts": 75, - "src/main/orca-profiles/profile-cloud-org-members-service.test.ts": 478, - "src/main/orca-profiles/profile-cloud-pkce.test.ts": 615, - "src/main/orca-profiles/profile-cloud-refresh-replay-guard.test.ts": 91, - "src/main/orca-profiles/profile-cloud-service-auth-retry.test.ts": 535, - "src/main/orca-profiles/profile-cloud-service-connect-overlap.test.ts": 688, - "src/main/orca-profiles/profile-cloud-service-refresh.test.ts": 548, - "src/main/orca-profiles/profile-cloud-service-sign-out-connect-race.test.ts": 529, - "src/main/orca-profiles/profile-cloud-service.test.ts": 541, - "src/main/orca-profiles/profile-cloud-session-mutation.test.ts": 96, - "src/main/orca-profiles/profile-cloud-session-refresh.test.ts": 86, - "src/main/orca-profiles/profile-cloud-session-store.test.ts": 176, - "src/main/orca-profiles/profile-index-store.test.ts": 1403, - "src/main/orca-profiles/profile-persistence-deadline.test.ts": 61, - "src/main/orca-profiles/profile-project-domain-transfer.test.ts": 2843, - "src/main/orca-profiles/profile-project-presence.test.ts": 680, - "src/main/orca-profiles/profile-project-session-field-disposition.test.ts": 367, - "src/main/orca-profiles/profile-project-session-state.test.ts": 326, - "src/main/orca-profiles/profile-project-state-file.test.ts": 384, - "src/main/orca-profiles/profile-project-transfer-migration.test.ts": 1054, - "src/main/orca-profiles/profile-project-transfer.test.ts": 1132, - "src/main/orca-profiles/profile-session-owner-transfer.test.ts": 268, - "src/main/orca-profiles/profile-telemetry-consent-seed.test.ts": 537, - "src/main/orca-profiles/profile-ui-scope.test.ts": 57, - "src/main/orcad-migration-export-holds.test.ts": 286, - "src/main/orcad/electron-serve-browser-process.test.ts": 2791, - "src/main/orcad/electron-serve-provider-selection.test.ts": 808, - "src/main/orcad/electron-sidecar-method-routing.test.ts": 304, - "src/main/orcad/electron-sidecar-tab-registry.test.ts": 207, - "src/main/orcad/external-chromium-browser-process.integration.test.ts": 429, - "src/main/orcad/external-chromium-browser-process.test.ts": 235, - "src/main/orcad/external-chromium-browser-session.test.ts": 239, - "src/main/orcad/main-preflight-order.test.ts": 808, - "src/main/orcad/native-host-abi.test.ts": 50, - "src/main/orcad/node-pty-prebuilt-slot.test.ts": 302, - "src/main/orcad/node-pty-precondition.test.ts": 614, - "src/main/orcad/orcad-app-paths.test.ts": 47, - "src/main/orcad/orcad-artifact-cache-retention.test.ts": 300, - "src/main/orcad/orcad-artifact-preflight.test.ts": 458, - "src/main/orcad/orcad-automations.test.ts": 2225, - "src/main/orcad/orcad-bind-address.test.ts": 41, - "src/main/orcad/orcad-browser-provider.test.ts": 420, - "src/main/orcad/orcad-browser-startup-cancellation.test.ts": 65, - "src/main/orcad/orcad-browser-startup.test.ts": 61, - "src/main/orcad/orcad-bundle-native-load-order.test.ts": 2425, - "src/main/orcad/orcad-bundled-runtime.integration.test.ts": 4817, - "src/main/orcad/orcad-bundled-runtime.test.ts": 97, - "src/main/orcad/orcad-cli-launcher.test.ts": 116, - "src/main/orcad/orcad-cross-runtime-daemon-adoption.integration.test.ts": 397, - "src/main/orcad/orcad-daemon-retirement.test.ts": 186, - "src/main/orcad/orcad-daemon-supervision.test.ts": 63, - "src/main/orcad/orcad-entry.test.ts": 583, - "src/main/orcad/orcad-forked-child-paths.test.ts": 1742, - "src/main/orcad/orcad-health.test.ts": 269, - "src/main/orcad/orcad-help.integration.test.ts": 4302, - "src/main/orcad/orcad-idle-exit-monitor.test.ts": 66, - "src/main/orcad/orcad-idle-stop-record.test.ts": 265, - "src/main/orcad/orcad-instance-lock.test.ts": 302, - "src/main/orcad/orcad-launch-contract.test.ts": 1502, - "src/main/orcad/orcad-launcher-isolation.integration.test.ts": 728, - "src/main/orcad/orcad-lifecycle-host.test.ts": 313, - "src/main/orcad/orcad-local-serve-selection.test.ts": 651, - "src/main/orcad/orcad-managed-idle-exit.test.ts": 230, - "src/main/orcad/orcad-managed-stop-cancel-race.test.ts": 255, - "src/main/orcad/orcad-managed-stop-cancellation.test.ts": 246, - "src/main/orcad/orcad-managed-stop.test.ts": 1323, - "src/main/orcad/orcad-native-preflight.test.ts": 259, - "src/main/orcad/orcad-node-launcher.integration.test.ts": 67, - "src/main/orcad/orcad-observability.test.ts": 280, - "src/main/orcad/orcad-packaged-node-pty.integration.test.ts": 80, - "src/main/orcad/orcad-process-start-time.test.ts": 62, - "src/main/orcad/orcad-profile-preflight.test.ts": 376, - "src/main/orcad/orcad-profile-state-startup.test.ts": 97, - "src/main/orcad/orcad-profile-state-telemetry.test.ts": 49, - "src/main/orcad/orcad-push-startup.test.ts": 2094, - "src/main/orcad/orcad-runtime-entry-artifacts.test.ts": 2239, - "src/main/orcad/orcad-runtime-lifetime.test.ts": 58, - "src/main/orcad/orcad-runtime-native-preflight.test.ts": 108, - "src/main/orcad/orcad-serve-parity.integration.test.ts": 355, - "src/main/orcad/orcad-sidecar-runtime-client.test.ts": 227, - "src/main/orcad/orcad-stop-request-listener.test.ts": 755, - "src/main/orcad/orcad-stop-request-shutdown.integration.test.ts": 482, - "src/main/orcad/orcad-terminal-census.test.ts": 2253, - "src/main/orcad/orcad-windows-conpty-breakaway.integration.test.ts": 74, - "src/main/orcad/serve-profile-ssh-targets.test.ts": 476, - "src/main/own-chromium-tree-kill-guard.test.ts": 158, - "src/main/persistence-async-write-syscalls.test.ts": 3814, - "src/main/persistence-automation-extra-agent-args.test.ts": 1200, - "src/main/persistence-automations.test.ts": 2450, - "src/main/persistence-clipboard-selection-migration.test.ts": 1767, - "src/main/persistence-cohort-and-identity-migration.test.ts": 1422, - "src/main/persistence-cross-host-pane-identity.test.ts": 1564, - "src/main/persistence-deregistered-repo-residue.test.ts": 1665, - "src/main/persistence-duplicate-repo-id-host-scope.test.ts": 1664, - "src/main/persistence-feature-interaction-broadcast.benchmark.test.ts": 1812, - "src/main/persistence-floating-terminal-trust.test.ts": 1766, - "src/main/persistence-flush-and-save-scheduling.test.ts": 2502, - "src/main/persistence-folder-scan-project-groups.test.ts": 1419, - "src/main/persistence-folder-workspace-notes.test.ts": 1820, - "src/main/persistence-host-admitted-terminal-membership.test.ts": 1706, - "src/main/persistence-host-partitioned-sessions.test.ts": 1731, - "src/main/persistence-host-partitioned-ssh-pty-bindings.test.ts": 1409, - "src/main/persistence-initial-load.test.ts": 1921, - "src/main/persistence-layout-binding-recovery.test.ts": 1504, - "src/main/persistence-load-repair-durability.test.ts": 1479, - "src/main/persistence-loading-store-extraction.test.ts": 1733, - "src/main/persistence-loading-store-write-risks.test.ts": 1592, - "src/main/persistence-native-chat-appearance.test.ts": 1702, - "src/main/persistence-native-chat-tab-view-mode.test.ts": 1389, - "src/main/persistence-native-chat-upgrade-tip-audience.test.ts": 1987, - "src/main/persistence-orcad-migration-catalog.test.ts": 1607, - "src/main/persistence-orcad-repository-cache-reimport.test.ts": 1240, - "src/main/persistence-pane-alias-selective-saves.test.ts": 1406, - "src/main/persistence-pane-identity-migration.test.ts": 10029, - "src/main/persistence-protected-secret-fail-closed.test.ts": 1679, - "src/main/persistence-protected-secret-write-race.test.ts": 1450, - "src/main/persistence-proxy-secret-recovery.test.ts": 1456, - "src/main/persistence-pty-binding-leaf-tab-resolution.test.ts": 1655, - "src/main/persistence-pty-binding-reconciliation.test.ts": 1737, - "src/main/persistence-remote-session-startup.test.ts": 1716, - "src/main/persistence-repo-lifecycle.test.ts": 2658, - "src/main/persistence-right-sidebar-tab.test.ts": 1623, - "src/main/persistence-settings-ui-defaults.test.ts": 2488, - "src/main/persistence-settings-update.test.ts": 1778, - "src/main/persistence-single-serialize.test.ts": 1683, - "src/main/persistence-source-control-ai-migration.test.ts": 1814, - "src/main/persistence-split-pane-incarnation.test.ts": 1439, - "src/main/persistence-ssh-lease-reattach-reclaim.test.ts": 1505, - "src/main/persistence-ssh-lease-tombstone-retention.test.ts": 1687, - "src/main/persistence-ssh-pending-pty-kill.test.ts": 2151, - "src/main/persistence-ssh-readoption-automation-migration.test.ts": 1910, - "src/main/persistence-ssh-remote-pty-binding-replay.test.ts": 1287, - "src/main/persistence-ssh-remote-pty-leases.test.ts": 1649, - "src/main/persistence-ssh-targets-and-pane-keys.test.ts": 1785, - "src/main/persistence-status-bar-compact-change-notice.test.ts": 1422, - "src/main/persistence-terminal-option-key-migration.test.ts": 1484, - "src/main/persistence-ui-state.test.ts": 2968, - "src/main/persistence-update-repo.test.ts": 1699, - "src/main/persistence-workspace-attachments.test.ts": 1648, - "src/main/persistence-workspace-pinned-automation-fence.test.ts": 1841, - "src/main/persistence-workspace-repin-automation-owner.test.ts": 2087, - "src/main/persistence-workspace-session-scrollback.test.ts": 1556, - "src/main/persistence-workspace-status-workflow.test.ts": 1917, - "src/main/persistence-worktree-card-properties.test.ts": 1715, - "src/main/persistence-worktree-deletion-fencing.test.ts": 1851, - "src/main/persistence-worktree-lineage-and-backups.test.ts": 1621, - "src/main/persistence-worktree-meta-and-folder-workspaces.test.ts": 1769, - "src/main/persistence-worktree-name-retirement.test.ts": 2258, - "src/main/persistence-worktree-visibility.test.ts": 1878, - "src/main/persistence/applying-settings/notification-settings-normalization.test.ts": 48, - "src/main/persistence/applying-settings/settings-update-machine-name.test.ts": 161, - "src/main/persistence/applying-settings/settings-update-terminal-contrast.test.ts": 174, - "src/main/persistence/applying-settings/terminal-settings-migrations.test.ts": 74, - "src/main/persistence/atomic-snapshot-writer.test.ts": 76, - "src/main/persistence/closed-terminal-tab-write-fence.test.ts": 5551, - "src/main/persistence/leasing-ssh-ptys/ssh-normalization-orcad-fence.test.ts": 200, - "src/main/persistence/leasing-ssh-ptys/ssh-pty-binding-cleanup.test.ts": 373, - "src/main/persistence/loading-store/durable-settings-write.test.ts": 1968, - "src/main/persistence/loading-store/legacy-opencode-go-api-key-migration.test.ts": 62, - "src/main/persistence/loading-store/metadata-lineage-batch-pruning.test.ts": 516, - "src/main/persistence/loading-store/normalize-loaded-explorer-root-migration.test.ts": 289, - "src/main/persistence/loading-store/normalize-loaded-global-settings.test.ts": 369, - "src/main/persistence/loading-store/normalize-loaded-project-catalog.test.ts": 370, - "src/main/persistence/loading-store/persisted-state-redundancy.test.ts": 2095, - "src/main/persistence/loading-store/profile-state-caller-cancellation.test.ts": 1569, - "src/main/persistence/loading-store/profile-state-checkpoints.test.ts": 1426, - "src/main/persistence/loading-store/profile-state-direct-flush.test.ts": 1898, - "src/main/persistence/loading-store/profile-state-import-lifetime.test.ts": 1339, - "src/main/persistence/loading-store/profile-state-maintenance-compatibility.test.ts": 1841, - "src/main/persistence/loading-store/profile-state-maintenance-final-failure.test.ts": 2130, - "src/main/persistence/loading-store/profile-state-maintenance-recovery.test.ts": 2887, - "src/main/persistence/loading-store/profile-state-maintenance.test.ts": 3136, - "src/main/persistence/loading-store/profile-state-pty-retirement-finalization.test.ts": 3327, - "src/main/persistence/loading-store/profile-state-selective-serialization.test.ts": 361, - "src/main/persistence/loading-store/profile-state-settings-writes.test.ts": 1962, - "src/main/persistence/loading-store/profile-state-sqlite-authority.test.ts": 3947, - "src/main/persistence/loading-store/profile-state-store-backups.test.ts": 1838, - "src/main/persistence/loading-store/profile-state-temp-cleanup-lifetime.test.ts": 1843, - "src/main/persistence/loading-store/profile-state-update-quit.test.ts": 2031, - "src/main/persistence/loading-store/profile-state-worker-coordination.test.ts": 1931, - "src/main/persistence/loading-store/profile-state-worker-secret-retention.test.ts": 1546, - "src/main/persistence/loading-store/profile-state-write-batching.test.ts": 1637, - "src/main/persistence/loading-store/pty-binding-async-durability.test.ts": 2161, - "src/main/persistence/loading-store/pty-binding-created-surface-rollback.test.ts": 1817, - "src/main/persistence/loading-store/pty-binding-fast-lane.test.ts": 406, - "src/main/persistence/loading-store/pty-binding-placement-inertness.test.ts": 1843, - "src/main/persistence/loading-store/pty-binding-restart-retirement.test.ts": 1599, - "src/main/persistence/loading-store/pty-binding-span.test.ts": 118, - "src/main/persistence/loading-store/pty-reattach-failure-routing.test.ts": 5933, - "src/main/persistence/loading-store/pty-retirement-async-durability.test.ts": 2298, - "src/main/persistence/loading-store/pty-retirement-publication-during-read.test.ts": 5281, - "src/main/persistence/loading-store/pty-spawn-exit-durability.test.ts": 5496, - "src/main/persistence/loading-store/pty-spawn-handle-publication.test.ts": 5526, - "src/main/persistence/loading-store/pty-spawn-replacement-durability.test.ts": 5757, - "src/main/persistence/loading-store/pty-spawn-restore-durability.test.ts": 7219, - "src/main/persistence/loading-store/secret-sentinel-substitution.test.ts": 99, - "src/main/persistence/loading-store/ssh-lease-async-durability.test.ts": 1576, - "src/main/persistence/loading-store/state-write-round-trip.test.ts": 1937, - "src/main/persistence/loading-store/store-prune-gate-signals.test.ts": 1724, - "src/main/persistence/loading-store/store-runtime-authored-session-writes.test.ts": 1738, - "src/main/persistence/loading-store/store-workspace-session-host-removal.test.ts": 1405, - "src/main/persistence/loading-store/terminal-close-async-durability.test.ts": 5695, - "src/main/persistence/loading-store/terminal-tab-pty-ownership.test.ts": 68, - "src/main/persistence/loading-store/workspace-session-partitions.test.ts": 435, - "src/main/persistence/loading-store/workspace-session-terminal-binding-replay.test.ts": 73, - "src/main/persistence/loading-store/workspace-session-write-listeners.test.ts": 1588, - "src/main/persistence/loading-store/worktree-meta-alias-projection.test.ts": 2168, - "src/main/persistence/loading-store/worktree-meta-write-normalization.test.ts": 98, - "src/main/persistence/migrating-orcad-catalog/orcad-catalog-receipt-ledger.test.ts": 365, - "src/main/persistence/migrating-orcad-catalog/orcad-destination-client-state.test.ts": 405, - "src/main/persistence/migrating-orcad-catalog/orcad-destination-workspace-session.test.ts": 395, - "src/main/persistence/migrating-orcad-catalog/orcad-dormant-metadata-defaults.test.ts": 358, - "src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts": 412, - "src/main/persistence/migrating-orcad-catalog/orcad-source-client-focus-census.test.ts": 603, + "src/main/opencode/opencode-structured-account-home.test.ts": 235, + "src/main/opencode/opencode-wsl-probe-environment.test.ts": 154, + "src/main/opencode/overlay-dir-gc-lifecycle.test.ts": 41, + "src/main/opencode/overlay-dir-gc.test.ts": 121, + "src/main/opencode/overlay-source-ownership.test.ts": 43, + "src/main/opencode/status-plugin-shared-server-capability.test.ts": 38, + "src/main/opencode2/status-plugin-setup-lifetime.test.ts": 77, + "src/main/orca-profiles/other-profile-workspace-catalog.test.ts": 518, + "src/main/orca-profiles/profile-active-transfer-worker.test.ts": 2278, + "src/main/orca-profiles/profile-active-transfer.test.ts": 2002, + "src/main/orca-profiles/profile-artifact-cloud-cleanup.test.ts": 579, + "src/main/orca-profiles/profile-cloud-auth-characterization.test.ts": 870, + "src/main/orca-profiles/profile-cloud-auth-config.test.ts": 62, + "src/main/orca-profiles/profile-cloud-auth-status.test.ts": 73, + "src/main/orca-profiles/profile-cloud-client.test.ts": 122, + "src/main/orca-profiles/profile-cloud-dev-service.test.ts": 509, + "src/main/orca-profiles/profile-cloud-org-members-client.test.ts": 59, + "src/main/orca-profiles/profile-cloud-org-members-service.test.ts": 488, + "src/main/orca-profiles/profile-cloud-pkce.test.ts": 649, + "src/main/orca-profiles/profile-cloud-refresh-replay-guard.test.ts": 73, + "src/main/orca-profiles/profile-cloud-service-auth-retry.test.ts": 462, + "src/main/orca-profiles/profile-cloud-service-connect-overlap.test.ts": 449, + "src/main/orca-profiles/profile-cloud-service-refresh.test.ts": 538, + "src/main/orca-profiles/profile-cloud-service-sign-out-connect-race.test.ts": 624, + "src/main/orca-profiles/profile-cloud-service.test.ts": 601, + "src/main/orca-profiles/profile-cloud-session-refresh-rotation.test.ts": 515, + "src/main/orca-profiles/profile-cloud-session-refresh.test.ts": 61, + "src/main/orca-profiles/profile-cloud-session-store.test.ts": 143, + "src/main/orca-profiles/profile-index-store.test.ts": 1170, + "src/main/orca-profiles/profile-persistence-deadline.test.ts": 48, + "src/main/orca-profiles/profile-project-domain-transfer.test.ts": 2842, + "src/main/orca-profiles/profile-project-presence.test.ts": 509, + "src/main/orca-profiles/profile-project-session-field-disposition.test.ts": 356, + "src/main/orca-profiles/profile-project-session-state.test.ts": 345, + "src/main/orca-profiles/profile-project-state-file.test.ts": 425, + "src/main/orca-profiles/profile-project-transfer-migration.test.ts": 1434, + "src/main/orca-profiles/profile-project-transfer.test.ts": 1495, + "src/main/orca-profiles/profile-session-owner-transfer.test.ts": 282, + "src/main/orca-profiles/profile-telemetry-consent-seed.test.ts": 533, + "src/main/orca-profiles/profile-ui-scope.test.ts": 43, + "src/main/orcad-migration-export-holds.test.ts": 282, + "src/main/orcad/electron-serve-browser-process.test.ts": 2915, + "src/main/orcad/electron-serve-provider-selection.test.ts": 763, + "src/main/orcad/electron-sidecar-method-routing.test.ts": 260, + "src/main/orcad/electron-sidecar-tab-registry.test.ts": 189, + "src/main/orcad/external-chromium-browser-process.integration.test.ts": 417, + "src/main/orcad/external-chromium-browser-process.test.ts": 248, + "src/main/orcad/external-chromium-browser-session.test.ts": 216, + "src/main/orcad/main-preflight-order.test.ts": 859, + "src/main/orcad/native-host-abi.test.ts": 48, + "src/main/orcad/node-pty-prebuilt-slot.test.ts": 306, + "src/main/orcad/node-pty-precondition.test.ts": 560, + "src/main/orcad/orcad-app-paths.test.ts": 45, + "src/main/orcad/orcad-artifact-cache-retention.test.ts": 354, + "src/main/orcad/orcad-artifact-materializer.test.ts": 1039, + "src/main/orcad/orcad-artifact-preflight.test.ts": 462, + "src/main/orcad/orcad-automations.test.ts": 2628, + "src/main/orcad/orcad-bind-address.test.ts": 51, + "src/main/orcad/orcad-browser-provider.test.ts": 365, + "src/main/orcad/orcad-browser-startup-cancellation.test.ts": 49, + "src/main/orcad/orcad-browser-startup.test.ts": 60, + "src/main/orcad/orcad-bundle-native-load-order.test.ts": 2460, + "src/main/orcad/orcad-bundled-runtime.integration.test.ts": 4851, + "src/main/orcad/orcad-bundled-runtime.test.ts": 89, + "src/main/orcad/orcad-cli-launcher.integration.test.ts": 331, + "src/main/orcad/orcad-cli-launcher.test.ts": 143, + "src/main/orcad/orcad-cli-orchestration.integration.test.ts": 217, + "src/main/orcad/orcad-codex-account-homes.test.ts": 574, + "src/main/orcad/orcad-compile-cache.test.ts": 40, + "src/main/orcad/orcad-cross-runtime-daemon-adoption.integration.test.ts": 430, + "src/main/orcad/orcad-daemon-retirement.test.ts": 176, + "src/main/orcad/orcad-daemon-supervision.test.ts": 83, + "src/main/orcad/orcad-entry.test.ts": 627, + "src/main/orcad/orcad-forked-child-paths.test.ts": 90, + "src/main/orcad/orcad-health.test.ts": 250, + "src/main/orcad/orcad-help.integration.test.ts": 3822, + "src/main/orcad/orcad-host-account-launch-env-store.test.ts": 219, + "src/main/orcad/orcad-host-unavailable.test.ts": 350, + "src/main/orcad/orcad-idle-exit-monitor.test.ts": 94, + "src/main/orcad/orcad-idle-stop-record.test.ts": 261, + "src/main/orcad/orcad-instance-lock.test.ts": 308, + "src/main/orcad/orcad-launch-contract.test.ts": 1772, + "src/main/orcad/orcad-launcher-isolation.integration.test.ts": 560, + "src/main/orcad/orcad-lifecycle-host.test.ts": 372, + "src/main/orcad/orcad-local-serve-selection.test.ts": 517, + "src/main/orcad/orcad-managed-idle-exit-chat-provider.test.ts": 1607, + "src/main/orcad/orcad-managed-idle-exit-provider-race.test.ts": 321, + "src/main/orcad/orcad-managed-idle-exit.test.ts": 284, + "src/main/orcad/orcad-managed-stop-cancel-race.test.ts": 293, + "src/main/orcad/orcad-managed-stop-cancellation.test.ts": 317, + "src/main/orcad/orcad-managed-stop-identity.test.ts": 339, + "src/main/orcad/orcad-managed-stop.test.ts": 1593, + "src/main/orcad/orcad-migration-wsl-destination.test.ts": 37, + "src/main/orcad/orcad-native-preflight.test.ts": 278, + "src/main/orcad/orcad-node-launcher.integration.test.ts": 69, + "src/main/orcad/orcad-observability.test.ts": 347, + "src/main/orcad/orcad-packaged-node-pty.integration.test.ts": 54, + "src/main/orcad/orcad-packaged-structured-chat.integration.test.ts": 384, + "src/main/orcad/orcad-process-start-time.test.ts": 55, + "src/main/orcad/orcad-profile-preflight.test.ts": 347, + "src/main/orcad/orcad-profile-state-startup.test.ts": 52, + "src/main/orcad/orcad-profile-state-telemetry.test.ts": 41, + "src/main/orcad/orcad-push-startup.test.ts": 2144, + "src/main/orcad/orcad-runtime-entry-artifacts.test.ts": 2502, + "src/main/orcad/orcad-runtime-lifetime.test.ts": 48, + "src/main/orcad/orcad-runtime-native-preflight.test.ts": 86, + "src/main/orcad/orcad-secret-protection-gap.test.ts": 28, + "src/main/orcad/orcad-serve-parity.integration.test.ts": 243, + "src/main/orcad/orcad-sidecar-runtime-client.test.ts": 259, + "src/main/orcad/orcad-stop-request-listener.test.ts": 771, + "src/main/orcad/orcad-stop-request-shutdown.integration.test.ts": 578, + "src/main/orcad/orcad-terminal-census.test.ts": 628, + "src/main/orcad/orcad-windows-conpty-breakaway.integration.test.ts": 54, + "src/main/orcad/orcad-wsl-guest-shell-path.test.ts": 51, + "src/main/orcad/orcad-wsl-host-login-environment.test.ts": 65, + "src/main/orcad/serve-profile-ssh-targets.test.ts": 429, + "src/main/own-chromium-tree-kill-guard.test.ts": 113, + "src/main/persistence-async-write-syscalls.test.ts": 2801, + "src/main/persistence-automation-extra-agent-args.test.ts": 1694, + "src/main/persistence-automations.test.ts": 2080, + "src/main/persistence-clipboard-selection-migration.test.ts": 1954, + "src/main/persistence-cohort-and-identity-migration.test.ts": 1714, + "src/main/persistence-cross-host-pane-identity.test.ts": 1627, + "src/main/persistence-deregistered-repo-residue.test.ts": 1689, + "src/main/persistence-duplicate-repo-id-host-scope.test.ts": 1946, + "src/main/persistence-empty-root-layout-load.test.ts": 1567, + "src/main/persistence-feature-interaction-broadcast.benchmark.test.ts": 1585, + "src/main/persistence-floating-terminal-trust.test.ts": 1893, + "src/main/persistence-flush-and-save-scheduling.test.ts": 2736, + "src/main/persistence-folder-scan-project-groups.test.ts": 1641, + "src/main/persistence-folder-workspace-notes.test.ts": 1578, + "src/main/persistence-host-partitioned-sessions.test.ts": 1852, + "src/main/persistence-host-partitioned-ssh-pty-bindings.test.ts": 1580, + "src/main/persistence-initial-load.test.ts": 1852, + "src/main/persistence-jira-project.test.ts": 1400, + "src/main/persistence-jira-saved-filters.test.ts": 1531, + "src/main/persistence-layout-binding-recovery.test.ts": 1550, + "src/main/persistence-load-repair-durability.test.ts": 1535, + "src/main/persistence-loading-store-extraction.test.ts": 1773, + "src/main/persistence-loading-store-write-risks.test.ts": 1640, + "src/main/persistence-native-chat-appearance.test.ts": 1369, + "src/main/persistence-native-chat-tab-view-mode.test.ts": 1365, + "src/main/persistence-native-chat-upgrade-tip-audience.test.ts": 1983, + "src/main/persistence-orcad-migration-catalog.test.ts": 2096, + "src/main/persistence-orcad-repository-cache-reimport.test.ts": 1587, + "src/main/persistence-pane-alias-selective-saves.test.ts": 1608, + "src/main/persistence-pane-identity-migration.test.ts": 4881, + "src/main/persistence-protected-secret-fail-closed.test.ts": 2040, + "src/main/persistence-protected-secret-write-race.test.ts": 1639, + "src/main/persistence-proxy-secret-recovery.test.ts": 1569, + "src/main/persistence-pty-binding-leaf-tab-resolution.test.ts": 1418, + "src/main/persistence-pty-binding-reconciliation.test.ts": 1631, + "src/main/persistence-remote-session-startup.test.ts": 1481, + "src/main/persistence-repo-lifecycle.test.ts": 3167, + "src/main/persistence-right-sidebar-tab.test.ts": 1444, + "src/main/persistence-runtime-admitted-terminal-membership.test.ts": 1419, + "src/main/persistence-settings-ui-defaults.test.ts": 2214, + "src/main/persistence-settings-update.test.ts": 2072, + "src/main/persistence-single-serialize.test.ts": 1630, + "src/main/persistence-source-control-ai-migration.test.ts": 1770, + "src/main/persistence-split-pane-incarnation.test.ts": 1586, + "src/main/persistence-ssh-lease-tombstone-retention.test.ts": 1552, + "src/main/persistence-ssh-pending-pty-kill.test.ts": 1505, + "src/main/persistence-ssh-readoption-automation-migration.test.ts": 2132, + "src/main/persistence-ssh-remote-pty-binding-replay.test.ts": 1537, + "src/main/persistence-ssh-remote-pty-leases.test.ts": 1759, + "src/main/persistence-ssh-targets-and-pane-keys.test.ts": 1788, + "src/main/persistence-status-bar-compact-change-notice.test.ts": 1688, + "src/main/persistence-terminal-option-key-migration.test.ts": 1645, + "src/main/persistence-ui-state.test.ts": 3132, + "src/main/persistence-update-repo.test.ts": 1760, + "src/main/persistence-workspace-attachments.test.ts": 1584, + "src/main/persistence-workspace-pinned-automation-fence.test.ts": 2063, + "src/main/persistence-workspace-repin-automation-owner.test.ts": 2056, + "src/main/persistence-workspace-session-scrollback.test.ts": 1638, + "src/main/persistence-workspace-status-workflow.test.ts": 1750, + "src/main/persistence-worktree-card-properties.test.ts": 1708, + "src/main/persistence-worktree-deletion-fencing.test.ts": 1210, + "src/main/persistence-worktree-lineage-and-backups.test.ts": 1734, + "src/main/persistence-worktree-meta-and-folder-workspaces.test.ts": 1544, + "src/main/persistence-worktree-name-retirement.test.ts": 2492, + "src/main/persistence-worktree-visibility.test.ts": 1839, + "src/main/persistence/applying-settings/notification-settings-normalization.test.ts": 56, + "src/main/persistence/applying-settings/settings-update-machine-name.test.ts": 149, + "src/main/persistence/applying-settings/settings-update-terminal-contrast.test.ts": 153, + "src/main/persistence/applying-settings/settings-update-terminal-default-shell.test.ts": 164, + "src/main/persistence/applying-settings/settings-update-terminal-link-preference.test.ts": 1536, + "src/main/persistence/applying-settings/terminal-settings-migrations.test.ts": 76, + "src/main/persistence/atomic-snapshot-writer.test.ts": 341, + "src/main/persistence/closed-terminal-tab-write-fence.test.ts": 5851, + "src/main/persistence/leasing-ssh-ptys/ssh-normalization-orcad-fence.test.ts": 219, + "src/main/persistence/leasing-ssh-ptys/ssh-pty-binding-cleanup.test.ts": 336, + "src/main/persistence/loading-store/durable-settings-write.test.ts": 1952, + "src/main/persistence/loading-store/legacy-opencode-go-api-key-migration.test.ts": 74, + "src/main/persistence/loading-store/metadata-lineage-batch-pruning.test.ts": 609, + "src/main/persistence/loading-store/normalize-loaded-explorer-root-migration.test.ts": 403, + "src/main/persistence/loading-store/normalize-loaded-global-settings.test.ts": 402, + "src/main/persistence/loading-store/normalize-loaded-project-catalog.test.ts": 369, + "src/main/persistence/loading-store/persisted-state-redundancy.test.ts": 1976, + "src/main/persistence/loading-store/profile-state-caller-cancellation.test.ts": 1931, + "src/main/persistence/loading-store/profile-state-checkpoints.test.ts": 1815, + "src/main/persistence/loading-store/profile-state-direct-flush.test.ts": 1619, + "src/main/persistence/loading-store/profile-state-import-lifetime.test.ts": 1535, + "src/main/persistence/loading-store/profile-state-maintenance-compatibility.test.ts": 1904, + "src/main/persistence/loading-store/profile-state-maintenance-final-failure.test.ts": 1809, + "src/main/persistence/loading-store/profile-state-maintenance-recovery.test.ts": 2380, + "src/main/persistence/loading-store/profile-state-maintenance.test.ts": 2759, + "src/main/persistence/loading-store/profile-state-pty-retirement-finalization.test.ts": 4119, + "src/main/persistence/loading-store/profile-state-selective-serialization.test.ts": 431, + "src/main/persistence/loading-store/profile-state-settings-writes.test.ts": 1770, + "src/main/persistence/loading-store/profile-state-sqlite-authority.test.ts": 2255, + "src/main/persistence/loading-store/profile-state-store-backups.test.ts": 1967, + "src/main/persistence/loading-store/profile-state-temp-cleanup-lifetime.test.ts": 1900, + "src/main/persistence/loading-store/profile-state-update-quit.test.ts": 1829, + "src/main/persistence/loading-store/profile-state-worker-coordination.test.ts": 1887, + "src/main/persistence/loading-store/profile-state-worker-secret-retention.test.ts": 1551, + "src/main/persistence/loading-store/profile-state-write-batching.test.ts": 1790, + "src/main/persistence/loading-store/pty-binding-async-durability.test.ts": 1820, + "src/main/persistence/loading-store/pty-binding-created-surface-rollback.test.ts": 1573, + "src/main/persistence/loading-store/pty-binding-fast-lane.test.ts": 293, + "src/main/persistence/loading-store/pty-binding-placement-inertness.test.ts": 2073, + "src/main/persistence/loading-store/pty-binding-restart-retirement.test.ts": 1611, + "src/main/persistence/loading-store/pty-binding-span.test.ts": 93, + "src/main/persistence/loading-store/pty-reattach-failure-routing.test.ts": 5492, + "src/main/persistence/loading-store/pty-retirement-async-durability.test.ts": 1872, + "src/main/persistence/loading-store/pty-retirement-publication-during-read.test.ts": 6062, + "src/main/persistence/loading-store/pty-spawn-exit-durability.test.ts": 8031, + "src/main/persistence/loading-store/pty-spawn-handle-publication.test.ts": 6193, + "src/main/persistence/loading-store/pty-spawn-replacement-durability.test.ts": 5838, + "src/main/persistence/loading-store/pty-spawn-restore-durability.test.ts": 7144, + "src/main/persistence/loading-store/secret-sentinel-substitution.test.ts": 89, + "src/main/persistence/loading-store/state-write-round-trip.test.ts": 1562, + "src/main/persistence/loading-store/store-host-partition-before-unload.test.ts": 1450, + "src/main/persistence/loading-store/store-prune-gate-signals.test.ts": 1466, + "src/main/persistence/loading-store/store-runtime-authored-session-writes.test.ts": 1513, + "src/main/persistence/loading-store/store-workspace-session-host-removal.test.ts": 1533, + "src/main/persistence/loading-store/terminal-close-async-durability.test.ts": 5272, + "src/main/persistence/loading-store/terminal-tab-pty-ownership.test.ts": 36, + "src/main/persistence/loading-store/workspace-session-partitions.test.ts": 379, + "src/main/persistence/loading-store/workspace-session-terminal-binding-replay.test.ts": 72, + "src/main/persistence/loading-store/workspace-session-write-listeners.test.ts": 1677, + "src/main/persistence/loading-store/worktree-meta-alias-projection.test.ts": 2365, + "src/main/persistence/loading-store/worktree-meta-write-normalization.test.ts": 103, + "src/main/persistence/migrating-orcad-catalog/orcad-catalog-receipt-ledger.test.ts": 429, + "src/main/persistence/migrating-orcad-catalog/orcad-destination-client-state.test.ts": 403, + "src/main/persistence/migrating-orcad-catalog/orcad-destination-workspace-session.test.ts": 462, + "src/main/persistence/migrating-orcad-catalog/orcad-dormant-metadata-defaults.test.ts": 489, + "src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts": 459, + "src/main/persistence/migrating-orcad-catalog/orcad-source-client-focus-census.test.ts": 626, "src/main/persistence/migrating-orcad-catalog/orcad-source-client-state.test.ts": 472, - "src/main/persistence/migrating-orcad-catalog/orcad-source-client-subtraction.test.ts": 390, - "src/main/persistence/migrating-orcad-catalog/orcad-source-dependency-census.test.ts": 475, - "src/main/persistence/migrating-orcad-catalog/orcad-source-export.test.ts": 1403, - "src/main/persistence/migrating-orcad-catalog/orcad-source-host-qualified-scope.test.ts": 581, - "src/main/persistence/migrating-orcad-catalog/orcad-source-legacy-key-scope.test.ts": 660, - "src/main/persistence/migrating-orcad-catalog/orcad-source-ownership.test.ts": 404, - "src/main/persistence/migrating-orcad-catalog/orcad-source-partial-partition.test.ts": 486, - "src/main/persistence/migrating-orcad-catalog/orcad-source-scope.test.ts": 71, - "src/main/persistence/migrating-orcad-catalog/orcad-source-scrollback-state.test.ts": 222, - "src/main/persistence/migrating-orcad-catalog/orcad-staged-catalog-claims.test.ts": 440, - "src/main/persistence/profile-state-cutover-fixture.test.ts": 1623, - "src/main/persistence/profile-state-cutover-soak.test.ts": 2103, - "src/main/persistence/profile-state/legacy-json/profile-state-versioned-export.test.ts": 134, - "src/main/persistence/profile-state/profile-state-access-identity.test.ts": 166, - "src/main/persistence/profile-state/profile-state-access-process.test.ts": 592, - "src/main/persistence/profile-state/profile-state-access-windows-native.win32.test.ts": 102, - "src/main/persistence/profile-state/profile-state-access-windows.test.ts": 195, - "src/main/persistence/profile-state/profile-state-access.test.ts": 277, - "src/main/persistence/profile-state/profile-state-authority-bootstrap.test.ts": 1809, - "src/main/persistence/profile-state/profile-state-authority-export-fencing.test.ts": 225, - "src/main/persistence/profile-state/profile-state-authority-marker-writes.test.ts": 541, - "src/main/persistence/profile-state/profile-state-authority-marker.test.ts": 1460, - "src/main/persistence/profile-state/profile-state-automation-runs-equality.test.ts": 203, - "src/main/persistence/profile-state/profile-state-automation-runs-serialization.test.ts": 416, - "src/main/persistence/profile-state/profile-state-automation-storage-upgrade.test.ts": 2903, - "src/main/persistence/profile-state/profile-state-backup-path.test.ts": 68, - "src/main/persistence/profile-state/profile-state-backup-rotation.test.ts": 730, - "src/main/persistence/profile-state/profile-state-backup-temporary-files.test.ts": 94, - "src/main/persistence/profile-state/profile-state-backup-worker.test.ts": 1602, - "src/main/persistence/profile-state/profile-state-bootstrap-publication-race.test.ts": 1679, - "src/main/persistence/profile-state/profile-state-complete-domain-writes.test.ts": 379, + "src/main/persistence/migrating-orcad-catalog/orcad-source-client-subtraction.test.ts": 497, + "src/main/persistence/migrating-orcad-catalog/orcad-source-dependency-census.test.ts": 712, + "src/main/persistence/migrating-orcad-catalog/orcad-source-export.test.ts": 1730, + "src/main/persistence/migrating-orcad-catalog/orcad-source-host-qualified-scope.test.ts": 684, + "src/main/persistence/migrating-orcad-catalog/orcad-source-legacy-key-scope.test.ts": 663, + "src/main/persistence/migrating-orcad-catalog/orcad-source-ownership.test.ts": 453, + "src/main/persistence/migrating-orcad-catalog/orcad-source-partial-partition.test.ts": 698, + "src/main/persistence/migrating-orcad-catalog/orcad-source-release-upgrade.test.ts": 715, + "src/main/persistence/migrating-orcad-catalog/orcad-source-scope.test.ts": 75, + "src/main/persistence/migrating-orcad-catalog/orcad-source-scrollback-state.test.ts": 236, + "src/main/persistence/migrating-orcad-catalog/orcad-staged-catalog-claims.test.ts": 443, + "src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.test.ts": 386, + "src/main/persistence/moving-wsl-projects/wsl-move-cutover-journal.test.ts": 684, + "src/main/persistence/moving-wsl-projects/wsl-move-exit-proof.test.ts": 69, + "src/main/persistence/moving-wsl-projects/wsl-move-fence.test.ts": 421, + "src/main/persistence/moving-wsl-projects/wsl-move-preflight.test.ts": 63, + "src/main/persistence/moving-wsl-projects/wsl-move-quiesce.test.ts": 416, + "src/main/persistence/moving-wsl-projects/wsl-move-restore-hints.test.ts": 61, + "src/main/persistence/moving-wsl-projects/wsl-move-scope-guard.test.ts": 568, + "src/main/persistence/moving-wsl-projects/wsl-move-scope.test.ts": 103, + "src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.test.ts": 1620, + "src/main/persistence/moving-wsl-projects/wsl-move-state-projection-scope.test.ts": 360, + "src/main/persistence/moving-wsl-projects/wsl-move-state-projection.test.ts": 424, + "src/main/persistence/moving-wsl-projects/wsl-move-terminal-gate.test.ts": 241, + "src/main/persistence/profile-state-cutover-fixture.test.ts": 1568, + "src/main/persistence/profile-state-cutover-soak.test.ts": 2160, + "src/main/persistence/profile-state/legacy-json/profile-state-versioned-export.test.ts": 133, + "src/main/persistence/profile-state/profile-state-access-identity.test.ts": 121, + "src/main/persistence/profile-state/profile-state-access-process.test.ts": 671, + "src/main/persistence/profile-state/profile-state-access-windows-native.win32.test.ts": 112, + "src/main/persistence/profile-state/profile-state-access-windows.test.ts": 148, + "src/main/persistence/profile-state/profile-state-access.test.ts": 340, + "src/main/persistence/profile-state/profile-state-authority-bootstrap.test.ts": 1854, + "src/main/persistence/profile-state/profile-state-authority-export-fencing.test.ts": 179, + "src/main/persistence/profile-state/profile-state-authority-marker-writes.test.ts": 547, + "src/main/persistence/profile-state/profile-state-authority-marker.test.ts": 1646, + "src/main/persistence/profile-state/profile-state-automation-runs-equality.test.ts": 235, + "src/main/persistence/profile-state/profile-state-automation-runs-serialization.test.ts": 439, + "src/main/persistence/profile-state/profile-state-automation-storage-upgrade.test.ts": 2632, + "src/main/persistence/profile-state/profile-state-backup-path.test.ts": 74, + "src/main/persistence/profile-state/profile-state-backup-rotation.test.ts": 627, + "src/main/persistence/profile-state/profile-state-backup-temporary-files.test.ts": 90, + "src/main/persistence/profile-state/profile-state-backup-worker.test.ts": 1733, + "src/main/persistence/profile-state/profile-state-bootstrap-publication-race.test.ts": 1530, + "src/main/persistence/profile-state/profile-state-complete-domain-writes.test.ts": 347, "src/main/persistence/profile-state/profile-state-crash-recovery.test.ts": 596, - "src/main/persistence/profile-state/profile-state-cross-runtime.integration.test.ts": 316, - "src/main/persistence/profile-state/profile-state-current-json-command.test.ts": 2938, - "src/main/persistence/profile-state/profile-state-current-sqlite-command.test.ts": 1748, - "src/main/persistence/profile-state/profile-state-database-export-crash.test.ts": 203, - "src/main/persistence/profile-state/profile-state-database-recovery.test.ts": 1415, - "src/main/persistence/profile-state/profile-state-database-rollback-export.test.ts": 1597, - "src/main/persistence/profile-state/profile-state-database-snapshot.test.ts": 454, - "src/main/persistence/profile-state/profile-state-database.test.ts": 243, - "src/main/persistence/profile-state/profile-state-documents.test.ts": 792, - "src/main/persistence/profile-state/profile-state-domain-equality.test.ts": 282, - "src/main/persistence/profile-state/profile-state-domain-reader.test.ts": 178, - "src/main/persistence/profile-state/profile-state-domain-writes.test.ts": 562, - "src/main/persistence/profile-state/profile-state-fragment-validation.test.ts": 1697, - "src/main/persistence/profile-state/profile-state-json-compatibility-recovery.test.ts": 1489, - "src/main/persistence/profile-state/profile-state-large-recovery-crash.test.ts": 3444, - "src/main/persistence/profile-state/profile-state-live-store-factory.test.ts": 2219, - "src/main/persistence/profile-state/profile-state-offline-settings.test.ts": 422, - "src/main/persistence/profile-state/profile-state-parsed-snapshot.test.ts": 229, - "src/main/persistence/profile-state/profile-state-read-concurrency.test.ts": 200, - "src/main/persistence/profile-state/profile-state-recovery-batch.test.ts": 164, - "src/main/persistence/profile-state/profile-state-recovery-copy.test.ts": 111, - "src/main/persistence/profile-state/profile-state-recovery-crash-boundaries.test.ts": 11352, - "src/main/persistence/profile-state/profile-state-recovery-crash-process.test.ts": 228, - "src/main/persistence/profile-state/profile-state-runtime-preflight.test.ts": 602, - "src/main/persistence/profile-state/profile-state-selected-profile-recovery.test.ts": 1692, - "src/main/persistence/profile-state/profile-state-startup-authority.test.ts": 2001, - "src/main/persistence/profile-state/profile-state-startup-failure.test.ts": 1463, - "src/main/persistence/profile-state/profile-state-startup-recovery-dialog.test.ts": 266, - "src/main/persistence/profile-state/profile-state-startup-secrets.test.ts": 1720, - "src/main/persistence/profile-state/profile-state-startup-snapshot.test.ts": 1603, - "src/main/persistence/profile-state/profile-state-store-factory.test.ts": 2673, - "src/main/persistence/profile-state/profile-state-streaming-validation.test.ts": 282, - "src/main/persistence/profile-state/profile-state-worker-authority.test.ts": 1651, - "src/main/persistence/profile-state/profile-state-worker-export-failures.test.ts": 440, - "src/main/persistence/profile-state/profile-state-write-transaction.test.ts": 74, - "src/main/persistence/profile-state/profile-state-writer-capacity-errors.test.ts": 66, - "src/main/persistence/profile-state/profile-state-writer-close.test.ts": 86, - "src/main/persistence/profile-state/profile-state-writer-protocol-faults.test.ts": 936, - "src/main/persistence/profile-state/profile-state-writer-sleep.test.ts": 782, - "src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts": 8846, - "src/main/persistence/profile-state/profile-state-writer-worker.test.ts": 1542, - "src/main/persistence/restoring-sessions/pane-alias-normalization-scaling.test.ts": 85, - "src/main/persistence/restoring-sessions/pane-alias-normalization.test.ts": 921, - "src/main/persistence/restoring-sessions/pane-key-remapping.test.ts": 50, - "src/main/persistence/restoring-sessions/session-owner-fields.test.ts": 333, - "src/main/persistence/restoring-sessions/session-worktree-ownership.test.ts": 348, - "src/main/persistence/restoring-sessions/terminal-layout-normalization.test.ts": 60, - "src/main/persistence/restoring-sessions/workspace-pane-normalization-index.test.ts": 101, - "src/main/persistence/runtime-authored-workspace-session-fields.test.ts": 49, - "src/main/persistence/scheduling-automations/automation-run-operations.test.ts": 143, - "src/main/persistence/terminal-topology/terminal-late-bind-layout-rules.test.ts": 1562, - "src/main/persistence/terminal-topology/terminal-leaf-move-concurrent-close.test.ts": 1529, - "src/main/persistence/terminal-topology/terminal-leaf-move.test.ts": 1619, - "src/main/persistence/terminal-topology/terminal-owner-invariants.test.ts": 299, - "src/main/persistence/terminal-topology/terminal-owner-report-only.test.ts": 1399, - "src/main/persistence/terminal-topology/terminal-sleep-quit-resume.test.ts": 1626, - "src/main/persistence/terminal-topology/terminal-stale-pane-tree-client.test.ts": 4000, - "src/main/persistence/terminal-topology/terminal-topology-commit.test.ts": 280, - "src/main/persistence/terminal-topology/terminal-topology-model.test.ts": 2243, - "src/main/persistence/terminal-topology/workspace-layout-rules.test.ts": 304, - "src/main/persistence/tracking-repos/local-worktree-metadata-batch-pruning.bench.test.ts": 409, - "src/main/persistence/tracking-repos/local-worktree-metadata-scan-expectation.test.ts": 418, - "src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts": 503, - "src/main/persistence/tracking-repos/probeable-local-worktree-metadata-candidates.test.ts": 400, - "src/main/persistence/tracking-repos/project-group-session-batch.test.ts": 353, - "src/main/persistence/tracking-repos/project-host-compatibility.test.ts": 61, - "src/main/persistence/tracking-repos/repo-update-host-guard.test.ts": 424, - "src/main/persistence/tracking-repos/worktree-identity-migration-field-coverage.test.ts": 351, - "src/main/persistence/tracking-repos/worktree-metadata-normalization.test.ts": 327, - "src/main/pi/agent-status-completion-delivery.test.ts": 1338, - "src/main/pi/agent-status-extension-async-subagents.test.ts": 1604, - "src/main/pi/agent-status-extension-nested-run-settlement.test.ts": 486, - "src/main/pi/agent-status-extension-omp-lifecycle.test.ts": 2614, - "src/main/pi/agent-status-extension-omp-model.test.ts": 1167, - "src/main/pi/agent-status-extension-omp-prefill.test.ts": 698, - "src/main/pi/agent-status-extension-session-change.test.ts": 2226, - "src/main/pi/agent-status-extension-source.test.ts": 2691, - "src/main/pi/agent-status-input-redaction.test.ts": 1660, - "src/main/pi/agent-status-omp-approval-forwarding.test.ts": 914, - "src/main/pi/agent-status-owner-recovery.test.ts": 1128, - "src/main/pi/agent-status-reload-delivery.test.ts": 680, - "src/main/pi/agent-status-ui-prompt.test.ts": 1662, - "src/main/pi/omp-model-command-source.test.ts": 789, - "src/main/pi/omp-session-status-owner.test.ts": 1010, - "src/main/pi/omp-transcript-metadata.test.ts": 1375, - "src/main/pi/rpc-capture-replay.test.ts": 518, - "src/main/pi/rpc-configured-default.test.ts": 1225, - "src/main/pi/rpc-launch-resolution.test.ts": 810, - "src/main/pi/rpc-launch.test.ts": 432, - "src/main/pi/rpc-location.test.ts": 1032, - "src/main/pi/rpc-message-dialog-shapes.test.ts": 205, - "src/main/pi/rpc-model-catalog-probe.test.ts": 481, - "src/main/pi/rpc-options.test.ts": 208, - "src/main/pi/rpc-prompt-delivery.test.ts": 300, - "src/main/pi/rpc-prompt.test.ts": 253, - "src/main/pi/rpc-runtime-registration.test.ts": 452, - "src/main/pi/rpc-session-adapter.test.ts": 1081, - "src/main/pi/rpc-turn-races.test.ts": 714, - "src/main/pi/titlebar-extension-overlay-path.test.ts": 83, - "src/main/pi/titlebar-extension-service.test.ts": 230, - "src/main/pi/titlebar-extension-source.test.ts": 1009, - "src/main/plugins/plugin-agent-status-event.test.ts": 227, - "src/main/plugins/plugin-atomic-file-write.test.ts": 2063, - "src/main/plugins/plugin-audit-log-scaling.test.ts": 405, - "src/main/plugins/plugin-audit-log.test.ts": 66, - "src/main/plugins/plugin-bundled-bootstrap-coordinator.test.ts": 425, - "src/main/plugins/plugin-bundled-bootstrap.test.ts": 486, - "src/main/plugins/plugin-command-registry.test.ts": 345, - "src/main/plugins/plugin-content-pack-registry.test.ts": 302, - "src/main/plugins/plugin-content-safety.test.ts": 850, - "src/main/plugins/plugin-dev-watcher-unknown-identity.test.ts": 252, - "src/main/plugins/plugin-dev-watcher.test.ts": 509, - "src/main/plugins/plugin-discovery.test.ts": 325, - "src/main/plugins/plugin-enablement.test.ts": 366, - "src/main/plugins/plugin-host-methods.test.ts": 300, + "src/main/persistence/profile-state/profile-state-cross-runtime.integration.test.ts": 322, + "src/main/persistence/profile-state/profile-state-current-json-command.test.ts": 2774, + "src/main/persistence/profile-state/profile-state-current-sqlite-command.test.ts": 1884, + "src/main/persistence/profile-state/profile-state-database-export-crash.test.ts": 219, + "src/main/persistence/profile-state/profile-state-database-recovery.test.ts": 1224, + "src/main/persistence/profile-state/profile-state-database-rollback-export.test.ts": 1462, + "src/main/persistence/profile-state/profile-state-database-snapshot.test.ts": 589, + "src/main/persistence/profile-state/profile-state-database.test.ts": 293, + "src/main/persistence/profile-state/profile-state-documents.test.ts": 637, + "src/main/persistence/profile-state/profile-state-domain-equality.test.ts": 206, + "src/main/persistence/profile-state/profile-state-domain-reader.test.ts": 212, + "src/main/persistence/profile-state/profile-state-domain-writes.test.ts": 556, + "src/main/persistence/profile-state/profile-state-fragment-validation.test.ts": 1366, + "src/main/persistence/profile-state/profile-state-json-compatibility-recovery.test.ts": 1584, + "src/main/persistence/profile-state/profile-state-large-recovery-crash.test.ts": 2706, + "src/main/persistence/profile-state/profile-state-live-store-factory.test.ts": 2473, + "src/main/persistence/profile-state/profile-state-offline-settings.test.ts": 471, + "src/main/persistence/profile-state/profile-state-parsed-snapshot.test.ts": 191, + "src/main/persistence/profile-state/profile-state-read-concurrency.test.ts": 246, + "src/main/persistence/profile-state/profile-state-recovery-batch.test.ts": 171, + "src/main/persistence/profile-state/profile-state-recovery-copy.test.ts": 105, + "src/main/persistence/profile-state/profile-state-recovery-crash-boundaries.test.ts": 8210, + "src/main/persistence/profile-state/profile-state-recovery-crash-process.test.ts": 255, + "src/main/persistence/profile-state/profile-state-runtime-preflight.test.ts": 622, + "src/main/persistence/profile-state/profile-state-selected-profile-recovery.test.ts": 1483, + "src/main/persistence/profile-state/profile-state-startup-authority.test.ts": 2391, + "src/main/persistence/profile-state/profile-state-startup-failure.test.ts": 1495, + "src/main/persistence/profile-state/profile-state-startup-recovery-dialog.test.ts": 223, + "src/main/persistence/profile-state/profile-state-startup-secrets.test.ts": 1956, + "src/main/persistence/profile-state/profile-state-startup-snapshot.test.ts": 1683, + "src/main/persistence/profile-state/profile-state-store-factory.test.ts": 2366, + "src/main/persistence/profile-state/profile-state-streaming-validation.test.ts": 281, + "src/main/persistence/profile-state/profile-state-worker-authority.test.ts": 1960, + "src/main/persistence/profile-state/profile-state-worker-export-failures.test.ts": 512, + "src/main/persistence/profile-state/profile-state-write-transaction.test.ts": 71, + "src/main/persistence/profile-state/profile-state-writer-capacity-errors.test.ts": 83, + "src/main/persistence/profile-state/profile-state-writer-close.test.ts": 101, + "src/main/persistence/profile-state/profile-state-writer-protocol-faults.test.ts": 967, + "src/main/persistence/profile-state/profile-state-writer-sleep.test.ts": 828, + "src/main/persistence/profile-state/profile-state-writer-worker.test.ts": 1419, + "src/main/persistence/restoring-sessions/pane-alias-normalization-scaling.test.ts": 77, + "src/main/persistence/restoring-sessions/pane-alias-normalization.test.ts": 793, + "src/main/persistence/restoring-sessions/pane-key-remapping.test.ts": 58, + "src/main/persistence/restoring-sessions/session-owner-fields.test.ts": 281, + "src/main/persistence/restoring-sessions/session-worktree-ownership.test.ts": 311, + "src/main/persistence/restoring-sessions/terminal-layout-normalization.test.ts": 70, + "src/main/persistence/restoring-sessions/workspace-pane-normalization-index.test.ts": 88, + "src/main/persistence/runtime-authored-workspace-session-fields.test.ts": 45, + "src/main/persistence/scheduling-automations/automation-run-operations.test.ts": 127, + "src/main/persistence/terminal-topology/terminal-late-bind-layout-rules.test.ts": 1639, + "src/main/persistence/terminal-topology/terminal-leaf-move-concurrent-close.test.ts": 1779, + "src/main/persistence/terminal-topology/terminal-leaf-move.test.ts": 1803, + "src/main/persistence/terminal-topology/terminal-owner-invariants.test.ts": 285, + "src/main/persistence/terminal-topology/terminal-owner-report-only.test.ts": 1617, + "src/main/persistence/terminal-topology/terminal-sleep-quit-resume.test.ts": 1542, + "src/main/persistence/terminal-topology/terminal-stale-pane-tree-client.test.ts": 5032, + "src/main/persistence/terminal-topology/terminal-topology-commit.test.ts": 508, + "src/main/persistence/terminal-topology/terminal-topology-model.test.ts": 2977, + "src/main/persistence/terminal-topology/workspace-layout-rules.test.ts": 318, + "src/main/persistence/tracking-repos/local-worktree-metadata-batch-pruning.bench.test.ts": 539, + "src/main/persistence/tracking-repos/local-worktree-metadata-scan-expectation.test.ts": 358, + "src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts": 551, + "src/main/persistence/tracking-repos/probeable-local-worktree-metadata-candidates.test.ts": 373, + "src/main/persistence/tracking-repos/project-group-session-batch.test.ts": 490, + "src/main/persistence/tracking-repos/project-host-compatibility.test.ts": 59, + "src/main/persistence/tracking-repos/repo-update-host-guard.test.ts": 468, + "src/main/persistence/tracking-repos/worktree-identity-migration-field-coverage.test.ts": 376, + "src/main/persistence/tracking-repos/worktree-metadata-normalization.test.ts": 288, + "src/main/pi/agent-status-completion-delivery.test.ts": 1000, + "src/main/pi/agent-status-extension-async-subagents.test.ts": 1328, + "src/main/pi/agent-status-extension-nested-run-settlement.test.ts": 288, + "src/main/pi/agent-status-extension-omp-lifecycle.test.ts": 2175, + "src/main/pi/agent-status-extension-omp-model.test.ts": 832, + "src/main/pi/agent-status-extension-omp-prefill.test.ts": 613, + "src/main/pi/agent-status-extension-session-change.test.ts": 2157, + "src/main/pi/agent-status-extension-source.test.ts": 2756, + "src/main/pi/agent-status-input-redaction.test.ts": 1445, + "src/main/pi/agent-status-omp-approval-forwarding.test.ts": 598, + "src/main/pi/agent-status-owner-recovery.test.ts": 883, + "src/main/pi/agent-status-reload-delivery.test.ts": 494, + "src/main/pi/agent-status-ui-prompt.test.ts": 1748, + "src/main/pi/omp-model-command-source.test.ts": 615, + "src/main/pi/omp-session-status-owner.test.ts": 823, + "src/main/pi/omp-transcript-metadata.test.ts": 1326, + "src/main/pi/rpc-capture-replay.test.ts": 632, + "src/main/pi/rpc-configured-default.test.ts": 1020, + "src/main/pi/rpc-launch-resolution.test.ts": 1078, + "src/main/pi/rpc-launch.test.ts": 438, + "src/main/pi/rpc-location.test.ts": 1332, + "src/main/pi/rpc-message-dialog-shapes.test.ts": 266, + "src/main/pi/rpc-model-catalog-probe.test.ts": 457, + "src/main/pi/rpc-options.test.ts": 251, + "src/main/pi/rpc-prompt-delivery.test.ts": 312, + "src/main/pi/rpc-prompt.test.ts": 269, + "src/main/pi/rpc-runtime-registration.test.ts": 643, + "src/main/pi/rpc-session-adapter.test.ts": 1035, + "src/main/pi/rpc-turn-races.test.ts": 895, + "src/main/pi/titlebar-extension-overlay-path.test.ts": 80, + "src/main/pi/titlebar-extension-service.test.ts": 251, + "src/main/pi/titlebar-extension-source.test.ts": 923, + "src/main/plugins/plugin-agent-status-event.test.ts": 186, + "src/main/plugins/plugin-atomic-file-write.test.ts": 2036, + "src/main/plugins/plugin-audit-log-scaling.test.ts": 226, + "src/main/plugins/plugin-audit-log.test.ts": 41, + "src/main/plugins/plugin-bundled-bootstrap-coordinator.test.ts": 446, + "src/main/plugins/plugin-bundled-bootstrap.test.ts": 565, + "src/main/plugins/plugin-command-registry.test.ts": 319, + "src/main/plugins/plugin-content-pack-registry.test.ts": 330, + "src/main/plugins/plugin-content-safety.test.ts": 867, + "src/main/plugins/plugin-dev-watcher-unknown-identity.test.ts": 208, + "src/main/plugins/plugin-dev-watcher.test.ts": 442, + "src/main/plugins/plugin-discovery.test.ts": 320, + "src/main/plugins/plugin-enablement.test.ts": 319, + "src/main/plugins/plugin-host-methods.test.ts": 291, "src/main/plugins/plugin-host-process.test.ts": 249, "src/main/plugins/plugin-host-runtime.test.ts": 258, - "src/main/plugins/plugin-install-trust.test.ts": 464, - "src/main/plugins/plugin-install.test.ts": 819, - "src/main/plugins/plugin-kill-list-content-revocation.test.ts": 541, + "src/main/plugins/plugin-install-trust.test.ts": 470, + "src/main/plugins/plugin-install.test.ts": 810, + "src/main/plugins/plugin-kill-list-content-revocation.test.ts": 556, "src/main/plugins/plugin-kill-list-service.test.ts": 292, - "src/main/plugins/plugin-language-pack-registry.test.ts": 352, - "src/main/plugins/plugin-launch-content.test.ts": 464, - "src/main/plugins/plugin-list-projection.test.ts": 320, - "src/main/plugins/plugin-marketplace-installer.test.ts": 425, - "src/main/plugins/plugin-marketplace-service.test.ts": 526, - "src/main/plugins/plugin-marketplace-store.test.ts": 269, - "src/main/plugins/plugin-panel-controller.test.ts": 337, - "src/main/plugins/plugin-panel-owner-lifecycle.test.ts": 51, - "src/main/plugins/plugin-panel-sessions.test.ts": 56, - "src/main/plugins/plugin-private-marketplace.integration.test.ts": 715, - "src/main/plugins/plugin-secrets-store.test.ts": 295, - "src/main/plugins/plugin-service-housekeeping.test.ts": 63, - "src/main/plugins/plugin-service-integrity.test.ts": 555, - "src/main/plugins/plugin-service-reconciliation.test.ts": 841, - "src/main/plugins/plugin-startup-budget.test.ts": 757, - "src/main/plugins/plugin-storage-store.test.ts": 286, - "src/main/plugins/plugin-uninstall-lifecycle.test.ts": 994, - "src/main/plugins/plugin-uninstall-log-retention.test.ts": 1223, - "src/main/plugins/plugin-uninstall-ordering.test.ts": 892, - "src/main/plugins/plugin-vm-recipe-registry.test.ts": 426, - "src/main/plugins/plugin-worker-controller.test.ts": 371, - "src/main/plugins/plugin-worker-env.test.ts": 48, - "src/main/plugins/plugin-worker-manager.test.ts": 311, - "src/main/plugins/plugin-worker-output-buffer.test.ts": 49, - "src/main/plugins/plugin-worker-output-retention.test.ts": 379, - "src/main/plugins/plugin-worker-supervision.integration.test.ts": 8486, + "src/main/plugins/plugin-language-pack-registry.test.ts": 310, + "src/main/plugins/plugin-launch-content.test.ts": 475, + "src/main/plugins/plugin-list-projection.test.ts": 321, + "src/main/plugins/plugin-marketplace-installer.test.ts": 423, + "src/main/plugins/plugin-marketplace-service.test.ts": 513, + "src/main/plugins/plugin-marketplace-store.test.ts": 297, + "src/main/plugins/plugin-panel-controller.test.ts": 301, + "src/main/plugins/plugin-panel-owner-lifecycle.test.ts": 32, + "src/main/plugins/plugin-panel-sessions.test.ts": 33, + "src/main/plugins/plugin-private-marketplace.integration.test.ts": 768, + "src/main/plugins/plugin-secrets-store.test.ts": 284, + "src/main/plugins/plugin-service-housekeeping.test.ts": 42, + "src/main/plugins/plugin-service-integrity.test.ts": 814, + "src/main/plugins/plugin-service-reconciliation.test.ts": 1041, + "src/main/plugins/plugin-startup-budget.test.ts": 944, + "src/main/plugins/plugin-storage-store.test.ts": 236, + "src/main/plugins/plugin-uninstall-lifecycle.test.ts": 699, + "src/main/plugins/plugin-uninstall-log-retention.test.ts": 1284, + "src/main/plugins/plugin-uninstall-ordering.test.ts": 1022, + "src/main/plugins/plugin-vm-recipe-registry.test.ts": 318, + "src/main/plugins/plugin-worker-controller.test.ts": 415, + "src/main/plugins/plugin-worker-env.test.ts": 37, + "src/main/plugins/plugin-worker-manager.test.ts": 245, + "src/main/plugins/plugin-worker-output-buffer.test.ts": 29, + "src/main/plugins/plugin-worker-output-retention.test.ts": 322, + "src/main/plugins/plugin-worker-supervision.integration.test.ts": 8259, "src/main/ports/advertised-url-retention.test.ts": 145, - "src/main/ports/advertised-url-watcher-pid-validation.test.ts": 66, - "src/main/ports/advertised-url-watcher.test.ts": 91, - "src/main/ports/local-workspace-port-scanner.test.ts": 144, - "src/main/ports/port-scan-command-client.test.ts": 1724, - "src/main/ports/port-scan-command-execution.test.ts": 5262, - "src/main/ports/ssh-advertised-url-enrichment.test.ts": 83, - "src/main/ports/workspace-port-execution-host.test.ts": 59, - "src/main/ports/workspace-port-ownership.test.ts": 62, - "src/main/powershell-osc133-bootstrap-windows-clm.test.ts": 109, - "src/main/powershell-osc133-bootstrap.test.ts": 159, - "src/main/preflight/workspace-agent-detection.test.ts": 575, - "src/main/project-groups/folder-workspace-path-status.test.ts": 73, - "src/main/project-groups/nested-repo-discovery-queue.test.ts": 208, - "src/main/project-groups/nested-repo-discovery.test.ts": 271, - "src/main/project-groups/nested-repo-glob-budget.test.ts": 391, - "src/main/project-groups/nested-repo-import-target.test.ts": 56, - "src/main/project-groups/nested-repo-import.test.ts": 63, - "src/main/project-groups/nested-repo-scan-rules.test.ts": 52, - "src/main/project-runtime-git-options.test.ts": 118, - "src/main/protected-secret-persistence-concurrency.test.ts": 61, - "src/main/protected-secret-persistence.test.ts": 77, - "src/main/provider-process/managed-provider-process-fallback-tree.test.ts": 98, - "src/main/provider-process/managed-provider-process-grace.test.ts": 108, - "src/main/provider-process/managed-provider-process-output.test.ts": 101, - "src/main/provider-process/managed-provider-process-root-only.test.ts": 105, - "src/main/provider-process/managed-provider-process.test.ts": 121, - "src/main/provider-process/provider-acquisition-starts.test.ts": 46, - "src/main/provider-process/provider-process-launch.test.ts": 46, - "src/main/provider-process/provider-process-supervisor.integration.test.ts": 14407, - "src/main/provider-process/provider-process-supervisor.test.ts": 65, - "src/main/provider-process/provider-process-teardown.test.ts": 140, - "src/main/provider-process/provider-spawn-failure-report.test.ts": 51, - "src/main/provider-process/provider-stdio-write-queue.test.ts": 48, - "src/main/providers/agent-foreground-command-line.test.ts": 157, - "src/main/providers/agent-foreground-process-batch.test.ts": 89, - "src/main/providers/agent-foreground-process-git-bash-launcher.test.ts": 137, - "src/main/providers/agent-foreground-process-git-bash.win32.test.ts": 445, - "src/main/providers/agent-foreground-process-pi.test.ts": 136, - "src/main/providers/agent-foreground-process-ps-scan-volume.test.ts": 155, - "src/main/providers/agent-foreground-process-real-rows.test.ts": 141, - "src/main/providers/agent-foreground-process-remote-evidence.test.ts": 108, - "src/main/providers/agent-foreground-process-shell-path.test.ts": 131, - "src/main/providers/agent-foreground-process-windows-job-rejection.test.ts": 107, - "src/main/providers/agent-foreground-process-wsl-shell-proof.test.ts": 125, - "src/main/providers/agent-foreground-process.test.ts": 186, - "src/main/providers/execution-host-provider-dispatch.test.ts": 404, - "src/main/providers/local-filesystem-provider.test.ts": 206, - "src/main/providers/local-git-provider.test.ts": 77, - "src/main/providers/local-pty-child-process-verdict.test.ts": 498, - "src/main/providers/local-pty-default-shell.test.ts": 168, - "src/main/providers/local-pty-foreground-inspection-cheap-tier.test.ts": 202, - "src/main/providers/local-pty-pending-native-spawn.test.ts": 306, - "src/main/providers/local-pty-pi-owner-env.test.ts": 257, - "src/main/providers/local-pty-provider-foreground-process.test.ts": 645, - "src/main/providers/local-pty-provider-io-events.test.ts": 438, - "src/main/providers/local-pty-provider-session-inventory.test.ts": 475, - "src/main/providers/local-pty-provider-shell-readiness.test.ts": 521, - "src/main/providers/local-pty-provider-shutdown.test.ts": 714, - "src/main/providers/local-pty-provider-spawn-cwd-safety.test.ts": 501, - "src/main/providers/local-pty-provider-spawn-env.test.ts": 493, - "src/main/providers/local-pty-provider-spawn-session.test.ts": 539, - "src/main/providers/local-pty-provider-windows-shell-launch.test.ts": 571, - "src/main/providers/local-pty-shell-ready-marker-scan.test.ts": 82, - "src/main/providers/local-pty-shell-ready-startup-command.test.ts": 74, - "src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts": 491, - "src/main/providers/local-pty-shell-startup-command.node-pty.test.ts": 310, - "src/main/providers/local-pty-utils-windows-fallback.test.ts": 79, - "src/main/providers/local-pty-utils.test.ts": 91, - "src/main/providers/macos-login-session-pty-probe.test.ts": 54, - "src/main/providers/macos-tcc-login-shell.test.ts": 148, - "src/main/providers/posix-pane-foreground-fingerprint.test.ts": 62, - "src/main/providers/process-cwd.test.ts": 136, - "src/main/providers/provider-dispatch.test.ts": 2275, - "src/main/providers/pty-default-cwd.test.ts": 64, - "src/main/providers/pty-process-inspection.test.ts": 55, - "src/main/providers/pty-process-list-admission.test.ts": 150, - "src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts": 1987, - "src/main/providers/ssh-filesystem-dispatch.test.ts": 52, - "src/main/providers/ssh-git-dispatch.test.ts": 54, - "src/main/providers/ssh-git-provider-commit-message.test.ts": 122, - "src/main/providers/ssh-git-provider-diff.test.ts": 140, - "src/main/providers/ssh-git-provider-exec.test.ts": 126, - "src/main/providers/ssh-git-provider-merge.test.ts": 103, - "src/main/providers/ssh-git-provider-remote-sync.test.ts": 115, - "src/main/providers/ssh-git-provider-staging.test.ts": 163, - "src/main/providers/ssh-git-provider-status-lease.test.ts": 102, - "src/main/providers/ssh-git-provider-status.test.ts": 112, - "src/main/providers/ssh-git-provider-upstream-lease.test.ts": 110, - "src/main/providers/ssh-git-provider-worktree.test.ts": 149, - "src/main/providers/ssh-git-worktree-list-dedupe.test.ts": 125, - "src/main/providers/ssh-worktree-catalog-authority.test.ts": 1068, - "src/main/providers/stable-foreground-process.test.ts": 68, - "src/main/providers/startup-line-typed-length.live-shell.test.ts": 165, - "src/main/providers/terminal-path-existence-batch.integration.test.ts": 150, - "src/main/providers/windows-agent-foreground-command-line.test.ts": 102, - "src/main/providers/windows-agent-foreground-process-scan-volume.test.ts": 107, - "src/main/providers/windows-cached-agent-revalidation.test.ts": 55, - "src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts": 59, - "src/main/providers/windows-console-attached-processes.test.ts": 56, - "src/main/providers/windows-foreground-process-inspection-cost.test.ts": 105, - "src/main/providers/windows-foreground-process-rows.test.ts": 90, - "src/main/providers/windows-powershell-executable.test.ts": 76, - "src/main/providers/windows-powershell.test.ts": 42, - "src/main/providers/windows-pty-job-membership.test.ts": 68, - "src/main/providers/windows-shell-args.test.ts": 192, - "src/main/providers/windows-shell-fallback-chain.test.ts": 131, - "src/main/providers/windows-shell-preflight-runtime.windows.test.ts": 123, - "src/main/providers/working-directory-validation-retention.test.ts": 132, - "src/main/providers/working-directory-validation.test.ts": 118, - "src/main/provisioned-root-ssh-adoption.test.ts": 332, - "src/main/pty-descendant-exit-verification.test.ts": 139, - "src/main/pty-descendant-termination.test.ts": 173, - "src/main/pty/appimage-terminal-env.test.ts": 68, - "src/main/pty/build-mode-env.test.ts": 49, - "src/main/pty/chromium-session-bus-env.test.ts": 202, - "src/main/pty/codex-home-wsl-env.test.ts": 44, - "src/main/pty/codex-launch-shell-wrapping.test.ts": 180, - "src/main/pty/codex-no-daemon-binary-contract.test.ts": 44, - "src/main/pty/codex-no-daemon-launch-command.test.ts": 96, - "src/main/pty/codex-preflight-profile-path-rewrite.test.ts": 137, - "src/main/pty/codex-shell-launch-preflight.test.ts": 2497, - "src/main/pty/codex-shell-no-daemon.test.ts": 7782, - "src/main/pty/conda-activation-env.test.ts": 64, - "src/main/pty/legacy-terminal-shim-dir.test.ts": 287, - "src/main/pty/legacy-terminal-windows-tombstone.test.ts": 85, - "src/main/pty/managed-ssh-host-login-shell.test.ts": 179, - "src/main/pty/node-pty-master-fd-retirement.test.ts": 2373, - "src/main/pty/node-pty-self-exit-pseudoconsole-close.test.ts": 55, - "src/main/pty/omp-sqlite-overlay.test.ts": 51, - "src/main/pty/overlay-mirror.test.ts": 69, - "src/main/pty/pi-process-owner-env.test.ts": 909, - "src/main/pty/posix-pty-foreground-group.test.ts": 65, - "src/main/pty/posix-pty-process-groups.integration.test.ts": 167, - "src/main/pty/posix-pty-process-groups.test.ts": 105, - "src/main/pty/powershell-profile-env.test.ts": 98, - "src/main/pty/shell-startup-env.test.ts": 131, - "src/main/pty/terminal-color-env.test.ts": 46, - "src/main/pty/windows-environment-path-main-loop.test.ts": 430, - "src/main/pty/windows-environment-path.test.ts": 118, - "src/main/pty/windows-path-registry-change.test.ts": 49, - "src/main/pty/windows-path-registry-fallback.test.ts": 69, - "src/main/pty/windows-path-registry-reader.test.ts": 55, + "src/main/ports/advertised-url-watcher-pid-validation.test.ts": 45, + "src/main/ports/advertised-url-watcher.test.ts": 69, + "src/main/ports/local-workspace-port-scanner.test.ts": 174, + "src/main/ports/port-scan-command-client.test.ts": 1705, + "src/main/ports/port-scan-command-execution.test.ts": 5255, + "src/main/ports/ssh-advertised-url-enrichment.test.ts": 86, + "src/main/ports/workspace-port-execution-host.test.ts": 61, + "src/main/ports/workspace-port-ownership.test.ts": 54, + "src/main/powershell-osc133-bootstrap-windows-clm.test.ts": 136, + "src/main/powershell-osc133-bootstrap.test.ts": 145, + "src/main/preflight/agent-command-detection.test.ts": 96, + "src/main/preflight/codex-cli-installation.test.ts": 68, + "src/main/preflight/codex-workspace-relative-path.test.ts": 1298, + "src/main/preflight/workspace-agent-detection.test.ts": 608, + "src/main/project-groups/folder-workspace-path-status.test.ts": 57, + "src/main/project-groups/nested-repo-discovery-queue.test.ts": 232, + "src/main/project-groups/nested-repo-discovery.test.ts": 305, + "src/main/project-groups/nested-repo-glob-budget.test.ts": 302, + "src/main/project-groups/nested-repo-import-target.test.ts": 35, + "src/main/project-groups/nested-repo-import.test.ts": 54, + "src/main/project-groups/nested-repo-scan-rules.test.ts": 40, + "src/main/project-runtime-git-options.test.ts": 291, + "src/main/protected-secret-persistence-concurrency.test.ts": 51, + "src/main/protected-secret-persistence.test.ts": 55, + "src/main/provider-process/managed-provider-process-fallback-tree.test.ts": 83, + "src/main/provider-process/managed-provider-process-grace.test.ts": 101, + "src/main/provider-process/managed-provider-process-output.test.ts": 106, + "src/main/provider-process/managed-provider-process-root-only.test.ts": 88, + "src/main/provider-process/managed-provider-process.test.ts": 114, + "src/main/provider-process/provider-acquisition-starts.test.ts": 35, + "src/main/provider-process/provider-process-launch.test.ts": 39, + "src/main/provider-process/provider-process-supervisor.integration.test.ts": 7553, + "src/main/provider-process/provider-process-supervisor.test.ts": 42, + "src/main/provider-process/provider-process-teardown.test.ts": 121, + "src/main/provider-process/provider-spawn-failure-report.test.ts": 38, + "src/main/provider-process/provider-stdio-write-queue.test.ts": 47, + "src/main/providers/agent-foreground-command-line.test.ts": 125, + "src/main/providers/agent-foreground-process-batch.test.ts": 106, + "src/main/providers/agent-foreground-process-git-bash-launcher.test.ts": 132, + "src/main/providers/agent-foreground-process-git-bash.win32.test.ts": 423, + "src/main/providers/agent-foreground-process-pi.test.ts": 116, + "src/main/providers/agent-foreground-process-ps-scan-volume.test.ts": 213, + "src/main/providers/agent-foreground-process-real-rows.test.ts": 115, + "src/main/providers/agent-foreground-process-remote-evidence.test.ts": 112, + "src/main/providers/agent-foreground-process-shell-path.test.ts": 103, + "src/main/providers/agent-foreground-process-windows-job-rejection.test.ts": 108, + "src/main/providers/agent-foreground-process-wsl-shell-proof.test.ts": 81, + "src/main/providers/agent-foreground-process.test.ts": 204, + "src/main/providers/execution-host-provider-dispatch.test.ts": 561, + "src/main/providers/filesystem-markdown-listing.test.ts": 77, + "src/main/providers/local-filesystem-provider.test.ts": 365, + "src/main/providers/local-git-provider.test.ts": 103, + "src/main/providers/local-pty-child-process-verdict.test.ts": 605, + "src/main/providers/local-pty-default-shell.test.ts": 157, + "src/main/providers/local-pty-foreground-inspection-cheap-tier.test.ts": 185, + "src/main/providers/local-pty-pending-native-spawn.test.ts": 341, + "src/main/providers/local-pty-pi-owner-env.test.ts": 251, + "src/main/providers/local-pty-provider-foreground-process.test.ts": 588, + "src/main/providers/local-pty-provider-io-events.test.ts": 526, + "src/main/providers/local-pty-provider-session-inventory.test.ts": 490, + "src/main/providers/local-pty-provider-shell-readiness.test.ts": 501, + "src/main/providers/local-pty-provider-shutdown.test.ts": 811, + "src/main/providers/local-pty-provider-spawn-cwd-safety.test.ts": 457, + "src/main/providers/local-pty-provider-spawn-env.test.ts": 554, + "src/main/providers/local-pty-provider-spawn-session.test.ts": 864, + "src/main/providers/local-pty-provider-windows-shell-launch.test.ts": 543, + "src/main/providers/local-pty-shell-ready-marker-scan.test.ts": 66, + "src/main/providers/local-pty-shell-ready-startup-command.test.ts": 51, + "src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts": 472, + "src/main/providers/local-pty-shell-startup-command.node-pty.test.ts": 233, + "src/main/providers/local-pty-utils-windows-fallback.test.ts": 63, + "src/main/providers/local-pty-utils.test.ts": 78, + "src/main/providers/macos-login-session-pty-probe.test.ts": 61, + "src/main/providers/macos-tcc-login-shell.test.ts": 162, + "src/main/providers/posix-pane-foreground-fingerprint.test.ts": 70, + "src/main/providers/process-cwd.test.ts": 121, + "src/main/providers/provider-dispatch.test.ts": 2388, + "src/main/providers/pty-default-cwd.test.ts": 30, + "src/main/providers/pty-process-inspection.test.ts": 34, + "src/main/providers/pty-process-list-admission.test.ts": 320, + "src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts": 2007, + "src/main/providers/stable-foreground-process.test.ts": 75, + "src/main/providers/startup-line-typed-length.live-shell.test.ts": 135, + "src/main/providers/terminal-path-existence-batch.integration.test.ts": 109, + "src/main/providers/windows-agent-foreground-command-line.test.ts": 85, + "src/main/providers/windows-agent-foreground-process-scan-volume.test.ts": 125, + "src/main/providers/windows-cached-agent-revalidation.test.ts": 49, + "src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts": 52, + "src/main/providers/windows-console-attached-processes.test.ts": 48, + "src/main/providers/windows-foreground-process-inspection-cost.test.ts": 107, + "src/main/providers/windows-foreground-process-rows.test.ts": 66, + "src/main/providers/windows-powershell-executable.test.ts": 44, + "src/main/providers/windows-powershell.test.ts": 38, + "src/main/providers/windows-pty-job-membership.test.ts": 64, + "src/main/providers/windows-shell-args.test.ts": 198, + "src/main/providers/windows-shell-fallback-chain.test.ts": 113, + "src/main/providers/windows-shell-preflight-runtime.windows.test.ts": 114, + "src/main/providers/working-directory-validation-retention.test.ts": 152, + "src/main/providers/working-directory-validation.test.ts": 144, + "src/main/provisioned-root-checkout.test.ts": 103, + "src/main/provisioned-root-ssh-adoption.test.ts": 408, + "src/main/pty-descendant-exit-verification.test.ts": 148, + "src/main/pty-descendant-termination.test.ts": 170, + "src/main/pty/appimage-terminal-env.test.ts": 45, + "src/main/pty/build-mode-env.test.ts": 41, + "src/main/pty/chromium-session-bus-env.test.ts": 319, + "src/main/pty/codex-home-wsl-env.test.ts": 30, + "src/main/pty/codex-launch-shell-wrapping.test.ts": 114, + "src/main/pty/codex-no-daemon-binary-contract.test.ts": 26, + "src/main/pty/codex-no-daemon-launch-command.test.ts": 92, + "src/main/pty/codex-preflight-profile-path-rewrite.test.ts": 122, + "src/main/pty/codex-shell-launch-preflight.test.ts": 2405, + "src/main/pty/codex-shell-no-daemon.test.ts": 11219, + "src/main/pty/conda-activation-env.test.ts": 40, + "src/main/pty/legacy-terminal-shim-dir.test.ts": 258, + "src/main/pty/legacy-terminal-windows-tombstone.test.ts": 58, + "src/main/pty/managed-ssh-host-login-shell.test.ts": 199, + "src/main/pty/node-pty-master-fd-retirement.test.ts": 2422, + "src/main/pty/node-pty-self-exit-pseudoconsole-close.test.ts": 57, + "src/main/pty/overlay-mirror.test.ts": 51, + "src/main/pty/pi-process-owner-env.test.ts": 821, + "src/main/pty/posix-pty-foreground-group.test.ts": 43, + "src/main/pty/posix-pty-process-groups.integration.test.ts": 156, + "src/main/pty/posix-pty-process-groups.test.ts": 95, + "src/main/pty/powershell-profile-env.test.ts": 80, + "src/main/pty/shell-startup-env.test.ts": 84, + "src/main/pty/terminal-color-env.test.ts": 29, + "src/main/pty/windows-environment-path-main-loop.test.ts": 425, + "src/main/pty/windows-environment-path.test.ts": 127, + "src/main/pty/windows-path-registry-change.test.ts": 62, + "src/main/pty/windows-path-registry-fallback.test.ts": 35, + "src/main/pty/windows-path-registry-reader.test.ts": 46, "src/main/pty/wsl-orca-env.test.ts": 107, - "src/main/pwsh.test.ts": 78, - "src/main/qoder/hook-service.test.ts": 146, - "src/main/qoder/real-cli.test.ts": 709, - "src/main/quit-path-durable-write-blocking.test.ts": 3220, - "src/main/quit-teardown-deadline.test.ts": 54, - "src/main/quit-teardown-start-gate.test.ts": 38, - "src/main/rate-limits/account-runtime-target-sync.test.ts": 277, - "src/main/rate-limits/antigravity-usage-error.test.ts": 52, - "src/main/rate-limits/antigravity-usage-fetcher.test.ts": 103, - "src/main/rate-limits/antigravity-usage-real-cli.test.ts": 124, - "src/main/rate-limits/antigravity-usage-response.test.ts": 66, - "src/main/rate-limits/auth-filesystem-operation-retention.test.ts": 140, - "src/main/rate-limits/auth-filesystem-operation.test.ts": 95, - "src/main/rate-limits/claude-oauth-usage-error.test.ts": 44, - "src/main/rate-limits/claude-oauth-usage-extra-usage.test.ts": 84, - "src/main/rate-limits/claude-profile-usage.test.ts": 275, - "src/main/rate-limits/claude-usage-error-classification.test.ts": 58, - "src/main/rate-limits/codex-auth-presence.test.ts": 63, - "src/main/rate-limits/codex-fetcher-auth-errors.test.ts": 120, - "src/main/rate-limits/codex-fetcher-backend.test.ts": 267, - "src/main/rate-limits/codex-fetcher-credit-balance.test.ts": 111, - "src/main/rate-limits/codex-fetcher-probe-shutdown.test.ts": 166, - "src/main/rate-limits/codex-fetcher-process-contract.test.ts": 237, - "src/main/rate-limits/codex-fetcher-rpc-exit-diagnostics.test.ts": 358, - "src/main/rate-limits/codex-fetcher-runtime-pairing.test.ts": 136, - "src/main/rate-limits/codex-fetcher-session-supplement.test.ts": 152, - "src/main/rate-limits/codex-fetcher.test.ts": 265, - "src/main/rate-limits/codex-probe-termination.test.ts": 81, - "src/main/rate-limits/codex-rate-limit-window-classification.test.ts": 41, - "src/main/rate-limits/codex-rpc-rate-limit-probe.test.ts": 156, - "src/main/rate-limits/cursor-auth.test.ts": 220, - "src/main/rate-limits/cursor-fetcher.test.ts": 370, - "src/main/rate-limits/cursor-session-token.test.ts": 214, - "src/main/rate-limits/cursor-usage-mapping.test.ts": 247, - "src/main/rate-limits/gemini-bucket-formatting.test.ts": 60, - "src/main/rate-limits/gemini-usage-fetcher.fallback.test.ts": 88, - "src/main/rate-limits/gemini-usage-fetcher.test.ts": 112, - "src/main/rate-limits/grok-auth.test.ts": 103, - "src/main/rate-limits/grok-fetcher.test.ts": 97, - "src/main/rate-limits/grok-weekly-zero.test.ts": 180, - "src/main/rate-limits/initial-account-rate-limit-target.test.ts": 295, - "src/main/rate-limits/kimi-fetcher-wsl-home.test.ts": 107, - "src/main/rate-limits/kimi-fetcher.test.ts": 175, - "src/main/rate-limits/minimax/minimax-fetcher.test.ts": 122, - "src/main/rate-limits/minimax/minimax-request-context.test.ts": 97, - "src/main/rate-limits/opencode-go-api-key-source.test.ts": 452, - "src/main/rate-limits/opencode-go-managed-account-source.test.ts": 514, - "src/main/rate-limits/opencode-go-status-parsing.test.ts": 56, - "src/main/rate-limits/opencode-go-usage-api.test.ts": 77, - "src/main/rate-limits/opencode-go-usage-fetcher.test.ts": 136, - "src/main/rate-limits/opencode-go-usage-source-selection.test.ts": 107, - "src/main/rate-limits/service-account-target-selection.test.ts": 1032, - "src/main/rate-limits/service-antigravity-usage.test.ts": 139, - "src/main/rate-limits/service-cursor-usage.test.ts": 430, - "src/main/rate-limits/service-inactive-account-previews.test.ts": 158, - "src/main/rate-limits/service-live-claude-usage.test.ts": 188, - "src/main/rate-limits/service-minimax-usage.test.ts": 159, - "src/main/rate-limits/service-opencode-go-credentials.test.ts": 136, - "src/main/rate-limits/service-refresh-orchestration.test.ts": 177, - "src/main/rate-limits/service-window-activation.test.ts": 363, - "src/main/rate-limits/service-zcode-usage.test.ts": 322, - "src/main/rate-limits/zcode-usage-fetcher.test.ts": 77, - "src/main/refused-tree-kill-root-termination.test.ts": 293, - "src/main/repo-git-remote-avatar-refresh.test.ts": 135, - "src/main/repo-git-remote-identity-enrichment.test.ts": 119, - "src/main/repo-git-remote-identity.test.ts": 402, - "src/main/repo-git-username-enrichment.test.ts": 65, - "src/main/repo-icon-autodetect.test.ts": 826, - "src/main/repo-icon-file-detection.test.ts": 455, - "src/main/repo-icon-source-href.test.ts": 81, - "src/main/repo-maintenance-idle-gate.test.ts": 219, - "src/main/repo-worktrees.test.ts": 372, - "src/main/ripgrep/bundled-ripgrep-path.test.ts": 107, - "src/main/ripgrep/bundled-ripgrep-real.test.ts": 166, - "src/main/ripgrep/bundled-ripgrep-spawn.test.ts": 80, - "src/main/ripgrep/bundled-ripgrep-stop.test.ts": 84, - "src/main/ripgrep/bundled-ripgrep-text-search.test.ts": 207, - "src/main/ripgrep/fd-exhausted-spawn.test.ts": 75, - "src/main/ripgrep/ripgrep-filename-identity-real.test.ts": 209, - "src/main/ripgrep/synchronous-spawn-failure.test.ts": 69, - "src/main/ripgrep/text-search-unicode-real.test.ts": 228, - "src/main/ripgrep/wsl-missing-cwd-exit.test.ts": 124, - "src/main/runtime/acknowledged-terminal-tab-retirement.test.ts": 5402, - "src/main/runtime/agent-detection-host-resolution.test.ts": 3351, - "src/main/runtime/agent-launch-record-warmup-gate.test.ts": 42, - "src/main/runtime/agent-launch-typed-line-shell.test.ts": 50, - "src/main/runtime/agent-prompt-line-settle.test.ts": 4010, - "src/main/runtime/agent-prompt-receipt-correlation.test.ts": 4528, - "src/main/runtime/agent-prompt-request-correlation.test.ts": 46, - "src/main/runtime/agent-prompt-submission-runtime-hook-and-generation.test.ts": 5685, - "src/main/runtime/agent-prompt-submission-runtime.test.ts": 6589, - "src/main/runtime/agent-prompt-submission-verification.test.ts": 140, - "src/main/runtime/agent-prompt-submission-windows-submit-delay.test.ts": 4945, - "src/main/runtime/agent-session-claim-identity.test.ts": 67, - "src/main/runtime/agent-session-conversation-clear-commit.test.ts": 705, - "src/main/runtime/agent-session-conversation-name-store.test.ts": 703, - "src/main/runtime/agent-session-death-evidence-persistence.test.ts": 730, - "src/main/runtime/agent-session-launch-directory.test.ts": 583, - "src/main/runtime/agent-session-launch-env-admission.test.ts": 812, - "src/main/runtime/agent-session-lease-renewal.test.ts": 706, - "src/main/runtime/agent-session-operation-admission.test.ts": 308, - "src/main/runtime/agent-session-process-identity-probe-windows-batch.test.ts": 88, - "src/main/runtime/agent-session-process-identity-probe.test.ts": 93, - "src/main/runtime/agent-session-provider-handle-transition.test.ts": 241, - "src/main/runtime/agent-session-record-conversation-name.test.ts": 296, - "src/main/runtime/agent-session-record-options.test.ts": 741, - "src/main/runtime/agent-session-record-store-slot.test.ts": 1888, - "src/main/runtime/agent-session-record-store-tab-table.test.ts": 847, - "src/main/runtime/agent-session-record-store.test.ts": 1364, - "src/main/runtime/agent-session-recovery-capsule.test.ts": 1090, - "src/main/runtime/agent-session-released-reservation-replay.test.ts": 851, - "src/main/runtime/agent-session-replaced-runtime.test.ts": 238, - "src/main/runtime/agent-session-reservation-admission.test.ts": 291, - "src/main/runtime/agent-session-runtime-attribution.test.ts": 799, - "src/main/runtime/agent-session-runtime-end-record.test.ts": 314, - "src/main/runtime/agent-session-spawn-token-process-scan.test.ts": 82, - "src/main/runtime/agent-session-spawn-token-readback.test.ts": 82, - "src/main/runtime/agent-session-store-transactions.test.ts": 575, - "src/main/runtime/agent-session-surface-release-transition.test.ts": 240, - "src/main/runtime/agent-session-unsaved-creation-supersession.test.ts": 730, - "src/main/runtime/agent-state-rules/agent-state-rule-regions.test.ts": 354, - "src/main/runtime/agent-state-rules/agent-state-rules-bundle.test.ts": 239, - "src/main/runtime/agent-state-rules/agent-state-rules-engine.test.ts": 325, - "src/main/runtime/agent-state-rules/agent-state-rules-live-update.test.ts": 470, - "src/main/runtime/agent-state-rules/agent-state-text-anchors.test.ts": 209, - "src/main/runtime/agent-terminal-launch-host.test.ts": 4780, - "src/main/runtime/agent-terminal-startup-prompt.test.ts": 4385, - "src/main/runtime/antigravity-readiness-transcripts.test.ts": 9127, - "src/main/runtime/antigravity-terminal-readiness.test.ts": 318, - "src/main/runtime/automatic-activation-host-sleep.test.ts": 5209, - "src/main/runtime/automation-change-publication.test.ts": 13953, - "src/main/runtime/browser-client-download-transfers.test.ts": 341, - "src/main/runtime/browser-dialog-settlement.test.ts": 415, - "src/main/runtime/browser-execution-host-key-resolution.test.ts": 4603, - "src/main/runtime/browser-host-capability-selection.test.ts": 270, - "src/main/runtime/browser-host-client-page-adoption-grants.test.ts": 56, - "src/main/runtime/browser-host-client-page-adoption.test.ts": 53, - "src/main/runtime/browser-host-client-page-creation.test.ts": 298, - "src/main/runtime/browser-host-command-ledger-capacity.test.ts": 233, - "src/main/runtime/browser-host-command-ledger.test.ts": 267, - "src/main/runtime/browser-host-file-channel-admission.test.ts": 228, - "src/main/runtime/browser-host-lease-download-transfer-cleanup.test.ts": 407, - "src/main/runtime/browser-host-lease-fenced-page-release.test.ts": 271, - "src/main/runtime/browser-host-lease-placement-retirement.test.ts": 299, - "src/main/runtime/browser-host-lease-registry.test.ts": 279, - "src/main/runtime/browser-host-page-placement-replacement.test.ts": 299, - "src/main/runtime/browser-host-page-placement.test.ts": 299, - "src/main/runtime/browser-host-page-reconciliation-executor.test.ts": 89, - "src/main/runtime/browser-host-page-reconciliation-orchestration.test.ts": 512, - "src/main/runtime/browser-host-page-reconciliation-plan.test.ts": 83, - "src/main/runtime/browser-host-page-reconciliation-preconditions.test.ts": 277, - "src/main/runtime/browser-host-page-retirement.test.ts": 58, - "src/main/runtime/browser-network-tunnel-paired-runtime.integration.test.ts": 5322, - "src/main/runtime/browser-screencast-driver-attribution.test.ts": 4877, - "src/main/runtime/browser-screencast-driver-scope.test.ts": 46, - "src/main/runtime/browser-screencast-ghost-subscriber-eviction.test.ts": 67, - "src/main/runtime/browser-screencast-remote-viewer-retention.test.ts": 5765, - "src/main/runtime/browser-screencast-renderer-throttle-lease.test.ts": 5601, - "src/main/runtime/browser-session-tab-selection-snapshot.test.ts": 60, - "src/main/runtime/browser-tab-create-caller-navigation.test.ts": 4661, - "src/main/runtime/browser-tab-create-folder-workspace.test.ts": 407, - "src/main/runtime/browser-tab-create-publication.test.ts": 550, - "src/main/runtime/claude-agent-teams-pty-exit-leak.test.ts": 4658, - "src/main/runtime/claude-agent-teams-service.test.ts": 92, - "src/main/runtime/claude-agent-teams-shim-env.test.ts": 79, - "src/main/runtime/claude-structured-exit-mid-response.test.ts": 1386, - "src/main/runtime/claude-structured-failed-start-resume.test.ts": 1942, - "src/main/runtime/claude-structured-idle-releases-unknown.test.ts": 2200, - "src/main/runtime/claude-structured-resumed-start-failure.test.ts": 1787, - "src/main/runtime/claude-structured-retired-saved-model.test.ts": 1984, - "src/main/runtime/claude-structured-send-during-startup.test.ts": 2343, - "src/main/runtime/claude-structured-send-restart-dies-before-dispatch.test.ts": 2482, - "src/main/runtime/claude-structured-session-integration.test.ts": 3307, - "src/main/runtime/claude-structured-startup-fault-is-not-an-exit.test.ts": 1987, - "src/main/runtime/claude-structured-startup-unanswered-control-request.test.ts": 2142, - "src/main/runtime/claude-trust-dialog-transcript.test.ts": 3395, - "src/main/runtime/cli-terminal-create-host-session-binding.test.ts": 4725, - "src/main/runtime/client-hosted-browser-page-persistence.integration.test.ts": 4478, - "src/main/runtime/client-hosted-browser-page-persistence.test.ts": 369, - "src/main/runtime/client-hosted-browser-row-projection.test.ts": 54, - "src/main/runtime/client-hosted-browser-row-publication.test.ts": 77, - "src/main/runtime/client-hosted-browser-row-push.integration.test.ts": 4403, - "src/main/runtime/client-hosted-page-reconciliation-hold.integration.test.ts": 4758, - "src/main/runtime/client-hosted-page-reconciliation-window.test.ts": 44, - "src/main/runtime/client-session-tab-selection.test.ts": 68, - "src/main/runtime/cline-screen-readiness-transcripts.test.ts": 8000, - "src/main/runtime/codex-header-readiness-transcripts.test.ts": 5220, - "src/main/runtime/codex-quiet-ready-screen.test.ts": 8412, - "src/main/runtime/codex-startup-paste-transcripts.test.ts": 503, - "src/main/runtime/created-mobile-session-tab-caller-selection.test.ts": 4175, - "src/main/runtime/decorative-title-fact-emission.test.ts": 48, - "src/main/runtime/draft-paste-ready-scanner-chunking.test.ts": 1720, - "src/main/runtime/dsb-terminal-runtime.test.ts": 4441, - "src/main/runtime/dsh-readiness-transcript.test.ts": 70, - "src/main/runtime/e2ee-keypair.test.ts": 92, - "src/main/runtime/exit-provenance-audit.test.ts": 5831, - "src/main/runtime/expired-ssh-lease-pane-candidacy.test.ts": 6032, - "src/main/runtime/external-worktree-paired-client-discovery.integration.test.ts": 5078, - "src/main/runtime/fetch-remote-cache.test.ts": 4636, - "src/main/runtime/file-watcher-host.test.ts": 393, - "src/main/runtime/fit-override-integration.test.ts": 4532, - "src/main/runtime/floating-terminal-inventory-ownership.test.ts": 4508, - "src/main/runtime/folder-workspace-pty-identity.test.ts": 4443, - "src/main/runtime/graph-sync-deletion-fence.test.ts": 4769, - "src/main/runtime/graph-sync-live-daemon-pty-tab-preservation.test.ts": 4728, - "src/main/runtime/graph-sync-mobile-snapshot-gating.test.ts": 4118, - "src/main/runtime/graph-sync-payload-partition.test.ts": 4542, - "src/main/runtime/headless-close-keeps-publication-epoch.test.ts": 4883, - "src/main/runtime/headless-hydration-ownership.test.ts": 6383, - "src/main/runtime/headless-placeholder-graph-surface-standing.test.ts": 4474, - "src/main/runtime/headless-runtime-graph.test.ts": 4867, - "src/main/runtime/headless-seed-ownership.test.ts": 6450, - "src/main/runtime/headless-tab-group-selection.test.ts": 39, - "src/main/runtime/headless-tab-order-stability.test.ts": 5018, - "src/main/runtime/headless-terminal-dispose-write-ordering.test.ts": 4223, - "src/main/runtime/headless-terminal-query-reply-policy.test.ts": 55, - "src/main/runtime/headless-terminal-split-layout.test.ts": 62, - "src/main/runtime/hidden-output-restored-provider-snapshot.test.ts": 4953, - "src/main/runtime/host-descriptor.test.ts": 299, - "src/main/runtime/host-terminal-close-persistence-durability.test.ts": 1889, - "src/main/runtime/launch-agent-terminal-followup.test.ts": 4203, - "src/main/runtime/launched-agent-composer-readiness-claude.test.ts": 4003, - "src/main/runtime/launched-agent-composer-readiness.test.ts": 581, - "src/main/runtime/launched-agent-crash-guard.live-shell.test.ts": 209, - "src/main/runtime/launched-agent-exit-delivery.test.ts": 342, - "src/main/runtime/launched-agent-foreground.test.ts": 92, - "src/main/runtime/launched-agent-shell-handoff-readiness.test.ts": 101, - "src/main/runtime/launched-agent-write-guard.test.ts": 63, - "src/main/runtime/leaf-pty-verdict-expiry.test.ts": 5707, - "src/main/runtime/linear-save-issue.test.ts": 4330, - "src/main/runtime/managed-worktree-create-execution-host.test.ts": 4606, - "src/main/runtime/managed-worktree-create-telemetry.test.ts": 4411, - "src/main/runtime/missing-worktree-terminal-reconciliation.test.ts": 347, - "src/main/runtime/mobile-agent-status-permission-renewal.test.ts": 4833, + "src/main/pwsh.test.ts": 62, + "src/main/qoder/hook-service.test.ts": 166, + "src/main/qoder/real-cli.test.ts": 684, + "src/main/quit-path-durable-write-blocking.test.ts": 3083, + "src/main/quit-teardown-deadline.test.ts": 57, + "src/main/quit-teardown-start-gate.test.ts": 52, + "src/main/rate-limits/account-runtime-target-sync.test.ts": 407, + "src/main/rate-limits/antigravity-usage-error.test.ts": 43, + "src/main/rate-limits/antigravity-usage-fetcher.test.ts": 110, + "src/main/rate-limits/antigravity-usage-real-cli.test.ts": 92, + "src/main/rate-limits/antigravity-usage-response.test.ts": 50, + "src/main/rate-limits/auth-filesystem-operation-retention.test.ts": 119, + "src/main/rate-limits/auth-filesystem-operation.test.ts": 36, + "src/main/rate-limits/claude-managed-usage-credential-source.test.ts": 282, + "src/main/rate-limits/claude-oauth-usage-error.test.ts": 38, + "src/main/rate-limits/claude-oauth-usage-extra-usage.test.ts": 49, + "src/main/rate-limits/claude-profile-usage.test.ts": 307, + "src/main/rate-limits/claude-usage-error-classification.test.ts": 60, + "src/main/rate-limits/codex-auth-presence.test.ts": 62, + "src/main/rate-limits/codex-fetcher-auth-errors.test.ts": 129, + "src/main/rate-limits/codex-fetcher-backend.test.ts": 337, + "src/main/rate-limits/codex-fetcher-credit-balance.test.ts": 169, + "src/main/rate-limits/codex-fetcher-probe-shutdown.test.ts": 179, + "src/main/rate-limits/codex-fetcher-process-contract.test.ts": 315, + "src/main/rate-limits/codex-fetcher-rpc-exit-diagnostics.test.ts": 401, + "src/main/rate-limits/codex-fetcher-runtime-pairing.test.ts": 156, + "src/main/rate-limits/codex-fetcher-session-supplement.test.ts": 177, + "src/main/rate-limits/codex-fetcher.test.ts": 209, + "src/main/rate-limits/codex-probe-termination.test.ts": 77, + "src/main/rate-limits/codex-rate-limit-window-classification.test.ts": 43, + "src/main/rate-limits/codex-rpc-failure-kind.test.ts": 31, + "src/main/rate-limits/codex-rpc-rate-limit-probe.test.ts": 113, + "src/main/rate-limits/cursor-auth.test.ts": 194, + "src/main/rate-limits/cursor-fetcher.test.ts": 281, + "src/main/rate-limits/cursor-session-token.test.ts": 228, + "src/main/rate-limits/cursor-usage-mapping.test.ts": 252, + "src/main/rate-limits/gemini-bucket-formatting.test.ts": 39, + "src/main/rate-limits/gemini-usage-fetcher.fallback.test.ts": 102, + "src/main/rate-limits/gemini-usage-fetcher.test.ts": 70, + "src/main/rate-limits/grok-auth.test.ts": 96, + "src/main/rate-limits/grok-fetcher.test.ts": 88, + "src/main/rate-limits/grok-weekly-zero.test.ts": 144, + "src/main/rate-limits/initial-account-rate-limit-target.test.ts": 277, + "src/main/rate-limits/kimi-fetcher-wsl-home.test.ts": 112, + "src/main/rate-limits/kimi-fetcher.test.ts": 126, + "src/main/rate-limits/minimax/minimax-fetcher.test.ts": 141, + "src/main/rate-limits/minimax/minimax-request-context.test.ts": 107, + "src/main/rate-limits/opencode-go-api-key-source.test.ts": 477, + "src/main/rate-limits/opencode-go-managed-account-source.test.ts": 546, + "src/main/rate-limits/opencode-go-status-parsing.test.ts": 58, + "src/main/rate-limits/opencode-go-usage-api.test.ts": 56, + "src/main/rate-limits/opencode-go-usage-fetcher.test.ts": 155, + "src/main/rate-limits/opencode-go-usage-source-selection.test.ts": 68, + "src/main/rate-limits/service-account-target-selection.test.ts": 1005, + "src/main/rate-limits/service-antigravity-usage.test.ts": 306, + "src/main/rate-limits/service-cursor-usage.test.ts": 494, + "src/main/rate-limits/service-inactive-account-previews.test.ts": 323, + "src/main/rate-limits/service-inactive-claude-cooldown.test.ts": 592, + "src/main/rate-limits/service-live-claude-usage.test.ts": 351, + "src/main/rate-limits/service-minimax-usage.test.ts": 325, + "src/main/rate-limits/service-opencode-go-credentials.test.ts": 281, + "src/main/rate-limits/service-refresh-orchestration.test.ts": 341, + "src/main/rate-limits/service-window-activation.test.ts": 532, + "src/main/rate-limits/service-zcode-usage.test.ts": 570, + "src/main/rate-limits/usage-request-failure-kind.test.ts": 35, + "src/main/rate-limits/zcode-usage-fetcher.test.ts": 290, + "src/main/refused-tree-kill-root-termination.test.ts": 334, + "src/main/remote-layout-snapshot/remote-layout-snapshot-persistence.test.ts": 1277, + "src/main/remote-layout-snapshot/remote-layout-snapshot-recorder.test.ts": 1617, + "src/main/repo-backfill-wsl-move.test.ts": 1562, + "src/main/repo-git-remote-avatar-refresh.test.ts": 501, + "src/main/repo-git-remote-identity-enrichment.test.ts": 464, + "src/main/repo-git-remote-identity.test.ts": 519, + "src/main/repo-git-username-enrichment.test.ts": 49, + "src/main/repo-icon-autodetect.test.ts": 755, + "src/main/repo-icon-file-detection.test.ts": 552, + "src/main/repo-icon-source-href.test.ts": 104, + "src/main/repo-maintenance-idle-gate.test.ts": 170, + "src/main/repo-worktrees.test.ts": 564, + "src/main/ripgrep/bundled-ripgrep-path.test.ts": 91, + "src/main/ripgrep/bundled-ripgrep-real.test.ts": 196, + "src/main/ripgrep/bundled-ripgrep-spawn.test.ts": 61, + "src/main/ripgrep/bundled-ripgrep-stop.test.ts": 63, + "src/main/ripgrep/bundled-ripgrep-text-search.test.ts": 205, + "src/main/ripgrep/bundled-ripgrep-wsl-routing.test.ts": 73, + "src/main/ripgrep/ripgrep-filename-identity-real.test.ts": 234, + "src/main/ripgrep/synchronous-spawn-failure.test.ts": 64, + "src/main/ripgrep/text-search-unicode-real.test.ts": 218, + "src/main/ripgrep/wsl-missing-cwd-exit.test.ts": 71, + "src/main/runtime/acknowledged-terminal-tab-retirement.test.ts": 4261, + "src/main/runtime/agent-detection-host-resolution.test.ts": 5077, + "src/main/runtime/agent-launch-record-warmup-gate.test.ts": 35, + "src/main/runtime/agent-launch-scope-runtime.test.ts": 270, + "src/main/runtime/agent-launch-typed-line-shell.test.ts": 34, + "src/main/runtime/agent-prompt-line-settle.test.ts": 4905, + "src/main/runtime/agent-prompt-receipt-correlation.test.ts": 4665, + "src/main/runtime/agent-prompt-request-correlation.test.ts": 31, + "src/main/runtime/agent-prompt-submission-runtime-hook-and-generation.test.ts": 4319, + "src/main/runtime/agent-prompt-submission-runtime.test.ts": 5371, + "src/main/runtime/agent-prompt-submission-verification.test.ts": 120, + "src/main/runtime/agent-prompt-submission-windows-submit-delay.test.ts": 4179, + "src/main/runtime/agent-session-claim-identity.test.ts": 213, + "src/main/runtime/agent-session-conversation-clear-commit.test.ts": 726, + "src/main/runtime/agent-session-conversation-name-store.test.ts": 734, + "src/main/runtime/agent-session-death-evidence-persistence.test.ts": 902, + "src/main/runtime/agent-session-launch-directory.test.ts": 818, + "src/main/runtime/agent-session-launch-env-admission.test.ts": 621, + "src/main/runtime/agent-session-lease-renewal.test.ts": 790, + "src/main/runtime/agent-session-operation-admission.test.ts": 239, + "src/main/runtime/agent-session-permission-backfill.test.ts": 827, + "src/main/runtime/agent-session-process-identity-probe-windows-batch.test.ts": 65, + "src/main/runtime/agent-session-process-identity-probe.test.ts": 92, + "src/main/runtime/agent-session-provider-handle-transition.test.ts": 271, + "src/main/runtime/agent-session-record-conversation-name.test.ts": 270, + "src/main/runtime/agent-session-record-options.test.ts": 793, + "src/main/runtime/agent-session-record-store-slot.test.ts": 1914, + "src/main/runtime/agent-session-record-store-tab-table.test.ts": 792, + "src/main/runtime/agent-session-record-store.test.ts": 1144, + "src/main/runtime/agent-session-recovery-capsule.test.ts": 1153, + "src/main/runtime/agent-session-released-reservation-replay.test.ts": 906, + "src/main/runtime/agent-session-replaced-runtime.test.ts": 276, + "src/main/runtime/agent-session-reservation-admission.test.ts": 278, + "src/main/runtime/agent-session-runtime-attribution.test.ts": 979, + "src/main/runtime/agent-session-runtime-end-record.test.ts": 391, + "src/main/runtime/agent-session-spawn-token-process-scan.test.ts": 36, + "src/main/runtime/agent-session-spawn-token-readback.test.ts": 53, + "src/main/runtime/agent-session-store-transactions.test.ts": 860, + "src/main/runtime/agent-session-surface-release-transition.test.ts": 273, + "src/main/runtime/agent-session-unsaved-creation-supersession.test.ts": 745, + "src/main/runtime/agent-state-rules/agent-state-rule-regions.test.ts": 361, + "src/main/runtime/agent-state-rules/agent-state-rules-bundle.test.ts": 280, + "src/main/runtime/agent-state-rules/agent-state-rules-engine.test.ts": 344, + "src/main/runtime/agent-state-rules/agent-state-rules-live-update.test.ts": 544, + "src/main/runtime/agent-state-rules/agent-state-text-anchors.test.ts": 314, + "src/main/runtime/agent-terminal-launch-host.test.ts": 4836, + "src/main/runtime/agent-terminal-startup-prompt.test.ts": 4094, + "src/main/runtime/antigravity-readiness-transcripts.test.ts": 4952, + "src/main/runtime/antigravity-terminal-readiness.test.ts": 313, + "src/main/runtime/automatic-activation-host-sleep.test.ts": 5992, + "src/main/runtime/automation-change-publication.test.ts": 21921, + "src/main/runtime/browser-client-download-transfers.test.ts": 288, + "src/main/runtime/browser-dialog-settlement.test.ts": 38, + "src/main/runtime/browser-execution-host-key-resolution.test.ts": 4671, + "src/main/runtime/browser-host-capability-selection.test.ts": 242, + "src/main/runtime/browser-host-client-page-adoption-grants.test.ts": 43, + "src/main/runtime/browser-host-client-page-adoption.test.ts": 47, + "src/main/runtime/browser-host-client-page-creation.test.ts": 283, + "src/main/runtime/browser-host-command-ledger-capacity.test.ts": 247, + "src/main/runtime/browser-host-command-ledger.test.ts": 245, + "src/main/runtime/browser-host-file-channel-admission.test.ts": 202, + "src/main/runtime/browser-host-lease-download-transfer-cleanup.test.ts": 331, + "src/main/runtime/browser-host-lease-fenced-page-release.test.ts": 234, + "src/main/runtime/browser-host-lease-placement-retirement.test.ts": 270, + "src/main/runtime/browser-host-lease-registry.test.ts": 284, + "src/main/runtime/browser-host-page-placement-replacement.test.ts": 254, + "src/main/runtime/browser-host-page-placement.test.ts": 267, + "src/main/runtime/browser-host-page-reconciliation-executor.test.ts": 46, + "src/main/runtime/browser-host-page-reconciliation-orchestration.test.ts": 557, + "src/main/runtime/browser-host-page-reconciliation-plan.test.ts": 57, + "src/main/runtime/browser-host-page-reconciliation-preconditions.test.ts": 240, + "src/main/runtime/browser-host-page-retirement.test.ts": 36, + "src/main/runtime/browser-network-tunnel-paired-runtime.integration.test.ts": 6195, + "src/main/runtime/browser-screencast-driver-attribution.test.ts": 4732, + "src/main/runtime/browser-screencast-driver-scope.test.ts": 38, + "src/main/runtime/browser-screencast-ghost-subscriber-eviction.test.ts": 40, + "src/main/runtime/browser-screencast-remote-viewer-retention.test.ts": 5842, + "src/main/runtime/browser-screencast-renderer-throttle-lease.test.ts": 5631, + "src/main/runtime/browser-screencast-subscription-id.test.ts": 45, + "src/main/runtime/browser-session-tab-selection-snapshot.test.ts": 34, + "src/main/runtime/browser-tab-create-caller-navigation.test.ts": 4715, + "src/main/runtime/browser-tab-create-folder-workspace.test.ts": 496, + "src/main/runtime/browser-tab-create-publication.test.ts": 476, + "src/main/runtime/browser/browser-screencasts.test.ts": 459, + "src/main/runtime/claude-agent-teams-pty-exit-leak.test.ts": 4455, + "src/main/runtime/claude-agent-teams-service.test.ts": 81, + "src/main/runtime/claude-agent-teams-shim-env.test.ts": 68, + "src/main/runtime/claude-structured-exit-mid-response.test.ts": 1908, + "src/main/runtime/claude-structured-failed-start-resume.test.ts": 2049, + "src/main/runtime/claude-structured-idle-releases-unknown.test.ts": 2253, + "src/main/runtime/claude-structured-resumed-start-failure.test.ts": 2253, + "src/main/runtime/claude-structured-retired-saved-model.test.ts": 1947, + "src/main/runtime/claude-structured-send-during-startup.test.ts": 2303, + "src/main/runtime/claude-structured-send-restart-dies-before-dispatch.test.ts": 2722, + "src/main/runtime/claude-structured-session-integration.test.ts": 4018, + "src/main/runtime/claude-structured-startup-fault-is-not-an-exit.test.ts": 2124, + "src/main/runtime/claude-structured-startup-unanswered-control-request.test.ts": 2119, + "src/main/runtime/claude-trust-dialog-transcript.test.ts": 4432, + "src/main/runtime/cli-terminal-create-host-session-binding.test.ts": 4504, + "src/main/runtime/client-hosted-browser-page-persistence.integration.test.ts": 4697, + "src/main/runtime/client-hosted-browser-page-persistence.test.ts": 311, + "src/main/runtime/client-hosted-browser-row-projection.test.ts": 32, + "src/main/runtime/client-hosted-browser-row-publication.test.ts": 55, + "src/main/runtime/client-hosted-browser-row-push.integration.test.ts": 4420, + "src/main/runtime/client-hosted-page-reconciliation-hold.integration.test.ts": 4121, + "src/main/runtime/client-hosted-page-reconciliation-window.test.ts": 39, + "src/main/runtime/client-session-tab-selection.test.ts": 41, + "src/main/runtime/cline-screen-readiness-transcripts.test.ts": 6415, + "src/main/runtime/codex-capacity-terminal-status.test.ts": 3982, + "src/main/runtime/codex-header-readiness-transcripts.test.ts": 6041, + "src/main/runtime/codex-quiet-ready-screen.test.ts": 8507, + "src/main/runtime/codex-startup-paste-transcripts.test.ts": 535, + "src/main/runtime/created-mobile-session-tab-caller-selection.test.ts": 5880, + "src/main/runtime/decorative-title-fact-emission.test.ts": 33, + "src/main/runtime/delegated-mobile-device.integration.test.ts": 5919, + "src/main/runtime/device-key-fork.test.ts": 87, + "src/main/runtime/device-principal.test.ts": 3240, + "src/main/runtime/draft-paste-ready-scanner-chunking.test.ts": 1537, + "src/main/runtime/dsb-terminal-runtime.test.ts": 4611, + "src/main/runtime/dsh-readiness-transcript.test.ts": 74, + "src/main/runtime/e2ee-keypair.test.ts": 71, + "src/main/runtime/ephemeral-device-rows.test.ts": 81, + "src/main/runtime/exit-provenance-audit.test.ts": 6026, + "src/main/runtime/expired-ssh-lease-pane-candidacy.test.ts": 9671, + "src/main/runtime/external-worktree-paired-client-discovery.integration.test.ts": 5250, + "src/main/runtime/fetch-remote-cache.test.ts": 4747, + "src/main/runtime/file-watcher-host.test.ts": 460, + "src/main/runtime/files/search/file-search.test.ts": 471, + "src/main/runtime/files/search/mobile-path-search-expiry.test.ts": 236, + "src/main/runtime/files/search/mobile-path-search.test.ts": 46, + "src/main/runtime/fit-override-integration.test.ts": 4650, + "src/main/runtime/floating-terminal-inventory-ownership.test.ts": 5150, + "src/main/runtime/folder-workspace-pty-identity.test.ts": 4471, + "src/main/runtime/graph-sync-deletion-fence.test.ts": 4351, + "src/main/runtime/graph-sync-live-daemon-pty-tab-preservation.test.ts": 4439, + "src/main/runtime/graph-sync-mobile-snapshot-gating.test.ts": 4458, + "src/main/runtime/graph-sync-payload-partition.test.ts": 4480, + "src/main/runtime/headless-close-keeps-publication-epoch.test.ts": 4509, + "src/main/runtime/headless-hydration-ownership.test.ts": 8048, + "src/main/runtime/headless-placeholder-graph-surface-standing.test.ts": 4175, + "src/main/runtime/headless-runtime-graph.test.ts": 4519, + "src/main/runtime/headless-seed-ownership.test.ts": 6549, + "src/main/runtime/headless-tab-group-selection.test.ts": 26, + "src/main/runtime/headless-tab-order-stability.test.ts": 4260, + "src/main/runtime/headless-terminal-dispose-write-ordering.test.ts": 4631, + "src/main/runtime/headless-terminal-query-reply-policy.test.ts": 36, + "src/main/runtime/headless-terminal-split-layout.test.ts": 32, + "src/main/runtime/hidden-output-restored-provider-snapshot.test.ts": 5446, + "src/main/runtime/host-account-launch-env.test.ts": 4485, + "src/main/runtime/host-descriptor.test.ts": 296, + "src/main/runtime/host-terminal-close-persistence-durability.test.ts": 1608, + "src/main/runtime/launch-agent-terminal-followup.test.ts": 3644, + "src/main/runtime/launched-agent-composer-readiness-claude.test.ts": 4535, + "src/main/runtime/launched-agent-composer-readiness.test.ts": 671, + "src/main/runtime/launched-agent-crash-guard.live-shell.test.ts": 203, + "src/main/runtime/launched-agent-exit-delivery.test.ts": 386, + "src/main/runtime/launched-agent-foreground.test.ts": 112, + "src/main/runtime/launched-agent-shell-handoff-readiness.test.ts": 133, + "src/main/runtime/launched-agent-write-guard.test.ts": 51, + "src/main/runtime/leaf-pty-verdict-expiry.test.ts": 4521, + "src/main/runtime/linear-save-issue.test.ts": 4300, + "src/main/runtime/managed-worktree-create-execution-host.test.ts": 4345, + "src/main/runtime/managed-worktree-create-telemetry.test.ts": 4498, + "src/main/runtime/missing-worktree-terminal-reconciliation.test.ts": 379, + "src/main/runtime/mobile-agent-status-permission-renewal.test.ts": 4057, + "src/main/runtime/mobile-desktop-relay/hosted-browser-page-route.test.ts": 831, + "src/main/runtime/mobile-desktop-relay/mobile-desktop-relay.integration.test.ts": 7777, + "src/main/runtime/mobile-desktop-relay/mobile-desktop-relay.test.ts": 1233, + "src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.integration.test.ts": 4800, + "src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.test.ts": 251, + "src/main/runtime/mobile-desktop-relay/relayed-phone-frames.test.ts": 241, + "src/main/runtime/mobile-desktop-relay/relayed-stream-carriers.test.ts": 3047, "src/main/runtime/mobile-notification-dismissal-read-failure.test.ts": 268, - "src/main/runtime/mobile-notification-dismissal-store.test.ts": 293, - "src/main/runtime/mobile-notification-replay.test.ts": 52, - "src/main/runtime/mobile-pairing-qr.test.ts": 1059, - "src/main/runtime/mobile-pairing-userdata-path.test.ts": 2373, - "src/main/runtime/mobile-presence-lock.test.ts": 16927, - "src/main/runtime/mobile-rpc-allowlist.test.ts": 3585, - "src/main/runtime/mobile-session-tab-close-refusal-message.test.ts": 46, - "src/main/runtime/mobile-session-tabs-agent-status-heartbeat.test.ts": 65, - "src/main/runtime/mobile-session-tabs-churn-coalescing.test.ts": 4509, - "src/main/runtime/mobile-session-tabs-notify-coalescer.test.ts": 67, - "src/main/runtime/mobile-session-terminal-persistence-retirement.test.ts": 331, - "src/main/runtime/mobile-session-terminal-retirement-proof.test.ts": 52, - "src/main/runtime/mobile-session-terminal-retirement.test.ts": 253, - "src/main/runtime/mobile-store-orphaned-temp-cleanup.test.ts": 381, - "src/main/runtime/mobile-subscribe-idle-pane-hydrate.test.ts": 5383, - "src/main/runtime/mobile-subscribe-integration.test.ts": 18007, - "src/main/runtime/multi-client-navigation-isolation.integration.test.ts": 7135, - "src/main/runtime/native-chat-visuals-workspace-verdict.test.ts": 323, - "src/main/runtime/omp-readiness-transcripts.test.ts": 8110, - "src/main/runtime/opencode-agent-row-enter-transcript.test.ts": 559, - "src/main/runtime/opencode-agent-row-structure-transcript.test.ts": 333, - "src/main/runtime/opencode-finished-session-authority.test.ts": 5338, - "src/main/runtime/opencode-model-worktree-create.test.ts": 964, - "src/main/runtime/opencode-run-lifetime-runtime.test.ts": 4511, - "src/main/runtime/opencode-run-lifetime-status.test.ts": 131, - "src/main/runtime/orca-runtime-agent-session-operation.test.ts": 4398, - "src/main/runtime/orca-runtime-agent-skill-share.test.ts": 5202, - "src/main/runtime/orca-runtime-automations.test.ts": 4377, - "src/main/runtime/orca-runtime-browser-client-hosted.test.ts": 573, - "src/main/runtime/orca-runtime-browser-ghost-session-row-close.test.ts": 519, - "src/main/runtime/orca-runtime-browser-headless.test.ts": 448, - "src/main/runtime/orca-runtime-browser-screencast-fanout.test.ts": 543, - "src/main/runtime/orca-runtime-browser.test.ts": 656, - "src/main/runtime/orca-runtime-cold-restore-terminal-membership.test.ts": 4342, - "src/main/runtime/orca-runtime-create-base-prefetch.test.ts": 4314, - "src/main/runtime/orca-runtime-default-tab-agent-status.test.ts": 4293, - "src/main/runtime/orca-runtime-emulator-folder-workspace.test.ts": 4356, - "src/main/runtime/orca-runtime-exact-worker-provider-session.test.ts": 2713, - "src/main/runtime/orca-runtime-files-mobile-explorer-reads.test.ts": 549, - "src/main/runtime/orca-runtime-files-preview-budget.test.ts": 634, - "src/main/runtime/orca-runtime-files-rename-authority.test.ts": 542, - "src/main/runtime/orca-runtime-files-search.test.ts": 622, - "src/main/runtime/orca-runtime-files-ssh-chunk-reads.test.ts": 636, - "src/main/runtime/orca-runtime-files-terminal-artifact-grants.test.ts": 663, - "src/main/runtime/orca-runtime-files-terminal-artifact-io.test.ts": 635, - "src/main/runtime/orca-runtime-files-terminal-artifact-swap-detection.test.ts": 617, - "src/main/runtime/orca-runtime-files-terminal-link-host-translation.test.ts": 590, - "src/main/runtime/orca-runtime-files-terminal-path-resolution.test.ts": 674, - "src/main/runtime/orca-runtime-files-watch-host-scope.test.ts": 466, - "src/main/runtime/orca-runtime-files-watch.test.ts": 533, - "src/main/runtime/orca-runtime-git-branch-diff.test.ts": 1006, - "src/main/runtime/orca-runtime-git-diff-budget.test.ts": 952, - "src/main/runtime/orca-runtime-git.test.ts": 1054, - "src/main/runtime/orca-runtime-headless-hydration-repo-gate.test.ts": 4653, - "src/main/runtime/orca-runtime-headless-terminal-close.test.ts": 4240, - "src/main/runtime/orca-runtime-hook-agent-status-projection.test.ts": 4572, - "src/main/runtime/orca-runtime-linked-issue-live-meta.integration.test.ts": 4470, - "src/main/runtime/orca-runtime-mobile-agent-status-title-truthfulness.test.ts": 4519, - "src/main/runtime/orca-runtime-mobile-close-preserved-resurrection.test.ts": 4450, - "src/main/runtime/orca-runtime-on-pty-data-redraw.test.ts": 4103, - "src/main/runtime/orca-runtime-path-candidate-history.test.ts": 64, - "src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts": 5125, - "src/main/runtime/orca-runtime-provider-reattach-launch-identity.test.ts": 4630, - "src/main/runtime/orca-runtime-quick-open-capabilities.test.ts": 618, - "src/main/runtime/orca-runtime-skill-recovery.test.ts": 4922, - "src/main/runtime/orca-runtime-startup-draft-launch-parity.test.ts": 5171, - "src/main/runtime/orca-runtime-structured-agent-registration-seam.test.ts": 4301, - "src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts": 4720, - "src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts": 4032, - "src/main/runtime/orca-runtime-structured-session-restore.test.ts": 4748, - "src/main/runtime/orca-runtime-structured-status-sink-wiring.test.ts": 4449, - "src/main/runtime/orca-runtime-tab-id-collision.test.ts": 4395, - "src/main/runtime/orca-runtime-tail-wait-memo.test.ts": 303, - "src/main/runtime/orca-runtime-terminal-close-agent-launch.test.ts": 3986, - "src/main/runtime/orca-runtime-terminal-close-continuity.test.ts": 4919, - "src/main/runtime/orca-runtime-terminal-close-no-widening.test.ts": 4813, - "src/main/runtime/orca-runtime-terminal-close-records.test.ts": 6684, - "src/main/runtime/orca-runtime-terminal-close-spill-partition.test.ts": 4512, - "src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts": 4787, - "src/main/runtime/orca-runtime-terminal-cwd.test.ts": 4884, - "src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts": 5490, - "src/main/runtime/orca-runtime-terminal-rename-retention.test.ts": 6075, - "src/main/runtime/orca-runtime-terminal-retirement-host-partition.test.ts": 4772, - "src/main/runtime/orca-runtime-terminal-retirement.test.ts": 4562, - "src/main/runtime/orca-runtime-terminal-split-authority.test.ts": 4555, - "src/main/runtime/orca-runtime-terminal-surface-close.test.ts": 5472, - "src/main/runtime/orca-runtime-workspace-ports-host.test.ts": 3791, - "src/main/runtime/orca-runtime-worktree-change-invalidation.test.ts": 4736, - "src/main/runtime/orca-runtime-worktree-provisioning-availability.test.ts": 5541, - "src/main/runtime/orca-runtime.test.ts": 36218, - "src/main/runtime/orcad-migration-catalog-import.test.ts": 45, - "src/main/runtime/orchestration-cli-subprocess.test.ts": 5861, - "src/main/runtime/orchestration-codex-completion-title.test.ts": 5337, - "src/main/runtime/orchestration-codex-real-pty.integration.test.ts": 6330, - "src/main/runtime/orchestration-compatibility-authority.test.ts": 4711, - "src/main/runtime/orchestration-configured-agent-target.test.ts": 4510, - "src/main/runtime/orchestration-dispatch-mailbox-delivery.test.ts": 6140, - "src/main/runtime/orchestration-live-agent-title-restore.test.ts": 5202, - "src/main/runtime/orchestration-mailbox-cold-park-idle.test.ts": 5151, - "src/main/runtime/orchestration-mailbox-crash-recovery.test.ts": 5866, - "src/main/runtime/orchestration-mailbox-detached-routing.test.ts": 5444, - "src/main/runtime/orchestration-mailbox-filtered-waiters.test.ts": 6051, - "src/main/runtime/orchestration-mailbox-notification-consistency.test.ts": 19774, - "src/main/runtime/orchestration-mailbox-pointer-cli-command.test.ts": 5540, - "src/main/runtime/orchestration-mailbox-routing-races.test.ts": 8786, - "src/main/runtime/orchestration-mailbox-transport-settlement.test.ts": 5363, - "src/main/runtime/orchestration-message-delivery-identity.test.ts": 6283, - "src/main/runtime/orchestration-messages-fake-parity.test.ts": 619, - "src/main/runtime/orchestration-opencode-model-host.test.ts": 4210, - "src/main/runtime/orchestration-worker-workspace-resolution.test.ts": 4914, - "src/main/runtime/orchestration/agent-facing-parity.test.ts": 866, - "src/main/runtime/orchestration/chat-assignee.test.ts": 5175, - "src/main/runtime/orchestration/cli-command.test.ts": 55, - "src/main/runtime/orchestration/configured-worker-agent-selector.test.ts": 73, - "src/main/runtime/orchestration/coordinator-decision-gates.test.ts": 589, - "src/main/runtime/orchestration/coordinator-dispatch-unobserved-prompt.test.ts": 614, - "src/main/runtime/orchestration/coordinator-drift-probe-coalescing.test.ts": 652, - "src/main/runtime/orchestration/coordinator-escalation-triage.test.ts": 533, - "src/main/runtime/orchestration/coordinator-stale-base-flag.test.ts": 41, - "src/main/runtime/orchestration/coordinator-terminal-census-unavailable.test.ts": 590, - "src/main/runtime/orchestration/coordinator.test.ts": 961, - "src/main/runtime/orchestration/db-empty-dispatch-shortcircuit.benchmark.test.ts": 609, - "src/main/runtime/orchestration/db-heartbeat-straggler-guard.test.ts": 599, - "src/main/runtime/orchestration/db-message-timestamp.test.ts": 562, - "src/main/runtime/orchestration/db-messages.test.ts": 551, - "src/main/runtime/orchestration/db-stopping-worker-task-guard.test.ts": 640, - "src/main/runtime/orchestration/db-task-create-readiness.test.ts": 786, - "src/main/runtime/orchestration/db-task-dispatch-invariant.test.ts": 1161, - "src/main/runtime/orchestration/db-task-dispatch-lifecycle-guards.test.ts": 1315, - "src/main/runtime/orchestration/db-task-dispatch-races.test.ts": 762, - "src/main/runtime/orchestration/db-task-promotion-projection.test.ts": 662, - "src/main/runtime/orchestration/db-undelivered-mailboxes.test.ts": 573, - "src/main/runtime/orchestration/db.test.ts": 1313, - "src/main/runtime/orchestration/db/attempt-outcome-projection.test.ts": 673, - "src/main/runtime/orchestration/db/decision-gate-lifecycle.test.ts": 527, - "src/main/runtime/orchestration/db/dispatch-depth.test.ts": 834, - "src/main/runtime/orchestration/db/dispatch-mailbox-consumer-fencing.test.ts": 627, - "src/main/runtime/orchestration/db/federated-worker-report-outcome.test.ts": 48, - "src/main/runtime/orchestration/db/federation/federated-dispatch-observation-fence.test.ts": 560, - "src/main/runtime/orchestration/db/federation/remote-dispatch-attachment-release.test.ts": 633, - "src/main/runtime/orchestration/db/hot-path-statement-compilation.test.ts": 597, - "src/main/runtime/orchestration/db/legacy-question-identity.test.ts": 52, - "src/main/runtime/orchestration/db/lifecycle-rejection-marker.test.ts": 51, - "src/main/runtime/orchestration/db/lifecycle-transition.test.ts": 572, - "src/main/runtime/orchestration/db/messages/mailbox-consumer-lifecycle-fencing.test.ts": 809, - "src/main/runtime/orchestration/db/pane-key-match.test.ts": 51, - "src/main/runtime/orchestration/db/remaining-dispatch-statement-compilation.test.ts": 1139, - "src/main/runtime/orchestration/db/row-column-lists.test.ts": 554, - "src/main/runtime/orchestration/db/run-list-cursor.test.ts": 46, - "src/main/runtime/orchestration/db/runs/run-coordinator-orca-session-binding.test.ts": 720, - "src/main/runtime/orchestration/db/schema/derived-delivery-migration.test.ts": 810, - "src/main/runtime/orchestration/db/schema/federated-home-run-migration.test.ts": 581, - "src/main/runtime/orchestration/db/schema/structured-pointer-schema-migration.test.ts": 620, - "src/main/runtime/orchestration/db/schema/structured-worker-orca-session-backfill.test.ts": 630, - "src/main/runtime/orchestration/db/worker-dispatch/worker-dispatch-abandon.test.ts": 627, - "src/main/runtime/orchestration/db/worker-dispatch/worker-dispatch-assignee-orca-session.test.ts": 575, - "src/main/runtime/orchestration/db/writer-run-required.test.ts": 659, - "src/main/runtime/orchestration/dispatch-consumer-generation-migration.test.ts": 720, - "src/main/runtime/orchestration/dispatch-creator-identity-migration.test.ts": 649, - "src/main/runtime/orchestration/dispatch-failure-idempotency.test.ts": 693, - "src/main/runtime/orchestration/federation-ack-checkpoints.test.ts": 38, - "src/main/runtime/orchestration/federation-acknowledgment-integrity.test.ts": 561, - "src/main/runtime/orchestration/federation-acknowledgment-migration.test.ts": 684, - "src/main/runtime/orchestration/federation-lifecycle-settlement.test.ts": 39, - "src/main/runtime/orchestration/federation-sync.test.ts": 5859, - "src/main/runtime/orchestration/federation-terminal-recovery.test.ts": 4138, - "src/main/runtime/orchestration/formatter.test.ts": 75, - "src/main/runtime/orchestration/groups.test.ts": 61, - "src/main/runtime/orchestration/lifecycle-caller-edges.test.ts": 653, - "src/main/runtime/orchestration/lifecycle-reconciliation.test.ts": 799, - "src/main/runtime/orchestration/lightweight-run-worker-exit-escalation.test.ts": 5376, - "src/main/runtime/orchestration/mailbox-pointer-eligibility.test.ts": 666, - "src/main/runtime/orchestration/mailbox-pointer-release-query.test.ts": 591, - "src/main/runtime/orchestration/mailbox-pointer-stage.test.ts": 691, - "src/main/runtime/orchestration/mailbox-pointer-submit.test.ts": 981, - "src/main/runtime/orchestration/message-batch-atomicity.test.ts": 641, - "src/main/runtime/orchestration/mutation-receipt-capacity.test.ts": 638, - "src/main/runtime/orchestration/nested-worker-depth-migration.test.ts": 676, - "src/main/runtime/orchestration/orchestration-adopted-run-binding.test.ts": 995, - "src/main/runtime/orchestration/orchestration-all-start-versions-migration.test.ts": 1990, - "src/main/runtime/orchestration/orchestration-creator-authority-performance.test.ts": 944, - "src/main/runtime/orchestration/orchestration-db-permissions.test.ts": 573, - "src/main/runtime/orchestration/orchestration-db-retention-pagination.test.ts": 776, - "src/main/runtime/orchestration/orchestration-delivery-consumption.test.ts": 643, + "src/main/runtime/mobile-notification-dismissal-store.test.ts": 275, + "src/main/runtime/mobile-notification-replay.test.ts": 42, + "src/main/runtime/mobile-pairing-qr.test.ts": 1063, + "src/main/runtime/mobile-pairing-userdata-path.test.ts": 2695, + "src/main/runtime/mobile-presence-lock.test.ts": 17896, + "src/main/runtime/mobile-rpc-allowlist.test.ts": 3443, + "src/main/runtime/mobile-session-tab-close-refusal-message.test.ts": 30, + "src/main/runtime/mobile-session-tabs-agent-status-heartbeat.test.ts": 59, + "src/main/runtime/mobile-session-tabs-churn-coalescing.test.ts": 4066, + "src/main/runtime/mobile-session-tabs-notify-coalescer.test.ts": 36, + "src/main/runtime/mobile-session-terminal-persistence-retirement.test.ts": 233, + "src/main/runtime/mobile-session-terminal-retirement-proof.test.ts": 34, + "src/main/runtime/mobile-session-terminal-retirement.test.ts": 300, + "src/main/runtime/mobile-store-orphaned-temp-cleanup.test.ts": 453, + "src/main/runtime/mobile-subscribe-idle-pane-hydrate.test.ts": 5902, + "src/main/runtime/mobile-subscribe-integration.test.ts": 22684, + "src/main/runtime/multi-client-navigation-isolation.integration.test.ts": 5282, + "src/main/runtime/native-chat-visuals-workspace-verdict.test.ts": 326, + "src/main/runtime/omp-readiness-transcripts.test.ts": 7797, + "src/main/runtime/opencode-agent-row-enter-transcript.test.ts": 562, + "src/main/runtime/opencode-agent-row-structure-transcript.test.ts": 347, + "src/main/runtime/opencode-finished-session-authority.test.ts": 5180, + "src/main/runtime/opencode-run-lifetime-runtime.test.ts": 5346, + "src/main/runtime/opencode-run-lifetime-status.test.ts": 143, + "src/main/runtime/opencode-wsl-folder-launch-host.test.ts": 4927, + "src/main/runtime/orca-runtime-agent-session-account-env.test.ts": 3829, + "src/main/runtime/orca-runtime-agent-session-operation.test.ts": 5689, + "src/main/runtime/orca-runtime-agent-skill-share.test.ts": 3916, + "src/main/runtime/orca-runtime-automations.test.ts": 5397, + "src/main/runtime/orca-runtime-browser-client-hosted.test.ts": 555, + "src/main/runtime/orca-runtime-browser-ghost-session-row-close.test.ts": 514, + "src/main/runtime/orca-runtime-browser-headless.test.ts": 530, + "src/main/runtime/orca-runtime-browser-screencast-e2ee-backlog.test.ts": 523, + "src/main/runtime/orca-runtime-browser-screencast-fanout.test.ts": 448, + "src/main/runtime/orca-runtime-browser-screencast-pending-frame-retry.test.ts": 518, + "src/main/runtime/orca-runtime-browser.test.ts": 551, + "src/main/runtime/orca-runtime-cold-restore-terminal-membership.test.ts": 5056, + "src/main/runtime/orca-runtime-create-base-prefetch.test.ts": 4973, + "src/main/runtime/orca-runtime-default-tab-agent-status.test.ts": 3898, + "src/main/runtime/orca-runtime-emulator-folder-workspace.test.ts": 4520, + "src/main/runtime/orca-runtime-exact-worker-provider-session.test.ts": 3927, + "src/main/runtime/orca-runtime-files-artifact-facade.test.ts": 4613, + "src/main/runtime/orca-runtime-files-chunk-reads.test.ts": 508, + "src/main/runtime/orca-runtime-files-mobile-explorer-reads.test.ts": 670, + "src/main/runtime/orca-runtime-files-preview-budget.test.ts": 950, + "src/main/runtime/orca-runtime-files-rename-authority.test.ts": 646, + "src/main/runtime/orca-runtime-files-search.test.ts": 749, + "src/main/runtime/orca-runtime-files-terminal-artifact-grants.test.ts": 747, + "src/main/runtime/orca-runtime-files-terminal-artifact-io.test.ts": 762, + "src/main/runtime/orca-runtime-files-terminal-artifact-swap-detection.test.ts": 725, + "src/main/runtime/orca-runtime-files-terminal-link-host-translation.test.ts": 628, + "src/main/runtime/orca-runtime-files-terminal-path-resolution.test.ts": 642, + "src/main/runtime/orca-runtime-files-watch-host-scope.test.ts": 673, + "src/main/runtime/orca-runtime-files-watch.test.ts": 765, + "src/main/runtime/orca-runtime-git-branch-diff.test.ts": 988, + "src/main/runtime/orca-runtime-git-diff-budget.test.ts": 1013, + "src/main/runtime/orca-runtime-git.test.ts": 1002, + "src/main/runtime/orca-runtime-headless-hydration-repo-gate.test.ts": 4466, + "src/main/runtime/orca-runtime-headless-terminal-close.test.ts": 4999, + "src/main/runtime/orca-runtime-hook-agent-status-projection.test.ts": 4679, + "src/main/runtime/orca-runtime-linked-issue-live-meta.integration.test.ts": 4413, + "src/main/runtime/orca-runtime-mobile-agent-status-title-truthfulness.test.ts": 4243, + "src/main/runtime/orca-runtime-mobile-close-preserved-resurrection.test.ts": 4606, + "src/main/runtime/orca-runtime-move-headless-mobile-session-tab.test.ts": 4208, + "src/main/runtime/orca-runtime-on-pty-data-redraw.test.ts": 4052, + "src/main/runtime/orca-runtime-path-candidate-history.test.ts": 42, + "src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts": 4495, + "src/main/runtime/orca-runtime-provider-reattach-launch-identity.test.ts": 4550, + "src/main/runtime/orca-runtime-quick-open-capabilities.test.ts": 631, + "src/main/runtime/orca-runtime-serve-browser-restore.test.ts": 5565, + "src/main/runtime/orca-runtime-skill-recovery.test.ts": 4382, + "src/main/runtime/orca-runtime-startup-draft-launch-parity.test.ts": 5622, + "src/main/runtime/orca-runtime-structured-agent-registration-seam.test.ts": 4257, + "src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts": 4533, + "src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts": 3753, + "src/main/runtime/orca-runtime-structured-session-restore.test.ts": 4449, + "src/main/runtime/orca-runtime-structured-status-sink-wiring.test.ts": 4891, + "src/main/runtime/orca-runtime-tab-id-collision.test.ts": 4432, + "src/main/runtime/orca-runtime-tail-wait-memo.test.ts": 272, + "src/main/runtime/orca-runtime-terminal-close-agent-launch.test.ts": 4325, + "src/main/runtime/orca-runtime-terminal-close-continuity.test.ts": 4477, + "src/main/runtime/orca-runtime-terminal-close-no-widening.test.ts": 4931, + "src/main/runtime/orca-runtime-terminal-close-records.test.ts": 7567, + "src/main/runtime/orca-runtime-terminal-close-spill-partition.test.ts": 4231, + "src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts": 4450, + "src/main/runtime/orca-runtime-terminal-cwd.test.ts": 4526, + "src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts": 5738, + "src/main/runtime/orca-runtime-terminal-rename-retention.test.ts": 6378, + "src/main/runtime/orca-runtime-terminal-retirement-host-partition.test.ts": 4778, + "src/main/runtime/orca-runtime-terminal-retirement.test.ts": 4644, + "src/main/runtime/orca-runtime-terminal-split-authority.test.ts": 4668, + "src/main/runtime/orca-runtime-terminal-surface-close.test.ts": 6591, + "src/main/runtime/orca-runtime-terminal-title-saved-layout.test.ts": 5455, + "src/main/runtime/orca-runtime-workspace-ports-host.test.ts": 4496, + "src/main/runtime/orca-runtime-worktree-change-invalidation.test.ts": 4183, + "src/main/runtime/orca-runtime-worktree-provisioning-availability.test.ts": 5891, + "src/main/runtime/orca-runtime.test.ts": 37405, + "src/main/runtime/orcad-migration-catalog-import.test.ts": 28, + "src/main/runtime/orchestration-cli-subprocess.test.ts": 6191, + "src/main/runtime/orchestration-codex-completion-title.test.ts": 5949, + "src/main/runtime/orchestration-codex-real-pty.integration.test.ts": 4837, + "src/main/runtime/orchestration-compatibility-authority.test.ts": 4511, + "src/main/runtime/orchestration-configured-agent-target.test.ts": 4312, + "src/main/runtime/orchestration-dispatch-mailbox-delivery.test.ts": 5037, + "src/main/runtime/orchestration-live-agent-title-restore.test.ts": 5381, + "src/main/runtime/orchestration-mailbox-cold-park-idle.test.ts": 5009, + "src/main/runtime/orchestration-mailbox-crash-recovery.test.ts": 6728, + "src/main/runtime/orchestration-mailbox-detached-routing.test.ts": 6903, + "src/main/runtime/orchestration-mailbox-filtered-waiters.test.ts": 5482, + "src/main/runtime/orchestration-mailbox-notification-consistency.test.ts": 19969, + "src/main/runtime/orchestration-mailbox-pointer-cli-command.test.ts": 5744, + "src/main/runtime/orchestration-mailbox-routing-races.test.ts": 5632, + "src/main/runtime/orchestration-mailbox-transport-settlement.test.ts": 5651, + "src/main/runtime/orchestration-message-delivery-identity.test.ts": 9401, + "src/main/runtime/orchestration-messages-fake-parity.test.ts": 631, + "src/main/runtime/orchestration-opencode-model-host.test.ts": 4141, + "src/main/runtime/orchestration-worker-workspace-resolution.test.ts": 4454, + "src/main/runtime/orchestration/agent-facing-parity.test.ts": 1063, + "src/main/runtime/orchestration/chat-assignee.test.ts": 5367, + "src/main/runtime/orchestration/cli-command.test.ts": 37, + "src/main/runtime/orchestration/configured-worker-agent-selector.test.ts": 54, + "src/main/runtime/orchestration/coordinator-decision-gates.test.ts": 577, + "src/main/runtime/orchestration/coordinator-dispatch-unobserved-prompt.test.ts": 536, + "src/main/runtime/orchestration/coordinator-drift-probe-coalescing.test.ts": 662, + "src/main/runtime/orchestration/coordinator-escalation-triage.test.ts": 544, + "src/main/runtime/orchestration/coordinator-stale-base-flag.test.ts": 44, + "src/main/runtime/orchestration/coordinator-terminal-census-unavailable.test.ts": 528, + "src/main/runtime/orchestration/coordinator.test.ts": 900, + "src/main/runtime/orchestration/db-empty-dispatch-shortcircuit.benchmark.test.ts": 573, + "src/main/runtime/orchestration/db-heartbeat-straggler-guard.test.ts": 635, + "src/main/runtime/orchestration/db-message-timestamp.test.ts": 559, + "src/main/runtime/orchestration/db-messages.test.ts": 655, + "src/main/runtime/orchestration/db-stopping-worker-task-guard.test.ts": 626, + "src/main/runtime/orchestration/db-task-create-readiness.test.ts": 699, + "src/main/runtime/orchestration/db-task-dispatch-invariant.test.ts": 1058, + "src/main/runtime/orchestration/db-task-dispatch-lifecycle-guards.test.ts": 1072, + "src/main/runtime/orchestration/db-task-dispatch-races.test.ts": 610, + "src/main/runtime/orchestration/db-task-promotion-projection.test.ts": 757, + "src/main/runtime/orchestration/db-undelivered-mailboxes.test.ts": 611, + "src/main/runtime/orchestration/db.test.ts": 1135, + "src/main/runtime/orchestration/db/attempt-outcome-projection.test.ts": 942, + "src/main/runtime/orchestration/db/decision-gate-lifecycle.test.ts": 601, + "src/main/runtime/orchestration/db/dispatch-depth.test.ts": 790, + "src/main/runtime/orchestration/db/dispatch-mailbox-consumer-fencing.test.ts": 636, + "src/main/runtime/orchestration/db/federated-worker-report-outcome.test.ts": 33, + "src/main/runtime/orchestration/db/federation/federated-dispatch-observation-fence.test.ts": 599, + "src/main/runtime/orchestration/db/federation/remote-dispatch-attachment-release.test.ts": 690, + "src/main/runtime/orchestration/db/hot-path-statement-compilation.test.ts": 592, + "src/main/runtime/orchestration/db/legacy-question-identity.test.ts": 36, + "src/main/runtime/orchestration/db/lifecycle-rejection-marker.test.ts": 32, + "src/main/runtime/orchestration/db/lifecycle-transition.test.ts": 647, + "src/main/runtime/orchestration/db/messages/mailbox-consumer-lifecycle-fencing.test.ts": 838, + "src/main/runtime/orchestration/db/pane-key-match.test.ts": 27, + "src/main/runtime/orchestration/db/remaining-dispatch-statement-compilation.test.ts": 1055, + "src/main/runtime/orchestration/db/row-column-lists.test.ts": 612, + "src/main/runtime/orchestration/db/run-list-cursor.test.ts": 35, + "src/main/runtime/orchestration/db/runs/run-coordinator-orca-session-binding.test.ts": 644, + "src/main/runtime/orchestration/db/schema/derived-delivery-migration.test.ts": 734, + "src/main/runtime/orchestration/db/schema/federated-home-run-migration.test.ts": 615, + "src/main/runtime/orchestration/db/schema/structured-pointer-schema-migration.test.ts": 560, + "src/main/runtime/orchestration/db/schema/structured-worker-orca-session-backfill.test.ts": 586, + "src/main/runtime/orchestration/db/worker-dispatch/failed-start-circuit-breaker.test.ts": 603, + "src/main/runtime/orchestration/db/worker-dispatch/worker-dispatch-abandon.test.ts": 742, + "src/main/runtime/orchestration/db/worker-dispatch/worker-dispatch-assignee-orca-session.test.ts": 616, + "src/main/runtime/orchestration/db/writer-run-required.test.ts": 607, + "src/main/runtime/orchestration/dispatch-consumer-generation-migration.test.ts": 715, + "src/main/runtime/orchestration/dispatch-creator-identity-migration.test.ts": 584, + "src/main/runtime/orchestration/dispatch-failure-idempotency.test.ts": 558, + "src/main/runtime/orchestration/federation-ack-checkpoints.test.ts": 31, + "src/main/runtime/orchestration/federation-acknowledgment-integrity.test.ts": 633, + "src/main/runtime/orchestration/federation-acknowledgment-migration.test.ts": 613, + "src/main/runtime/orchestration/federation-lifecycle-settlement.test.ts": 31, + "src/main/runtime/orchestration/federation-sync.test.ts": 4893, + "src/main/runtime/orchestration/federation-terminal-recovery.test.ts": 4724, + "src/main/runtime/orchestration/formatter.test.ts": 226, + "src/main/runtime/orchestration/groups.test.ts": 58, + "src/main/runtime/orchestration/lifecycle-caller-edges.test.ts": 583, + "src/main/runtime/orchestration/lifecycle-reconciliation.test.ts": 823, + "src/main/runtime/orchestration/lightweight-run-worker-exit-escalation.test.ts": 5766, + "src/main/runtime/orchestration/mailbox-pointer-eligibility.test.ts": 655, + "src/main/runtime/orchestration/mailbox-pointer-release-query.test.ts": 600, + "src/main/runtime/orchestration/mailbox-pointer-stage.test.ts": 687, + "src/main/runtime/orchestration/mailbox-pointer-submit.test.ts": 921, + "src/main/runtime/orchestration/message-batch-atomicity.test.ts": 639, + "src/main/runtime/orchestration/mutation-receipt-capacity.test.ts": 650, + "src/main/runtime/orchestration/nested-worker-depth-migration.test.ts": 715, + "src/main/runtime/orchestration/orchestration-adopted-run-binding.test.ts": 1143, + "src/main/runtime/orchestration/orchestration-all-start-versions-migration.test.ts": 2102, + "src/main/runtime/orchestration/orchestration-creator-authority-performance.test.ts": 902, + "src/main/runtime/orchestration/orchestration-db-permissions.test.ts": 586, + "src/main/runtime/orchestration/orchestration-db-retention-pagination.test.ts": 716, + "src/main/runtime/orchestration/orchestration-delivery-consumption.test.ts": 613, "src/main/runtime/orchestration/orchestration-derived-delivery.test.ts": 567, - "src/main/runtime/orchestration/orchestration-federated-legacy-probe.test.ts": 868, - "src/main/runtime/orchestration/orchestration-legacy-coordinator-authority-db.test.ts": 956, - "src/main/runtime/orchestration/orchestration-legacy-question-migration-db.test.ts": 1066, - "src/main/runtime/orchestration/orchestration-legacy-storage-db.test.ts": 2229, - "src/main/runtime/orchestration/orchestration-legacy-worker-terminal-recovery.test.ts": 347, - "src/main/runtime/orchestration/orchestration-mutation-question-db.test.ts": 660, - "src/main/runtime/orchestration/orchestration-orca-session-column-migration.test.ts": 957, - "src/main/runtime/orchestration/orchestration-party-cli-address.test.ts": 281, - "src/main/runtime/orchestration/orchestration-party-location.test.ts": 616, - "src/main/runtime/orchestration/orchestration-peer-capability-cache.test.ts": 79, - "src/main/runtime/orchestration/orchestration-reset-db.test.ts": 588, - "src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts": 768, - "src/main/runtime/orchestration/orchestration-run-list-compatibility.test.ts": 593, - "src/main/runtime/orchestration/orchestration-version-skew-migration.test.ts": 1120, - "src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts": 822, - "src/main/runtime/orchestration/preamble.test.ts": 174, - "src/main/runtime/orchestration/r1-identity-migration.test.ts": 634, - "src/main/runtime/orchestration/run-coordinator-handle-migration.test.ts": 691, - "src/main/runtime/orchestration/run-coordinator-orca-session-address.test.ts": 693, - "src/main/runtime/orchestration/send-agent-turn-host.test.ts": 1639, - "src/main/runtime/orchestration/send-agent-turn.test.ts": 267, - "src/main/runtime/orchestration/settled-question-threads-migration.test.ts": 570, - "src/main/runtime/orchestration/setup-completion-signal.test.ts": 54, - "src/main/runtime/orchestration/structured-mail-source.test.ts": 302, - "src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts": 664, - "src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts": 401, - "src/main/runtime/orchestration/structured-pointer-operation-id.test.ts": 91, - "src/main/runtime/orchestration/structured-session-mail-target.test.ts": 4683, - "src/main/runtime/orchestration/structured-session-pointer-delivery.test.ts": 288, - "src/main/runtime/orchestration/structured-worker-direct-mailbox-target.test.ts": 4219, - "src/main/runtime/orchestration/structured-worker-group-addressing.test.ts": 513, - "src/main/runtime/orchestration/structured-worker-journal-archive.test.ts": 354, - "src/main/runtime/orchestration/structured-worker-journal-page.test.ts": 978, - "src/main/runtime/orchestration/task-deps-flag.test.ts": 70, - "src/main/runtime/orchestration/worker-attention-context.test.ts": 89, - "src/main/runtime/orchestration/worker-dispatch-repair-safety.test.ts": 2467, - "src/main/runtime/orchestration/worker-dispatch-settlement.test.ts": 1181, - "src/main/runtime/orchestration/worker-output-archive-bounding.test.ts": 794, - "src/main/runtime/orchestration/worker-output-archive.test.ts": 801, - "src/main/runtime/orchestration/worker-output-cursor.test.ts": 51, - "src/main/runtime/orchestration/worker-provider-session.test.ts": 59, - "src/main/runtime/orchestration/worker-start-unobserved-prompt-settlement.test.ts": 506, - "src/main/runtime/orchestration/worker-terminal-process-liveness.test.ts": 58, - "src/main/runtime/orchestration/worker-transcript-payload.test.ts": 319, - "src/main/runtime/orchestration/worker-transcript-read.test.ts": 844, - "src/main/runtime/orchestration/worker-transcript-remote-read.test.ts": 668, - "src/main/runtime/paired-close-retirement-proof-publication-order.test.ts": 4291, - "src/main/runtime/pairing-endpoint.test.ts": 264, - "src/main/runtime/prime-agent-screen-readiness-transcripts.test.ts": 7440, - "src/main/runtime/project-host-setup-delete-guard.test.ts": 387, - "src/main/runtime/proven-absent-leaf-pty-verdicts.test.ts": 41, - "src/main/runtime/pty-exit-agent-status-reconciliation.test.ts": 4014, - "src/main/runtime/pty-exit-record-wait.test.ts": 4307, - "src/main/runtime/pty-inventory-lifecycle-admission.test.ts": 4897, - "src/main/runtime/pty-inventory-lifecycle-scope.test.ts": 5629, - "src/main/runtime/pty-inventory-liveness-verdict.test.ts": 4512, - "src/main/runtime/pty-inventory-partial-relay-liveness.test.ts": 4555, - "src/main/runtime/pty-lifecycle-generation-retention.test.ts": 5736, - "src/main/runtime/pty-recorded-surface-topology.test.ts": 47, - "src/main/runtime/pty-shell-ownership-mirror.test.ts": 1615, - "src/main/runtime/pty-transcript-prune-wait-cache.test.ts": 4405, - "src/main/runtime/public-ssh-state.test.ts": 42, - "src/main/runtime/push/desktop-push-service-unreadable-outbox.test.ts": 369, - "src/main/runtime/push/desktop-push-service.test.ts": 455, - "src/main/runtime/push/push-agent-state.test.ts": 61, - "src/main/runtime/push/push-cleanup-auth-expiry.test.ts": 392, - "src/main/runtime/push/push-delivery-policy.test.ts": 75, - "src/main/runtime/push/push-device-registration-persistence.test.ts": 129, - "src/main/runtime/push/push-dispatcher.test.ts": 84, - "src/main/runtime/push/push-gateway-client.test.ts": 415, - "src/main/runtime/push/push-gateway-session.test.ts": 392, - "src/main/runtime/push/push-host-proof-vector.test.ts": 60, - "src/main/runtime/push/push-host-proof.test.ts": 155, - "src/main/runtime/push/push-outcome-counters.test.ts": 39, - "src/main/runtime/push/push-policy-pipeline.integration.test.ts": 358, - "src/main/runtime/push/push-preferences.test.ts": 66, - "src/main/runtime/push/push-registration-races.test.ts": 398, - "src/main/runtime/push/push-registration-rpc.test.ts": 5420, - "src/main/runtime/push/push-unpair-persistence.test.ts": 5651, - "src/main/runtime/push/push-unregister-outbox.test.ts": 91, - "src/main/runtime/qoder-independent-spawn-review.test.ts": 4429, - "src/main/runtime/qoder-mobile-resume-adoption.test.ts": 6484, - "src/main/runtime/qoder-terminal-command-resolution.test.ts": 148, - "src/main/runtime/quarter-circle-title-send-authorization.test.ts": 16020, - "src/main/runtime/queued-terminal-graph-exit.test.ts": 5194, - "src/main/runtime/recent-pty-output-buffer.test.ts": 137, - "src/main/runtime/recent-pty-output-retention.test.ts": 160, - "src/main/runtime/relay/desktop-relay-service-broker-liveness.test.ts": 306, - "src/main/runtime/relay/desktop-relay-service-installer.test.ts": 48, - "src/main/runtime/relay/desktop-relay-service.test.ts": 743, - "src/main/runtime/relay/mobile-relay-e2ee.integration.test.ts": 526, - "src/main/runtime/relay/relay-auth-coordinator-readiness.test.ts": 526, - "src/main/runtime/relay/relay-auth-coordinator-recovery.test.ts": 326, + "src/main/runtime/orchestration/orchestration-federated-legacy-probe.test.ts": 1087, + "src/main/runtime/orchestration/orchestration-legacy-coordinator-authority-db.test.ts": 1143, + "src/main/runtime/orchestration/orchestration-legacy-question-migration-db.test.ts": 902, + "src/main/runtime/orchestration/orchestration-legacy-storage-db.test.ts": 1695, + "src/main/runtime/orchestration/orchestration-legacy-worker-terminal-recovery.test.ts": 294, + "src/main/runtime/orchestration/orchestration-mutation-question-db.test.ts": 569, + "src/main/runtime/orchestration/orchestration-orca-session-column-migration.test.ts": 912, + "src/main/runtime/orchestration/orchestration-party-cli-address.test.ts": 315, + "src/main/runtime/orchestration/orchestration-party-location.test.ts": 617, + "src/main/runtime/orchestration/orchestration-peer-capability-cache.test.ts": 267, + "src/main/runtime/orchestration/orchestration-reset-db.test.ts": 587, + "src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts": 704, + "src/main/runtime/orchestration/orchestration-run-list-compatibility.test.ts": 558, + "src/main/runtime/orchestration/orchestration-version-skew-migration.test.ts": 1124, + "src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts": 846, + "src/main/runtime/orchestration/preamble.test.ts": 148, + "src/main/runtime/orchestration/r1-identity-migration.test.ts": 842, + "src/main/runtime/orchestration/run-coordinator-handle-migration.test.ts": 688, + "src/main/runtime/orchestration/run-coordinator-orca-session-address.test.ts": 751, + "src/main/runtime/orchestration/send-agent-turn-host.test.ts": 2017, + "src/main/runtime/orchestration/send-agent-turn.test.ts": 261, + "src/main/runtime/orchestration/settled-question-threads-migration.test.ts": 585, + "src/main/runtime/orchestration/setup-completion-signal.test.ts": 47, + "src/main/runtime/orchestration/structured-mail-source.test.ts": 324, + "src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts": 574, + "src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts": 337, + "src/main/runtime/orchestration/structured-pointer-operation-id.test.ts": 266, + "src/main/runtime/orchestration/structured-session-mail-target.test.ts": 4901, + "src/main/runtime/orchestration/structured-session-pointer-delivery.test.ts": 305, + "src/main/runtime/orchestration/structured-worker-direct-mailbox-target.test.ts": 3717, + "src/main/runtime/orchestration/structured-worker-group-addressing.test.ts": 461, + "src/main/runtime/orchestration/structured-worker-journal-archive.test.ts": 340, + "src/main/runtime/orchestration/structured-worker-journal-page.test.ts": 1086, + "src/main/runtime/orchestration/task-deps-flag.test.ts": 53, + "src/main/runtime/orchestration/worker-attention-context.test.ts": 38, + "src/main/runtime/orchestration/worker-dispatch-repair-safety.test.ts": 2611, + "src/main/runtime/orchestration/worker-dispatch-settlement.test.ts": 1004, + "src/main/runtime/orchestration/worker-output-archive-bounding.test.ts": 798, + "src/main/runtime/orchestration/worker-output-archive.test.ts": 685, + "src/main/runtime/orchestration/worker-output-cursor.test.ts": 37, + "src/main/runtime/orchestration/worker-provider-session.test.ts": 37, + "src/main/runtime/orchestration/worker-start-unobserved-prompt-settlement.test.ts": 570, + "src/main/runtime/orchestration/worker-terminal-process-liveness.test.ts": 33, + "src/main/runtime/orchestration/worker-terminal-redaction.test.ts": 58, + "src/main/runtime/orchestration/worker-transcript-payload.test.ts": 305, + "src/main/runtime/orchestration/worker-transcript-read.test.ts": 619, + "src/main/runtime/paired-close-retirement-proof-publication-order.test.ts": 5259, + "src/main/runtime/pairing-endpoint.test.ts": 242, + "src/main/runtime/prime-agent-screen-readiness-transcripts.test.ts": 7502, + "src/main/runtime/project-host-setup-delete-guard.test.ts": 752, + "src/main/runtime/proven-absent-leaf-pty-verdicts.test.ts": 40, + "src/main/runtime/pty-exit-agent-status-reconciliation.test.ts": 4257, + "src/main/runtime/pty-exit-record-wait.test.ts": 4417, + "src/main/runtime/pty-inventory-lifecycle-admission.test.ts": 4218, + "src/main/runtime/pty-inventory-lifecycle-scope.test.ts": 3977, + "src/main/runtime/pty-inventory-liveness-verdict.test.ts": 4048, + "src/main/runtime/pty-inventory-partial-relay-liveness.test.ts": 4199, + "src/main/runtime/pty-lifecycle-generation-retention.test.ts": 5257, + "src/main/runtime/pty-recorded-surface-topology.test.ts": 37, + "src/main/runtime/pty-shell-ownership-mirror.test.ts": 1597, + "src/main/runtime/pty-transcript-prune-wait-cache.test.ts": 5374, + "src/main/runtime/public-ssh-state.test.ts": 28, + "src/main/runtime/push/desktop-push-service-unreadable-outbox.test.ts": 299, + "src/main/runtime/push/desktop-push-service.test.ts": 413, + "src/main/runtime/push/push-agent-state.test.ts": 38, + "src/main/runtime/push/push-cleanup-auth-expiry.test.ts": 512, + "src/main/runtime/push/push-delivery-policy.test.ts": 41, + "src/main/runtime/push/push-device-registration-persistence.test.ts": 76, + "src/main/runtime/push/push-dispatcher.test.ts": 65, + "src/main/runtime/push/push-gateway-client.test.ts": 412, + "src/main/runtime/push/push-gateway-session.test.ts": 299, + "src/main/runtime/push/push-host-proof-vector.test.ts": 36, + "src/main/runtime/push/push-host-proof.test.ts": 157, + "src/main/runtime/push/push-outcome-counters.test.ts": 46, + "src/main/runtime/push/push-policy-pipeline.integration.test.ts": 379, + "src/main/runtime/push/push-preferences.test.ts": 39, + "src/main/runtime/push/push-registration-races.test.ts": 423, + "src/main/runtime/push/push-registration-rpc.test.ts": 5720, + "src/main/runtime/push/push-unpair-persistence.test.ts": 5266, + "src/main/runtime/push/push-unregister-outbox.test.ts": 54, + "src/main/runtime/qoder-independent-spawn-review.test.ts": 4131, + "src/main/runtime/qoder-mobile-resume-adoption.test.ts": 7211, + "src/main/runtime/qoder-terminal-command-resolution.test.ts": 137, + "src/main/runtime/quarter-circle-title-send-authorization.test.ts": 19646, + "src/main/runtime/queued-terminal-graph-exit.test.ts": 4535, + "src/main/runtime/recent-pty-output-buffer.test.ts": 121, + "src/main/runtime/recent-pty-output-retention.test.ts": 179, + "src/main/runtime/relay/desktop-relay-service-broker-liveness.test.ts": 323, + "src/main/runtime/relay/desktop-relay-service-installer.test.ts": 40, + "src/main/runtime/relay/desktop-relay-service.test.ts": 690, + "src/main/runtime/relay/mobile-relay-e2ee.integration.test.ts": 481, + "src/main/runtime/relay/relay-auth-coordinator-readiness.test.ts": 507, + "src/main/runtime/relay/relay-auth-coordinator-recovery.test.ts": 296, "src/main/runtime/relay/relay-auth-coordinator.test.ts": 1435, - "src/main/runtime/relay/relay-auth-host-close-reason.test.ts": 355, - "src/main/runtime/relay/relay-control-client.test.ts": 788, - "src/main/runtime/relay/relay-control-close-reason.test.ts": 339, - "src/main/runtime/relay/relay-control-origin.test.ts": 284, - "src/main/runtime/relay/relay-control-request-retirement.test.ts": 296, - "src/main/runtime/relay/relay-control-timeout-classification.test.ts": 244, - "src/main/runtime/relay/relay-demand-ledger.test.ts": 98, - "src/main/runtime/relay/relay-host-proof.test.ts": 166, - "src/main/runtime/relay/relay-http-client.test.ts": 279, - "src/main/runtime/relay/relay-region-correction.test.ts": 312, - "src/main/runtime/relay/relay-region-preference.test.ts": 352, - "src/main/runtime/relay/relay-region-probe-log.test.ts": 275, - "src/main/runtime/relay/relay-region-refresh.test.ts": 249, - "src/main/runtime/relay/relay-renewal-jitter.test.ts": 254, - "src/main/runtime/relay/relay-revoke-outbox.test.ts": 102, - "src/main/runtime/relay/relay-session-broker.test.ts": 859, - "src/main/runtime/remote-agent-session-host-authority.integration.test.ts": 5984, - "src/main/runtime/remote-browser-screencast-frame-admission.test.ts": 409, - "src/main/runtime/remote-desktop-driver.test.ts": 4594, - "src/main/runtime/remote-desktop-terminal-floor-reconnect.test.ts": 46, - "src/main/runtime/remote-runtime-close-intent.integration.test.ts": 4011, - "src/main/runtime/remote-runtime-request-connection.integration.test.ts": 3568, - "src/main/runtime/remote-server-updater.test.ts": 51, - "src/main/runtime/renderer-browser-session-reconciliation.test.ts": 2554, - "src/main/runtime/repo-icon-fork-backfill.test.ts": 4230, - "src/main/runtime/repo-worktree-admin-fingerprint.test.ts": 813, - "src/main/runtime/repo-worktree-resolution-scan.test.ts": 54, - "src/main/runtime/repo-worktree-row-resolution.test.ts": 231, - "src/main/runtime/retained-tail-redraw-window.equivalence.test.ts": 724, - "src/main/runtime/ripgrep-filename-identity.test.ts": 68, - "src/main/runtime/rpc/core-typed-method-contract.test.ts": 257, - "src/main/runtime/rpc/dispatcher-browser-client-automation.test.ts": 482, - "src/main/runtime/rpc/dispatcher-computer-errors.test.ts": 559, - "src/main/runtime/rpc/dispatcher-feature-interactions.test.ts": 601, - "src/main/runtime/rpc/dispatcher-request-parsing.test.ts": 296, - "src/main/runtime/rpc/dispatcher-transport-parity.test.ts": 5920, - "src/main/runtime/rpc/e2ee-channel-text-backpressure.test.ts": 300, - "src/main/runtime/rpc/e2ee-channel-v2.test.ts": 412, - "src/main/runtime/rpc/e2ee-channel.test.ts": 659, - "src/main/runtime/rpc/e2ee-crypto.test.ts": 139, - "src/main/runtime/rpc/e2ee-integration.test.ts": 357, - "src/main/runtime/rpc/errors.test.ts": 357, - "src/main/runtime/rpc/methods/accounts.test.ts": 248, - "src/main/runtime/rpc/methods/agent-hooks.test.ts": 233, - "src/main/runtime/rpc/methods/agent-launch-caller-renders.test.ts": 1188, - "src/main/runtime/rpc/methods/agent-launch-caller-selection.test.ts": 1753, - "src/main/runtime/rpc/methods/agent-launch-floating-workspace.test.ts": 4476, - "src/main/runtime/rpc/methods/agent-launch-folder-workspace-create.test.ts": 1776, - "src/main/runtime/rpc/methods/agent-launch-instant-tab.test.ts": 2435, - "src/main/runtime/rpc/methods/agent-launch-pane-reservation.test.ts": 2058, - "src/main/runtime/rpc/methods/agent-launch-prestart-failure.test.ts": 2073, - "src/main/runtime/rpc/methods/agent-launch-prompt-carry.test.ts": 1280, - "src/main/runtime/rpc/methods/agent-launch-replay.test.ts": 1990, - "src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts": 5221, - "src/main/runtime/rpc/methods/agent-launch-session-reservation.test.ts": 2077, - "src/main/runtime/rpc/methods/agent-launch-structured-prompt.test.ts": 245, - "src/main/runtime/rpc/methods/agent-launch-terminal-identity.test.ts": 1355, - "src/main/runtime/rpc/methods/agent-launch-terminal-preferences.test.ts": 1325, - "src/main/runtime/rpc/methods/agent-launch-terminal-prompt.test.ts": 385, - "src/main/runtime/rpc/methods/agent-launch.test.ts": 1715, - "src/main/runtime/rpc/methods/agent-session.test.ts": 699, - "src/main/runtime/rpc/methods/agent-skill-sharing-capability-grant.test.ts": 4568, - "src/main/runtime/rpc/methods/ai-vault-search.test.ts": 4950, - "src/main/runtime/rpc/methods/ai-vault-structured-ownership.test.ts": 568, - "src/main/runtime/rpc/methods/ai-vault.test.ts": 5161, - "src/main/runtime/rpc/methods/artifact-sharing-capability-grant.test.ts": 4562, - "src/main/runtime/rpc/methods/artifacts.test.ts": 371, - "src/main/runtime/rpc/methods/automation-scoped-list-methods.test.ts": 533, - "src/main/runtime/rpc/methods/automations.test.ts": 607, - "src/main/runtime/rpc/methods/browser-client-host-attach-adoption.test.ts": 690, - "src/main/runtime/rpc/methods/browser-client-host-reconciliation.test.ts": 684, - "src/main/runtime/rpc/methods/browser-client-host.test.ts": 609, - "src/main/runtime/rpc/methods/browser-client-page-metadata.test.ts": 637, - "src/main/runtime/rpc/methods/browser-identity-rpc.test.ts": 562, - "src/main/runtime/rpc/methods/browser-network-tunnel.test.ts": 798, - "src/main/runtime/rpc/methods/browser-tab-create-schema.test.ts": 235, - "src/main/runtime/rpc/methods/browser.test.ts": 704, - "src/main/runtime/rpc/methods/client-events.test.ts": 3207, - "src/main/runtime/rpc/methods/client-native-chat-settings.test.ts": 654, - "src/main/runtime/rpc/methods/client-ui-pairing-local-fields.test.ts": 675, - "src/main/runtime/rpc/methods/client-ui-task-resume-state.test.ts": 659, - "src/main/runtime/rpc/methods/client-ui.test.ts": 661, - "src/main/runtime/rpc/methods/clipboard-image-upload-decoding.test.ts": 680, - "src/main/runtime/rpc/methods/clipboard.test.ts": 627, - "src/main/runtime/rpc/methods/computer-actions.test.ts": 326, - "src/main/runtime/rpc/methods/computer.test.ts": 296, - "src/main/runtime/rpc/methods/diagnostics.test.ts": 471, - "src/main/runtime/rpc/methods/file-watch-event-batcher.test.ts": 57, - "src/main/runtime/rpc/methods/files-base64-padding.test.ts": 626, - "src/main/runtime/rpc/methods/files-doc-preview.test.ts": 568, - "src/main/runtime/rpc/methods/files-list-all-page-size.test.ts": 647, - "src/main/runtime/rpc/methods/files-open-navigation.test.ts": 597, - "src/main/runtime/rpc/methods/files-path-search.test.ts": 624, - "src/main/runtime/rpc/methods/files-preview-transport-budget.test.ts": 547, - "src/main/runtime/rpc/methods/files-read-dir-remote-budget.test.ts": 4567, - "src/main/runtime/rpc/methods/files-terminal-path-resolution.test.ts": 568, - "src/main/runtime/rpc/methods/files-watch-cancellation.test.ts": 672, - "src/main/runtime/rpc/methods/files-watch-cleanup.test.ts": 565, - "src/main/runtime/rpc/methods/files.test.ts": 815, - "src/main/runtime/rpc/methods/git-diff-transport-budget.test.ts": 1484, - "src/main/runtime/rpc/methods/git.test.ts": 596, - "src/main/runtime/rpc/methods/github-pr-refresh-reason.test.ts": 538, - "src/main/runtime/rpc/methods/github.test.ts": 619, - "src/main/runtime/rpc/methods/gitlab.test.ts": 600, - "src/main/runtime/rpc/methods/host-capabilities.test.ts": 508, - "src/main/runtime/rpc/methods/hosted-review.test.ts": 1004, - "src/main/runtime/rpc/methods/jira.test.ts": 584, - "src/main/runtime/rpc/methods/linear-agent-access.test.ts": 4650, - "src/main/runtime/rpc/methods/linear-agent-project-access.test.ts": 5785, - "src/main/runtime/rpc/methods/linear.test.ts": 555, - "src/main/runtime/rpc/methods/managed-server.test.ts": 3128, - "src/main/runtime/rpc/methods/mobile-web-bundle-range.test.ts": 3197, - "src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts": 3452, - "src/main/runtime/rpc/methods/mobile-web-bundle-window-refusals.test.ts": 3539, - "src/main/runtime/rpc/methods/mobile-web-bundle.test.ts": 2747, - "src/main/runtime/rpc/methods/native-chat-opencode-semantic-pages.test.ts": 699, - "src/main/runtime/rpc/methods/native-chat-opencode-wire-page-budget.test.ts": 2650, - "src/main/runtime/rpc/methods/native-chat-rpc-block-sanitize.test.ts": 69, - "src/main/runtime/rpc/methods/native-chat-ssh-transcript.test.ts": 1634, + "src/main/runtime/relay/relay-auth-host-close-reason.test.ts": 323, + "src/main/runtime/relay/relay-control-client.test.ts": 735, + "src/main/runtime/relay/relay-control-close-reason.test.ts": 291, + "src/main/runtime/relay/relay-control-origin.test.ts": 230, + "src/main/runtime/relay/relay-control-request-retirement.test.ts": 239, + "src/main/runtime/relay/relay-control-timeout-classification.test.ts": 241, + "src/main/runtime/relay/relay-demand-ledger.test.ts": 89, + "src/main/runtime/relay/relay-host-proof.test.ts": 128, + "src/main/runtime/relay/relay-http-client.test.ts": 271, + "src/main/runtime/relay/relay-region-correction.test.ts": 263, + "src/main/runtime/relay/relay-region-preference.test.ts": 327, + "src/main/runtime/relay/relay-region-probe-log.test.ts": 284, + "src/main/runtime/relay/relay-region-refresh.test.ts": 229, + "src/main/runtime/relay/relay-renewal-jitter.test.ts": 278, + "src/main/runtime/relay/relay-revoke-outbox.test.ts": 50, + "src/main/runtime/relay/relay-session-broker.test.ts": 982, + "src/main/runtime/remote-agent-session-host-authority.integration.test.ts": 7059, + "src/main/runtime/remote-browser-screencast-frame-admission.test.ts": 394, + "src/main/runtime/remote-desktop-driver.test.ts": 4641, + "src/main/runtime/remote-desktop-terminal-floor-reconnect.test.ts": 37, + "src/main/runtime/remote-runtime-close-intent.integration.test.ts": 3427, + "src/main/runtime/remote-runtime-request-connection.integration.test.ts": 3495, + "src/main/runtime/remote-server-updater.test.ts": 34, + "src/main/runtime/renderer-browser-session-reconciliation.test.ts": 2697, + "src/main/runtime/repo-icon-fork-backfill.test.ts": 4918, + "src/main/runtime/repo-worktree-admin-fingerprint.test.ts": 564, + "src/main/runtime/repo-worktree-resolution-scan.test.ts": 40, + "src/main/runtime/repo-worktree-row-resolution.test.ts": 538, + "src/main/runtime/retained-tail-redraw-window.equivalence.test.ts": 812, + "src/main/runtime/ripgrep-filename-identity.test.ts": 81, + "src/main/runtime/rpc/core-typed-method-contract.test.ts": 235, + "src/main/runtime/rpc/device-key-auth.test.ts": 581, + "src/main/runtime/rpc/dispatcher-browser-client-automation.test.ts": 581, + "src/main/runtime/rpc/dispatcher-computer-errors.test.ts": 604, + "src/main/runtime/rpc/dispatcher-feature-interactions.test.ts": 637, + "src/main/runtime/rpc/dispatcher-request-parsing.test.ts": 290, + "src/main/runtime/rpc/dispatcher-transport-parity.test.ts": 7600, + "src/main/runtime/rpc/e2ee-channel-text-backpressure.test.ts": 333, + "src/main/runtime/rpc/e2ee-channel-v2.test.ts": 463, + "src/main/runtime/rpc/e2ee-channel.test.ts": 694, + "src/main/runtime/rpc/e2ee-crypto.test.ts": 116, + "src/main/runtime/rpc/e2ee-integration.test.ts": 393, + "src/main/runtime/rpc/errors.test.ts": 452, + "src/main/runtime/rpc/methods/accounts.test.ts": 259, + "src/main/runtime/rpc/methods/agent-hooks.test.ts": 217, + "src/main/runtime/rpc/methods/agent-launch-caller-renders.test.ts": 1318, + "src/main/runtime/rpc/methods/agent-launch-caller-selection.test.ts": 1682, + "src/main/runtime/rpc/methods/agent-launch-floating-workspace.test.ts": 5095, + "src/main/runtime/rpc/methods/agent-launch-folder-workspace-create.test.ts": 2026, + "src/main/runtime/rpc/methods/agent-launch-instant-tab.test.ts": 2830, + "src/main/runtime/rpc/methods/agent-launch-pane-reservation.test.ts": 2039, + "src/main/runtime/rpc/methods/agent-launch-prestart-failure.test.ts": 1848, + "src/main/runtime/rpc/methods/agent-launch-prompt-carry.test.ts": 1443, + "src/main/runtime/rpc/methods/agent-launch-replay.test.ts": 2421, + "src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts": 4550, + "src/main/runtime/rpc/methods/agent-launch-session-reservation.test.ts": 2135, + "src/main/runtime/rpc/methods/agent-launch-structured-prompt.test.ts": 230, + "src/main/runtime/rpc/methods/agent-launch-terminal-identity.test.ts": 1495, + "src/main/runtime/rpc/methods/agent-launch-terminal-preferences.test.ts": 1617, + "src/main/runtime/rpc/methods/agent-launch-terminal-prompt.test.ts": 380, + "src/main/runtime/rpc/methods/agent-launch.test.ts": 1882, + "src/main/runtime/rpc/methods/agent-session.test.ts": 1680, + "src/main/runtime/rpc/methods/agent-skill-sharing-capability-grant.test.ts": 4489, + "src/main/runtime/rpc/methods/ai-vault-search.test.ts": 4340, + "src/main/runtime/rpc/methods/ai-vault-structured-ownership.test.ts": 1479, + "src/main/runtime/rpc/methods/ai-vault.test.ts": 4712, + "src/main/runtime/rpc/methods/artifact-sharing-capability-grant.test.ts": 4296, + "src/main/runtime/rpc/methods/artifacts.test.ts": 361, + "src/main/runtime/rpc/methods/automatic-work.test.ts": 4674, + "src/main/runtime/rpc/methods/automation-scoped-list-methods.test.ts": 635, + "src/main/runtime/rpc/methods/automations.test.ts": 617, + "src/main/runtime/rpc/methods/browser-client-host-attach-adoption.test.ts": 855, + "src/main/runtime/rpc/methods/browser-client-host-reconciliation.test.ts": 614, + "src/main/runtime/rpc/methods/browser-client-host.test.ts": 681, + "src/main/runtime/rpc/methods/browser-client-page-metadata.test.ts": 909, + "src/main/runtime/rpc/methods/browser-identity-rpc.test.ts": 582, + "src/main/runtime/rpc/methods/browser-network-tunnel.test.ts": 801, + "src/main/runtime/rpc/methods/browser-tab-create-schema.test.ts": 233, + "src/main/runtime/rpc/methods/browser.test.ts": 594, + "src/main/runtime/rpc/methods/client-events.test.ts": 3014, + "src/main/runtime/rpc/methods/client-native-chat-settings.test.ts": 630, + "src/main/runtime/rpc/methods/client-ui-pairing-local-fields.test.ts": 774, + "src/main/runtime/rpc/methods/client-ui-task-resume-state.test.ts": 660, + "src/main/runtime/rpc/methods/client-ui.test.ts": 810, + "src/main/runtime/rpc/methods/clipboard-image-upload-decoding.test.ts": 713, + "src/main/runtime/rpc/methods/clipboard.test.ts": 630, + "src/main/runtime/rpc/methods/computer-actions.test.ts": 316, + "src/main/runtime/rpc/methods/computer.test.ts": 268, + "src/main/runtime/rpc/methods/diagnostics.test.ts": 460, + "src/main/runtime/rpc/methods/file-watch-event-batcher.test.ts": 49, + "src/main/runtime/rpc/methods/files-base64-padding.test.ts": 498, + "src/main/runtime/rpc/methods/files-doc-preview.test.ts": 676, + "src/main/runtime/rpc/methods/files-list-all-page-size.test.ts": 572, + "src/main/runtime/rpc/methods/files-open-navigation.test.ts": 523, + "src/main/runtime/rpc/methods/files-path-search.test.ts": 680, + "src/main/runtime/rpc/methods/files-preview-transport-budget.test.ts": 615, + "src/main/runtime/rpc/methods/files-read-dir-remote-budget.test.ts": 4393, + "src/main/runtime/rpc/methods/files-terminal-path-resolution.test.ts": 636, + "src/main/runtime/rpc/methods/files-watch-cancellation.test.ts": 625, + "src/main/runtime/rpc/methods/files-watch-cleanup.test.ts": 614, + "src/main/runtime/rpc/methods/files.test.ts": 865, + "src/main/runtime/rpc/methods/git-diff-transport-budget.test.ts": 601, + "src/main/runtime/rpc/methods/git.test.ts": 747, + "src/main/runtime/rpc/methods/github-pr-refresh-reason.test.ts": 547, + "src/main/runtime/rpc/methods/github.test.ts": 675, + "src/main/runtime/rpc/methods/gitlab.test.ts": 618, + "src/main/runtime/rpc/methods/host-capabilities.test.ts": 555, + "src/main/runtime/rpc/methods/hosted-review.test.ts": 1241, + "src/main/runtime/rpc/methods/jira-attachment-image.test.ts": 572, + "src/main/runtime/rpc/methods/jira-board-filter-rpc.test.ts": 576, + "src/main/runtime/rpc/methods/jira-saved-filter-state.test.ts": 524, + "src/main/runtime/rpc/methods/jira.test.ts": 639, + "src/main/runtime/rpc/methods/linear-agent-access.test.ts": 4572, + "src/main/runtime/rpc/methods/linear-agent-project-access.test.ts": 7359, + "src/main/runtime/rpc/methods/linear.test.ts": 673, + "src/main/runtime/rpc/methods/managed-server.test.ts": 3191, + "src/main/runtime/rpc/methods/mobile-web-bundle-range.test.ts": 2902, + "src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts": 2455, + "src/main/runtime/rpc/methods/mobile-web-bundle-window-refusals.test.ts": 2455, + "src/main/runtime/rpc/methods/mobile-web-bundle.test.ts": 2575, + "src/main/runtime/rpc/methods/native-chat-opencode-semantic-pages.test.ts": 607, + "src/main/runtime/rpc/methods/native-chat-opencode-wire-page-budget.test.ts": 2617, + "src/main/runtime/rpc/methods/native-chat-rpc-block-sanitize.test.ts": 48, + "src/main/runtime/rpc/methods/native-chat-ssh-transcript.test.ts": 449, "src/main/runtime/rpc/methods/native-chat-subscription-token.test.ts": 212, - "src/main/runtime/rpc/methods/native-chat.test.ts": 293, - "src/main/runtime/rpc/methods/notification-preferences.test.ts": 330, - "src/main/runtime/rpc/methods/notification-reconnect-cooldown.test.ts": 297, - "src/main/runtime/rpc/methods/orcad-migration.test.ts": 3207, - "src/main/runtime/rpc/methods/orcad-terminal-census.test.ts": 214, - "src/main/runtime/rpc/methods/orchestration-dispatch-error-codes.test.ts": 6710, - "src/main/runtime/rpc/methods/orchestration-structured-worker-abandon.test.ts": 3849, - "src/main/runtime/rpc/methods/orchestration-structured-worker-lifecycle.test.ts": 395, - "src/main/runtime/rpc/methods/orchestration-structured-worker-redrive.test.ts": 5166, - "src/main/runtime/rpc/methods/orchestration-structured-worker-session.test.ts": 462, - "src/main/runtime/rpc/methods/orchestration-structured-worker-start-failure.test.ts": 1247, - "src/main/runtime/rpc/methods/orchestration-worker-mode-opacity.test.ts": 5434, - "src/main/runtime/rpc/methods/orchestration-worker-release-incarnation-fallback.test.ts": 5547, - "src/main/runtime/rpc/methods/orchestration-worker-start-mode-selection.test.ts": 5253, - "src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts": 1089, - "src/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.ts": 1242, - "src/main/runtime/rpc/methods/orchestration-worker-start-registered-agent.test.ts": 920, - "src/main/runtime/rpc/methods/orchestration-worker-support-unknown.test.ts": 1109, - "src/main/runtime/rpc/methods/orchestration/cli-runtime-boundary.test.ts": 5257, - "src/main/runtime/rpc/methods/orchestration/federation/federated-attach-receipt.test.ts": 54, - "src/main/runtime/rpc/methods/orchestration/federation/federated-fleet-snapshot.test.ts": 580, - "src/main/runtime/rpc/methods/orchestration/federation/federated-message-targeting.test.ts": 5648, - "src/main/runtime/rpc/methods/orchestration/federation/federated-release-safety.test.ts": 5387, - "src/main/runtime/rpc/methods/orchestration/federation/federated-transport-safety.test.ts": 4379, - "src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.test.ts": 1057, - "src/main/runtime/rpc/methods/orchestration/federation/federated-worker-start-receipt.test.ts": 5244, - "src/main/runtime/rpc/methods/orchestration/federation/federation-agent-launch.test.ts": 5782, - "src/main/runtime/rpc/methods/orchestration/federation/federation-control-mail.test.ts": 6612, - "src/main/runtime/rpc/methods/orchestration/federation/federation-effects.test.ts": 49, - "src/main/runtime/rpc/methods/orchestration/federation/federation-folder-placement.test.ts": 5952, - "src/main/runtime/rpc/methods/orchestration/federation/federation-launched-agent-readiness.test.ts": 5779, - "src/main/runtime/rpc/methods/orchestration/federation/federation-lifecycle-settlement.test.ts": 10158, - "src/main/runtime/rpc/methods/orchestration/federation/federation-liveness-verdict.test.ts": 5824, - "src/main/runtime/rpc/methods/orchestration/federation/federation-output.test.ts": 6850, - "src/main/runtime/rpc/methods/orchestration/federation/federation-setup.test.ts": 5357, - "src/main/runtime/rpc/methods/orchestration/federation/federation-start-prompt-budget.test.ts": 5348, - "src/main/runtime/rpc/methods/orchestration/federation/federation-start-schema.test.ts": 230, - "src/main/runtime/rpc/methods/orchestration/federation/federation-worker-brief-agent-in-front.test.ts": 5354, - "src/main/runtime/rpc/methods/orchestration/federation/federation.test.ts": 6454, - "src/main/runtime/rpc/methods/orchestration/gates/gate-run-authorization.test.ts": 6061, - "src/main/runtime/rpc/methods/orchestration/gates/gates.test.ts": 4295, - "src/main/runtime/rpc/methods/orchestration/messaging/ask.test.ts": 4846, - "src/main/runtime/rpc/methods/orchestration/messaging/check-worker-consumer-fencing.test.ts": 5619, - "src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts": 5245, - "src/main/runtime/rpc/methods/orchestration/messaging/check.test.ts": 4797, - "src/main/runtime/rpc/methods/orchestration/messaging/recipient-routing.test.ts": 4757, - "src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-history.test.ts": 4560, - "src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-interleavings.test.ts": 4411, - "src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-safety.test.ts": 4829, - "src/main/runtime/rpc/methods/orchestration/messaging/send-dispatch-authority.test.ts": 4949, - "src/main/runtime/rpc/methods/orchestration/messaging/send-group.test.ts": 4642, - "src/main/runtime/rpc/methods/orchestration/messaging/send-invalid-type.test.ts": 4314, - "src/main/runtime/rpc/methods/orchestration/messaging/send.test.ts": 4897, - "src/main/runtime/rpc/methods/orchestration/runs/migration-behavior.test.ts": 5370, - "src/main/runtime/rpc/methods/orchestration/runs/run-receipt.test.ts": 223, - "src/main/runtime/rpc/methods/orchestration/runs/runs.test.ts": 4878, - "src/main/runtime/rpc/methods/orchestration/runs/tasks-dispatch.test.ts": 4979, - "src/main/runtime/rpc/methods/orchestration/worker/agent-status-producer-census.test.ts": 6689, - "src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts": 5051, - "src/main/runtime/rpc/methods/orchestration/worker/fleet-status-observed-identity.test.ts": 3341, - "src/main/runtime/rpc/methods/orchestration/worker/fleet-status-terminal-identity.test.ts": 3237, - "src/main/runtime/rpc/methods/orchestration/worker/manual-dispatch-observation.test.ts": 5925, - "src/main/runtime/rpc/methods/orchestration/worker/manual-dispatch-release.test.ts": 5791, - "src/main/runtime/rpc/methods/orchestration/worker/structured-worker-launch-seed-options.test.ts": 332, - "src/main/runtime/rpc/methods/orchestration/worker/worker-interactive-wait.test.ts": 5697, - "src/main/runtime/rpc/methods/orchestration/worker/worker-launch-preferences.test.ts": 289, - "src/main/runtime/rpc/methods/orchestration/worker/worker-list-pagination.test.ts": 5550, - "src/main/runtime/rpc/methods/orchestration/worker/worker-list-run-scope-rpc.test.ts": 5064, - "src/main/runtime/rpc/methods/orchestration/worker/worker-observation.test.ts": 492, - "src/main/runtime/rpc/methods/orchestration/worker/worker-output.test.ts": 732, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-archive.test.ts": 4946, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-inventory.test.ts": 4973, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-liveness-verdict.test.ts": 829, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-mobile-report.test.ts": 4843, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-ownership-guard.test.ts": 4849, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-recovery.test.ts": 5546, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-runtime-incarnation.test.ts": 4885, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release.test.ts": 5097, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-agent-placement-teardown.test.ts": 1020, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-budgets.test.ts": 48, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-launch-executor-parity.test.ts": 1191, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-outcome-classification.test.ts": 418, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-budget.test.ts": 99, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-contract.test.ts": 6316, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-readiness-settlement.test.ts": 350, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-terminal-target.test.ts": 6240, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-turn-observation.test.ts": 60, - "src/main/runtime/rpc/methods/orchestration/worker/worker-stop-capability.test.ts": 482, - "src/main/runtime/rpc/methods/orchestration/worker/worker-stop-exit-race.test.ts": 4286, - "src/main/runtime/rpc/methods/orchestration/worker/worker-stop-liveness-verdict.test.ts": 5777, - "src/main/runtime/rpc/methods/orchestration/worker/worker-terminal-custody-at-creation.test.ts": 5458, - "src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts": 5092, - "src/main/runtime/rpc/methods/orchestration/worker/workers-recovery.test.ts": 5732, - "src/main/runtime/rpc/methods/paired-caller-host-id.test.ts": 65, - "src/main/runtime/rpc/methods/pairing.test.ts": 4533, - "src/main/runtime/rpc/methods/plugins.test.ts": 411, - "src/main/runtime/rpc/methods/preflight.test.ts": 520, - "src/main/runtime/rpc/methods/project-host-setup-self-host-stamp.test.ts": 256, - "src/main/runtime/rpc/methods/repo-badge-color.test.ts": 605, - "src/main/runtime/rpc/methods/repo.test.ts": 4674, - "src/main/runtime/rpc/methods/runtime-client-capabilities.test.ts": 565, - "src/main/runtime/rpc/methods/session-tab-agent-capability-mutations.test.ts": 615, - "src/main/runtime/rpc/methods/session-tab-agent-status-projection.test.ts": 88, - "src/main/runtime/rpc/methods/session-tab-browser-placement-mutations.test.ts": 643, - "src/main/runtime/rpc/methods/session-tab-browser-placement-projection.test.ts": 66, - "src/main/runtime/rpc/methods/session-tab-registered-agent-projection.test.ts": 69, - "src/main/runtime/rpc/methods/session-tabs-inventory-census-race.test.ts": 3462, - "src/main/runtime/rpc/methods/session-tabs-inventory-rpc.test.ts": 4327, - "src/main/runtime/rpc/methods/session-tabs-move-validation.test.ts": 4892, - "src/main/runtime/rpc/methods/session-tabs-pane-layout-intent.test.ts": 604, - "src/main/runtime/rpc/methods/session-tabs-retirement-proof-delta.test.ts": 578, - "src/main/runtime/rpc/methods/session-tabs-schemas.test.ts": 255, - "src/main/runtime/rpc/methods/session-tabs-structured-restore.test.ts": 540, - "src/main/runtime/rpc/methods/session-tabs-subscribe-admission.test.ts": 559, - "src/main/runtime/rpc/methods/session-tabs-unsubscribe.test.ts": 639, - "src/main/runtime/rpc/methods/session-tabs.test.ts": 676, - "src/main/runtime/rpc/methods/skills.test.ts": 341, - "src/main/runtime/rpc/methods/speech.test.ts": 550, - "src/main/runtime/rpc/methods/ssh.test.ts": 558, - "src/main/runtime/rpc/methods/status-machine-name-publish-budget.test.ts": 4652, - "src/main/runtime/rpc/methods/status.test.ts": 4704, - "src/main/runtime/rpc/methods/structured-agent-session-admission.test.ts": 3034, - "src/main/runtime/rpc/methods/structured-agent-session-adoption-replay.test.ts": 5689, - "src/main/runtime/rpc/methods/structured-agent-session-agents.test.ts": 1000, - "src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts": 6678, - "src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts": 2917, - "src/main/runtime/rpc/methods/structured-agent-session-attention-ack.test.ts": 3201, - "src/main/runtime/rpc/methods/structured-agent-session-background-task-capability.test.ts": 3064, - "src/main/runtime/rpc/methods/structured-agent-session-conversation-outline.test.ts": 3264, - "src/main/runtime/rpc/methods/structured-agent-session-create-message.test.ts": 1688, - "src/main/runtime/rpc/methods/structured-agent-session-create-tab-publication.test.ts": 290, - "src/main/runtime/rpc/methods/structured-agent-session-hold.test.ts": 5963, - "src/main/runtime/rpc/methods/structured-agent-session-optional-model-capability.test.ts": 2875, - "src/main/runtime/rpc/methods/structured-agent-session-options-read.test.ts": 3814, - "src/main/runtime/rpc/methods/structured-agent-session-pi-dialog-capability.test.ts": 2971, - "src/main/runtime/rpc/methods/structured-agent-session-policy.test.ts": 89, - "src/main/runtime/rpc/methods/structured-agent-session-precommit-refusal.test.ts": 862, - "src/main/runtime/rpc/methods/structured-agent-session-queued-edit.test.ts": 3153, - "src/main/runtime/rpc/methods/structured-agent-session-respond-params.test.ts": 2947, - "src/main/runtime/rpc/methods/structured-agent-session-restart-dismiss-fence.test.ts": 3237, - "src/main/runtime/rpc/methods/structured-agent-session-restart-resume.test.ts": 3111, - "src/main/runtime/rpc/methods/structured-agent-session-restart-unregistered-agent.test.ts": 3764, - "src/main/runtime/rpc/methods/structured-agent-session-send-compatibility.test.ts": 2815, - "src/main/runtime/rpc/methods/structured-agent-session-turn-completion-stream.test.ts": 2944, - "src/main/runtime/rpc/methods/structured-agent-session-turn-item-capability.test.ts": 3068, - "src/main/runtime/rpc/methods/structured-agent-session-unsubscribe.test.ts": 5112, - "src/main/runtime/rpc/methods/structured-agent-session-visual.test.ts": 3030, - "src/main/runtime/rpc/methods/structured-agent-session.test.ts": 3424, - "src/main/runtime/rpc/methods/structured-chat-tab-table.test.ts": 6245, - "src/main/runtime/rpc/methods/structured-worker-read-cursor.test.ts": 447, - "src/main/runtime/rpc/methods/structured-worker-stop-receipt.test.ts": 5290, - "src/main/runtime/rpc/methods/structured-worker-tab-retirement.test.ts": 4358, - "src/main/runtime/rpc/methods/task-provider-identity.test.ts": 247, - "src/main/runtime/rpc/methods/terminal-create-idempotency.test.ts": 312, - "src/main/runtime/rpc/methods/terminal-legacy-stream-id-characterization.test.ts": 853, - "src/main/runtime/rpc/methods/terminal-manifest-characterization.test.ts": 378, - "src/main/runtime/rpc/methods/terminal-navigation.test.ts": 830, - "src/main/runtime/rpc/methods/terminal-read-screen-cursor.test.ts": 340, - "src/main/runtime/rpc/methods/terminal-stream-extraction-characterization.test.ts": 83, - "src/main/runtime/rpc/methods/terminal/terminal-input-sequence-ledger.test.ts": 82, - "src/main/runtime/rpc/methods/terminal/terminal-inspect-process-params.test.ts": 266, - "src/main/runtime/rpc/methods/terminal/terminal-snapshot-json-byte-budget.test.ts": 190, - "src/main/runtime/rpc/methods/terminal/terminal-snapshot-scrollback-rows.test.ts": 51, - "src/main/runtime/rpc/methods/terminal/terminal-subscription-registration.test.ts": 122, - "src/main/runtime/rpc/methods/terminal/terminal-viewer-colors-method.test.ts": 583, - "src/main/runtime/rpc/methods/updater.test.ts": 275, - "src/main/runtime/rpc/methods/workspace-cleanup-ui-schema.test.ts": 304, - "src/main/runtime/rpc/methods/workspace-layout.test.ts": 3003, - "src/main/runtime/rpc/methods/workspace-ports.test.ts": 559, - "src/main/runtime/rpc/methods/worktree-catalog-snapshot-method.test.ts": 1589, - "src/main/runtime/rpc/methods/worktree-catalog-version-stamp.test.ts": 1496, - "src/main/runtime/rpc/methods/worktree-create-args.test.ts": 351, - "src/main/runtime/rpc/methods/worktree-create-launch-source.test.ts": 1520, - "src/main/runtime/rpc/methods/worktree-create-navigation.test.ts": 1623, - "src/main/runtime/rpc/methods/worktree-create-startup-agent.test.ts": 1540, - "src/main/runtime/rpc/methods/worktree-github-pr-suppression.test.ts": 1481, - "src/main/runtime/rpc/methods/worktree-missing-terminal-teardown.test.ts": 1296, - "src/main/runtime/rpc/methods/worktree-retired-names.test.ts": 1517, - "src/main/runtime/rpc/methods/worktree-rm-background-reply.test.ts": 1604, - "src/main/runtime/rpc/methods/worktree-rm-host-qualification.test.ts": 1546, - "src/main/runtime/rpc/methods/worktree-rm-pty-waiver.test.ts": 1498, - "src/main/runtime/rpc/methods/worktree-schemas.test.ts": 356, - "src/main/runtime/rpc/methods/worktree.test.ts": 1713, - "src/main/runtime/rpc/mobile-auth-acl-critical-path.test.ts": 835, - "src/main/runtime/rpc/mobile-clipboard-image-provenance.test.ts": 72, - "src/main/runtime/rpc/mobile-e2ee-outbound-memory-budget.test.ts": 52, - "src/main/runtime/rpc/mobile-e2ee-v2-desktop-session.test.ts": 102, - "src/main/runtime/rpc/mobile-e2ee-v2-key-schedule.test.ts": 103, - "src/main/runtime/rpc/mobile-socket-wiring.test.ts": 402, - "src/main/runtime/rpc/orchestration-11745-regression-verification.test.ts": 11255, - "src/main/runtime/rpc/orchestration-caller-show.test.ts": 5152, - "src/main/runtime/rpc/orchestration-check-wait-session.test.ts": 5368, - "src/main/runtime/rpc/orchestration-commit-notify-characterization.test.ts": 6014, - "src/main/runtime/rpc/orchestration-contract-fence.test.ts": 5967, - "src/main/runtime/rpc/orchestration-current-authority-precedence.test.ts": 5673, - "src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher.test.ts": 5854, - "src/main/runtime/rpc/orchestration-legacy-coordinator-race.test.ts": 6448, - "src/main/runtime/rpc/orchestration-legacy-fence-jurisdiction.test.ts": 5939, - "src/main/runtime/rpc/orchestration-legacy-question-takeover.test.ts": 5215, - "src/main/runtime/rpc/orchestration-legacy-run-routing.test.ts": 5130, - "src/main/runtime/rpc/orchestration-legacy-session-address-migration.test.ts": 5254, - "src/main/runtime/rpc/orchestration-legacy-takeover-current-authority.test.ts": 4232, - "src/main/runtime/rpc/orchestration-legacy-takeover-delivery.test.ts": 5733, - "src/main/runtime/rpc/orchestration-legacy-takeover-dispatcher.test.ts": 5702, - "src/main/runtime/rpc/orchestration-mutation-executor.test.ts": 5127, - "src/main/runtime/rpc/orchestration-mutation-ledger.test.ts": 5979, - "src/main/runtime/rpc/orchestration-mutation-request-show-legacy.test.ts": 5437, - "src/main/runtime/rpc/orchestration-mutation-request-show.test.ts": 5333, - "src/main/runtime/rpc/orchestration-party-addressing.test.ts": 5971, - "src/main/runtime/rpc/orchestration-party-agreement.test.ts": 5225, - "src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts": 5613, - "src/main/runtime/rpc/orchestration-session-caller.test.ts": 5634, - "src/main/runtime/rpc/orchestration-session-coordinator.test.ts": 6501, - "src/main/runtime/rpc/orchestration-session-recipient.test.ts": 5755, - "src/main/runtime/rpc/orchestration-session-run-binding.test.ts": 5278, - "src/main/runtime/rpc/orchestration-task-dispatch-invariant.test.ts": 5900, - "src/main/runtime/rpc/relay-transport.test.ts": 193, - "src/main/runtime/rpc/remote-runtime-server-heartbeat-missed-probe-tolerance.test.ts": 67, - "src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts": 82, - "src/main/runtime/rpc/rpc-caller-scope.test.ts": 5768, - "src/main/runtime/rpc/runtime-client-capabilities.test.ts": 40, - "src/main/runtime/rpc/runtime-close-attribution-topology.test.ts": 565, - "src/main/runtime/rpc/runtime-close-attribution.test.ts": 937, - "src/main/runtime/rpc/runtime-source-admission.test.ts": 4337, - "src/main/runtime/rpc/schemas.test.ts": 1500, - "src/main/runtime/rpc/static-web-client-handler-stream.test.ts": 1767, - "src/main/runtime/rpc/streaming.test.ts": 690, - "src/main/runtime/rpc/terminal-agent-prompt-send.test.ts": 611, - "src/main/runtime/rpc/terminal-agent-send-guard.test.ts": 612, - "src/main/runtime/rpc/terminal-geometry-stale-handle.test.ts": 706, - "src/main/runtime/rpc/terminal-lease-stale-handle-survival.test.ts": 861, - "src/main/runtime/rpc/terminal-list-host-scope-transport.test.ts": 531, - "src/main/runtime/rpc/terminal-multiplex-ack-output-budget.test.ts": 1151, - "src/main/runtime/rpc/terminal-multiplex-ack-overflow-recovery-history.test.ts": 703, - "src/main/runtime/rpc/terminal-multiplex-ack-overflow-recovery.test.ts": 2045, - "src/main/runtime/rpc/terminal-multiplex-desktop-resize-routing.test.ts": 1317, - "src/main/runtime/rpc/terminal-multiplex-end-verdict.test.ts": 3540, - "src/main/runtime/rpc/terminal-multiplex-escape-tail.test.ts": 633, - "src/main/runtime/rpc/terminal-multiplex-flow-control.bench.test.ts": 90, - "src/main/runtime/rpc/terminal-multiplex-initial-snapshot-buffering.test.ts": 945, - "src/main/runtime/rpc/terminal-multiplex-input-sequence-replay.test.ts": 1902, - "src/main/runtime/rpc/terminal-multiplex-input-write-rejection.test.ts": 1429, - "src/main/runtime/rpc/terminal-multiplex-output-pause-and-viewport.test.ts": 1047, - "src/main/runtime/rpc/terminal-multiplex-pty-wait-capacity.test.ts": 1137, - "src/main/runtime/rpc/terminal-multiplex-resync-replay-trim.test.ts": 641, - "src/main/runtime/rpc/terminal-multiplex-round-robin.test.ts": 49, - "src/main/runtime/rpc/terminal-multiplex-snapshot-serialization.test.ts": 1278, - "src/main/runtime/rpc/terminal-multiplex-subscribe-slot-recovery.test.ts": 790, - "src/main/runtime/rpc/terminal-opencode-send-guard.integration.test.ts": 4307, - "src/main/runtime/rpc/terminal-output-batching.test.ts": 965, - "src/main/runtime/rpc/terminal-output-frame-chunks.test.ts": 66, - "src/main/runtime/rpc/terminal-prompt-delivery-receipt.test.ts": 6663, - "src/main/runtime/rpc/terminal-provider-snapshot-sequence.test.ts": 807, - "src/main/runtime/rpc/terminal-requested-snapshot-unavailability.test.ts": 1018, - "src/main/runtime/rpc/terminal-send-launch-draft-resolution.test.ts": 543, - "src/main/runtime/rpc/terminal-send.test.ts": 832, - "src/main/runtime/rpc/terminal-stream-byte-length.test.ts": 450, - "src/main/runtime/rpc/terminal-subscribe-admission.test.ts": 5270, - "src/main/runtime/rpc/terminal-subscribe-blank-mount.test.ts": 685, - "src/main/runtime/rpc/terminal-subscribe-buffer.test.ts": 1142, - "src/main/runtime/rpc/terminal-subscribe-lease-only.test.ts": 734, - "src/main/runtime/rpc/terminal-subscribe-mount-replay.test.ts": 844, - "src/main/runtime/rpc/terminal-subscribe-mounted-pane.test.ts": 631, - "src/main/runtime/rpc/terminal-subscribe-ownership.test.ts": 1030, - "src/main/runtime/rpc/terminal-subscribe-reconnect-rebind.test.ts": 792, - "src/main/runtime/rpc/terminal-subscribe-relay-drop-lease-survival.test.ts": 4313, - "src/main/runtime/rpc/terminal-subscribe-renderer-recovery-output.test.ts": 674, - "src/main/runtime/rpc/unix-socket-transport.test.ts": 86, - "src/main/runtime/rpc/unpaired-device-auth-throttle.test.ts": 45, - "src/main/runtime/rpc/worktree-catalog-snapshot.test.ts": 64, - "src/main/runtime/rpc/worktree-removal-marker-projection.test.ts": 289, - "src/main/runtime/rpc/ws-empty-payload-frame-write.test.ts": 118, - "src/main/runtime/rpc/ws-fallback-port-store.test.ts": 51, - "src/main/runtime/rpc/ws-transport-accept-order.test.ts": 78, - "src/main/runtime/rpc/ws-transport-static-web.test.ts": 99, - "src/main/runtime/rpc/ws-transport-transient-packet-loss.test.ts": 691, - "src/main/runtime/rpc/ws-transport.test.ts": 1073, - "src/main/runtime/run-terminal-shell-alone.node-pty.test.ts": 2371, - "src/main/runtime/run-terminal-shell-alone.test.ts": 114, - "src/main/runtime/runtime-agent-row-main-agent-projection.test.ts": 94, - "src/main/runtime/runtime-binary-message-router.test.ts": 49, - "src/main/runtime/runtime-browser-client-automation.test.ts": 301, - "src/main/runtime/runtime-browser-client-page-adoption.test.ts": 275, - "src/main/runtime/runtime-browser-client-page-creation.test.ts": 254, - "src/main/runtime/runtime-browser-client-page-recovery.test.ts": 302, - "src/main/runtime/runtime-browser-client-page-restored-recovery.test.ts": 237, - "src/main/runtime/runtime-browser-network-execution-host.test.ts": 45, - "src/main/runtime/runtime-browser-page-registry.test.ts": 55, - "src/main/runtime/runtime-client-settings-linear-team-projection.test.ts": 776, - "src/main/runtime/runtime-client-settings-minimax-projection.test.ts": 568, - "src/main/runtime/runtime-client-settings-source-control-projection.test.ts": 645, - "src/main/runtime/runtime-client-settings-terminal-copy-projection.test.ts": 605, - "src/main/runtime/runtime-created-worktree-terminal-target.test.ts": 4338, - "src/main/runtime/runtime-extraction-regressions.test.ts": 4478, - "src/main/runtime/runtime-failed-local-worktree-removal.test.ts": 723, - "src/main/runtime/runtime-file-listing-producer-budgets.test.ts": 528, - "src/main/runtime/runtime-file-target-execution-host.test.ts": 4300, - "src/main/runtime/runtime-file-upload-decode.test.ts": 421, - "src/main/runtime/runtime-folder-workspace.test.ts": 65, - "src/main/runtime/runtime-folder-worktree-create.test.ts": 132, - "src/main/runtime/runtime-git-api-contract.test.ts": 982, - "src/main/runtime/runtime-git-branch-compare-admission.test.ts": 525, - "src/main/runtime/runtime-git-command-target.test.ts": 630, - "src/main/runtime/runtime-git-conflict-operation-routing.test.ts": 561, - "src/main/runtime/runtime-git-execution-host-ownership.test.ts": 959, - "src/main/runtime/runtime-git-generation-admission.test.ts": 551, - "src/main/runtime/runtime-git-stage-worktree-scope.test.ts": 959, - "src/main/runtime/runtime-git-status-admission.test.ts": 1309, - "src/main/runtime/runtime-git-sync-commands.test.ts": 995, - "src/main/runtime/runtime-git-target-execution-host.test.ts": 5048, - "src/main/runtime/runtime-graph-reload-lifecycle.test.ts": 51, - "src/main/runtime/runtime-hook-agent-row-selection.test.ts": 99, - "src/main/runtime/runtime-interrupted-local-worktree-removal.test.ts": 1473, - "src/main/runtime/runtime-legacy-worker-missing-workspace-recovery.test.ts": 81, - "src/main/runtime/runtime-legacy-worker-recovery-lifetime.test.ts": 150, - "src/main/runtime/runtime-legacy-worker-terminal-recovery-persistence.test.ts": 399, - "src/main/runtime/runtime-legacy-worker-terminal-recovery-retry.test.ts": 96, - "src/main/runtime/runtime-linear-read-commands.test.ts": 161, - "src/main/runtime/runtime-local-create-rearm-ordering.test.ts": 4256, - "src/main/runtime/runtime-local-worktree-create.test.ts": 588, - "src/main/runtime/runtime-local-worktree-materialization.test.ts": 130, - "src/main/runtime/runtime-local-worktree-setup.test.ts": 373, - "src/main/runtime/runtime-local-worktree-terminal-startup.test.ts": 81, - "src/main/runtime/runtime-machine-name.test.ts": 78, - "src/main/runtime/runtime-managed-worktree-metadata-sweep.test.ts": 1804, - "src/main/runtime/runtime-managed-worktree-metadata.test.ts": 94, - "src/main/runtime/runtime-managed-worktree-queries.test.ts": 543, - "src/main/runtime/runtime-metadata-ownership-watch.test.ts": 116, - "src/main/runtime/runtime-metadata.test.ts": 380, - "src/main/runtime/runtime-mobile-agent-status-builder.test.ts": 91, - "src/main/runtime/files/search/mobile-path-search-expiry.test.ts": 223, - "src/main/runtime/files/search/mobile-path-search.test.ts": 75, - "src/main/runtime/runtime-mobile-session-incarnation-projection.test.ts": 80, - "src/main/runtime/runtime-orchestration-sender-names.test.ts": 948, - "src/main/runtime/runtime-owned-terminal-publication-lineage.test.ts": 5659, - "src/main/runtime/runtime-pairing-desktop-control-grant.test.ts": 5025, - "src/main/runtime/runtime-project-group-controller-folder-create.test.ts": 57, - "src/main/runtime/runtime-project-group-controller-folder-delete.test.ts": 56, - "src/main/runtime/runtime-project-host-setup-controller.test.ts": 410, - "src/main/runtime/runtime-reference-agents.test.ts": 274, + "src/main/runtime/rpc/methods/native-chat.test.ts": 274, + "src/main/runtime/rpc/methods/notification-preferences.test.ts": 272, + "src/main/runtime/rpc/methods/notification-reconnect-cooldown.test.ts": 281, + "src/main/runtime/rpc/methods/orcad-migration.test.ts": 2684, + "src/main/runtime/rpc/methods/orcad-terminal-census.test.ts": 213, + "src/main/runtime/rpc/methods/orchestration-dispatch-error-codes.test.ts": 6440, + "src/main/runtime/rpc/methods/orchestration-structured-worker-abandon.test.ts": 4782, + "src/main/runtime/rpc/methods/orchestration-structured-worker-lifecycle.test.ts": 473, + "src/main/runtime/rpc/methods/orchestration-structured-worker-redrive.test.ts": 5450, + "src/main/runtime/rpc/methods/orchestration-structured-worker-session.test.ts": 546, + "src/main/runtime/rpc/methods/orchestration-structured-worker-start-failure.test.ts": 1492, + "src/main/runtime/rpc/methods/orchestration-worker-mode-opacity.test.ts": 6174, + "src/main/runtime/rpc/methods/orchestration-worker-release-incarnation-fallback.test.ts": 5582, + "src/main/runtime/rpc/methods/orchestration-worker-start-mode-selection.test.ts": 6301, + "src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts": 1106, + "src/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.ts": 1227, + "src/main/runtime/rpc/methods/orchestration-worker-start-registered-agent.test.ts": 1237, + "src/main/runtime/rpc/methods/orchestration-worker-support-unknown.test.ts": 1193, + "src/main/runtime/rpc/methods/orchestration/cli-runtime-boundary.test.ts": 5949, + "src/main/runtime/rpc/methods/orchestration/federation/federated-attach-receipt.test.ts": 45, + "src/main/runtime/rpc/methods/orchestration/federation/federated-fleet-snapshot.test.ts": 463, + "src/main/runtime/rpc/methods/orchestration/federation/federated-message-targeting.test.ts": 5468, + "src/main/runtime/rpc/methods/orchestration/federation/federated-release-safety.test.ts": 4364, + "src/main/runtime/rpc/methods/orchestration/federation/federated-transport-safety.test.ts": 4461, + "src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.test.ts": 1205, + "src/main/runtime/rpc/methods/orchestration/federation/federated-worker-start-receipt.test.ts": 5806, + "src/main/runtime/rpc/methods/orchestration/federation/federation-agent-launch.test.ts": 5149, + "src/main/runtime/rpc/methods/orchestration/federation/federation-control-mail.test.ts": 6085, + "src/main/runtime/rpc/methods/orchestration/federation/federation-effects.test.ts": 27, + "src/main/runtime/rpc/methods/orchestration/federation/federation-folder-placement.test.ts": 5796, + "src/main/runtime/rpc/methods/orchestration/federation/federation-launched-agent-readiness.test.ts": 5382, + "src/main/runtime/rpc/methods/orchestration/federation/federation-lifecycle-settlement.test.ts": 7705, + "src/main/runtime/rpc/methods/orchestration/federation/federation-liveness-verdict.test.ts": 5800, + "src/main/runtime/rpc/methods/orchestration/federation/federation-output.test.ts": 7774, + "src/main/runtime/rpc/methods/orchestration/federation/federation-setup.test.ts": 4269, + "src/main/runtime/rpc/methods/orchestration/federation/federation-start-prompt-budget.test.ts": 4274, + "src/main/runtime/rpc/methods/orchestration/federation/federation-start-schema.test.ts": 204, + "src/main/runtime/rpc/methods/orchestration/federation/federation-worker-brief-agent-in-front.test.ts": 5603, + "src/main/runtime/rpc/methods/orchestration/federation/federation.test.ts": 8450, + "src/main/runtime/rpc/methods/orchestration/gates/gate-run-authorization.test.ts": 7032, + "src/main/runtime/rpc/methods/orchestration/gates/gates.test.ts": 4535, + "src/main/runtime/rpc/methods/orchestration/messaging/ask.test.ts": 4467, + "src/main/runtime/rpc/methods/orchestration/messaging/check-worker-consumer-fencing.test.ts": 4128, + "src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts": 5193, + "src/main/runtime/rpc/methods/orchestration/messaging/check.test.ts": 4820, + "src/main/runtime/rpc/methods/orchestration/messaging/recipient-routing.test.ts": 4865, + "src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-history.test.ts": 4739, + "src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-interleavings.test.ts": 4788, + "src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-safety.test.ts": 4888, + "src/main/runtime/rpc/methods/orchestration/messaging/send-dispatch-authority.test.ts": 4634, + "src/main/runtime/rpc/methods/orchestration/messaging/send-group.test.ts": 5287, + "src/main/runtime/rpc/methods/orchestration/messaging/send-invalid-type.test.ts": 4022, + "src/main/runtime/rpc/methods/orchestration/messaging/send.test.ts": 4934, + "src/main/runtime/rpc/methods/orchestration/runs/migration-behavior.test.ts": 5974, + "src/main/runtime/rpc/methods/orchestration/runs/run-receipt.test.ts": 288, + "src/main/runtime/rpc/methods/orchestration/runs/runs.test.ts": 4221, + "src/main/runtime/rpc/methods/orchestration/runs/tasks-dispatch.test.ts": 4612, + "src/main/runtime/rpc/methods/orchestration/worker/agent-status-producer-census.test.ts": 6840, + "src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts": 4444, + "src/main/runtime/rpc/methods/orchestration/worker/failed-start-retry-release.test.ts": 5155, + "src/main/runtime/rpc/methods/orchestration/worker/fleet-status-observed-identity.test.ts": 2855, + "src/main/runtime/rpc/methods/orchestration/worker/fleet-status-terminal-identity.test.ts": 3069, + "src/main/runtime/rpc/methods/orchestration/worker/manual-dispatch-observation.test.ts": 5231, + "src/main/runtime/rpc/methods/orchestration/worker/manual-dispatch-release.test.ts": 6413, + "src/main/runtime/rpc/methods/orchestration/worker/structured-worker-launch-seed-options.test.ts": 308, + "src/main/runtime/rpc/methods/orchestration/worker/worker-interactive-wait.test.ts": 5356, + "src/main/runtime/rpc/methods/orchestration/worker/worker-launch-preferences.test.ts": 294, + "src/main/runtime/rpc/methods/orchestration/worker/worker-list-pagination.test.ts": 5228, + "src/main/runtime/rpc/methods/orchestration/worker/worker-list-run-scope-rpc.test.ts": 4762, + "src/main/runtime/rpc/methods/orchestration/worker/worker-observation.test.ts": 426, + "src/main/runtime/rpc/methods/orchestration/worker/worker-output.test.ts": 582, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-archive.test.ts": 4438, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-inventory.test.ts": 4297, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-liveness-verdict.test.ts": 716, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-mobile-report.test.ts": 4344, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-ownership-guard.test.ts": 4874, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-recovery.test.ts": 5609, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-runtime-incarnation.test.ts": 4203, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release.test.ts": 5571, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-agent-placement-teardown.test.ts": 1262, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-budgets.test.ts": 31, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-launch-executor-parity.test.ts": 1242, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-outcome-classification.test.ts": 444, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-budget.test.ts": 100, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-contract.test.ts": 6233, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-readiness-settlement.test.ts": 432, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-terminal-target.test.ts": 5653, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-turn-observation.test.ts": 50, + "src/main/runtime/rpc/methods/orchestration/worker/worker-stop-capability.test.ts": 471, + "src/main/runtime/rpc/methods/orchestration/worker/worker-stop-exit-race.test.ts": 4485, + "src/main/runtime/rpc/methods/orchestration/worker/worker-stop-liveness-verdict.test.ts": 5582, + "src/main/runtime/rpc/methods/orchestration/worker/worker-terminal-custody-at-creation.test.ts": 5330, + "src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts": 6057, + "src/main/runtime/rpc/methods/orchestration/worker/workers-recovery.test.ts": 5706, + "src/main/runtime/rpc/methods/paired-caller-host-id.test.ts": 49, + "src/main/runtime/rpc/methods/pairing.test.ts": 4388, + "src/main/runtime/rpc/methods/plugins.test.ts": 420, + "src/main/runtime/rpc/methods/preflight.test.ts": 576, + "src/main/runtime/rpc/methods/project-host-setup-self-host-stamp.test.ts": 278, + "src/main/runtime/rpc/methods/repo-badge-color.test.ts": 674, + "src/main/runtime/rpc/methods/repo.test.ts": 4446, + "src/main/runtime/rpc/methods/runtime-client-capabilities.test.ts": 688, + "src/main/runtime/rpc/methods/session-tab-agent-capability-mutations.test.ts": 1495, + "src/main/runtime/rpc/methods/session-tab-agent-status-projection.test.ts": 195, + "src/main/runtime/rpc/methods/session-tab-browser-placement-mutations.test.ts": 1426, + "src/main/runtime/rpc/methods/session-tab-browser-placement-projection.test.ts": 247, + "src/main/runtime/rpc/methods/session-tab-registered-agent-projection.test.ts": 356, + "src/main/runtime/rpc/methods/session-tabs-inventory-census-race.test.ts": 4614, + "src/main/runtime/rpc/methods/session-tabs-inventory-rpc.test.ts": 4621, + "src/main/runtime/rpc/methods/session-tabs-move-validation.test.ts": 4095, + "src/main/runtime/rpc/methods/session-tabs-pane-layout-intent.test.ts": 1502, + "src/main/runtime/rpc/methods/session-tabs-retirement-proof-delta.test.ts": 1394, + "src/main/runtime/rpc/methods/session-tabs-schemas.test.ts": 261, + "src/main/runtime/rpc/methods/session-tabs-structured-restore.test.ts": 1543, + "src/main/runtime/rpc/methods/session-tabs-subscribe-admission.test.ts": 1595, + "src/main/runtime/rpc/methods/session-tabs-unsubscribe.test.ts": 1385, + "src/main/runtime/rpc/methods/session-tabs.test.ts": 1498, + "src/main/runtime/rpc/methods/skills.test.ts": 344, + "src/main/runtime/rpc/methods/speech.test.ts": 540, + "src/main/runtime/rpc/methods/ssh-target-management.test.ts": 4753, + "src/main/runtime/rpc/methods/ssh.test.ts": 692, + "src/main/runtime/rpc/methods/status-machine-name-publish-budget.test.ts": 4290, + "src/main/runtime/rpc/methods/status.test.ts": 4333, + "src/main/runtime/rpc/methods/structured-agent-session-admission.test.ts": 2947, + "src/main/runtime/rpc/methods/structured-agent-session-adoption-replay.test.ts": 5335, + "src/main/runtime/rpc/methods/structured-agent-session-agents.test.ts": 1228, + "src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts": 7709, + "src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts": 3379, + "src/main/runtime/rpc/methods/structured-agent-session-attention-ack.test.ts": 3131, + "src/main/runtime/rpc/methods/structured-agent-session-background-task-capability.test.ts": 2901, + "src/main/runtime/rpc/methods/structured-agent-session-bounded-frames-rpc.test.ts": 3127, + "src/main/runtime/rpc/methods/structured-agent-session-conversation-outline.test.ts": 3215, + "src/main/runtime/rpc/methods/structured-agent-session-create-message.test.ts": 1984, + "src/main/runtime/rpc/methods/structured-agent-session-create-tab-publication.test.ts": 266, + "src/main/runtime/rpc/methods/structured-agent-session-hold.test.ts": 6384, + "src/main/runtime/rpc/methods/structured-agent-session-optional-model-capability.test.ts": 2992, + "src/main/runtime/rpc/methods/structured-agent-session-options-read.test.ts": 3254, + "src/main/runtime/rpc/methods/structured-agent-session-options-workspace-scope.test.ts": 5673, + "src/main/runtime/rpc/methods/structured-agent-session-permission-create.test.ts": 2743, + "src/main/runtime/rpc/methods/structured-agent-session-pi-dialog-capability.test.ts": 3102, + "src/main/runtime/rpc/methods/structured-agent-session-policy.test.ts": 231, + "src/main/runtime/rpc/methods/structured-agent-session-precommit-refusal.test.ts": 791, + "src/main/runtime/rpc/methods/structured-agent-session-queued-edit.test.ts": 3203, + "src/main/runtime/rpc/methods/structured-agent-session-respond-params.test.ts": 3517, + "src/main/runtime/rpc/methods/structured-agent-session-restart-dismiss-fence.test.ts": 3145, + "src/main/runtime/rpc/methods/structured-agent-session-restart-resume.test.ts": 3513, + "src/main/runtime/rpc/methods/structured-agent-session-restart-unregistered-agent.test.ts": 4737, + "src/main/runtime/rpc/methods/structured-agent-session-send-compatibility.test.ts": 3326, + "src/main/runtime/rpc/methods/structured-agent-session-turn-completion-stream.test.ts": 3315, + "src/main/runtime/rpc/methods/structured-agent-session-turn-item-capability.test.ts": 3252, + "src/main/runtime/rpc/methods/structured-agent-session-unsubscribe.test.ts": 4980, + "src/main/runtime/rpc/methods/structured-agent-session-visual.test.ts": 6517, + "src/main/runtime/rpc/methods/structured-agent-session.test.ts": 3051, + "src/main/runtime/rpc/methods/structured-chat-tab-table.test.ts": 7243, + "src/main/runtime/rpc/methods/structured-worker-read-cursor.test.ts": 419, + "src/main/runtime/rpc/methods/structured-worker-stop-receipt.test.ts": 5551, + "src/main/runtime/rpc/methods/structured-worker-tab-retirement.test.ts": 4638, + "src/main/runtime/rpc/methods/task-provider-identity.test.ts": 258, + "src/main/runtime/rpc/methods/terminal-create-idempotency.test.ts": 1464, + "src/main/runtime/rpc/methods/terminal-legacy-stream-id-characterization.test.ts": 1602, + "src/main/runtime/rpc/methods/terminal-manifest-characterization.test.ts": 1262, + "src/main/runtime/rpc/methods/terminal-navigation.test.ts": 1520, + "src/main/runtime/rpc/methods/terminal-read-screen-cursor.test.ts": 1340, + "src/main/runtime/rpc/methods/terminal-stream-extraction-characterization.test.ts": 61, + "src/main/runtime/rpc/methods/terminal/terminal-input-sequence-ledger.test.ts": 68, + "src/main/runtime/rpc/methods/terminal/terminal-inspect-process-params.test.ts": 232, + "src/main/runtime/rpc/methods/terminal/terminal-snapshot-json-byte-budget.test.ts": 187, + "src/main/runtime/rpc/methods/terminal/terminal-snapshot-scrollback-rows.test.ts": 47, + "src/main/runtime/rpc/methods/terminal/terminal-subscription-registration.test.ts": 100, + "src/main/runtime/rpc/methods/terminal/terminal-viewer-colors-method.test.ts": 1541, + "src/main/runtime/rpc/methods/updater.test.ts": 241, + "src/main/runtime/rpc/methods/workspace-cleanup-ui-schema.test.ts": 273, + "src/main/runtime/rpc/methods/workspace-layout.test.ts": 3590, + "src/main/runtime/rpc/methods/workspace-ports.test.ts": 535, + "src/main/runtime/rpc/methods/worktree-catalog-snapshot-method.test.ts": 1579, + "src/main/runtime/rpc/methods/worktree-catalog-version-stamp.test.ts": 1698, + "src/main/runtime/rpc/methods/worktree-create-args.test.ts": 310, + "src/main/runtime/rpc/methods/worktree-create-launch-source.test.ts": 1894, + "src/main/runtime/rpc/methods/worktree-create-navigation.test.ts": 1814, + "src/main/runtime/rpc/methods/worktree-create-setup-choice.test.ts": 320, + "src/main/runtime/rpc/methods/worktree-create-startup-agent.test.ts": 1603, + "src/main/runtime/rpc/methods/worktree-github-pr-suppression.test.ts": 1770, + "src/main/runtime/rpc/methods/worktree-missing-terminal-teardown.test.ts": 2004, + "src/main/runtime/rpc/methods/worktree-retired-names.test.ts": 1603, + "src/main/runtime/rpc/methods/worktree-rm-background-reply.test.ts": 1709, + "src/main/runtime/rpc/methods/worktree-rm-host-qualification.test.ts": 1665, + "src/main/runtime/rpc/methods/worktree-rm-pty-waiver.test.ts": 1841, + "src/main/runtime/rpc/methods/worktree-schemas.test.ts": 369, + "src/main/runtime/rpc/methods/worktree.test.ts": 1682, + "src/main/runtime/rpc/mobile-auth-acl-critical-path.test.ts": 499, + "src/main/runtime/rpc/mobile-clipboard-image-provenance.test.ts": 45, + "src/main/runtime/rpc/mobile-e2ee-auth-classification.test.ts": 204, + "src/main/runtime/rpc/mobile-e2ee-outbound-memory-budget.test.ts": 46, + "src/main/runtime/rpc/mobile-e2ee-v2-desktop-session.test.ts": 81, + "src/main/runtime/rpc/mobile-socket-wiring.test.ts": 488, + "src/main/runtime/rpc/orchestration-11745-regression-verification.test.ts": 7446, + "src/main/runtime/rpc/orchestration-caller-show.test.ts": 5080, + "src/main/runtime/rpc/orchestration-check-wait-session.test.ts": 5916, + "src/main/runtime/rpc/orchestration-commit-notify-characterization.test.ts": 5082, + "src/main/runtime/rpc/orchestration-contract-fence.test.ts": 5716, + "src/main/runtime/rpc/orchestration-current-authority-precedence.test.ts": 5533, + "src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher.test.ts": 5911, + "src/main/runtime/rpc/orchestration-legacy-coordinator-race.test.ts": 11342, + "src/main/runtime/rpc/orchestration-legacy-fence-jurisdiction.test.ts": 4933, + "src/main/runtime/rpc/orchestration-legacy-question-takeover.test.ts": 5765, + "src/main/runtime/rpc/orchestration-legacy-run-routing.test.ts": 5826, + "src/main/runtime/rpc/orchestration-legacy-session-address-migration.test.ts": 5520, + "src/main/runtime/rpc/orchestration-legacy-takeover-current-authority.test.ts": 4786, + "src/main/runtime/rpc/orchestration-legacy-takeover-delivery.test.ts": 6856, + "src/main/runtime/rpc/orchestration-legacy-takeover-dispatcher.test.ts": 6087, + "src/main/runtime/rpc/orchestration-mutation-executor.test.ts": 5308, + "src/main/runtime/rpc/orchestration-mutation-ledger.test.ts": 5880, + "src/main/runtime/rpc/orchestration-mutation-request-show-legacy.test.ts": 6000, + "src/main/runtime/rpc/orchestration-mutation-request-show.test.ts": 5473, + "src/main/runtime/rpc/orchestration-party-addressing.test.ts": 6675, + "src/main/runtime/rpc/orchestration-party-agreement.test.ts": 4678, + "src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts": 5101, + "src/main/runtime/rpc/orchestration-session-caller.test.ts": 5492, + "src/main/runtime/rpc/orchestration-session-coordinator.test.ts": 5828, + "src/main/runtime/rpc/orchestration-session-recipient.test.ts": 6145, + "src/main/runtime/rpc/orchestration-session-run-binding.test.ts": 5700, + "src/main/runtime/rpc/orchestration-task-dispatch-invariant.test.ts": 6327, + "src/main/runtime/rpc/relay-transport.test.ts": 196, + "src/main/runtime/rpc/remote-runtime-server-heartbeat-missed-probe-tolerance.test.ts": 55, + "src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts": 38, + "src/main/runtime/rpc/rpc-caller-scope.test.ts": 4947, + "src/main/runtime/rpc/runtime-client-capabilities.test.ts": 28, + "src/main/runtime/rpc/runtime-close-attribution-topology.test.ts": 1679, + "src/main/runtime/rpc/runtime-close-attribution.test.ts": 1401, + "src/main/runtime/rpc/runtime-source-admission.test.ts": 4516, + "src/main/runtime/rpc/schemas.test.ts": 1798, + "src/main/runtime/rpc/static-web-client-handler-stream.test.ts": 1818, + "src/main/runtime/rpc/streaming.test.ts": 1486, + "src/main/runtime/rpc/terminal-agent-prompt-send.test.ts": 1451, + "src/main/runtime/rpc/terminal-agent-send-guard.test.ts": 1417, + "src/main/runtime/rpc/terminal-geometry-stale-handle.test.ts": 1529, + "src/main/runtime/rpc/terminal-lease-stale-handle-survival.test.ts": 1722, + "src/main/runtime/rpc/terminal-list-host-scope-transport.test.ts": 1584, + "src/main/runtime/rpc/terminal-multiplex-ack-output-budget.test.ts": 1767, + "src/main/runtime/rpc/terminal-multiplex-ack-overflow-recovery-history.test.ts": 1667, + "src/main/runtime/rpc/terminal-multiplex-ack-overflow-recovery.test.ts": 2140, + "src/main/runtime/rpc/terminal-multiplex-desktop-resize-routing.test.ts": 2187, + "src/main/runtime/rpc/terminal-multiplex-end-verdict.test.ts": 3046, + "src/main/runtime/rpc/terminal-multiplex-escape-tail.test.ts": 1473, + "src/main/runtime/rpc/terminal-multiplex-flow-control.bench.test.ts": 40, + "src/main/runtime/rpc/terminal-multiplex-initial-snapshot-buffering.test.ts": 1757, + "src/main/runtime/rpc/terminal-multiplex-input-sequence-replay.test.ts": 2667, + "src/main/runtime/rpc/terminal-multiplex-input-write-rejection.test.ts": 2375, + "src/main/runtime/rpc/terminal-multiplex-output-pause-and-viewport.test.ts": 2491, + "src/main/runtime/rpc/terminal-multiplex-pty-wait-capacity.test.ts": 1780, + "src/main/runtime/rpc/terminal-multiplex-resync-replay-trim.test.ts": 1478, + "src/main/runtime/rpc/terminal-multiplex-round-robin.test.ts": 27, + "src/main/runtime/rpc/terminal-multiplex-snapshot-serialization.test.ts": 2130, + "src/main/runtime/rpc/terminal-multiplex-source-range-request.test.ts": 1729, + "src/main/runtime/rpc/terminal-multiplex-subscribe-slot-recovery.test.ts": 1673, + "src/main/runtime/rpc/terminal-opencode-send-guard.integration.test.ts": 4427, + "src/main/runtime/rpc/terminal-output-batching.test.ts": 1826, + "src/main/runtime/rpc/terminal-output-frame-chunks.test.ts": 61, + "src/main/runtime/rpc/terminal-prompt-delivery-receipt.test.ts": 6691, + "src/main/runtime/rpc/terminal-provider-snapshot-sequence.test.ts": 1648, + "src/main/runtime/rpc/terminal-requested-snapshot-unavailability.test.ts": 2084, + "src/main/runtime/rpc/terminal-send-launch-draft-resolution.test.ts": 1532, + "src/main/runtime/rpc/terminal-send.test.ts": 1621, + "src/main/runtime/rpc/terminal-stream-byte-length.test.ts": 509, + "src/main/runtime/rpc/terminal-subscribe-admission.test.ts": 5440, + "src/main/runtime/rpc/terminal-subscribe-blank-mount.test.ts": 1511, + "src/main/runtime/rpc/terminal-subscribe-buffer.test.ts": 1899, + "src/main/runtime/rpc/terminal-subscribe-lease-only.test.ts": 1700, + "src/main/runtime/rpc/terminal-subscribe-mount-replay.test.ts": 1691, + "src/main/runtime/rpc/terminal-subscribe-mounted-pane.test.ts": 1478, + "src/main/runtime/rpc/terminal-subscribe-ownership.test.ts": 1775, + "src/main/runtime/rpc/terminal-subscribe-reconnect-rebind.test.ts": 1448, + "src/main/runtime/rpc/terminal-subscribe-relay-drop-lease-survival.test.ts": 5109, + "src/main/runtime/rpc/terminal-subscribe-renderer-recovery-output.test.ts": 1550, + "src/main/runtime/rpc/unix-socket-transport.test.ts": 66, + "src/main/runtime/rpc/unpaired-device-auth-throttle.test.ts": 32, + "src/main/runtime/rpc/web-session-pairing.test.ts": 511, + "src/main/runtime/rpc/worktree-catalog-snapshot.test.ts": 45, + "src/main/runtime/rpc/worktree-removal-marker-projection.test.ts": 502, + "src/main/runtime/rpc/ws-empty-payload-frame-write.test.ts": 121, + "src/main/runtime/rpc/ws-fallback-port-store.test.ts": 40, + "src/main/runtime/rpc/ws-transport-accept-order.test.ts": 62, + "src/main/runtime/rpc/ws-transport-static-web.test.ts": 104, + "src/main/runtime/rpc/ws-transport-transient-packet-loss.test.ts": 655, + "src/main/runtime/rpc/ws-transport.test.ts": 999, + "src/main/runtime/run-terminal-shell-alone.node-pty.test.ts": 1526, + "src/main/runtime/run-terminal-shell-alone.test.ts": 116, + "src/main/runtime/runtime-agent-row-main-agent-projection.test.ts": 70, + "src/main/runtime/runtime-agent-terminal-startup.test.ts": 396, + "src/main/runtime/runtime-binary-message-router.test.ts": 33, + "src/main/runtime/runtime-browser-client-automation.test.ts": 286, + "src/main/runtime/runtime-browser-client-page-adoption.test.ts": 310, + "src/main/runtime/runtime-browser-client-page-creation.test.ts": 268, + "src/main/runtime/runtime-browser-client-page-recovery.test.ts": 269, + "src/main/runtime/runtime-browser-client-page-restored-recovery.test.ts": 195, + "src/main/runtime/runtime-browser-network-execution-host.test.ts": 44, + "src/main/runtime/runtime-browser-page-registry.test.ts": 53, + "src/main/runtime/runtime-client-agent-launch-settings-durability.test.ts": 1693, + "src/main/runtime/runtime-client-agent-launch-settings.test.ts": 423, + "src/main/runtime/runtime-client-settings-linear-team-projection.test.ts": 651, + "src/main/runtime/runtime-client-settings-minimax-projection.test.ts": 693, + "src/main/runtime/runtime-client-settings-source-control-projection.test.ts": 660, + "src/main/runtime/runtime-client-settings-terminal-copy-projection.test.ts": 708, + "src/main/runtime/runtime-created-worktree-terminal-target.test.ts": 5015, + "src/main/runtime/runtime-extraction-regressions.test.ts": 3812, + "src/main/runtime/runtime-failed-local-worktree-removal.test.ts": 975, + "src/main/runtime/runtime-file-listing-producer-budgets.test.ts": 756, + "src/main/runtime/runtime-file-target-execution-host.test.ts": 5186, + "src/main/runtime/runtime-file-upload-decode.test.ts": 419, + "src/main/runtime/runtime-folder-workspace.test.ts": 52, + "src/main/runtime/runtime-folder-worktree-create.test.ts": 111, + "src/main/runtime/runtime-git-api-contract.test.ts": 1210, + "src/main/runtime/runtime-git-branch-compare-admission.test.ts": 507, + "src/main/runtime/runtime-git-command-target.test.ts": 615, + "src/main/runtime/runtime-git-conflict-operation-routing.test.ts": 639, + "src/main/runtime/runtime-git-execution-host-ownership.test.ts": 1020, + "src/main/runtime/runtime-git-generation-admission.test.ts": 534, + "src/main/runtime/runtime-git-stage-worktree-scope.test.ts": 895, + "src/main/runtime/runtime-git-status-admission.test.ts": 1084, + "src/main/runtime/runtime-git-sync-commands.test.ts": 887, + "src/main/runtime/runtime-git-target-execution-host.test.ts": 4325, + "src/main/runtime/runtime-graph-mirror-capture.test.ts": 4691, + "src/main/runtime/runtime-graph-reload-lifecycle.test.ts": 36, + "src/main/runtime/runtime-hook-agent-row-selection.test.ts": 75, + "src/main/runtime/runtime-interrupted-local-worktree-removal.test.ts": 1818, + "src/main/runtime/runtime-leaf-record-projection.test.ts": 91, + "src/main/runtime/runtime-legacy-worker-missing-workspace-recovery.test.ts": 75, + "src/main/runtime/runtime-legacy-worker-recovery-lifetime.test.ts": 168, + "src/main/runtime/runtime-legacy-worker-terminal-recovery-persistence.test.ts": 838, + "src/main/runtime/runtime-legacy-worker-terminal-recovery-retry.test.ts": 99, + "src/main/runtime/runtime-linear-read-commands.test.ts": 471, + "src/main/runtime/runtime-local-create-rearm-ordering.test.ts": 4288, + "src/main/runtime/runtime-local-worktree-create.test.ts": 707, + "src/main/runtime/runtime-local-worktree-materialization.test.ts": 116, + "src/main/runtime/runtime-local-worktree-setup.test.ts": 364, + "src/main/runtime/runtime-local-worktree-terminal-startup.test.ts": 54, + "src/main/runtime/runtime-machine-name.test.ts": 60, + "src/main/runtime/runtime-managed-worktree-metadata-sweep.test.ts": 1835, + "src/main/runtime/runtime-managed-worktree-metadata-wsl-move.test.ts": 1423, + "src/main/runtime/runtime-managed-worktree-metadata.test.ts": 360, + "src/main/runtime/runtime-managed-worktree-queries.test.ts": 859, + "src/main/runtime/runtime-metadata-ownership-watch.test.ts": 122, + "src/main/runtime/runtime-metadata.test.ts": 428, + "src/main/runtime/runtime-migration-catalog-capabilities.test.ts": 5201, + "src/main/runtime/runtime-mobile-agent-status-builder.test.ts": 72, + "src/main/runtime/runtime-mobile-session-incarnation-projection.test.ts": 95, + "src/main/runtime/runtime-orchestration-sender-names.test.ts": 879, + "src/main/runtime/runtime-owned-terminal-publication-lineage.test.ts": 4992, + "src/main/runtime/runtime-pairing-desktop-control-grant.test.ts": 5698, + "src/main/runtime/runtime-project-group-controller-folder-create.test.ts": 450, + "src/main/runtime/runtime-project-group-controller-folder-delete.test.ts": 389, + "src/main/runtime/runtime-project-host-setup-controller.test.ts": 688, + "src/main/runtime/runtime-project-host-setup-publication.test.ts": 719, + "src/main/runtime/runtime-provisioned-root-adoption.test.ts": 520, + "src/main/runtime/runtime-reference-agents.test.ts": 279, "src/main/runtime/runtime-reference-catalog.test.ts": 97, - "src/main/runtime/runtime-reference-find.test.ts": 264, - "src/main/runtime/runtime-reference-integration.test.ts": 4869, - "src/main/runtime/runtime-remote-fetch-ref-maintenance.test.ts": 506, - "src/main/runtime/runtime-remove-project-host-scope.test.ts": 5466, - "src/main/runtime/runtime-repository-issue-command.test.ts": 769, - "src/main/runtime/runtime-repository-ref-queries.test.ts": 196, - "src/main/runtime/runtime-resolved-worktree-cache.test.ts": 53, - "src/main/runtime/runtime-rpc-browser-host-admission.test.ts": 5730, - "src/main/runtime/runtime-rpc-client-activity.test.ts": 5083, - "src/main/runtime/runtime-rpc-device-revocation.test.ts": 5464, - "src/main/runtime/runtime-rpc-dispatch-inject-long-poll.test.ts": 5309, - "src/main/runtime/runtime-rpc-long-poll-transport.test.ts": 10349, - "src/main/runtime/runtime-rpc-metadata-lifecycle.test.ts": 5255, - "src/main/runtime/runtime-rpc-mobile-method-allowlist.test.ts": 3159, - "src/main/runtime/runtime-rpc-mobile-terminal-streaming.test.ts": 5792, - "src/main/runtime/runtime-rpc-mobile-unknown-method-scope-refusal.test.ts": 3140, - "src/main/runtime/runtime-rpc-orchestration-db-migration.test.ts": 625, - "src/main/runtime/runtime-rpc-pairing-mode-persistence.test.ts": 5329, - "src/main/runtime/runtime-rpc-pairing-offer.test.ts": 4839, - "src/main/runtime/runtime-rpc-relay-pairing.test.ts": 6365, - "src/main/runtime/runtime-rpc-relay-provider-install.test.ts": 5568, - "src/main/runtime/runtime-rpc-request-authorization.test.ts": 6177, - "src/main/runtime/runtime-rpc-startup-failure.test.ts": 93, - "src/main/runtime/runtime-rpc-terminal-list.test.ts": 5576, - "src/main/runtime/runtime-rpc-websocket-bind-host.test.ts": 5192, - "src/main/runtime/runtime-rpc-websocket-fallback-port.test.ts": 5232, - "src/main/runtime/runtime-rpc-websocket-long-poll-caps.test.ts": 7485, - "src/main/runtime/runtime-rpc-websocket-reply-size-limit.test.ts": 5190, - "src/main/runtime/runtime-rpc-worktree-queries.test.ts": 5245, - "src/main/runtime/runtime-search-line-fragments.test.ts": 572, - "src/main/runtime/runtime-server-git-availability.test.ts": 44, - "src/main/runtime/runtime-session-search-settings.test.ts": 236, - "src/main/runtime/runtime-skill-install-authority.test.ts": 102, - "src/main/runtime/runtime-skill-install-commands.test.ts": 673, - "src/main/runtime/runtime-skill-install-queries.test.ts": 752, - "src/main/runtime/runtime-socket-sweep.test.ts": 50, - "src/main/runtime/runtime-subscription-registry.test.ts": 138, - "src/main/runtime/runtime-terminal-client-use.test.ts": 4273, - "src/main/runtime/runtime-terminal-idle-polls.test.ts": 404, - "src/main/runtime/runtime-terminal-orphan-topology-validation.test.ts": 58, - "src/main/runtime/runtime-terminal-spawn-push-target-materialization.test.ts": 77, - "src/main/runtime/runtime-terminal-writer.test.ts": 85, - "src/main/runtime/runtime-worktree-agent-rows-structured.test.ts": 650, - "src/main/runtime/runtime-worktree-agent-rows-verdict.test.ts": 1049, - "src/main/runtime/runtime-worktree-agent-sources.test.ts": 62, - "src/main/runtime/runtime-worktree-agent-startup.test.ts": 451, - "src/main/runtime/runtime-worktree-ps-summaries.test.ts": 93, - "src/main/runtime/runtime-worktree-ps-unverifiable-terminals.test.ts": 45, - "src/main/runtime/runtime-worktree-selection.test.ts": 62, - "src/main/runtime/runtime-worktree-startup-readiness.test.ts": 140, - "src/main/runtime/runtime-worktree-structured-agent-rows-liveness.test.ts": 1126, - "src/main/runtime/runtime-worktree-terminal-provisioning.test.ts": 67, - "src/main/runtime/saved-structured-agent-session-restoration.test.ts": 40, - "src/main/runtime/selected-review-branch.test.ts": 61, - "src/main/runtime/session-tabs-empty-worktree-publication.test.ts": 4763, - "src/main/runtime/session-tabs-inventory-publication.test.ts": 4384, - "src/main/runtime/settled-worker-process-replacement.test.ts": 5611, - "src/main/runtime/stale-title-completion-recovery.test.ts": 5144, - "src/main/runtime/stale-title-display-surfaces.test.ts": 5236, - "src/main/runtime/structured-agent-account-kind.test.ts": 58, - "src/main/runtime/structured-agent-create-support-opencode-rig.test.ts": 981, - "src/main/runtime/structured-agent-launch-args-wiring.test.ts": 1637, - "src/main/runtime/structured-agent-launch-support-probe-omp.test.ts": 978, - "src/main/runtime/structured-agent-launch-support-probe.test.ts": 1015, - "src/main/runtime/structured-agent-launch-support.test.ts": 958, - "src/main/runtime/structured-agent-model-catalog-discovery.test.ts": 987, - "src/main/runtime/structured-agent-model-catalog-first-frame.test.ts": 1196, - "src/main/runtime/structured-agent-runtime-registrations-acp.test.ts": 996, - "src/main/runtime/structured-agent-session-attachment-wiring.test.ts": 257, - "src/main/runtime/structured-agent-session-close.test.ts": 357, - "src/main/runtime/structured-agent-session-codex-compact-exit.test.ts": 3778, - "src/main/runtime/structured-agent-session-codex-interrupt-order.test.ts": 2081, - "src/main/runtime/structured-agent-session-codex-opening-send-hold.test.ts": 9062, - "src/main/runtime/structured-agent-session-codex-settlement-order.test.ts": 1766, - "src/main/runtime/structured-agent-session-codex-stopped-send-order.test.ts": 12290, - "src/main/runtime/structured-agent-session-codex-turn-end-settlement.test.ts": 5239, - "src/main/runtime/structured-agent-session-failure-logging.test.ts": 1791, - "src/main/runtime/structured-agent-session-held-signal.test.ts": 1930, - "src/main/runtime/structured-agent-session-host-refusal.test.ts": 5070, - "src/main/runtime/structured-agent-session-install-failure.test.ts": 1569, - "src/main/runtime/structured-agent-session-integration-replay.test.ts": 4482, - "src/main/runtime/structured-agent-session-integration.test.ts": 6902, - "src/main/runtime/structured-agent-session-legacy-send-behind-command.test.ts": 3540, - "src/main/runtime/structured-agent-session-lifecycle-delivery.test.ts": 205, - "src/main/runtime/structured-agent-session-mobile-attention.test.ts": 212, - "src/main/runtime/structured-agent-session-mobile-restore.test.ts": 1047, - "src/main/runtime/structured-agent-session-mobile-retirement.test.ts": 315, - "src/main/runtime/structured-agent-session-paired-caller.test.ts": 3386, - "src/main/runtime/structured-agent-session-rollback-compatibility.test.ts": 740, - "src/main/runtime/structured-agent-session-runtime-exit.test.ts": 1929, - "src/main/runtime/structured-agent-session-runtime-model-catalog-lifetime.test.ts": 1605, - "src/main/runtime/structured-agent-session-runtime-provider-started.test.ts": 2396, - "src/main/runtime/structured-agent-session-runtime-teardown.test.ts": 253, - "src/main/runtime/structured-agent-session-runtime.test.ts": 2060, - "src/main/runtime/structured-agent-session-spawn-token-descendants.test.ts": 1655, - "src/main/runtime/structured-agent-session-startup-reconcile.test.ts": 4757, - "src/main/runtime/structured-agent-session-startup-tab-restore.test.ts": 5325, - "src/main/runtime/structured-agent-session-support-probe.test.ts": 4686, - "src/main/runtime/structured-agent-session-tab-close-cause.test.ts": 51, - "src/main/runtime/structured-agent-shell-environment.test.ts": 91, - "src/main/runtime/structured-agent-stored-definitions.test.ts": 334, - "src/main/runtime/structured-chat-coordinator-mail-queue.test.ts": 7105, - "src/main/runtime/structured-chat-coordinator-mail.test.ts": 13367, - "src/main/runtime/structured-chat-dispatch-worker-lifecycle.test.ts": 5228, - "src/main/runtime/structured-chat-dispatch-worker-mail.test.ts": 6477, - "src/main/runtime/structured-chat-dispatch-worker.test.ts": 5004, - "src/main/runtime/structured-chat-naming-runtime.test.ts": 440, - "src/main/runtime/structured-chat-worker-start.test.ts": 4438, - "src/main/runtime/structured-child-records-switch.test.ts": 2097, - "src/main/runtime/structured-claude-attachment-grant.test.ts": 1678, - "src/main/runtime/structured-claude-auth-policy-wiring.test.ts": 1703, - "src/main/runtime/structured-claude-pending-rewind.test.ts": 1486, - "src/main/runtime/structured-claude-thinking-display-refusal.test.ts": 865, - "src/main/runtime/structured-codex-child-work-runtime.test.ts": 1600, - "src/main/runtime/structured-conversation-tab-title.test.ts": 38, - "src/main/runtime/structured-foreground-command-strip.test.ts": 2430, - "src/main/runtime/structured-session-child-identity-env.test.ts": 312, - "src/main/runtime/structured-session-cli-login-shell.test.ts": 334, - "src/main/runtime/structured-session-mail-redrive-wiring.test.ts": 4818, - "src/main/runtime/structured-session-worktree-teardown.test.ts": 452, - "src/main/runtime/structured-worker-agent-presence.test.ts": 345, - "src/main/runtime/structured-worker-at-rest.test.ts": 1193, - "src/main/runtime/structured-worker-authority.test.ts": 332, - "src/main/runtime/structured-worker-cleared-session.test.ts": 5252, - "src/main/runtime/structured-worker-hook-attestation.test.ts": 3190, - "src/main/runtime/structured-worker-identity.test.ts": 433, - "src/main/runtime/structured-worker-mail-routing.test.ts": 3023, - "src/main/runtime/structured-worker-takeover-pane-key.test.ts": 4488, - "src/main/runtime/structured-worker-terminal-read.test.ts": 2993, - "src/main/runtime/structured-worker-terminal-refusal.test.ts": 357, - "src/main/runtime/terminal-ansi-pending-retention.test.ts": 405, - "src/main/runtime/terminal-chat-owner-persistence.test.ts": 6950, - "src/main/runtime/terminal-close-observed-exit.test.ts": 4756, - "src/main/runtime/terminal-close-slow-agent-exit.test.ts": 6616, - "src/main/runtime/terminal-command-paint.test.ts": 56, - "src/main/runtime/terminal-focus-navigation-coalescer.test.ts": 66, - "src/main/runtime/terminal-foreground-group.test.ts": 78, - "src/main/runtime/terminal-identity-probe.test.ts": 50, - "src/main/runtime/terminal-intentional-stop-exit.test.ts": 4300, - "src/main/runtime/terminal-intentional-stops.test.ts": 42, - "src/main/runtime/terminal-interactive-wait-visibility.test.ts": 5553, - "src/main/runtime/terminal-leaf-tab-resolution.test.ts": 64, - "src/main/runtime/terminal-list-execution-host-scope.test.ts": 6664, - "src/main/runtime/terminal-list-payload-size.test.ts": 4739, - "src/main/runtime/terminal-list-pending-pty-registration.test.ts": 4304, - "src/main/runtime/terminal-list-stale-leaf-liveness.test.ts": 4787, - "src/main/runtime/terminal-list-surface-lost-orphan.test.ts": 4463, - "src/main/runtime/terminal-mobile-subscribe-tab-mount.test.ts": 3776, - "src/main/runtime/terminal-model-query-authority.test.ts": 60, - "src/main/runtime/terminal-orphan-owner.test.ts": 41, - "src/main/runtime/terminal-orphan-topology.test.ts": 52, - "src/main/runtime/terminal-pane-recovery-liveness-gate.test.ts": 5332, - "src/main/runtime/terminal-projection.test.ts": 54, - "src/main/runtime/terminal-pty-exit-waiter.test.ts": 4535, - "src/main/runtime/terminal-query-responder.test.ts": 15573, - "src/main/runtime/terminal-restore-record-seed.test.ts": 4386, - "src/main/runtime/terminal-retirement-async-durability.test.ts": 5424, - "src/main/runtime/terminal-retirement-proof-emitted-frame.test.ts": 5172, - "src/main/runtime/terminal-retirement-proof-publication.test.ts": 4203, - "src/main/runtime/terminal-run-facts-input.test.ts": 4384, - "src/main/runtime/terminal-run-facts.test.ts": 55, - "src/main/runtime/terminal-send-stale-leaf-liveness.test.ts": 4372, - "src/main/runtime/terminal-shell-override-host-support.test.ts": 55, - "src/main/runtime/terminal-subscribe-exit-waiter-leak.test.ts": 4589, - "src/main/runtime/terminal-subscriber-driven-daemon-attach.test.ts": 4236, - "src/main/runtime/terminal-surface-close.test.ts": 203, - "src/main/runtime/terminal-tail-buffer.test.ts": 6881, - "src/main/runtime/terminal-tail-redraw-row.equivalence.test.ts": 1562, - "src/main/runtime/terminal-tail-redraw-scaling.test.ts": 344, - "src/main/runtime/terminal-tail-redraw-spans.test.ts": 251, - "src/main/runtime/terminal-tail-redraw-wall-time.test.ts": 21650, - "src/main/runtime/terminal-tail-row-retention.test.ts": 1528, - "src/main/runtime/terminal-tail-sentinel-index.test.ts": 3542, - "src/main/runtime/terminal-tail-whitespace.test.ts": 276, - "src/main/runtime/terminal-vertical-control-scan.test.ts": 307, - "src/main/runtime/terminal-wait-detection.test.ts": 350, - "src/main/runtime/terminal-wait-name-only-idle.test.ts": 4243, - "src/main/runtime/terminal-wait-results.test.ts": 38, - "src/main/runtime/terminal-wait-tail-window.test.ts": 263, - "src/main/runtime/tui-idle-claude-task-wakeup.test.ts": 282, - "src/main/runtime/tui-idle-command-paint.test.ts": 4068, - "src/main/runtime/tui-idle-delivery-and-quiescence.test.ts": 5085, - "src/main/runtime/tui-idle-evidence-source.test.ts": 3277, - "src/main/runtime/tui-idle-evidence.test.ts": 374, - "src/main/runtime/tui-idle-hook-lane-runtime.test.ts": 7403, - "src/main/runtime/tui-idle-hook-lane.test.ts": 301, - "src/main/runtime/tui-idle-name-only-real-pty.integration.test.ts": 35459, - "src/main/runtime/tui-idle-weak-evidence-seam.test.ts": 3901, - "src/main/runtime/unreadable-secret-store-preservation.win32.test.ts": 54, - "src/main/runtime/wait-blocked-check-state.test.ts": 90, - "src/main/runtime/wait-blocked-keyword-carry-retention.test.ts": 3758, - "src/main/runtime/windows-default-route-interfaces.test.ts": 62, - "src/main/runtime/windows-drive-listing.test.ts": 54, - "src/main/runtime/windows-firewall-remote-scope.test.ts": 81, - "src/main/runtime/windows-mobile-firewall.test.ts": 99, - "src/main/runtime/workspace-layout-stream.test.ts": 257, - "src/main/runtime/workspace-session-membership-scaling.test.ts": 99, - "src/main/runtime/workspace-session-worktree-id.test.ts": 254, - "src/main/runtime/worktree-launch-host-repo.test.ts": 52, - "src/main/runtime/worktree-list-host-scope.test.ts": 1039, - "src/main/runtime/worktree-path-selector-wsl-posix.test.ts": 4715, - "src/main/runtime/worktree-ps-degraded-repo-scan.test.ts": 5316, - "src/main/runtime/worktree-ps-host-scope.test.ts": 4255, - "src/main/runtime/worktree-ps-unverifiable-terminals.test.ts": 4242, - "src/main/runtime/worktree-rm-id-selector-path-spelling.test.ts": 4532, - "src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts": 4765, - "src/main/runtime/worktree-scan-execution-host-routing.test.ts": 4500, - "src/main/runtime/worktree-scan-repeated-path.test.ts": 3815, - "src/main/runtime/worktree-teardown-unstopped-pty.test.ts": 554, - "src/main/runtime/worktree-teardown.test.ts": 863, - "src/main/runtime/worktree-terminal-mutation-lock.test.ts": 57, - "src/main/serve-update-handoff.app-environment.test.ts": 1475, - "src/main/serve-update-handoff.test.ts": 58, - "src/main/server/serve-readiness.test.ts": 69, - "src/main/server/serve-stdout-boundary.test.ts": 47, - "src/main/shell-prompt-readiness-probe.test.ts": 94, - "src/main/shell-startup-identity-scanner.test.ts": 43, - "src/main/shell-startup-output-scanner.test.ts": 84, - "src/main/shell-wrapper-content-address.test.ts": 56, - "src/main/shell-wrapper-generated-file-snapshot.test.ts": 118, - "src/main/sidecar-snapshot-file.test.ts": 59, - "src/main/skills/agent-skill-selection.test.ts": 238, - "src/main/skills/antigravity-skill-discovery.test.ts": 95, - "src/main/skills/claude-plugin-skill-sources-wsl.test.ts": 106, - "src/main/skills/claude-plugin-skill-sources.test.ts": 95, - "src/main/skills/discovery-filter-sharing.test.ts": 247, - "src/main/skills/discovery-source-filter-order.test.ts": 103, - "src/main/skills/discovery.test.ts": 187, + "src/main/runtime/runtime-reference-find.test.ts": 215, + "src/main/runtime/runtime-reference-integration.test.ts": 4784, + "src/main/runtime/runtime-remote-fetch-ref-maintenance.test.ts": 528, + "src/main/runtime/runtime-remove-project-host-scope.test.ts": 4900, + "src/main/runtime/runtime-repository-issue-command.test.ts": 489, + "src/main/runtime/runtime-repository-ref-queries.test.ts": 176, + "src/main/runtime/runtime-resolved-worktree-cache.test.ts": 40, + "src/main/runtime/runtime-rpc-browser-host-admission.test.ts": 4866, + "src/main/runtime/runtime-rpc-client-activity.test.ts": 5291, + "src/main/runtime/runtime-rpc-device-key-dispatch.test.ts": 3122, + "src/main/runtime/runtime-rpc-device-revocation.test.ts": 6347, + "src/main/runtime/runtime-rpc-dispatch-inject-long-poll.test.ts": 5541, + "src/main/runtime/runtime-rpc-long-poll-transport.test.ts": 9425, + "src/main/runtime/runtime-rpc-metadata-lifecycle.test.ts": 5413, + "src/main/runtime/runtime-rpc-mobile-legacy-socket-relay-refusal.test.ts": 2909, + "src/main/runtime/runtime-rpc-mobile-method-allowlist.test.ts": 2953, + "src/main/runtime/runtime-rpc-mobile-terminal-streaming.test.ts": 6412, + "src/main/runtime/runtime-rpc-mobile-unknown-method-scope-refusal.test.ts": 2997, + "src/main/runtime/runtime-rpc-orchestration-db-migration.test.ts": 536, + "src/main/runtime/runtime-rpc-pairing-mode-persistence.test.ts": 6252, + "src/main/runtime/runtime-rpc-pairing-offer.test.ts": 5913, + "src/main/runtime/runtime-rpc-relay-pairing.test.ts": 6508, + "src/main/runtime/runtime-rpc-relay-provider-install.test.ts": 6621, + "src/main/runtime/runtime-rpc-request-authorization.test.ts": 6790, + "src/main/runtime/runtime-rpc-startup-failure.test.ts": 72, + "src/main/runtime/runtime-rpc-terminal-list.test.ts": 5719, + "src/main/runtime/runtime-rpc-websocket-bind-host.test.ts": 5661, + "src/main/runtime/runtime-rpc-websocket-fallback-port.test.ts": 5395, + "src/main/runtime/runtime-rpc-websocket-long-poll-caps.test.ts": 9511, + "src/main/runtime/runtime-rpc-websocket-reply-size-limit.test.ts": 4833, + "src/main/runtime/runtime-rpc-worktree-queries.test.ts": 5679, + "src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-routing.test.ts": 42, + "src/main/runtime/runtime-search-line-fragments.test.ts": 547, + "src/main/runtime/runtime-server-git-availability.test.ts": 33, + "src/main/runtime/runtime-session-search-settings.test.ts": 213, + "src/main/runtime/runtime-skill-install-authority.test.ts": 87, + "src/main/runtime/runtime-skill-install-commands.test.ts": 617, + "src/main/runtime/runtime-skill-install-queries.test.ts": 790, + "src/main/runtime/runtime-socket-sweep.test.ts": 34, + "src/main/runtime/runtime-sort-order-scan-cache.test.ts": 5489, + "src/main/runtime/runtime-subscription-registry.test.ts": 127, + "src/main/runtime/runtime-terminal-client-use.test.ts": 4251, + "src/main/runtime/runtime-terminal-idle-polls.test.ts": 361, + "src/main/runtime/runtime-terminal-orphan-topology-validation.test.ts": 62, + "src/main/runtime/runtime-terminal-spawn-push-target-materialization.test.ts": 31, + "src/main/runtime/runtime-terminal-writer.test.ts": 77, + "src/main/runtime/runtime-visible-catalog.test.ts": 45, + "src/main/runtime/runtime-worktree-agent-rows-structured.test.ts": 685, + "src/main/runtime/runtime-worktree-agent-rows-verdict.test.ts": 1068, + "src/main/runtime/runtime-worktree-agent-sources.test.ts": 52, + "src/main/runtime/runtime-worktree-agent-startup.test.ts": 454, + "src/main/runtime/runtime-worktree-create-git.test.ts": 633, + "src/main/runtime/runtime-worktree-ps-summaries.test.ts": 59, + "src/main/runtime/runtime-worktree-ps-unverifiable-terminals.test.ts": 33, + "src/main/runtime/runtime-worktree-selection.test.ts": 42, + "src/main/runtime/runtime-worktree-startup-readiness.test.ts": 161, + "src/main/runtime/runtime-worktree-structured-agent-rows-liveness.test.ts": 1189, + "src/main/runtime/runtime-worktree-terminal-provisioning.test.ts": 62, + "src/main/runtime/saved-structured-agent-session-restoration.test.ts": 39, + "src/main/runtime/selected-review-branch.test.ts": 40, + "src/main/runtime/serve-browser-tab-layout.test.ts": 348, + "src/main/runtime/session-tabs-empty-worktree-publication.test.ts": 4143, + "src/main/runtime/session-tabs-inventory-publication.test.ts": 4481, + "src/main/runtime/session-tabs/headless-browser-group-persistence.test.ts": 4669, + "src/main/runtime/session-tabs/session-tab-prototype-dispatch.test.ts": 4643, + "src/main/runtime/session-tabs/session-tabs-inventory-publication.test.ts": 4662, + "src/main/runtime/settled-worker-process-replacement.test.ts": 4613, + "src/main/runtime/stale-title-completion-recovery.test.ts": 5362, + "src/main/runtime/stale-title-display-surfaces.test.ts": 5546, + "src/main/runtime/structured-agent-account-kind.test.ts": 48, + "src/main/runtime/structured-agent-create-support-opencode-rig.test.ts": 1062, + "src/main/runtime/structured-agent-install-refresh.test.ts": 643, + "src/main/runtime/structured-agent-launch-args-wiring.test.ts": 1870, + "src/main/runtime/structured-agent-launch-parity-coverage.test.ts": 1087, + "src/main/runtime/structured-agent-launch-parity.test.ts": 4863, + "src/main/runtime/structured-agent-launch-support-probe-omp.test.ts": 1480, + "src/main/runtime/structured-agent-launch-support-probe.test.ts": 1239, + "src/main/runtime/structured-agent-launch-support.test.ts": 1440, + "src/main/runtime/structured-agent-model-catalog-discovery.test.ts": 1265, + "src/main/runtime/structured-agent-model-catalog-first-frame.test.ts": 1155, + "src/main/runtime/structured-agent-new-chat-effort-at-rest.test.ts": 732, + "src/main/runtime/structured-agent-runtime-registrations-acp.test.ts": 1166, + "src/main/runtime/structured-agent-session-attachment-wiring.test.ts": 273, + "src/main/runtime/structured-agent-session-close.test.ts": 306, + "src/main/runtime/structured-agent-session-codex-compact-exit.test.ts": 4454, + "src/main/runtime/structured-agent-session-codex-interrupt-order.test.ts": 2483, + "src/main/runtime/structured-agent-session-codex-opening-send-hold.test.ts": 12784, + "src/main/runtime/structured-agent-session-codex-settlement-order.test.ts": 2190, + "src/main/runtime/structured-agent-session-codex-stopped-send-order.test.ts": 13028, + "src/main/runtime/structured-agent-session-codex-turn-end-settlement.test.ts": 5209, + "src/main/runtime/structured-agent-session-failure-logging.test.ts": 2288, + "src/main/runtime/structured-agent-session-held-signal.test.ts": 2279, + "src/main/runtime/structured-agent-session-host-refusal.test.ts": 5023, + "src/main/runtime/structured-agent-session-install-failure.test.ts": 2058, + "src/main/runtime/structured-agent-session-integration-replay.test.ts": 3906, + "src/main/runtime/structured-agent-session-integration.test.ts": 2913, + "src/main/runtime/structured-agent-session-legacy-send-behind-command.test.ts": 3938, + "src/main/runtime/structured-agent-session-lifecycle-delivery.test.ts": 258, + "src/main/runtime/structured-agent-session-mobile-attention.test.ts": 260, + "src/main/runtime/structured-agent-session-mobile-restore.test.ts": 1554, + "src/main/runtime/structured-agent-session-mobile-retirement.test.ts": 330, + "src/main/runtime/structured-agent-session-paired-caller.test.ts": 3844, + "src/main/runtime/structured-agent-session-rollback-compatibility.test.ts": 674, + "src/main/runtime/structured-agent-session-runtime-exit.test.ts": 2410, + "src/main/runtime/structured-agent-session-runtime-model-catalog-lifetime.test.ts": 1893, + "src/main/runtime/structured-agent-session-runtime-provider-started.test.ts": 2429, + "src/main/runtime/structured-agent-session-runtime-teardown.test.ts": 285, + "src/main/runtime/structured-agent-session-runtime.test.ts": 2056, + "src/main/runtime/structured-agent-session-spawn-token-descendants.test.ts": 2281, + "src/main/runtime/structured-agent-session-startup-reconcile.test.ts": 5466, + "src/main/runtime/structured-agent-session-startup-tab-restore.test.ts": 4920, + "src/main/runtime/structured-agent-session-support-probe.test.ts": 4474, + "src/main/runtime/structured-agent-session-tab-close-cause.test.ts": 29, + "src/main/runtime/structured-agent-session-windowless-layout-host.test.ts": 5678, + "src/main/runtime/structured-agent-session-windowless-layout.test.ts": 4850, + "src/main/runtime/structured-agent-shell-environment.test.ts": 646, + "src/main/runtime/structured-agent-stored-definitions.test.ts": 388, + "src/main/runtime/structured-chat-coordinator-mail-queue.test.ts": 7480, + "src/main/runtime/structured-chat-coordinator-mail.test.ts": 30995, + "src/main/runtime/structured-chat-dispatch-worker-lifecycle.test.ts": 5718, + "src/main/runtime/structured-chat-dispatch-worker-mail.test.ts": 5975, + "src/main/runtime/structured-chat-dispatch-worker.test.ts": 5546, + "src/main/runtime/structured-chat-naming-runtime.test.ts": 486, + "src/main/runtime/structured-chat-worker-start.test.ts": 5608, + "src/main/runtime/structured-child-records-switch.test.ts": 3148, + "src/main/runtime/structured-claude-attachment-grant.test.ts": 2010, + "src/main/runtime/structured-claude-auth-policy-wiring.test.ts": 1794, + "src/main/runtime/structured-claude-pending-rewind.test.ts": 1750, + "src/main/runtime/structured-claude-thinking-display-refusal.test.ts": 806, + "src/main/runtime/structured-codex-child-work-runtime.test.ts": 4653, + "src/main/runtime/structured-conversation-tab-title.test.ts": 36, + "src/main/runtime/structured-foreground-command-strip.test.ts": 3120, + "src/main/runtime/structured-session-child-env.test.ts": 310, + "src/main/runtime/structured-session-cli-login-shell.test.ts": 433, + "src/main/runtime/structured-session-mail-redrive-wiring.test.ts": 5320, + "src/main/runtime/structured-session-worktree-teardown.test.ts": 424, + "src/main/runtime/structured-worker-agent-presence.test.ts": 278, + "src/main/runtime/structured-worker-at-rest.test.ts": 1745, + "src/main/runtime/structured-worker-authority.test.ts": 346, + "src/main/runtime/structured-worker-cleared-session.test.ts": 4498, + "src/main/runtime/structured-worker-hook-attestation.test.ts": 2655, + "src/main/runtime/structured-worker-identity.test.ts": 411, + "src/main/runtime/structured-worker-mail-routing.test.ts": 2912, + "src/main/runtime/structured-worker-takeover-pane-key.test.ts": 4447, + "src/main/runtime/structured-worker-terminal-read.test.ts": 3267, + "src/main/runtime/structured-worker-terminal-refusal.test.ts": 321, + "src/main/runtime/terminal-ansi-pending-retention.test.ts": 316, + "src/main/runtime/terminal-chat-owner-persistence.test.ts": 4944, + "src/main/runtime/terminal-close-observed-exit.test.ts": 5503, + "src/main/runtime/terminal-close-slow-agent-exit.test.ts": 7870, + "src/main/runtime/terminal-command-paint.test.ts": 42, + "src/main/runtime/terminal-focus-navigation-coalescer.test.ts": 45, + "src/main/runtime/terminal-foreground-group.test.ts": 121, + "src/main/runtime/terminal-identity-probe.test.ts": 32, + "src/main/runtime/terminal-intentional-stop-exit.test.ts": 5289, + "src/main/runtime/terminal-intentional-stops.test.ts": 45, + "src/main/runtime/terminal-interactive-wait-visibility.test.ts": 4225, + "src/main/runtime/terminal-leaf-tab-resolution.test.ts": 40, + "src/main/runtime/terminal-list-execution-host-scope.test.ts": 7247, + "src/main/runtime/terminal-list-payload-size.test.ts": 4546, + "src/main/runtime/terminal-list-pending-pty-registration.test.ts": 3947, + "src/main/runtime/terminal-list-stale-leaf-liveness.test.ts": 4165, + "src/main/runtime/terminal-list-surface-lost-orphan.test.ts": 3974, + "src/main/runtime/terminal-mobile-subscribe-tab-mount.test.ts": 5041, + "src/main/runtime/terminal-model-query-authority.test.ts": 41, + "src/main/runtime/terminal-orphan-owner.test.ts": 30, + "src/main/runtime/terminal-orphan-topology.test.ts": 40, + "src/main/runtime/terminal-pane-recovery-liveness-gate.test.ts": 6266, + "src/main/runtime/terminal-projection.test.ts": 35, + "src/main/runtime/terminal-pty-exit-waiter.test.ts": 4167, + "src/main/runtime/terminal-query-responder.test.ts": 19211, + "src/main/runtime/terminal-restore-record-seed.test.ts": 4619, + "src/main/runtime/terminal-retirement-async-durability.test.ts": 5733, + "src/main/runtime/terminal-retirement-proof-emitted-frame.test.ts": 5801, + "src/main/runtime/terminal-retirement-proof-publication.test.ts": 4802, + "src/main/runtime/terminal-run-facts-input.test.ts": 4586, + "src/main/runtime/terminal-run-facts.test.ts": 51, + "src/main/runtime/terminal-screen-row-metadata.test.ts": 193, + "src/main/runtime/terminal-send-stale-leaf-liveness.test.ts": 4542, + "src/main/runtime/terminal-shell-override-host-support.test.ts": 45, + "src/main/runtime/terminal-subscribe-exit-waiter-leak.test.ts": 4671, + "src/main/runtime/terminal-subscriber-driven-daemon-attach.test.ts": 6357, + "src/main/runtime/terminal-surface-close.test.ts": 245, + "src/main/runtime/terminal-tail-buffer.test.ts": 7350, + "src/main/runtime/terminal-tail-crlf.test.ts": 4828, + "src/main/runtime/terminal-tail-redraw-row.equivalence.test.ts": 1862, + "src/main/runtime/terminal-tail-redraw-scaling.test.ts": 463, + "src/main/runtime/terminal-tail-redraw-spans.test.ts": 287, + "src/main/runtime/terminal-tail-redraw-wall-time.test.ts": 41259, + "src/main/runtime/terminal-tail-row-retention.test.ts": 1561, + "src/main/runtime/terminal-tail-sentinel-index.test.ts": 3453, + "src/main/runtime/terminal-tail-whitespace.test.ts": 284, + "src/main/runtime/terminal-vertical-control-scan.test.ts": 342, + "src/main/runtime/terminal-wait-detection.test.ts": 301, + "src/main/runtime/terminal-wait-name-only-idle.test.ts": 4498, + "src/main/runtime/terminal-wait-results.test.ts": 35, + "src/main/runtime/terminal-wait-tail-window.test.ts": 251, + "src/main/runtime/terminal/artifacts/file-grants.test.ts": 560, + "src/main/runtime/tui-idle-claude-task-wakeup.test.ts": 335, + "src/main/runtime/tui-idle-command-paint.test.ts": 4275, + "src/main/runtime/tui-idle-delivery-and-quiescence.test.ts": 4142, + "src/main/runtime/tui-idle-evidence-source.test.ts": 4203, + "src/main/runtime/tui-idle-evidence.test.ts": 461, + "src/main/runtime/tui-idle-hook-lane-runtime.test.ts": 6744, + "src/main/runtime/tui-idle-hook-lane.test.ts": 316, + "src/main/runtime/tui-idle-name-only-real-pty.integration.test.ts": 35446, + "src/main/runtime/tui-idle-weak-evidence-seam.test.ts": 4548, + "src/main/runtime/unreadable-secret-store-preservation.win32.test.ts": 45, + "src/main/runtime/wait-blocked-check-state.test.ts": 94, + "src/main/runtime/wait-blocked-keyword-carry-retention.test.ts": 3823, + "src/main/runtime/windows-default-route-interfaces.test.ts": 41, + "src/main/runtime/windows-drive-listing.test.ts": 38, + "src/main/runtime/windows-firewall-remote-scope.test.ts": 61, + "src/main/runtime/windows-mobile-firewall.test.ts": 66, + "src/main/runtime/worker-terminal-archive-release.test.ts": 7746, + "src/main/runtime/worker-terminal-archive-screen.test.ts": 6002, + "src/main/runtime/worker-terminal-wrap-provenance.test.ts": 6757, + "src/main/runtime/workspace-layout-stream.test.ts": 425, + "src/main/runtime/workspace-session-membership-scaling.test.ts": 77, + "src/main/runtime/workspace-session-worktree-id.test.ts": 229, + "src/main/runtime/workspaces/workspace-query-prototype.test.ts": 5484, + "src/main/runtime/worktree-launch-host-repo.test.ts": 40, + "src/main/runtime/worktree-list-host-scope.test.ts": 1474, + "src/main/runtime/worktree-path-selector-wsl-posix.test.ts": 4150, + "src/main/runtime/worktree-ps-degraded-repo-scan.test.ts": 3674, + "src/main/runtime/worktree-ps-host-scope.test.ts": 3663, + "src/main/runtime/worktree-ps-unverifiable-terminals.test.ts": 4273, + "src/main/runtime/worktree-rm-id-selector-path-spelling.test.ts": 4381, + "src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts": 4510, + "src/main/runtime/worktree-scan-execution-host-routing.test.ts": 4398, + "src/main/runtime/worktree-scan-repeated-path.test.ts": 4897, + "src/main/runtime/worktree-teardown-unstopped-pty.test.ts": 860, + "src/main/runtime/worktree-teardown.test.ts": 1009, + "src/main/runtime/worktree-terminal-mutation-lock.test.ts": 46, + "src/main/serve-update-handoff.app-environment.test.ts": 1353, + "src/main/serve-update-handoff.test.ts": 49, + "src/main/server/serve-readiness.test.ts": 49, + "src/main/server/serve-stdout-boundary.test.ts": 43, + "src/main/shell-prompt-readiness-probe.test.ts": 74, + "src/main/shell-startup-identity-scanner.test.ts": 24, + "src/main/shell-startup-output-scanner.test.ts": 56, + "src/main/shell-wrapper-content-address.test.ts": 45, + "src/main/shell-wrapper-generated-file-snapshot.test.ts": 105, + "src/main/sidecar-snapshot-file.test.ts": 52, + "src/main/skills/agent-skill-selection.test.ts": 246, + "src/main/skills/antigravity-skill-discovery.test.ts": 85, + "src/main/skills/claude-plugin-skill-sources-wsl.test.ts": 80, + "src/main/skills/claude-plugin-skill-sources.test.ts": 89, + "src/main/skills/discovery-filter-sharing.test.ts": 242, + "src/main/skills/discovery-source-filter-order.test.ts": 94, + "src/main/skills/discovery.test.ts": 194, "src/main/skills/skill-bundle-artifacts.test.ts": 285, - "src/main/skills/skill-bundle-creation.test.ts": 636, - "src/main/skills/skill-bundle-install-service.test.ts": 447, - "src/main/skills/skill-bundle-observability-summary.test.ts": 39, - "src/main/skills/skill-bundle-process-termination-child.test.ts": 276, - "src/main/skills/skill-bundle-process-termination.integration.test.ts": 294, - "src/main/skills/skill-candidate-concurrency.test.ts": 67, - "src/main/skills/skill-client-mediated-transfer-cancellation.test.ts": 278, - "src/main/skills/skill-client-mediated-transfer.test.ts": 295, - "src/main/skills/skill-cloud-direct-upload.test.ts": 110, - "src/main/skills/skill-cloud-grant-installation.test.ts": 253, - "src/main/skills/skill-cloud-install-target.test.ts": 54, - "src/main/skills/skill-cloud-request.test.ts": 62, - "src/main/skills/skill-cloud-service.test.ts": 512, + "src/main/skills/skill-bundle-creation.test.ts": 686, + "src/main/skills/skill-bundle-install-service.test.ts": 565, + "src/main/skills/skill-bundle-observability-summary.test.ts": 31, + "src/main/skills/skill-bundle-process-termination-child.test.ts": 299, + "src/main/skills/skill-bundle-process-termination.integration.test.ts": 287, + "src/main/skills/skill-candidate-concurrency.test.ts": 46, + "src/main/skills/skill-client-mediated-transfer-cancellation.test.ts": 271, + "src/main/skills/skill-client-mediated-transfer.test.ts": 296, + "src/main/skills/skill-cloud-direct-upload.test.ts": 106, + "src/main/skills/skill-cloud-grant-installation.test.ts": 225, + "src/main/skills/skill-cloud-install-target.test.ts": 38, + "src/main/skills/skill-cloud-request.test.ts": 41, + "src/main/skills/skill-cloud-service.test.ts": 479, "src/main/skills/skill-delete/plan.test.ts": 217, - "src/main/skills/skill-delete/recovery.test.ts": 266, - "src/main/skills/skill-delete/roots.test.ts": 98, - "src/main/skills/skill-delete/service.test.ts": 497, - "src/main/skills/skill-delete/staging-visibility.test.ts": 124, - "src/main/skills/skill-delete/wsl-enumeration-protocol.test.ts": 98, - "src/main/skills/skill-discovery-concurrency.test.ts": 152, - "src/main/skills/skill-discovery-order.test.ts": 267, - "src/main/skills/skill-discovery-source-filter.test.ts": 41, - "src/main/skills/skill-discovery-target.test.ts": 121, - "src/main/skills/skill-discovery-wsl-alias-sharing.test.ts": 124, - "src/main/skills/skill-discovery-wsl-bash-filter.test.ts": 2071, - "src/main/skills/skill-discovery-wsl-plugins.test.ts": 93, - "src/main/skills/skill-discovery-wsl-script-roundtrip.test.ts": 112, - "src/main/skills/skill-discovery-wsl.test.ts": 111, - "src/main/skills/skill-freshness-eligibility.test.ts": 84, - "src/main/skills/skill-freshness-inventory-limits.test.ts": 254, - "src/main/skills/skill-freshness-inventory.test.ts": 902, - "src/main/skills/skill-git-tree-identity.test.ts": 79, - "src/main/skills/skill-install-destinations.test.ts": 68, - "src/main/skills/skill-install-discovery-verification.test.ts": 129, - "src/main/skills/skill-install-lock-release.test.ts": 48, - "src/main/skills/skill-install-lock.test.ts": 438, - "src/main/skills/skill-install-management-service.test.ts": 319, - "src/main/skills/skill-install-provenance.test.ts": 85, - "src/main/skills/skill-install-recovery.test.ts": 513, - "src/main/skills/skill-install-request-service.test.ts": 714, - "src/main/skills/skill-install-service.test.ts": 1015, - "src/main/skills/skill-install-transaction.test.ts": 1418, - "src/main/skills/skill-operation-observability.test.ts": 330, - "src/main/skills/skill-package-creation.test.ts": 502, - "src/main/skills/skill-package-deterministic-gzip.test.ts": 1040, - "src/main/skills/skill-package-download-response-lifetime.test.ts": 904, - "src/main/skills/skill-package-download.test.ts": 318, - "src/main/skills/skill-package-identity.test.ts": 117, - "src/main/skills/skill-package-tar.test.ts": 422, - "src/main/skills/skill-placement-alias-repair.test.ts": 238, - "src/main/skills/skill-placement-copy-drift.test.ts": 211, - "src/main/skills/skill-placement-reconciliation.test.ts": 280, - "src/main/skills/skill-placement-transaction.test.ts": 322, - "src/main/skills/skill-plugin-cache-scan.test.ts": 325, - "src/main/skills/skill-process-contention-child.test.ts": 259, - "src/main/skills/skill-process-contention.integration.test.ts": 245, - "src/main/skills/skill-process-termination-child.test.ts": 270, - "src/main/skills/skill-process-termination.integration.test.ts": 310, - "src/main/skills/skill-provider-destinations.test.ts": 64, - "src/main/skills/skill-provider-runtime-roots.test.ts": 79, - "src/main/skills/skill-remote-error-category-parity.test.ts": 264, - "src/main/skills/skill-remote-install-cancellation.test.ts": 46, - "src/main/skills/skill-remote-install-service.test.ts": 273, - "src/main/skills/skill-remove-transaction.test.ts": 852, - "src/main/skills/skill-root-file-walk.test.ts": 67, - "src/main/skills/skill-runtime-capability.test.ts": 77, - "src/main/skills/skill-scan-coalescer.test.ts": 62, - "src/main/skills/skill-share-preparation-service.test.ts": 426, - "src/main/skills/skill-transaction-startup-recovery.test.ts": 471, - "src/main/skills/skill-update-convergence.test.ts": 73, - "src/main/skills/skill-update-outcome.test.ts": 313, - "src/main/skills/skill-update-registration.test.ts": 51, - "src/main/skills/skill-update-run.test.ts": 154, - "src/main/skills/skill-upload-process-restart-child.test.ts": 222, - "src/main/skills/skill-upload-process-restart.integration.test.ts": 244, - "src/main/skills/skill-upload-session-admission-regression.test.ts": 301, - "src/main/skills/skill-upload-session-service.test.ts": 390, - "src/main/skills/skill-windows-rename-contention.integration.test.ts": 43, - "src/main/skills/skill-windows-workspace.integration.test.ts": 345, - "src/main/skills/skill-wsl-delete.integration.test.ts": 70, - "src/main/skills/skill-wsl-install-filesystem.test.ts": 82, - "src/main/skills/skill-wsl-install-transaction.integration.test.ts": 254, - "src/main/skills/skill-wsl-posix-semantics.integration.test.ts": 326, - "src/main/skills/skill-wsl-provider-detection.test.ts": 84, - "src/main/source-control/forge-provider.test.ts": 418, - "src/main/source-control/hosted-review-azure-devops.integration.test.ts": 842, - "src/main/source-control/hosted-review-base-ref-suffix.test.ts": 428, - "src/main/source-control/hosted-review-bitbucket.integration.test.ts": 2640, - "src/main/source-control/hosted-review-branch-cache.test.ts": 216, - "src/main/source-control/hosted-review-creation-eligibility.test.ts": 488, - "src/main/source-control/hosted-review-creation-gitlab-self-hosted.test.ts": 507, - "src/main/source-control/hosted-review-creation-shared-symlinks.test.ts": 488, - "src/main/source-control/hosted-review-creation.test.ts": 487, - "src/main/source-control/hosted-review-dirty-preflight-wsl-paths.test.ts": 440, - "src/main/source-control/hosted-review-execution-host-routing.test.ts": 406, - "src/main/source-control/hosted-review-gitea.integration.test.ts": 771, - "src/main/source-control/hosted-review-inflight-invalidation.test.ts": 131, - "src/main/source-control/hosted-review-inflight-lookups.test.ts": 49, - "src/main/source-control/hosted-review.test.ts": 465, - "src/main/source-control/pull-request-linked-issue.test.ts": 63, - "src/main/source-control/repo-default-branch.test.ts": 137, - "src/main/source-control/stacked-hosted-review-creation.test.ts": 51, - "src/main/speech/model-catalog.test.ts": 48, - "src/main/speech/model-manager-download-error.test.ts": 152, + "src/main/skills/skill-delete/recovery.test.ts": 378, + "src/main/skills/skill-delete/roots.test.ts": 64, + "src/main/skills/skill-delete/service.test.ts": 579, + "src/main/skills/skill-delete/staging-visibility.test.ts": 95, + "src/main/skills/skill-delete/wsl-enumeration-protocol.test.ts": 45, + "src/main/skills/skill-discovery-concurrency.test.ts": 151, + "src/main/skills/skill-discovery-order.test.ts": 244, + "src/main/skills/skill-discovery-source-filter.test.ts": 33, + "src/main/skills/skill-discovery-target.test.ts": 112, + "src/main/skills/skill-discovery-wsl-alias-sharing.test.ts": 110, + "src/main/skills/skill-discovery-wsl-bash-filter.test.ts": 2072, + "src/main/skills/skill-discovery-wsl-plugins.test.ts": 74, + "src/main/skills/skill-discovery-wsl-script-roundtrip.test.ts": 136, + "src/main/skills/skill-discovery-wsl.test.ts": 85, + "src/main/skills/skill-freshness-eligibility.test.ts": 48, + "src/main/skills/skill-freshness-inventory-limits.test.ts": 292, + "src/main/skills/skill-freshness-inventory.test.ts": 951, + "src/main/skills/skill-install-destinations.test.ts": 57, + "src/main/skills/skill-install-discovery-verification.test.ts": 98, + "src/main/skills/skill-install-lock-release.test.ts": 31, + "src/main/skills/skill-install-lock.test.ts": 465, + "src/main/skills/skill-install-management-service.test.ts": 266, + "src/main/skills/skill-install-provenance.test.ts": 56, + "src/main/skills/skill-install-recovery.test.ts": 463, + "src/main/skills/skill-install-request-service.test.ts": 572, + "src/main/skills/skill-install-service.test.ts": 1431, + "src/main/skills/skill-install-transaction.test.ts": 1483, + "src/main/skills/skill-lock-sources.test.ts": 40, + "src/main/skills/skill-operation-observability.test.ts": 341, + "src/main/skills/skill-package-creation.test.ts": 616, + "src/main/skills/skill-package-deterministic-gzip.test.ts": 977, + "src/main/skills/skill-package-download-response-lifetime.test.ts": 975, + "src/main/skills/skill-package-download.test.ts": 273, + "src/main/skills/skill-package-identity.test.ts": 100, + "src/main/skills/skill-package-tar.test.ts": 400, + "src/main/skills/skill-placement-alias-repair.test.ts": 242, + "src/main/skills/skill-placement-copy-drift.test.ts": 250, + "src/main/skills/skill-placement-reconciliation.test.ts": 263, + "src/main/skills/skill-placement-transaction.test.ts": 301, + "src/main/skills/skill-plugin-cache-scan.test.ts": 352, + "src/main/skills/skill-process-contention-child.test.ts": 285, + "src/main/skills/skill-process-contention.integration.test.ts": 226, + "src/main/skills/skill-process-termination-child.test.ts": 289, + "src/main/skills/skill-process-termination.integration.test.ts": 326, + "src/main/skills/skill-provider-destinations.test.ts": 36, + "src/main/skills/skill-provider-runtime-roots.test.ts": 80, + "src/main/skills/skill-remote-error-category-parity.test.ts": 284, + "src/main/skills/skill-remote-install-cancellation.test.ts": 40, + "src/main/skills/skill-remote-install-service.test.ts": 276, + "src/main/skills/skill-remove-transaction.test.ts": 662, + "src/main/skills/skill-root-file-walk.test.ts": 62, + "src/main/skills/skill-runtime-capability.test.ts": 48, + "src/main/skills/skill-scan-coalescer.test.ts": 49, + "src/main/skills/skill-share-preparation-service.test.ts": 425, + "src/main/skills/skill-transaction-startup-recovery.test.ts": 521, + "src/main/skills/skill-update-outcome.test.ts": 48, + "src/main/skills/skill-update-run.test.ts": 160, + "src/main/skills/skill-upload-process-restart-child.test.ts": 210, + "src/main/skills/skill-upload-process-restart.integration.test.ts": 232, + "src/main/skills/skill-upload-session-admission-regression.test.ts": 263, + "src/main/skills/skill-upload-session-service.test.ts": 368, + "src/main/skills/skill-windows-rename-contention.integration.test.ts": 27, + "src/main/skills/skill-windows-workspace.integration.test.ts": 313, + "src/main/skills/skill-wsl-delete.integration.test.ts": 60, + "src/main/skills/skill-wsl-install-filesystem.test.ts": 49, + "src/main/skills/skill-wsl-install-transaction.integration.test.ts": 295, + "src/main/skills/skill-wsl-posix-semantics.integration.test.ts": 343, + "src/main/skills/skill-wsl-provider-detection.test.ts": 41, + "src/main/source-control/forge-provider.test.ts": 516, + "src/main/source-control/hosted-review-azure-devops.integration.test.ts": 795, + "src/main/source-control/hosted-review-base-ref-suffix.test.ts": 579, + "src/main/source-control/hosted-review-bitbucket.integration.test.ts": 2944, + "src/main/source-control/hosted-review-branch-cache.test.ts": 198, + "src/main/source-control/hosted-review-creation-eligibility.test.ts": 606, + "src/main/source-control/hosted-review-creation-gitlab-self-hosted.test.ts": 543, + "src/main/source-control/hosted-review-creation-shared-symlinks.test.ts": 595, + "src/main/source-control/hosted-review-creation.test.ts": 610, + "src/main/source-control/hosted-review-dirty-preflight-wsl-paths.test.ts": 539, + "src/main/source-control/hosted-review-execution-host-routing.test.ts": 556, + "src/main/source-control/hosted-review-gitea.integration.test.ts": 897, + "src/main/source-control/hosted-review-inflight-invalidation.test.ts": 124, + "src/main/source-control/hosted-review-inflight-lookups.test.ts": 42, + "src/main/source-control/hosted-review.test.ts": 600, + "src/main/source-control/pull-request-linked-issue.test.ts": 32, + "src/main/source-control/repo-default-branch.test.ts": 151, + "src/main/source-control/repo-remote-head-branch.test.ts": 102, + "src/main/source-control/stacked-hosted-review-creation.test.ts": 38, + "src/main/speech/model-catalog.test.ts": 33, + "src/main/speech/model-manager-download-error.test.ts": 109, "src/main/speech/model-manager-download-resume.test.ts": 3126, - "src/main/speech/model-manager-progress-callback.test.ts": 65, - "src/main/speech/model-manager-stream-cleanup.test.ts": 124, - "src/main/speech/model-manager-windows-path.test.ts": 170, - "src/main/speech/model-manager.test.ts": 90, - "src/main/speech/openai-api-key-store.test.ts": 82, - "src/main/speech/openai-transcription-client.test.ts": 49, - "src/main/speech/speech-model-deletion.test.ts": 292, - "src/main/speech/speech-model-download-response.test.ts": 49, - "src/main/speech/stt-offline-audio-chunker.test.ts": 222, - "src/main/speech/stt-service.test.ts": 109, - "src/main/speech/stt-worker-model-config.test.ts": 76, - "src/main/speech/stt-worker.test.ts": 49, - "src/main/sqlite/node-sqlite-reader-api.test.ts": 54, - "src/main/sqlite/sqlite-read-failure.test.ts": 67, - "src/main/sqlite/sync-database-concurrent-backup.test.ts": 1201, - "src/main/sqlite/sync-database-portability.test.ts": 114, - "src/main/sqlite/sync-database.test.ts": 895, - "src/main/ssh-expired-lease-pane-readoption.test.ts": 1865, - "src/main/ssh-reattach-pane-cardinality.test.ts": 1839, - "src/main/ssh/managed-server-fence-recheck.test.ts": 459, - "src/main/ssh/orcad-activation-crash-recovery.test.ts": 754, - "src/main/ssh/orcad-activation-fence-generation.test.ts": 645, + "src/main/speech/model-manager-progress-callback.test.ts": 57, + "src/main/speech/model-manager-stream-cleanup.test.ts": 171, + "src/main/speech/model-manager-windows-path.test.ts": 115, + "src/main/speech/model-manager.test.ts": 102, + "src/main/speech/openai-api-key-store.test.ts": 73, + "src/main/speech/openai-transcription-client.test.ts": 42, + "src/main/speech/speech-model-deletion.test.ts": 251, + "src/main/speech/speech-model-download-response.test.ts": 33, + "src/main/speech/stt-offline-audio-chunker.test.ts": 234, + "src/main/speech/stt-service.test.ts": 148, + "src/main/speech/stt-worker-model-config.test.ts": 41, + "src/main/speech/stt-worker.test.ts": 21, + "src/main/sqlite/node-sqlite-reader-api.test.ts": 64, + "src/main/sqlite/sqlite-read-failure.test.ts": 68, + "src/main/sqlite/sync-database-concurrent-backup.test.ts": 1286, + "src/main/sqlite/sync-database-portability.test.ts": 127, + "src/main/sqlite/sync-database.test.ts": 877, + "src/main/ssh-expired-lease-pane-readoption.test.ts": 1555, + "src/main/ssh-reattach-pane-cardinality.test.ts": 1274, + "src/main/ssh/legacy-relay-bridge/legacy-relay-bridge-teardown.test.ts": 326, + "src/main/ssh/legacy-relay-bridge/legacy-relay-client.test.ts": 196, + "src/main/ssh/legacy-relay-bridge/legacy-relay-endpoints.test.ts": 173, + "src/main/ssh/legacy-relay-bridge/legacy-relay-target-bridge.test.ts": 74, + "src/main/ssh/managed-server-fence-recheck.test.ts": 484, + "src/main/ssh/orcad-activation-crash-recovery.test.ts": 1032, + "src/main/ssh/orcad-activation-fence-generation.test.ts": 826, "src/main/ssh/orcad-activation-fence-hold.test.ts": 397, - "src/main/ssh/orcad-activation-gate.test.ts": 62, - "src/main/ssh/orcad-activation-outcome-log.test.ts": 55, - "src/main/ssh/orcad-activation-record.test.ts": 254, - "src/main/ssh/orcad-activation-transaction.test.ts": 398, - "src/main/orcad/orcad-artifact-materializer.test.ts": 722, - "src/main/ssh/orcad-decommission-released-peer-recovery.test.ts": 295, - "src/main/ssh/orcad-decommission-transaction.test.ts": 232, - "src/main/ssh/orcad-deployment-target.test.ts": 234, - "src/main/ssh/orcad-exited-own-fence-mutation-race.test.ts": 775, - "src/main/ssh/orcad-exited-own-fence.test.ts": 5738, - "src/main/ssh/orcad-exited-own-install-lock.test.ts": 4492, - "src/main/ssh/orcad-exited-own-lock-windows.test.ts": 350, - "src/main/ssh/orcad-fence-lost-classification.test.ts": 287, - "src/main/orcad/orcad-host-unavailable.test.ts": 325, - "src/main/ssh/orcad-managed-auto-update.test.ts": 319, - "src/main/ssh/orcad-managed-bound-port.test.ts": 386, - "src/main/ssh/orcad-managed-serving.test.ts": 480, - "src/main/ssh/orcad-managed-tunnel-identity-restart.test.ts": 4422, - "src/main/ssh/orcad-managed-tunnel-identity.test.ts": 403, - "src/main/ssh/orcad-managed-tunnel-target.test.ts": 401, - "src/main/ssh/orcad-managed-tunnel-transport.test.ts": 367, - "src/main/ssh/orcad-managed-tunnel.test.ts": 901, - "src/main/ssh/orcad-managed-update-deferrals.test.ts": 37, - "src/main/ssh/orcad-managed-update-on-restore.test.ts": 334, - "src/main/ssh/orcad-managed-version-gc.test.ts": 375, - "src/main/ssh/orcad-managed-wake.test.ts": 467, - "src/main/ssh/orcad-migration-catalog-client-extra-agent-args.test.ts": 339, - "src/main/ssh/orcad-migration-catalog-client.test.ts": 294, - "src/main/ssh/orcad-migration-cutover-coordinator.test.ts": 1549, - "src/main/ssh/orcad-migration-cutover-journal.test.ts": 342, - "src/main/ssh/orcad-migration-delta-move.test.ts": 3105, - "src/main/ssh/orcad-migration-delta-snapshot.test.ts": 1922, - "src/main/ssh/orcad-migration-manifest-export.test.ts": 384, - "src/main/ssh/orcad-migration-refusal-reason.test.ts": 40, - "src/main/ssh/orcad-migration-scrollback-retention-wiring.test.ts": 59, - "src/main/ssh/orcad-migration-snapshot-coordinator.test.ts": 309, - "src/main/ssh/orcad-migration-snapshot-resume.test.ts": 1220, - "src/main/ssh/orcad-migration-source-fence.test.ts": 1306, - "src/main/ssh/orcad-migration-source-retention.test.ts": 385, - "src/main/ssh/orcad-migration-terminal-gate-host-wide.test.ts": 238, - "src/main/ssh/orcad-migration-terminal-gate.test.ts": 60, - "src/main/ssh/orcad-remote-build-hash.integration.test.ts": 404, - "src/main/ssh/orcad-remote-decommission.test.ts": 387, - "src/main/ssh/orcad-remote-deploy.test.ts": 603, - "src/main/ssh/orcad-remote-gc.test.ts": 468, - "src/main/ssh/orcad-remote-install-termination.test.ts": 495, - "src/main/ssh/orcad-remote-launch-file-modes.test.ts": 421, - "src/main/ssh/orcad-remote-launch.test.ts": 348, - "src/main/ssh/orcad-remote-lifecycle-windows.test.ts": 454, - "src/main/ssh/orcad-remote-log-tail.test.ts": 409, - "src/main/ssh/orcad-remote-node-runtime-promotion-lock.test.ts": 185, - "src/main/ssh/orcad-remote-node-runtime-slow-hash.test.ts": 1212, - "src/main/ssh/orcad-remote-node-runtime-windows.test.ts": 221, - "src/main/ssh/orcad-remote-node-runtime.test.ts": 220, - "src/main/ssh/orcad-remote-primitives.test.ts": 429, - "src/main/ssh/orcad-remote-readiness-wait.test.ts": 1837, - "src/main/ssh/orcad-remote-rollback.test.ts": 570, - "src/main/ssh/orcad-remote-runtime-control.test.ts": 281, - "src/main/ssh/orcad-remote-runtime.test.ts": 680, - "src/main/ssh/orcad-remote-shell-commands.integration.test.ts": 10068, - "src/main/ssh/orcad-remote-windows-commands.test.ts": 374, - "src/main/ssh/orcad-remote-windows-decommission.test.ts": 448, - "src/main/ssh/orcad-remote-windows-scripts.test.ts": 3448, - "src/main/ssh/orcad-retained-fence-takeover.test.ts": 194, - "src/main/ssh/orcad-retained-source-lists.test.ts": 325, - "src/main/ssh/orcad-runtime-conversion.test.ts": 2065, - "src/main/ssh/orcad-runtime-decommission.test.ts": 886, - "src/main/ssh/orcad-runtime-deployment.test.ts": 767, - "src/main/ssh/orcad-runtime-forget.test.ts": 558, - "src/main/ssh/orcad-runtime-maintenance.test.ts": 744, - "src/main/ssh/orcad-runtime-target.test.ts": 393, - "src/main/ssh/orcad-ssh-provisioning.test.ts": 279, - "src/main/ssh/orcad-state-mutation-exec.test.ts": 343, - "src/main/ssh/orcad-state-mutation-fence-heartbeat.test.ts": 17438, - "src/main/ssh/orcad-state-mutation-timeout.test.ts": 460, - "src/main/ssh/orcad-state-snapshot-shell.test.ts": 726, - "src/main/ssh/orcad-state-snapshot.test.ts": 382, - "src/main/ssh/orcad-stdio-bridge-provider.test.ts": 686, - "src/main/ssh/orcad-stdio-bridge.test.ts": 502, - "src/main/ssh/orcad-terminal-census-client.test.ts": 188, - "src/main/ssh/orcad-tunneled-pairing.test.ts": 209, - "src/main/ssh/orcad-unreachable-setup-release.test.ts": 1751, - "src/main/ssh/orcad-unreachable-setup-stop.test.ts": 424, - "src/main/ssh/orcad-update-plan.test.ts": 2663, - "src/main/ssh/orcad-windows-host-fence-ops.test.ts": 763, - "src/main/ssh/orcad-windows-host-lane.test.ts": 745, - "src/main/ssh/orcad-windows-host-state-ops.test.ts": 2035, - "src/main/node-runtime/pinned-runtime-materializer-node.test.ts": 114, - "src/main/node-runtime/pinned-runtime-materializer.test.ts": 3192, - "src/main/ssh/relay-bundle-paths.test.ts": 62, - "src/main/ssh/relay-protocol.test.ts": 120, - "src/main/ssh/relay-socket-path-limit-shell.integration.test.ts": 107, - "src/main/ssh/relay-socket-path-limit.test.ts": 559, - "src/main/ssh/relay-version-dir-liveness.test.ts": 577, - "src/main/ssh/remote-install-gc-termination.test.ts": 267, - "src/main/ssh/remote-install-model.test.ts": 199, - "src/main/ssh/remote-install-previous-version.test.ts": 187, - "src/main/ssh/remote-node-runtime-store-gc.test.ts": 2001, - "src/main/ssh/remote-node-runtime-store-lock-host-release.test.ts": 2390, - "src/main/ssh/remote-node-runtime-store-windows.test.ts": 12643, - "src/main/ssh/removed-ssh-target-tombstone-retention.test.ts": 54, - "src/main/node-runtime/runtime-archive-download.test.ts": 99, - "src/main/ssh/runtime-ssh-access-verification.test.ts": 228, - "src/main/ssh/runtime-ssh-access.test.ts": 548, - "src/main/ssh/sftp-namespace-resolution.test.ts": 91, - "src/main/ssh/sftp-stream-late-error.test.ts": 232, - "src/main/ssh/sftp-upload.test.ts": 298, - "src/main/ssh/ssh-agent-hook-interrupt-reconciliation.test.ts": 530, - "src/main/ssh/ssh-agent-identity-filter.test.ts": 67, - "src/main/ssh/ssh-channel-multiplexer-backpressure.test.ts": 103, - "src/main/ssh/ssh-channel-multiplexer-saturation-wedge.test.ts": 72, - "src/main/ssh/ssh-channel-multiplexer-settlement.test.ts": 61, - "src/main/ssh/ssh-channel-multiplexer.test.ts": 133, - "src/main/ssh/ssh-config-alias-claim.test.ts": 87, - "src/main/ssh/ssh-config-host-picker.test.ts": 200, - "src/main/ssh/ssh-config-loader-regression.test.ts": 134, - "src/main/ssh/ssh-config-loader.test.ts": 117, - "src/main/ssh/ssh-config-parser-host-patterns.test.ts": 66, - "src/main/ssh/ssh-config-parser.test.ts": 327, - "src/main/ssh/ssh-config-resolver.test.ts": 72, - "src/main/ssh/ssh-connect-attempt-cancellation.test.ts": 94, - "src/main/ssh/ssh-connection-attribution.test.ts": 53, - "src/main/ssh/ssh-connection-auth-fallback.test.ts": 266, - "src/main/ssh/ssh-connection-channel-error.test.ts": 144, - "src/main/ssh/ssh-connection-channel-lifetime.test.ts": 41, - "src/main/ssh/ssh-connection-channel-open.test.ts": 932, - "src/main/ssh/ssh-connection-direct-startup.test.ts": 141, - "src/main/ssh/ssh-connection-file-transfer.test.ts": 178, - "src/main/ssh/ssh-connection-generation.test.ts": 52, - "src/main/ssh/ssh-connection-github-probe.test.ts": 196, - "src/main/ssh/ssh-connection-gssapi-fallback.test.ts": 254, - "src/main/ssh/ssh-connection-host-key-store-wiring.test.ts": 579, - "src/main/ssh/ssh-connection-host-key-verification.test.ts": 201, - "src/main/ssh/ssh-connection-manager-registry.test.ts": 258, - "src/main/ssh/ssh-connection-manager.test.ts": 73, - "src/main/ssh/ssh-connection-reconnect-handshake-loss.test.ts": 225, - "src/main/ssh/ssh-connection-reconnect-ladder.test.ts": 382, - "src/main/ssh/ssh-connection-sftp-namespace.test.ts": 991, - "src/main/ssh/ssh-connection-sftp-wire.test.ts": 368, - "src/main/ssh/ssh-connection-store-orcad-ownership.test.ts": 234, - "src/main/ssh/ssh-connection-store.test.ts": 320, - "src/main/ssh/ssh-connection-system-transport.test.ts": 241, - "src/main/ssh/ssh-connection-utils.test.ts": 217, - "src/main/ssh/ssh-connection.test.ts": 1261, - "src/main/ssh/ssh-control-socket.test.ts": 62, - "src/main/ssh/ssh-exec-rc-stdout-noise.test.ts": 255, - "src/main/ssh/ssh-exec-stdin-file-transfer.test.ts": 246, - "src/main/ssh/ssh-file-stream-inactivity-deadline.test.ts": 58, - "src/main/ssh/ssh-file-transfer-abort.test.ts": 105, - "src/main/ssh/ssh-g-config-resolution.test.ts": 112, - "src/main/ssh/ssh-git-response-pending-retention.test.ts": 238, - "src/main/ssh/ssh-git-response-stream-reader.test.ts": 59, - "src/main/ssh/ssh-git-stream-idle-timer.test.ts": 109, - "src/main/ssh/ssh-host-key-decision.test.ts": 74, - "src/main/ssh/ssh-host-key-store.test.ts": 193, - "src/main/ssh/ssh-host-key-verifier.test.ts": 92, - "src/main/ssh/ssh-host-platform-memo.test.ts": 40, - "src/main/ssh/ssh-host-relay-census-for-target.test.ts": 53, - "src/main/ssh/ssh-host-relay-endpoint-census.test.ts": 112, - "src/main/ssh/ssh-host-relay-terminals-on-connect.test.ts": 576, - "src/main/ssh/ssh-host-relay-windows-census.test.ts": 209, - "src/main/ssh/ssh-host-relay-windows-inventory.test.ts": 179, - "src/main/ssh/ssh-host-server-on-connect-stopped-elsewhere.test.ts": 674, - "src/main/ssh/ssh-host-server-on-connect-telemetry.test.ts": 547, - "src/main/ssh/ssh-host-server-on-connect.test.ts": 539, - "src/main/ssh/ssh-host-server-telemetry.test.ts": 228, - "src/main/ssh/ssh-hostile-host-cells.test.ts": 137, - "src/main/ssh/ssh-hostile-host-local-sshd.test.ts": 69, - "src/main/ssh/ssh-keyboard-interactive-wire.test.ts": 290, - "src/main/ssh/ssh-keyboard-interactive.test.ts": 72, - "src/main/ssh/ssh-known-hosts-source.test.ts": 128, - "src/main/ssh/ssh-known-hosts.test.ts": 96, - "src/main/ssh/ssh-listing-response-retention.test.ts": 77, - "src/main/ssh/ssh-multi-factor-authentication.test.ts": 168, - "src/main/ssh/ssh-multi-key-authentication.test.ts": 126, - "src/main/ssh/ssh-multiplexer-transport-writer.test.ts": 89, - "src/main/ssh/ssh-multiplexer-writer-retention.test.ts": 480, - "src/main/ssh/ssh-port-forward.test.ts": 253, - "src/main/ssh/ssh-posix-command-wrapper.test.ts": 140, - "src/main/ssh/ssh-provider-authority.test.ts": 67, - "src/main/ssh/ssh-proxy-command.test.ts": 120, - "src/main/ssh/ssh-quit-abort-upload.test.ts": 256, - "src/main/ssh/ssh-reconnect-error-classification.test.ts": 123, - "src/main/ssh/ssh-reconnect-ladder.test.ts": 96, - "src/main/ssh/ssh-relay-answered-failure-producers.test.ts": 301, - "src/main/ssh/ssh-relay-sentinel.test.ts": 571, - "src/main/ssh/ssh-relay-endpoint-incumbent-shell.integration.test.ts": 2228, - "src/main/ssh/ssh-relay-endpoint-incumbent.test.ts": 115, - "src/main/ssh/ssh-relay-endpoint-pty-count.test.ts": 695, - "src/main/ssh/ssh-relay-endpoint-runtime.test.ts": 102, - "src/main/ssh/ssh-relay-endpoint-takeover.test.ts": 141, - "src/main/ssh/ssh-relay-exec-input.test.ts": 91, - "src/main/ssh/ssh-relay-gc-claim-termination.test.ts": 261, - "src/main/ssh/ssh-relay-host-answered-failure.test.ts": 100, - "src/main/ssh/ssh-relay-incumbent-process.test.ts": 5678, - "src/main/ssh/ssh-relay-install-lock-exited-owner.test.ts": 324, - "src/main/ssh/ssh-relay-install-lock-termination.test.ts": 245, - "src/main/ssh/ssh-relay-install-lock.test.ts": 178, - "src/main/ssh/ssh-relay-install-namespace.test.ts": 227, - "src/main/ssh/ssh-relay-install-noisy-sftp-wire.test.ts": 292, - "src/main/ssh/ssh-relay-install-transfers-exec-fallback.test.ts": 113, - "src/main/ssh/ssh-relay-native-deps-cache-gc-real.test.ts": 706, - "src/main/ssh/ssh-relay-native-deps-cache-shell.test.ts": 421, - "src/main/ssh/ssh-relay-native-deps-cache.test.ts": 220, - "src/main/ssh/ssh-relay-pinned-node.integration.test.ts": 352, - "src/main/ssh/ssh-relay-pinned-node.test.ts": 421, - "src/main/ssh/ssh-relay-runtime-ladder.test.ts": 346, - "src/main/ssh/ssh-relay-runtime-never-worse.test.ts": 422, - "src/main/ssh/ssh-relay-runtime-resolution.test.ts": 438, - "src/main/ssh/ssh-relay-runtime-self-test.test.ts": 655, - "src/main/ssh/ssh-relay-sentinel-copy-budget.test.ts": 165, - "src/main/ssh/ssh-relay-superseded-endpoints.test.ts": 146, - "src/main/ssh/ssh-relay-versioned-install-termination.test.ts": 239, - "src/main/ssh/ssh-relay-versioned-install.test.ts": 327, - "src/main/ssh/ssh-relay-windows-launch-command.test.ts": 151, - "src/main/ssh/ssh-remote-commands.test.ts": 7310, - "src/main/ssh/ssh-remote-node-resolution.test.ts": 318, - "src/main/ssh/ssh-remote-node-toolchain-probe.test.ts": 173, - "src/main/ssh/ssh-remote-platform-detection.test.ts": 218, - "src/main/ssh/ssh-remote-platform.test.ts": 58, - "src/main/ssh/ssh-remote-powershell.test.ts": 328, - "src/main/ssh/ssh-remote-runtime-telemetry.test.ts": 286, - "src/main/ssh/ssh-remote-windows-command-line-limit.test.ts": 208, - "src/main/ssh/ssh-request-outcome-verdict.test.ts": 52, - "src/main/ssh/ssh-response-pending-frames.test.ts": 96, - "src/main/ssh/ssh-security-key-identity.test.ts": 156, - "src/main/ssh/ssh-session-limit-error.test.ts": 49, - "src/main/ssh/ssh-system-fallback.test.ts": 307, - "src/main/ssh/ssh-system-transport.integration.test.ts": 1301, - "src/main/ssh/ssh-target-id-migration.test.ts": 62, - "src/main/ssh/ssh-target-orcad-claims.test.ts": 221, - "src/main/ssh/ssh-target-orcad-dependents.test.ts": 247, - "src/main/ssh/ssh-target-orcad-preflight.test.ts": 1492, - "src/main/ssh/ssh-target-readoption.test.ts": 56, - "src/main/ssh/ssh-tcp-forwarding-probe.test.ts": 109, - "src/main/ssh/ssh-windows-host-cells.test.ts": 67, - "src/main/ssh/ssh2-port-forward-listen.test.ts": 49, - "src/main/ssh/system-ssh-binary.test.ts": 62, - "src/main/ssh/system-ssh-dynamic-forward-process.test.ts": 236, - "src/main/ssh/system-ssh-forward-process.test.ts": 228, - "src/main/ssh/system-ssh-sftp-args.test.ts": 57, - "src/main/ssh/system-ssh-sftp-path.test.ts": 54, - "src/main/ssh/system-ssh-upload-plan-budget.test.ts": 344, - "src/main/ssh/system-ssh-windows-upload.test.ts": 379, - "src/main/ssh/system-ssh-windows-write-capabilities.test.ts": 87, - "src/main/ssh/vscode-ssh-authority.test.ts": 71, - "src/main/star-nag/service-direct-star.test.ts": 381, - "src/main/star-nag/service-force-show-races.test.ts": 346, - "src/main/star-nag/service-outcome-telemetry.test.ts": 353, - "src/main/star-nag/service-prompt-moments.test.ts": 340, - "src/main/star-nag/service-threshold-prompt.test.ts": 410, - "src/main/startup/bootstrap-fatal-exit-guard.test.ts": 55, - "src/main/startup/branch-rename-hook-structured-session.test.ts": 197, - "src/main/startup/browser-process-user-agent-ordering.test.ts": 5756, - "src/main/startup/cli-launch-redirect.test.ts": 114, - "src/main/startup/codex-account-restart-resume.test.ts": 292, - "src/main/startup/codex-hook-startup-wiring.test.ts": 6575, - "src/main/startup/codex-launch-per-agent-hook-opt-out.test.ts": 187, - "src/main/startup/codex-session-resume-daemon-guard.test.ts": 143, - "src/main/startup/configure-process-dev-parent-shutdown.test.ts": 113, - "src/main/startup/configure-process-profile-state.test.ts": 143, - "src/main/startup/configure-process.test.ts": 235, - "src/main/startup/desktop-profile-instance-lock.test.ts": 219, - "src/main/startup/dev-education-suppression.test.ts": 306, - "src/main/startup/dev-instance-identity.test.ts": 57, - "src/main/startup/ensure-virtual-display.test.ts": 77, - "src/main/startup/first-window-startup-services.test.ts": 83, - "src/main/startup/gpu-fallback-marker.test.ts": 75, - "src/main/startup/gpu-fallback-switches.test.ts": 52, - "src/main/startup/gpu-lifecycle-install-dir-acl-guard.test.ts": 546, - "src/main/startup/headless-serve-ssh-registration.test.ts": 4078, - "src/main/startup/http1-compatibility-profile-state.test.ts": 90, - "src/main/startup/hydrate-shell-path.test.ts": 451, - "src/main/startup/hydrate-shell-path.windows.test.ts": 322, - "src/main/startup/legacy-worker-renderer-recovery.test.ts": 100, - "src/main/startup/linux-dev-shm-policy.test.ts": 74, - "src/main/startup/linux-keyring-backend-selection.test.ts": 73, - "src/main/startup/linux-secret-service-probe.test.ts": 51, - "src/main/startup/login-shell-environment-cache.test.ts": 90, - "src/main/startup/login-shell-environment.test.ts": 104, - "src/main/startup/main-process-error-guards.test.ts": 108, - "src/main/startup/main-process-observers-hang-breadcrumb.test.ts": 160, - "src/main/startup/main-process-quit-pending-writer.test.ts": 118, - "src/main/startup/main-process-quit-update-veto.test.ts": 91, - "src/main/startup/main-process-ready-activation-failure.test.ts": 261, - "src/main/startup/main-process-ready-identity-write.test.ts": 1168, - "src/main/startup/main-process-ready-persistence-cleanup.test.ts": 213, - "src/main/startup/main-process-ready-phase-ordering.test.ts": 235, - "src/main/startup/main-process-runtime-launch-activation.test.ts": 5532, - "src/main/startup/main-window-actions-user-quit.test.ts": 193, - "src/main/startup/main-window-core-services.test.ts": 87, - "src/main/startup/main-window-structured-status-filter.test.ts": 195, - "src/main/startup/os-opened-document-arguments.test.ts": 167, - "src/main/startup/os-opened-document-delivery.test.ts": 168, - "src/main/startup/os-opened-document-resolution.test.ts": 254, - "src/main/startup/os-opened-documents.test.ts": 180, - "src/main/startup/profile-state-recovery-preflight.test.ts": 690, - "src/main/startup/profile-state-save-delay.test.ts": 53, - "src/main/startup/profile-state-write-failure.test.ts": 446, - "src/main/startup/renderer-heap-headroom.test.ts": 54, + "src/main/ssh/orcad-activation-gate.test.ts": 40, + "src/main/ssh/orcad-activation-outcome-log.test.ts": 40, + "src/main/ssh/orcad-activation-record.test.ts": 245, + "src/main/ssh/orcad-activation-transaction.test.ts": 451, + "src/main/ssh/orcad-decommission-released-peer-recovery.test.ts": 266, + "src/main/ssh/orcad-decommission-transaction.test.ts": 309, + "src/main/ssh/orcad-deployment-target.test.ts": 263, + "src/main/ssh/orcad-exited-own-fence-mutation-race.test.ts": 822, + "src/main/ssh/orcad-exited-own-fence.test.ts": 5949, + "src/main/ssh/orcad-exited-own-install-lock.test.ts": 4491, + "src/main/ssh/orcad-exited-own-lock-windows.test.ts": 422, + "src/main/ssh/orcad-fence-lost-classification.test.ts": 374, + "src/main/ssh/orcad-managed-auto-update.test.ts": 438, + "src/main/ssh/orcad-managed-bound-port.test.ts": 442, + "src/main/ssh/orcad-managed-serving.test.ts": 489, + "src/main/ssh/orcad-managed-tunnel-bridge-port.test.ts": 414, + "src/main/ssh/orcad-managed-tunnel-identity-restart.test.ts": 5282, + "src/main/ssh/orcad-managed-tunnel-identity.test.ts": 447, + "src/main/ssh/orcad-managed-tunnel-target.test.ts": 558, + "src/main/ssh/orcad-managed-tunnel-transport.test.ts": 437, + "src/main/ssh/orcad-managed-tunnel.test.ts": 914, + "src/main/ssh/orcad-managed-update-deferrals.test.ts": 30, + "src/main/ssh/orcad-managed-update-on-restore.test.ts": 422, + "src/main/ssh/orcad-managed-version-gc.test.ts": 380, + "src/main/ssh/orcad-managed-wake.test.ts": 567, + "src/main/ssh/orcad-migration-catalog-client-extra-agent-args.test.ts": 351, + "src/main/ssh/orcad-migration-catalog-client-source-kind.test.ts": 378, + "src/main/ssh/orcad-migration-catalog-client.test.ts": 373, + "src/main/ssh/orcad-migration-cutover-coordinator.test.ts": 2115, + "src/main/ssh/orcad-migration-cutover-journal.test.ts": 426, + "src/main/ssh/orcad-migration-delta-move.test.ts": 3104, + "src/main/ssh/orcad-migration-delta-snapshot.test.ts": 2081, + "src/main/ssh/orcad-migration-manifest-export.test.ts": 584, + "src/main/ssh/orcad-migration-manifest-golden.test.ts": 517, + "src/main/ssh/orcad-migration-path-rewrite.test.ts": 461, + "src/main/ssh/orcad-migration-refusal-reason.test.ts": 30, + "src/main/ssh/orcad-migration-scrollback-retention-wiring.test.ts": 62, + "src/main/ssh/orcad-migration-snapshot-coordinator.test.ts": 353, + "src/main/ssh/orcad-migration-snapshot-resume.test.ts": 1625, + "src/main/ssh/orcad-migration-source-fence.test.ts": 1790, + "src/main/ssh/orcad-migration-source-retention.test.ts": 476, + "src/main/ssh/orcad-migration-ssh-relay-source.test.ts": 1623, + "src/main/ssh/orcad-migration-terminal-gate-host-wide.test.ts": 240, + "src/main/ssh/orcad-migration-terminal-gate.test.ts": 43, + "src/main/ssh/orcad-remote-build-hash.integration.test.ts": 375, + "src/main/ssh/orcad-remote-decommission.test.ts": 459, + "src/main/ssh/orcad-remote-deploy.test.ts": 574, + "src/main/ssh/orcad-remote-gc.test.ts": 422, + "src/main/ssh/orcad-remote-install-termination.test.ts": 411, + "src/main/ssh/orcad-remote-launch-file-modes.test.ts": 401, + "src/main/ssh/orcad-remote-launch-setsid.test.ts": 401, + "src/main/ssh/orcad-remote-launch.test.ts": 334, + "src/main/ssh/orcad-remote-lifecycle-windows.test.ts": 474, + "src/main/ssh/orcad-remote-log-tail.test.ts": 443, + "src/main/ssh/orcad-remote-node-runtime-promotion-lock.test.ts": 182, + "src/main/ssh/orcad-remote-node-runtime-recorded-probe.test.ts": 242, + "src/main/ssh/orcad-remote-node-runtime-slow-hash.test.ts": 1230, + "src/main/ssh/orcad-remote-node-runtime-windows.test.ts": 234, + "src/main/ssh/orcad-remote-node-runtime.test.ts": 212, + "src/main/ssh/orcad-remote-primitives.test.ts": 459, + "src/main/ssh/orcad-remote-readiness-wait.test.ts": 3110, + "src/main/ssh/orcad-remote-rollback.test.ts": 575, + "src/main/ssh/orcad-remote-runtime-control.test.ts": 365, + "src/main/ssh/orcad-remote-runtime.test.ts": 702, + "src/main/ssh/orcad-remote-shell-commands.integration.test.ts": 11199, + "src/main/ssh/orcad-remote-windows-commands.test.ts": 403, + "src/main/ssh/orcad-remote-windows-decommission.test.ts": 457, + "src/main/ssh/orcad-remote-windows-scripts.test.ts": 3486, + "src/main/ssh/orcad-retained-fence-takeover.test.ts": 200, + "src/main/ssh/orcad-retained-source-lists.test.ts": 504, + "src/main/ssh/orcad-running-version-adoption.test.ts": 383, + "src/main/ssh/orcad-runtime-conversion.test.ts": 2187, + "src/main/ssh/orcad-runtime-decommission.test.ts": 877, + "src/main/ssh/orcad-runtime-deployment.test.ts": 873, + "src/main/ssh/orcad-runtime-forget.test.ts": 814, + "src/main/ssh/orcad-runtime-maintenance.test.ts": 894, + "src/main/ssh/orcad-runtime-target.test.ts": 217, + "src/main/ssh/orcad-ssh-provisioning.test.ts": 271, + "src/main/ssh/orcad-state-mutation-exec.test.ts": 360, + "src/main/ssh/orcad-state-mutation-fence-heartbeat.test.ts": 4403, + "src/main/ssh/orcad-state-mutation-timeout.test.ts": 434, + "src/main/ssh/orcad-state-snapshot-shell.test.ts": 832, + "src/main/ssh/orcad-state-snapshot.test.ts": 334, + "src/main/ssh/orcad-stdio-bridge-provider.test.ts": 705, + "src/main/ssh/orcad-stdio-bridge.test.ts": 597, + "src/main/ssh/orcad-terminal-census-client.test.ts": 302, + "src/main/ssh/orcad-tunneled-pairing.test.ts": 257, + "src/main/ssh/orcad-unreachable-setup-release.test.ts": 1871, + "src/main/ssh/orcad-unreachable-setup-stop.test.ts": 421, + "src/main/ssh/orcad-update-plan.test.ts": 557, + "src/main/ssh/orcad-windows-host-fence-ops.test.ts": 995, + "src/main/ssh/orcad-windows-host-lane.test.ts": 647, + "src/main/ssh/orcad-windows-host-state-ops.test.ts": 2049, + "src/main/ssh/pinned-runtime-host-target.test.ts": 51, + "src/main/ssh/relay-bundle-paths.test.ts": 32, + "src/main/ssh/relay-install-artifacts.test.ts": 37, + "src/main/ssh/relay-protocol.test.ts": 113, + "src/main/ssh/remote-install-gc-termination.test.ts": 187, + "src/main/ssh/remote-install-model.test.ts": 184, + "src/main/ssh/remote-node-runtime-store-gc.test.ts": 1625, + "src/main/ssh/remote-node-runtime-store-lock-host-release.test.ts": 2252, + "src/main/ssh/remote-node-runtime-store-windows.test.ts": 7184, + "src/main/ssh/remote-versioned-install.test.ts": 283, + "src/main/ssh/removed-ssh-target-tombstone-retention.test.ts": 72, + "src/main/ssh/runtime-ssh-access-verification.test.ts": 290, + "src/main/ssh/runtime-ssh-access.test.ts": 621, + "src/main/ssh/sftp-namespace-resolution.test.ts": 66, + "src/main/ssh/sftp-stream-late-error.test.ts": 166, + "src/main/ssh/sftp-upload.test.ts": 292, + "src/main/ssh/ssh-agent-hook-interrupt-reconciliation.test.ts": 729, + "src/main/ssh/ssh-agent-identity-filter.test.ts": 47, + "src/main/ssh/ssh-channel-multiplexer-backpressure.test.ts": 99, + "src/main/ssh/ssh-channel-multiplexer-saturation-wedge.test.ts": 61, + "src/main/ssh/ssh-channel-multiplexer-settlement.test.ts": 37, + "src/main/ssh/ssh-channel-multiplexer.test.ts": 120, + "src/main/ssh/ssh-config-alias-claim.test.ts": 58, + "src/main/ssh/ssh-config-host-picker.test.ts": 223, + "src/main/ssh/ssh-config-loader-regression.test.ts": 132, + "src/main/ssh/ssh-config-loader.test.ts": 114, + "src/main/ssh/ssh-config-parser-host-patterns.test.ts": 49, + "src/main/ssh/ssh-config-parser.test.ts": 331, + "src/main/ssh/ssh-config-resolver.test.ts": 40, + "src/main/ssh/ssh-connect-attempt-cancellation.test.ts": 85, + "src/main/ssh/ssh-connection-attribution.test.ts": 34, + "src/main/ssh/ssh-connection-auth-fallback.test.ts": 269, + "src/main/ssh/ssh-connection-channel-error.test.ts": 142, + "src/main/ssh/ssh-connection-channel-lifetime.test.ts": 58, + "src/main/ssh/ssh-connection-channel-open.test.ts": 930, + "src/main/ssh/ssh-connection-direct-startup.test.ts": 128, + "src/main/ssh/ssh-connection-file-transfer.test.ts": 142, + "src/main/ssh/ssh-connection-generation.test.ts": 41, + "src/main/ssh/ssh-connection-github-probe.test.ts": 174, + "src/main/ssh/ssh-connection-gssapi-fallback.test.ts": 237, + "src/main/ssh/ssh-connection-host-key-store-wiring.test.ts": 524, + "src/main/ssh/ssh-connection-host-key-verification.test.ts": 216, + "src/main/ssh/ssh-connection-manager-registry.test.ts": 402, + "src/main/ssh/ssh-connection-manager-sign-in-reuse.test.ts": 522, + "src/main/ssh/ssh-connection-manager.test.ts": 274, + "src/main/ssh/ssh-connection-reconnect-handshake-loss.test.ts": 254, + "src/main/ssh/ssh-connection-reconnect-ladder.test.ts": 354, + "src/main/ssh/ssh-connection-sftp-namespace.test.ts": 1227, + "src/main/ssh/ssh-connection-sftp-wire.test.ts": 254, + "src/main/ssh/ssh-connection-store-orcad-ownership.test.ts": 293, + "src/main/ssh/ssh-connection-store.test.ts": 337, + "src/main/ssh/ssh-connection-system-transport.test.ts": 206, + "src/main/ssh/ssh-connection-utils.test.ts": 244, + "src/main/ssh/ssh-connection.test.ts": 1226, + "src/main/ssh/ssh-control-socket.test.ts": 50, + "src/main/ssh/ssh-credential-decline.test.ts": 252, + "src/main/ssh/ssh-exec-rc-stdout-noise.test.ts": 260, + "src/main/ssh/ssh-exec-stdin-file-transfer.test.ts": 334, + "src/main/ssh/ssh-file-stream-inactivity-deadline.test.ts": 38, + "src/main/ssh/ssh-file-transfer-abort.test.ts": 77, + "src/main/ssh/ssh-g-config-resolution.test.ts": 128, + "src/main/ssh/ssh-host-key-decision.test.ts": 67, + "src/main/ssh/ssh-host-key-store.test.ts": 207, + "src/main/ssh/ssh-host-key-verifier.test.ts": 77, + "src/main/ssh/ssh-host-platform-memo.test.ts": 51, + "src/main/ssh/ssh-host-relay-census-for-target.test.ts": 43, + "src/main/ssh/ssh-host-relay-endpoint-census.test.ts": 133, + "src/main/ssh/ssh-host-relay-terminals-on-connect.test.ts": 272, + "src/main/ssh/ssh-host-relay-windows-census.test.ts": 247, + "src/main/ssh/ssh-host-relay-windows-inventory.test.ts": 201, + "src/main/ssh/ssh-host-server-on-connect-stopped-elsewhere.test.ts": 865, + "src/main/ssh/ssh-host-server-on-connect-telemetry.test.ts": 597, + "src/main/ssh/ssh-host-server-on-connect.test.ts": 553, + "src/main/ssh/ssh-host-server-telemetry.test.ts": 316, + "src/main/ssh/ssh-hostile-host-cells.test.ts": 105, + "src/main/ssh/ssh-hostile-host-local-sshd.test.ts": 71, + "src/main/ssh/ssh-hostile-hosts.docker.test.ts": 574, + "src/main/ssh/ssh-keyboard-interactive-wire.test.ts": 261, + "src/main/ssh/ssh-keyboard-interactive.test.ts": 41, + "src/main/ssh/ssh-known-hosts-source.test.ts": 106, + "src/main/ssh/ssh-known-hosts.test.ts": 89, + "src/main/ssh/ssh-multi-factor-authentication.test.ts": 109, + "src/main/ssh/ssh-multi-key-authentication.test.ts": 87, + "src/main/ssh/ssh-multiplexer-transport-writer.test.ts": 75, + "src/main/ssh/ssh-multiplexer-writer-retention.test.ts": 511, + "src/main/ssh/ssh-port-forward.test.ts": 261, + "src/main/ssh/ssh-posix-command-wrapper.test.ts": 122, + "src/main/ssh/ssh-provider-authority.test.ts": 50, + "src/main/ssh/ssh-proxy-command.test.ts": 108, + "src/main/ssh/ssh-quit-abort-upload.test.ts": 252, + "src/main/ssh/ssh-reconnect-error-classification.test.ts": 98, + "src/main/ssh/ssh-reconnect-ladder.test.ts": 75, + "src/main/ssh/ssh-relay-answered-failure-producers.test.ts": 321, + "src/main/ssh/ssh-relay-endpoint-incumbent-shell.integration.test.ts": 1326, + "src/main/ssh/ssh-relay-endpoint-incumbent.test.ts": 100, + "src/main/ssh/ssh-relay-endpoint-pty-count.test.ts": 90, + "src/main/ssh/ssh-relay-endpoint-runtime.test.ts": 69, + "src/main/ssh/ssh-relay-exec-closed-stdin.test.ts": 89, + "src/main/ssh/ssh-relay-exec-command.test.ts": 130, + "src/main/ssh/ssh-relay-exec-input.test.ts": 92, + "src/main/ssh/ssh-relay-gc-claim-termination.test.ts": 249, + "src/main/ssh/ssh-relay-host-answered-failure.test.ts": 101, + "src/main/ssh/ssh-relay-incumbent-process.test.ts": 5329, + "src/main/ssh/ssh-relay-install-lock-exited-owner.test.ts": 380, + "src/main/ssh/ssh-relay-install-lock-termination.test.ts": 276, + "src/main/ssh/ssh-relay-install-lock.test.ts": 157, + "src/main/ssh/ssh-relay-install-noisy-sftp-wire.test.ts": 299, + "src/main/ssh/ssh-relay-install-transfers-exec-fallback.test.ts": 101, + "src/main/ssh/ssh-relay-sentinel-copy-budget.test.ts": 114, + "src/main/ssh/ssh-relay-sentinel.test.ts": 400, + "src/main/ssh/ssh-relay-versioned-install-termination.test.ts": 236, + "src/main/ssh/ssh-remote-commands.test.ts": 8588, + "src/main/ssh/ssh-remote-node-resolution.test.ts": 317, + "src/main/ssh/ssh-remote-node-toolchain-probe.test.ts": 133, + "src/main/ssh/ssh-remote-platform-detection.test.ts": 206, + "src/main/ssh/ssh-remote-platform.test.ts": 34, + "src/main/ssh/ssh-remote-powershell.test.ts": 267, + "src/main/ssh/ssh-remote-windows-command-line-limit.test.ts": 179, + "src/main/ssh/ssh-request-outcome-verdict.test.ts": 37, + "src/main/ssh/ssh-response-pending-frames.test.ts": 76, + "src/main/ssh/ssh-security-key-identity.test.ts": 147, + "src/main/ssh/ssh-session-limit-error.test.ts": 27, + "src/main/ssh/ssh-system-fallback.test.ts": 275, + "src/main/ssh/ssh-system-transport.integration.test.ts": 277, + "src/main/ssh/ssh-target-id-migration.test.ts": 46, + "src/main/ssh/ssh-target-orcad-claims.test.ts": 306, + "src/main/ssh/ssh-target-orcad-dependents.test.ts": 269, + "src/main/ssh/ssh-target-orcad-preflight.test.ts": 1582, + "src/main/ssh/ssh-target-readoption.test.ts": 52, + "src/main/ssh/ssh-tcp-forwarding-probe.test.ts": 810, + "src/main/ssh/ssh-windows-host-cells.test.ts": 39, + "src/main/ssh/ssh2-port-forward-listen.test.ts": 30, + "src/main/ssh/system-ssh-binary.test.ts": 35, + "src/main/ssh/system-ssh-dynamic-forward-process.test.ts": 240, + "src/main/ssh/system-ssh-forward-process.test.ts": 251, + "src/main/ssh/system-ssh-orphaned-forward.test.ts": 86, + "src/main/ssh/system-ssh-sftp-args.test.ts": 44, + "src/main/ssh/system-ssh-sftp-path.test.ts": 38, + "src/main/ssh/system-ssh-upload-plan-budget.test.ts": 336, + "src/main/ssh/system-ssh-windows-upload.test.ts": 343, + "src/main/ssh/system-ssh-windows-write-capabilities.test.ts": 35, + "src/main/ssh/vscode-ssh-authority.test.ts": 48, + "src/main/star-nag/service-direct-star.test.ts": 323, + "src/main/star-nag/service-force-show-races.test.ts": 322, + "src/main/star-nag/service-outcome-telemetry.test.ts": 354, + "src/main/star-nag/service-prompt-moments.test.ts": 297, + "src/main/star-nag/service-threshold-prompt.test.ts": 295, + "src/main/startup/bootstrap-fatal-exit-guard.test.ts": 25, + "src/main/startup/branch-rename-hook-structured-session.test.ts": 515, + "src/main/startup/browser-process-user-agent-ordering.test.ts": 2454, + "src/main/startup/cli-launch-redirect.test.ts": 108, + "src/main/startup/cloud-session-shutdown-flush.test.ts": 37, + "src/main/startup/codex-account-restart-resume.test.ts": 327, + "src/main/startup/codex-hook-startup-wiring.test.ts": 8111, + "src/main/startup/codex-launch-per-agent-hook-opt-out.test.ts": 175, + "src/main/startup/codex-session-resume-daemon-guard.test.ts": 139, + "src/main/startup/configure-process-dev-parent-shutdown.test.ts": 103, + "src/main/startup/configure-process-profile-state.test.ts": 99, + "src/main/startup/configure-process.test.ts": 237, + "src/main/startup/desktop-profile-instance-lock.test.ts": 245, + "src/main/startup/dev-education-suppression.test.ts": 268, + "src/main/startup/dev-instance-identity.test.ts": 32, + "src/main/startup/ensure-virtual-display.test.ts": 99, + "src/main/startup/first-window-startup-services.test.ts": 36, + "src/main/startup/gpu-fallback-marker.test.ts": 58, + "src/main/startup/gpu-fallback-switches.test.ts": 40, + "src/main/startup/gpu-lifecycle-install-dir-acl-guard.test.ts": 427, + "src/main/startup/headless-serve-ssh-registration.test.ts": 1124, + "src/main/startup/http1-compatibility-profile-state.test.ts": 66, + "src/main/startup/hydrate-shell-path.test.ts": 436, + "src/main/startup/hydrate-shell-path.windows.test.ts": 327, + "src/main/startup/legacy-worker-renderer-recovery.test.ts": 52, + "src/main/startup/linux-dev-shm-policy.test.ts": 49, + "src/main/startup/linux-keyring-backend-selection.test.ts": 40, + "src/main/startup/linux-secret-service-probe.test.ts": 47, + "src/main/startup/login-shell-environment-cache.test.ts": 113, + "src/main/startup/login-shell-environment.test.ts": 75, + "src/main/startup/main-process-error-guards.test.ts": 76, + "src/main/startup/main-process-observers-hang-breadcrumb.test.ts": 214, + "src/main/startup/main-process-quit-pending-writer.test.ts": 711, + "src/main/startup/main-process-quit-update-veto.test.ts": 384, + "src/main/startup/main-process-ready-activation-failure.test.ts": 273, + "src/main/startup/main-process-ready-identity-write.test.ts": 1211, + "src/main/startup/main-process-ready-persistence-cleanup.test.ts": 196, + "src/main/startup/main-process-ready-phase-ordering.test.ts": 196, + "src/main/startup/main-process-runtime-launch-activation.test.ts": 8361, + "src/main/startup/main-window-actions-menu-target.test.ts": 204, + "src/main/startup/main-window-actions-user-quit.test.ts": 214, + "src/main/startup/main-window-core-services.test.ts": 88, + "src/main/startup/main-window-structured-status-filter.test.ts": 227, + "src/main/startup/os-opened-document-arguments.test.ts": 235, + "src/main/startup/os-opened-document-delivery.test.ts": 190, + "src/main/startup/os-opened-document-resolution.test.ts": 211, + "src/main/startup/os-opened-documents.test.ts": 211, + "src/main/startup/profile-state-recovery-preflight.test.ts": 790, + "src/main/startup/profile-state-save-delay.test.ts": 45, + "src/main/startup/profile-state-write-failure.test.ts": 426, + "src/main/startup/renderer-heap-headroom.test.ts": 50, "src/main/startup/run-electron-vite-dev-web.test.ts": 541, - "src/main/startup/run-electron-vite-dev.test.ts": 914, - "src/main/startup/select-linux-keyring-backend.test.ts": 50, - "src/main/startup/serve-desktop-activation.test.ts": 48, - "src/main/startup/serve-mode-argv-cli-redirect-order.test.ts": 85, - "src/main/startup/serve-mode-argv.test.ts": 88, - "src/main/startup/serve-native-quit-guard.test.ts": 55, - "src/main/startup/serve-options.test.ts": 104, - "src/main/startup/serve-signal-handlers.test.ts": 53, - "src/main/startup/serve-user-data-path-guard.test.ts": 165, - "src/main/startup/single-instance-lock-exit.electron.test.ts": 402, - "src/main/startup/single-instance-lock.test.ts": 80, - "src/main/startup/skill-share-deep-link-state.test.ts": 52, - "src/main/startup/startup-diagnostics.test.ts": 51, - "src/main/startup/window-all-closed-quit-policy.test.ts": 48, - "src/main/startup/windows-app-data-path.test.ts": 53, - "src/main/startup/windows-desktop-shell-path-startup.test.ts": 99, - "src/main/startup/windows-install-dir-acl-probe.test.ts": 144, - "src/main/startup/windows-install-dir-acl-recovery.test.ts": 578, - "src/main/startup/windows-install-dir-acl-repair.win32.test.ts": 124, - "src/main/startup/windows-install-dir-package-acl-repair.test.ts": 153, - "src/main/startup/windows-shell-path-hydration.test.ts": 388, - "src/main/startup/windows-shell-path-ownership.test.ts": 56, - "src/main/startup/windows-shell-path-restoration.windows.test.ts": 48, - "src/main/startup/windows-user-data-acl.test.ts": 73, - "src/main/startup/wsl-cli-reconciliation-startup-barrier.test.ts": 58, - "src/main/stats/agent-session-transition-recorder.test.ts": 604, - "src/main/stats/collector-async-save.test.ts": 513, - "src/main/synthetic-title-frame-routing.test.ts": 42, - "src/main/synthetic-title-spinner.test.ts": 51, - "src/main/synthetic-title-visibility.test.ts": 37, - "src/main/system-fonts.test.ts": 80, - "src/main/system-power-lifecycle.test.ts": 43, - "src/main/system-resume-broadcast.test.ts": 68, - "src/main/telemetry/burst-cap.test.ts": 343, - "src/main/telemetry/classify-error.test.ts": 48, - "src/main/telemetry/client-lifecycle.test.ts": 359, - "src/main/telemetry/client.test.ts": 344, - "src/main/telemetry/cohort-classifier.test.ts": 42, - "src/main/telemetry/consent.test.ts": 71, - "src/main/telemetry/install-id.test.ts": 79, - "src/main/telemetry/onboarding-cohort-classifier.test.ts": 70, - "src/main/telemetry/onboarding-feature-setup-validator.test.ts": 277, - "src/main/telemetry/validator-warn-cache.test.ts": 290, - "src/main/telemetry/validator.test.ts": 364, - "src/main/terminal-history-async-delete.test.ts": 402, - "src/main/terminal-history-gc-fs-call-count.test.ts": 176, - "src/main/terminal-history-gc.test.ts": 821, - "src/main/terminal-history-tombstone-retry.test.ts": 764, - "src/main/terminal-history-tombstone-scan-cost.test.ts": 319, - "src/main/terminal-history.test.ts": 153, - "src/main/text-generation/agent-failure-output.test.ts": 57, - "src/main/text-generation/command-environment-process.test.ts": 915, - "src/main/text-generation/command-environment-wsl.test.ts": 83, - "src/main/text-generation/commit-message-agent-environment.test.ts": 73, - "src/main/text-generation/commit-message-command-backslash-mode.test.ts": 499, - "src/main/text-generation/commit-message-text-generation-branch-name.test.ts": 498, - "src/main/text-generation/commit-message-text-generation-cancellation.test.ts": 1070, - "src/main/text-generation/commit-message-text-generation-failure-sanitization.test.ts": 406, - "src/main/text-generation/commit-message-text-generation-generated-output.test.ts": 426, - "src/main/text-generation/commit-message-text-generation-linked-issue.test.ts": 401, - "src/main/text-generation/commit-message-text-generation-local-subprocess.test.ts": 463, - "src/main/text-generation/commit-message-text-generation-model-discovery.test.ts": 504, - "src/main/text-generation/commit-message-text-generation-regression.test.ts": 463, - "src/main/text-generation/commit-message-text-generation-remote-execution.test.ts": 418, - "src/main/text-generation/commit-message-text-generation-settings.test.ts": 451, - "src/main/text-generation/conversation-name-generation-request.test.ts": 355, - "src/main/text-generation/omp-model-discovery-policy.test.ts": 251, - "src/main/text-generation/opencode-generation-events.test.ts": 351, - "src/main/text-generation/pull-request-context-errors.test.ts": 71, - "src/main/text-generation/pull-request-context.test.ts": 89, - "src/main/text-generation/source-control-agent-launch.test.ts": 1915, - "src/main/text-generation/source-control-local-process.test.ts": 1522, - "src/main/tray/system-tray.test.ts": 178, - "src/main/tray/tray-attention-icon.test.ts": 63, - "src/main/tray/tray-dev-badge.test.ts": 56, - "src/main/update-install-exit-watchdog.test.ts": 77, - "src/main/updater-agent-state-rules-release.test.ts": 103, - "src/main/updater-changelog.test.ts": 63, - "src/main/updater-events.test.ts": 227, - "src/main/updater-lifecycle-diagnostics.test.ts": 57, - "src/main/updater-linux-package-recovery-actions.test.ts": 4060, + "src/main/startup/run-electron-vite-dev.test.ts": 916, + "src/main/startup/select-linux-keyring-backend.test.ts": 53, + "src/main/startup/serve-desktop-activation.test.ts": 33, + "src/main/startup/serve-mode-argv-cli-redirect-order.test.ts": 73, + "src/main/startup/serve-mode-argv.test.ts": 69, + "src/main/startup/serve-native-quit-guard.test.ts": 47, + "src/main/startup/serve-options.test.ts": 66, + "src/main/startup/serve-signal-handlers.test.ts": 42, + "src/main/startup/serve-user-data-path-guard.test.ts": 91, + "src/main/startup/single-instance-lock-exit.electron.test.ts": 306, + "src/main/startup/single-instance-lock.test.ts": 58, + "src/main/startup/skill-share-deep-link-state.test.ts": 43, + "src/main/startup/startup-diagnostics.test.ts": 34, + "src/main/startup/window-all-closed-quit-policy.test.ts": 52, + "src/main/startup/windows-app-data-path.test.ts": 42, + "src/main/startup/windows-desktop-shell-path-startup.test.ts": 40, + "src/main/startup/windows-install-dir-acl-probe.test.ts": 102, + "src/main/startup/windows-install-dir-acl-recovery.test.ts": 581, + "src/main/startup/windows-install-dir-acl-repair.win32.test.ts": 73, + "src/main/startup/windows-install-dir-package-acl-grant.test.ts": 66, + "src/main/startup/windows-install-dir-package-acl-repair.test.ts": 124, + "src/main/startup/windows-shell-path-hydration.test.ts": 381, + "src/main/startup/windows-shell-path-ownership.test.ts": 31, + "src/main/startup/windows-shell-path-restoration.windows.test.ts": 52, + "src/main/startup/windows-user-data-acl.test.ts": 58, + "src/main/startup/wsl-cli-reconciliation-startup-barrier.test.ts": 56, + "src/main/startup/wsl-orcad-account-env-wiring.test.ts": 402, + "src/main/stats/agent-session-transition-recorder.test.ts": 757, + "src/main/stats/collector-async-save.test.ts": 554, + "src/main/synthetic-title-frame-routing.test.ts": 37, + "src/main/synthetic-title-spinner.test.ts": 34, + "src/main/synthetic-title-visibility.test.ts": 28, + "src/main/system-fonts.test.ts": 58, + "src/main/system-power-lifecycle.test.ts": 34, + "src/main/system-resume-broadcast.test.ts": 70, + "src/main/telemetry/burst-cap.test.ts": 322, + "src/main/telemetry/classify-error.test.ts": 32, + "src/main/telemetry/client-lifecycle.test.ts": 445, + "src/main/telemetry/client.test.ts": 294, + "src/main/telemetry/cohort-classifier.test.ts": 34, + "src/main/telemetry/consent.test.ts": 49, + "src/main/telemetry/install-id.test.ts": 43, + "src/main/telemetry/onboarding-cohort-classifier.test.ts": 35, + "src/main/telemetry/onboarding-feature-setup-validator.test.ts": 273, + "src/main/telemetry/validator-warn-cache.test.ts": 355, + "src/main/telemetry/validator.test.ts": 517, + "src/main/terminal-history-async-delete.test.ts": 288, + "src/main/terminal-history-gc-fs-call-count.test.ts": 152, + "src/main/terminal-history-gc.test.ts": 692, + "src/main/terminal-history-tombstone-retry.test.ts": 676, + "src/main/terminal-history-tombstone-scan-cost.test.ts": 294, + "src/main/terminal-history.test.ts": 106, + "src/main/text-generation/agent-failure-output.test.ts": 41, + "src/main/text-generation/command-environment-process.test.ts": 875, + "src/main/text-generation/command-environment-wsl.test.ts": 100, + "src/main/text-generation/commit-message-agent-environment.test.ts": 87, + "src/main/text-generation/commit-message-command-backslash-mode.test.ts": 342, + "src/main/text-generation/commit-message-text-generation-branch-name.test.ts": 418, + "src/main/text-generation/commit-message-text-generation-cancellation.test.ts": 1109, + "src/main/text-generation/commit-message-text-generation-failure-sanitization.test.ts": 371, + "src/main/text-generation/commit-message-text-generation-generated-output.test.ts": 393, + "src/main/text-generation/commit-message-text-generation-linked-issue.test.ts": 405, + "src/main/text-generation/commit-message-text-generation-local-subprocess.test.ts": 433, + "src/main/text-generation/commit-message-text-generation-model-discovery.test.ts": 476, + "src/main/text-generation/commit-message-text-generation-regression.test.ts": 442, + "src/main/text-generation/commit-message-text-generation-remote-execution.test.ts": 446, + "src/main/text-generation/commit-message-text-generation-settings.test.ts": 419, + "src/main/text-generation/conversation-name-generation-request.test.ts": 439, + "src/main/text-generation/jira-issue-summary-requests.test.ts": 259, + "src/main/text-generation/jira-issue-summary-text-generation.test.ts": 375, + "src/main/text-generation/omp-model-discovery-policy.test.ts": 242, + "src/main/text-generation/opencode-generation-events.test.ts": 336, + "src/main/text-generation/pull-request-context-errors.test.ts": 61, + "src/main/text-generation/pull-request-context.test.ts": 100, + "src/main/text-generation/source-control-agent-launch.test.ts": 1928, + "src/main/text-generation/source-control-local-process.test.ts": 1508, + "src/main/tray/system-tray.test.ts": 288, + "src/main/tray/tray-attention-icon.test.ts": 40, + "src/main/tray/tray-dev-badge.test.ts": 40, + "src/main/update-install-exit-watchdog.test.ts": 60, + "src/main/updater-agent-state-rules-release.test.ts": 76, + "src/main/updater-changelog.test.ts": 56, + "src/main/updater-events.test.ts": 250, + "src/main/updater-lifecycle-diagnostics.test.ts": 44, + "src/main/updater-linux-package-recovery-actions.test.ts": 3886, "src/main/updater-mac-quit-guard.test.ts": 40, - "src/main/updater-nudge.test.ts": 247, + "src/main/updater-nudge.test.ts": 234, "src/main/updater-prerelease-feed-readiness.test.ts": 156, - "src/main/updater-prerelease-feed.test.ts": 237, - "src/main/updater-release-api-token.test.ts": 63, - "src/main/updater-release-build-cache.test.ts": 58, - "src/main/updater-release-builds.test.ts": 95, - "src/main/updater-sqlite-profile-downgrade.test.ts": 1672, - "src/main/updater.build-channel-selection.test.ts": 3459, - "src/main/updater.check-failure.test.ts": 3131, - "src/main/updater.check-preflight.test.ts": 4281, - "src/main/updater.check-settlement.test.ts": 3437, - "src/main/updater.fallback.test.ts": 54, - "src/main/updater.feed-attempt-lifetime.test.ts": 2886, - "src/main/updater.headless-serve-install.test.ts": 431, - "src/main/updater.install-failure-cause.test.ts": 3737, - "src/main/updater.linux-externally-managed.test.ts": 2482, - "src/main/updater.linux-root-package-install.test.ts": 2779, - "src/main/updater.mac-install.test.ts": 55, - "src/main/updater.nudge-campaign.test.ts": 2950, - "src/main/updater.prerelease-fallback.test.ts": 4430, - "src/main/updater.publishing-window-feed.test.ts": 4333, - "src/main/updater.quit-and-install.test.ts": 4367, - "src/main/updater.startup-scheduling.test.ts": 3238, - "src/main/updater/rollout-flags.test.ts": 338, - "src/main/usage-worktree-canonicalizer.test.ts": 47, - "src/main/usage-worktree-metadata.test.ts": 60, - "src/main/usage/agent-token-usage-reporter.test.ts": 802, - "src/main/usage/agent-token-usage.test.ts": 46, - "src/main/usage/analytics-session-id-store.test.ts": 247, - "src/main/usage/highest-usage-key.test.ts": 68, - "src/main/usage/jsonl-file-reader.test.ts": 87, - "src/main/usage/jsonl-file-snapshot.test.ts": 73, - "src/main/usage/jsonl-line-offsets.test.ts": 67, - "src/main/usage/usage-breakdown-scaling.test.ts": 93, - "src/main/usage/usage-calendar-range.test.ts": 59, - "src/main/usage/usage-provider-store-initialization.test.ts": 734, - "src/main/usage/usage-provider-store-lifecycle.test.ts": 751, - "src/main/usage/usage-scan-worker-cache-cleanup.test.ts": 130, - "src/main/usage/usage-scan-worker-client.test.ts": 98, - "src/main/usage/usage-scan-worker-event-loop.test.ts": 2406, - "src/main/usage/usage-source-cache-file.test.ts": 77, - "src/main/usage/usage-worktree-refs.test.ts": 43, - "src/main/usage/usage-worktree-resolver.test.ts": 125, - "src/main/warp-themes/discovery.test.ts": 76, - "src/main/warp-themes/index.test.ts": 685, - "src/main/warp-themes/manual-warp-theme-files.test.ts": 61, - "src/main/warp-themes/parser-runner.test.ts": 55, - "src/main/warp-themes/parser.test.ts": 108, - "src/main/warp-themes/theme-file-scanner.test.ts": 70, - "src/main/win32-utils-async-acl.test.ts": 56, - "src/main/win32-utils.test.ts": 70, - "src/main/window/attach-main-window-services.test.ts": 1420, - "src/main/window/clipboard-agent-session-attachment-paste.test.ts": 438, - "src/main/window/clipboard-copied-file-paths.test.ts": 75, - "src/main/window/clipboard-dashboard-popout-access.test.ts": 1333, - "src/main/window/clipboard-file-copy.test.ts": 68, - "src/main/window/clipboard-image-source.test.ts": 60, - "src/main/window/clipboard-image-temp-file.test.ts": 61, - "src/main/window/clipboard-image-thumbnail.test.ts": 44, - "src/main/window/clipboard-ipc-handlers.test.ts": 477, - "src/main/window/clipboard-remote-file-cleanup.bench.test.ts": 46, - "src/main/window/clipboard-remote-file-copy.test.ts": 82, - "src/main/window/clipboard-remote-file-staging-fs.test.ts": 59, - "src/main/window/clipboard-remote-file-staging.test.ts": 107, - "src/main/window/clipboard-runtime-owned-ssh-paste.test.ts": 341, - "src/main/window/clipboard-text-write-verify.test.ts": 62, - "src/main/window/clipboard-windows-image-file.test.ts": 91, - "src/main/window/createMainWindow-close-confirmation.test.ts": 2132, - "src/main/window/createMainWindow-low-commit-oom-recovery.test.ts": 1748, - "src/main/window/createMainWindow-markdown-editor-focus.test.ts": 1897, - "src/main/window/createMainWindow-recovery-reload-watchdog.test.ts": 2346, - "src/main/window/createMainWindow-renderer-crash-recovery.test.ts": 2303, - "src/main/window/createMainWindow-renderer-launch-failure.test.ts": 1598, - "src/main/window/createMainWindow-startup-reveal.test.ts": 2157, - "src/main/window/createMainWindow-system-resume-relay.test.ts": 2342, - "src/main/window/createMainWindow-terminal-focus-shortcuts.test.ts": 1846, - "src/main/window/createMainWindow-tray-minimize-close.test.ts": 1969, - "src/main/window/createMainWindow-user-quit-close.test.ts": 2017, - "src/main/window/createMainWindow-zoom-and-tab-switch-shortcuts.test.ts": 1751, - "src/main/window/createMainWindow.test.ts": 2184, - "src/main/window/darwin-user-temp-dir.test.ts": 56, - "src/main/window/dashboard-popout-window.test.ts": 122, - "src/main/window/dragged-temp-file-copy.test.ts": 189, - "src/main/window/dropped-path-preparation-ipc.test.ts": 103, - "src/main/window/dropped-path-preparation.test.ts": 89, - "src/main/window/editable-context-menu.test.ts": 80, - "src/main/window/focus-existing-window.test.ts": 70, - "src/main/window/foreground-activation-policy.test.ts": 59, - "src/main/window/history-gc-worktree-ids.test.ts": 311, - "src/main/window/host-frame-navigation-guard.test.ts": 237, - "src/main/window/macos-app-activation.test.ts": 67, - "src/main/window/macos-tahoe-release.test.ts": 55, - "src/main/window/main-window-state-lifecycle.test.ts": 54, - "src/main/window/main-window-updater-deferred-setup.test.ts": 146, - "src/main/window/main-window-visibility.test.ts": 56, - "src/main/window/main-window-webview-security.test.ts": 344, - "src/main/window/mobile-markdown-request-relay.test.ts": 61, - "src/main/window/native-chat-paste-files.test.ts": 497, - "src/main/window/privileged-window-navigation.test.ts": 266, - "src/main/window/renderer-document-navigation.test.ts": 60, - "src/main/window/renderer-launch-failure-backoff.test.ts": 37, - "src/main/window/renderer-launch-failure-probe.spawn.test.ts": 71, - "src/main/window/renderer-launch-failure-probe.test.ts": 69, - "src/main/window/renderer-publication-throttle.test.ts": 55, - "src/main/window/renderer-recovery-prompt.test.ts": 109, - "src/main/window/renderer-recovery-reload-watchdog.test.ts": 76, - "src/main/window/runtime-renderer-notification-sender.test.ts": 47, - "src/main/window/runtime-window-lifecycle-file-open.test.ts": 57, - "src/main/window/session-tab-close-request-relay.test.ts": 74, - "src/main/window/terminal-tab-close-request-relay.test.ts": 63, - "src/main/window/updater-package-recovery-ipc.test.ts": 7052, - "src/main/window/window-close-decision.test.ts": 52, - "src/main/windows-live-tree-kill.win32.test.ts": 60, - "src/main/windows-process-tree-kill.test.ts": 81, - "src/main/windows-pty-root-identity.test.ts": 129, - "src/main/windows-registry-addon.test.ts": 41, - "src/main/windows/windows-command-line-recovery-health.test.ts": 50, - "src/main/windows/windows-host-job.win32.test.ts": 44, - "src/main/windows/windows-msys-job.win32.test.ts": 60, - "src/main/windows/windows-process-creation-time.test.ts": 80, - "src/main/windows/windows-process-table-cim-scan.test.ts": 63, - "src/main/windows/windows-process-table-native-addon.win32.test.ts": 60, - "src/main/windows/windows-process-table.test.ts": 177, - "src/main/windows/windows-process-tree-command-line-patch.test.ts": 61, - "src/main/windows/windows-pty-job.test.ts": 74, - "src/main/windows/windows-pty-job.win32.test.ts": 71, - "src/main/worker-thread-request-queue.test.ts": 94, - "src/main/workspace-cleanup-removal-snapshot-prune.test.ts": 78, - "src/main/workspace-cleanup-scan-snapshot.test.ts": 166, - "src/main/workspace-create-event-fields.test.ts": 300, - "src/main/workspace-snapshot-pruning.bench.test.ts": 53, - "src/main/workspace-space-analysis-capacity.test.ts": 475, - "src/main/workspace-space-analysis-du-timeout.test.ts": 587, - "src/main/workspace-space-analysis-snapshot.test.ts": 130, - "src/main/workspace-space-analysis.test.ts": 652, - "src/main/workspace-space-repo-scan.test.ts": 388, - "src/main/workspace-space-scan-control.test.ts": 55, - "src/main/worktree-background-removal-records.test.ts": 417, - "src/main/worktree-background-removal.test.ts": 346, - "src/main/worktree-create-base-prefetch.test.ts": 308, - "src/main/worktree-create-base.test.ts": 50, - "src/main/worktree-create-candidates.test.ts": 49, - "src/main/worktree-create-concurrency.test.ts": 67, - "src/main/worktree-create-execution-host-route.test.ts": 385, - "src/main/worktree-create-preparation-activity.test.ts": 43, - "src/main/worktree-create-preparation-cancellation.test.ts": 353, - "src/main/worktree-create-preparation-claim.test.ts": 55, - "src/main/worktree-create-preparation-contention.test.ts": 363, - "src/main/worktree-create-preparation-timing.test.ts": 316, - "src/main/worktree-create-preparation-wsl-root.test.ts": 348, - "src/main/worktree-create-preparation.test.ts": 299, - "src/main/worktree-create-timing.test.ts": 119, - "src/main/worktree-failed-removal.test.ts": 400, - "src/main/worktree-identity-persistence.test.ts": 1751, - "src/main/worktree-lineage-pruning.test.ts": 76, - "src/main/worktree-metadata-ownership.test.ts": 46, - "src/main/worktree-name-retirement.test.ts": 175, - "src/main/worktree-preparation-discard-retry.test.ts": 245, - "src/main/worktree-preparation-fetch-barrier.test.ts": 343, - "src/main/worktree-preparation-tip-refresh.test.ts": 500, - "src/main/worktree-prunable-git-file.test.ts": 88, - "src/main/worktree-removal-authority.test.ts": 104, - "src/main/worktree-removal-close-records.test.ts": 1393, - "src/main/worktree-removal-execution-host-route.test.ts": 397, - "src/main/worktree-removal-home-guard.test.ts": 94, - "src/main/worktree-removal-repo-owner.test.ts": 49, - "src/main/worktree-removal-safety.test.ts": 177, - "src/main/worktree-removal-session-partition-fencing.test.ts": 1465, - "src/main/worktree-retirement-backfill-scan.test.ts": 76, - "src/main/worktree-retirement-backfill-stall.test.ts": 141, - "src/main/worktree-retirement-discovery-wsl.test.ts": 184, - "src/main/worktree-retirement-discovery.test.ts": 174, - "src/main/worktree-retirement-namespace.test.ts": 84, - "src/main/worktree-root-preparation.test.ts": 116, - "src/main/worktree-trash.test.ts": 138, - "src/main/wsl-approved-root-race.wsl.test.ts": 65, - "src/main/wsl-availability-missing-kernel.test.ts": 68, - "src/main/wsl-bash-command.test.ts": 47, - "src/main/wsl-distro-list-output.test.ts": 40, - "src/main/wsl-distro-list-single-flight.test.ts": 103, - "src/main/wsl-fish-history-cleanup.test.ts": 94, - "src/main/wsl-interop-spawn-directory.test.ts": 54, - "src/main/wsl-managed-cli-path-restore.test.ts": 71, - "src/main/wsl-running-distros.test.ts": 217, - "src/main/wsl-running-path-filter.test.ts": 52, - "src/main/wsl-unc-delete-symlink-repro.test.ts": 78, - "src/main/wsl-unc-delete.test.ts": 92, - "src/main/wsl-unc-delete.wsl.test.ts": 71, - "src/main/wsl.test.ts": 152, - "src/main/wsl/wsl-executable-path.win32.test.ts": 44, - "src/main/wsl/wsl-guest-environment.test.ts": 109, - "src/main/wsl/wsl-pinned-runtime.test.ts": 413, - "src/main/wsl/wsl-runner.test.ts": 86, - "src/main/wsl/wsl-runner.wsl.test.ts": 71, - "src/main/wsl/wsl-w1-w3-contract.test.ts": 79, - "src/main/zcode/hook-service.test.ts": 130, - "src/main/zcode/interactive-capability.live.test.ts": 61, - "src/main/zcode/zcode-plan-api-key-store.test.ts": 72, - "src/packages/process-host/src/bounded-output-sink.test.ts": 98, - "src/packages/process-host/src/close-process-registry.test.ts": 51, - "src/packages/process-host/src/growing-byte-buffer.test.ts": 119, - "src/packages/process-host/src/posix-lsof-probe.test.ts": 4335, - "src/packages/process-host/src/process-tree-termination.test.ts": 86, - "src/packages/process-host/src/retryable-process-exit-proof.test.ts": 53, - "src/packages/process-host/src/run-process-bytes.test.ts": 2227, - "src/packages/process-host/src/run-process-termination-failure.test.ts": 85, - "src/packages/process-host/src/run-process.test.ts": 7249, - "src/packages/process-host/src/spawn-observer.test.ts": 181, - "src/packages/process-host/src/spawn-process-stdio.test.ts": 247, - "src/packages/process-host/src/windows-command-line.test.ts": 81, - "src/preload/api/notification-sound-playback.test.ts": 52, - "src/preload/api/platform-bridge.test.ts": 71, - "src/preload/app-restart-checkpoint-routing.test.ts": 1800, - "src/preload/browser-client-page-renderer-requests.test.ts": 262, - "src/preload/browser-find-subscriptions.test.ts": 58, - "src/preload/browser-window-close.test.ts": 45, - "src/preload/close-active-tab-payload-admission.test.ts": 54, - "src/preload/doc-preview-link-interception.test.ts": 213, - "src/preload/opencode-go-credentials.test.ts": 43, - "src/preload/preload-file-drop-absence.test.ts": 293, - "src/preload/pty-snapshot-capability-ipc.test.ts": 992, - "src/preload/renderer-heap-statistics-reader.test.ts": 58, - "src/preload/renderer-process-memory-reader.test.ts": 56, - "src/preload/renderer-restart-wiring.test.ts": 84, - "src/preload/runtime-environment-subscriptions.test.ts": 73, - "src/preload/ssh-authority-forwarding.test.ts": 1025, - "src/preload/updater-package-recovery.test.ts": 867, - "src/preload/usage-provider-api.test.ts": 52, - "src/renderer/src/app-shell/app-command-handlers-child-workspaces.test.ts": 1064, - "src/renderer/src/app-shell/app-command-handlers-tab-rename.test.ts": 1261, - "src/renderer/src/app-shell/app-command-handlers-workspace-delete.test.ts": 646, - "src/renderer/src/app-shell/app-root-surface-settings.test.tsx": 455, - "src/renderer/src/app-shell/app-window-chrome.test.ts": 37, - "src/renderer/src/app-shell/reconcile-hydrated-workspace-tab-models.test.ts": 40, - "src/renderer/src/app-shell/shutdown-checkpoint-persist.test.ts": 62, - "src/renderer/src/app-shell/shutdown-checkpoint-restart-lifecycle.test.ts": 171, - "src/renderer/src/app-shell/startup-actions-selector.test.ts": 78, - "src/renderer/src/app-shell/use-document-appearance.test.tsx": 1472, - "src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx": 454, - "src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx": 291, - "src/renderer/src/app-shell/use-terminal-viewer-color-publication.test.tsx": 1708, - "src/renderer/src/app-shell/use-visible-hosted-review-refresh.test.tsx": 338, - "src/renderer/src/app-shell/visible-review-reporting.test.ts": 1357, - "src/renderer/src/app-shell/window-visibility-actions-selector.test.ts": 228, - "src/renderer/src/app-shell/workspace-view-cross-client-sync.test.tsx": 927, - "src/renderer/src/attention/agent-attention-policy.test.ts": 58, - "src/renderer/src/attention/notification-sources.test.ts": 1870, - "src/renderer/src/components/AgentStateDot.test.ts": 179, - "src/renderer/src/components/AgentWorkingSpinner.test.tsx": 249, - "src/renderer/src/components/ButtonKeyHint.test.tsx": 348, - "src/renderer/src/components/LinuxPackageInstallRecoveryCard.test.tsx": 868, - "src/renderer/src/components/NativeChatResumeOnRestartGroups.selection.test.tsx": 2230, - "src/renderer/src/components/NativeChatResumeOnRestartGroups.test.tsx": 2007, - "src/renderer/src/components/NativeChatResumeOnRestartModal.list.test.tsx": 2572, - "src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx": 3280, - "src/renderer/src/components/NewWorkspaceComposerCard.file-drop.test.tsx": 884, - "src/renderer/src/components/NewWorkspaceComposerCard.quick-file-drop.test.tsx": 3372, - "src/renderer/src/components/NewWorkspaceComposerCard.set-location-warm.test.tsx": 1056, - "src/renderer/src/components/NewWorkspaceComposerCard.set-location.test.tsx": 1064, - "src/renderer/src/components/NewWorkspaceComposerCard.start-from.test.tsx": 929, - "src/renderer/src/components/NewWorkspaceComposerCard.test.tsx": 1755, - "src/renderer/src/components/QuickOpen.mount-gating.test.tsx": 1998, - "src/renderer/src/components/QuickOpen.selection.test.tsx": 720, - "src/renderer/src/components/SelectedTextCopyMenu.test.tsx": 343, - "src/renderer/src/components/StarNagCard.test.tsx": 322, - "src/renderer/src/components/StateIndicatorTooltip.test.tsx": 76, - "src/renderer/src/components/TerminalSearch.test.tsx": 518, - "src/renderer/src/components/TerminalTitlebarTabs.test.tsx": 222, - "src/renderer/src/components/TerminalWorkbenchContainer.test.tsx": 293, - "src/renderer/src/components/UpdateCard.error-card.test.tsx": 1835, - "src/renderer/src/components/UpdateCard.test.ts": 1484, - "src/renderer/src/components/WorktreeBaseFallbackDialog.test.tsx": 1751, - "src/renderer/src/components/WorktreeJumpPalette.linear-url.test.tsx": 3680, - "src/renderer/src/components/WorktreeJumpPalette.mount-gating.test.tsx": 2974, - "src/renderer/src/components/WorktreeJumpPalette.recent-tabs.behavior.test.tsx": 3247, - "src/renderer/src/components/WorktreeJumpPalette.recent-tabs.test.tsx": 5538, - "src/renderer/src/components/WorktreeJumpPalette.test.tsx": 3096, - "src/renderer/src/components/activity/ActivityPrototypePage.filter-focus-shortcut.test.ts": 54, - "src/renderer/src/components/activity/ActivityPrototypePage.test.ts": 443, - "src/renderer/src/components/activity/ActivityPrototypePage.thread-grouping.test.ts": 1500, - "src/renderer/src/components/activity/ActivityThreadOptionsMenu.test.tsx": 2356, - "src/renderer/src/components/activity/activity-answered-ask-timeline.test.tsx": 1673, - "src/renderer/src/components/activity/activity-auto-mark-read-loop.react185.test.tsx": 3249, - "src/renderer/src/components/activity/activity-clear-completed-hmr.test.ts": 567, - "src/renderer/src/components/activity/activity-clear-completed-state-lifetime.test.ts": 2446, - "src/renderer/src/components/activity/activity-clear-completed.test.ts": 406, - "src/renderer/src/components/activity/activity-event-builder.identity-reuse.test.ts": 438, - "src/renderer/src/components/activity/activity-portal-churn-budget.test.ts": 47, - "src/renderer/src/components/activity/activity-portal-readiness-decay.react185.test.tsx": 200, - "src/renderer/src/components/activity/activity-portal-readiness-loop.react185.test.tsx": 372, - "src/renderer/src/components/activity/activity-portal-readiness-oscillation.test.ts": 62, - "src/renderer/src/components/activity/activity-portal-readiness-subscription-churn.react185.test.tsx": 303, - "src/renderer/src/components/activity/activity-portal-thread-reconciliation.test.ts": 55, - "src/renderer/src/components/activity/activity-scope-filter.test.ts": 588, - "src/renderer/src/components/activity/activity-search-query-work-budget.test.tsx": 1691, - "src/renderer/src/components/activity/activity-terminal-portal-publication-loop.react185.test.tsx": 169, - "src/renderer/src/components/activity/activity-terminal-portal.test.tsx": 162, - "src/renderer/src/components/activity/activity-thread-actions.test.ts": 739, - "src/renderer/src/components/activity/activity-thread-context-menu.test.tsx": 2485, - "src/renderer/src/components/activity/activity-thread-grouping.status-order.test.ts": 435, - "src/renderer/src/components/activity/activity-thread-hover-card.test.tsx": 3220, - "src/renderer/src/components/activity/activity-thread-list-pane-collapsible.test.tsx": 3450, - "src/renderer/src/components/activity/activity-thread-list-pane.multi-select.test.tsx": 3049, - "src/renderer/src/components/activity/activity-thread-list-pane.virtualization.test.tsx": 3728, - "src/renderer/src/components/activity/dev-activity-fixture.test.ts": 62, - "src/renderer/src/components/activity/event-time-clock-refresh.test.tsx": 1553, - "src/renderer/src/components/activity/use-activity-thread-action-bindings.test.tsx": 1820, - "src/renderer/src/components/activity/useActivityUnreadCount.freshness.test.tsx": 1645, - "src/renderer/src/components/activity/useActivityUnreadCount.test.ts": 1635, - "src/renderer/src/components/agent-child-row-parity.test.tsx": 641, - "src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.test.tsx": 376, - "src/renderer/src/components/agent-session-continuation/agent-session-continuation-selection.test.ts": 55, - "src/renderer/src/components/agent/AgentCombobox.test.tsx": 1407, - "src/renderer/src/components/agent/AgentSettingsDialog.test.tsx": 555, - "src/renderer/src/components/agent/agent-combobox-command-state.test.ts": 45, - "src/renderer/src/components/artifacts/ArtifactCollection.test.tsx": 1101, - "src/renderer/src/components/artifacts/ArtifactPreview.test.tsx": 381, - "src/renderer/src/components/artifacts/ArtifactPublishButton.test.tsx": 616, - "src/renderer/src/components/artifacts/ArtifactsPage.test.tsx": 1568, - "src/renderer/src/components/artifacts/artifact-display-labels.test.ts": 88, - "src/renderer/src/components/artifacts/artifact-list-search.test.ts": 104, - "src/renderer/src/components/artifacts/artifact-list-windowing.test.tsx": 3096, - "src/renderer/src/components/artifacts/artifact-publish-flow.test.ts": 125, - "src/renderer/src/components/artifacts/artifact-published-link-client.test.ts": 51, - "src/renderer/src/components/automations/AutomationDestinationField.test.tsx": 622, - "src/renderer/src/components/automations/AutomationDetail.host.test.tsx": 751, - "src/renderer/src/components/automations/AutomationDetail.test.tsx": 681, - "src/renderer/src/components/automations/AutomationEditorPromptEditor.test.tsx": 318, - "src/renderer/src/components/automations/AutomationEditorSettingsSidebar.test.tsx": 655, - "src/renderer/src/components/automations/AutomationHostBadges.test.tsx": 332, - "src/renderer/src/components/automations/AutomationHostFilterNotice.test.tsx": 379, - "src/renderer/src/components/automations/AutomationListEmptyView.test.tsx": 244, - "src/renderer/src/components/automations/AutomationListHostGroups.test.tsx": 579, - "src/renderer/src/components/automations/AutomationListLocalRows.test.tsx": 1000, - "src/renderer/src/components/automations/AutomationListSearchField.test.tsx": 345, - "src/renderer/src/components/automations/AutomationListTableHeader.test.tsx": 448, - "src/renderer/src/components/automations/AutomationOwnerConflictNotice.test.tsx": 476, - "src/renderer/src/components/automations/AutomationPromptDisclosure.test.tsx": 1051, - "src/renderer/src/components/automations/AutomationRunHistory.test.tsx": 1017, - "src/renderer/src/components/automations/AutomationRunsDashboardSurface.test.tsx": 351, - "src/renderer/src/components/automations/AutomationRunsTable.test.tsx": 540, - "src/renderer/src/components/automations/AutomationSchedulePicker.render.test.tsx": 962, - "src/renderer/src/components/automations/AutomationSchedulePicker.test.ts": 875, - "src/renderer/src/components/automations/AutomationTimeField.test.tsx": 593, - "src/renderer/src/components/automations/AutomationsDetailPane.run-count.test.tsx": 2326, - "src/renderer/src/components/automations/AutomationsDetailPane.test.tsx": 2585, - "src/renderer/src/components/automations/AutomationsListPanel.test.tsx": 1015, - "src/renderer/src/components/automations/AutomationsPage.create-destination.test.tsx": 2412, - "src/renderer/src/components/automations/AutomationsPage.cross-authority-actions.test.tsx": 2046, - "src/renderer/src/components/automations/AutomationsPage.escape-precedence.test.tsx": 1344, - "src/renderer/src/components/automations/AutomationsPage.external-scope.test.tsx": 1416, - "src/renderer/src/components/automations/AutomationsPage.notice-recovery.test.tsx": 2091, - "src/renderer/src/components/automations/AutomationsPage.refresh-selection.test.tsx": 1405, - "src/renderer/src/components/automations/AutomationsPage.run-visibility.test.tsx": 1871, - "src/renderer/src/components/automations/AutomationsPage.save-visibility.test.tsx": 1326, - "src/renderer/src/components/automations/AutomationsPage.strict-mode-save-visibility.test.tsx": 2040, - "src/renderer/src/components/automations/AutomationsPage.test.tsx": 2751, - "src/renderer/src/components/automations/AutomationsPageBreadcrumb.test.tsx": 304, - "src/renderer/src/components/automations/ExternalAutomationManagers.test.tsx": 897, - "src/renderer/src/components/automations/automation-authority-identity.test.ts": 54, - "src/renderer/src/components/automations/automation-captured-owner.test.ts": 177, - "src/renderer/src/components/automations/automation-create-destination.test.ts": 115, - "src/renderer/src/components/automations/automation-create-projects.test.ts": 45, - "src/renderer/src/components/automations/automation-detail-tab-navigation.test.ts": 153, - "src/renderer/src/components/automations/automation-editor-prompt-options.test.ts": 44, - "src/renderer/src/components/automations/automation-external-target-match.test.ts": 56, - "src/renderer/src/components/automations/automation-extra-agent-args-capability.test.ts": 115, - "src/renderer/src/components/automations/automation-host-cache-controller.test.ts": 185, - "src/renderer/src/components/automations/automation-host-cache-health.test.ts": 63, - "src/renderer/src/components/automations/automation-host-cache.test.ts": 71, - "src/renderer/src/components/automations/automation-host-catalog-generation.test.ts": 113, - "src/renderer/src/components/automations/automation-host-catalog-order.test.ts": 57, - "src/renderer/src/components/automations/automation-host-catalog-source.test.ts": 87, - "src/renderer/src/components/automations/automation-host-catalog.test.ts": 97, - "src/renderer/src/components/automations/automation-host-client.test.ts": 97, - "src/renderer/src/components/automations/automation-host-detail-display.test.ts": 54, - "src/renderer/src/components/automations/automation-host-diagnostics.test.ts": 206, - "src/renderer/src/components/automations/automation-host-filter-resolution.test.ts": 107, - "src/renderer/src/components/automations/automation-host-health.test.ts": 55, - "src/renderer/src/components/automations/automation-host-invalidation-window-events.test.ts": 78, - "src/renderer/src/components/automations/automation-host-invalidation.test.ts": 63, - "src/renderer/src/components/automations/automation-host-list-rows.test.ts": 181, - "src/renderer/src/components/automations/automation-host-orphan-entry.test.ts": 212, - "src/renderer/src/components/automations/automation-host-picker-groups.test.ts": 50, - "src/renderer/src/components/automations/automation-host-recovery.test.ts": 65, - "src/renderer/src/components/automations/automation-host-scale-gates.test.ts": 329, - "src/renderer/src/components/automations/automation-host-scheduler.test.ts": 271, - "src/renderer/src/components/automations/automation-host-status-descriptors.test.ts": 125, - "src/renderer/src/components/automations/automation-legacy-schedule-editing.test.ts": 1590, - "src/renderer/src/components/automations/automation-list-empty-state.test.ts": 120, - "src/renderer/src/components/automations/automation-list-focus-recovery.test.ts": 52, - "src/renderer/src/components/automations/automation-list-keyboard-navigation.test.ts": 80, - "src/renderer/src/components/automations/automation-list-last-run.test.ts": 111, - "src/renderer/src/components/automations/automation-list-row-identity.test.ts": 513, - "src/renderer/src/components/automations/automation-list-search-rows.test.ts": 483, - "src/renderer/src/components/automations/automation-list-search.test.ts": 78, - "src/renderer/src/components/automations/automation-list-view-sort.test.ts": 447, - "src/renderer/src/components/automations/automation-list-view.test.ts": 127, - "src/renderer/src/components/automations/automation-project-groups.test.ts": 57, - "src/renderer/src/components/automations/automation-run-completion-evidence.test.ts": 425, - "src/renderer/src/components/automations/automation-run-context.test.ts": 55, - "src/renderer/src/components/automations/automation-run-history-keyboard-navigation.test.ts": 383, - "src/renderer/src/components/automations/automation-run-open-target.test.ts": 319, - "src/renderer/src/components/automations/automation-run-output-snapshot-equivalence.test.ts": 121, - "src/renderer/src/components/automations/automation-run-output-snapshot.test.ts": 101, - "src/renderer/src/components/automations/automation-run-view-state.test.ts": 55, - "src/renderer/src/components/automations/automation-run-workspace-action.paired-server.test.ts": 425, - "src/renderer/src/components/automations/automation-runs-dashboard-model.test.ts": 52, - "src/renderer/src/components/automations/automation-scoped-list-client.test.ts": 188, - "src/renderer/src/components/automations/automation-setup-decision.test.ts": 1440, - "src/renderer/src/components/automations/automation-source-display.test.ts": 52, - "src/renderer/src/components/automations/automation-target-availability.test.ts": 77, - "src/renderer/src/components/automations/automation-usage-model.test.ts": 53, - "src/renderer/src/components/automations/automation-write-invalidation.test.ts": 52, - "src/renderer/src/components/automations/external-automation-display.test.ts": 67, - "src/renderer/src/components/automations/external-automation-list-entries.test.ts": 53, - "src/renderer/src/components/automations/external-automation-run-table-state.test.ts": 50, - "src/renderer/src/components/automations/external-automation-scope-gating.test.ts": 68, - "src/renderer/src/components/automations/external-automation-scope-keys.test.ts": 76, - "src/renderer/src/components/automations/external-automation-source-availability.test.ts": 56, - "src/renderer/src/components/automations/hermes-cron-output-parse.test.ts": 75, - "src/renderer/src/components/automations/use-automation-host-catalog-readoption.test.tsx": 305, - "src/renderer/src/components/automations/use-automation-host-catalog.test.tsx": 341, - "src/renderer/src/components/automations/use-automation-list-focus-recovery.test.tsx": 294, - "src/renderer/src/components/automations/use-automation-list-search.test.tsx": 598, - "src/renderer/src/components/automations/use-automation-runs-dashboard-payload.test.tsx": 1210, - "src/renderer/src/components/automations/use-automation-runs-dashboard.test.tsx": 631, - "src/renderer/src/components/automations/use-external-automation-scope-retention.test.tsx": 227, - "src/renderer/src/components/automations/use-selected-automation-run-history.test.tsx": 434, - "src/renderer/src/components/browser-cookie-import-google-disclosure.test.tsx": 886, - "src/renderer/src/components/browser-favicon.test.tsx": 383, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.chrome-parity.test.tsx": 2452, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.dead-guest.test.tsx": 2788, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.download-notices.test.tsx": 2232, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.drag-focus.test.tsx": 2135, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.failure-remount.test.tsx": 2806, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.markup.test.tsx": 2693, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.popup-notices.test.tsx": 2474, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.test.tsx": 2380, - "src/renderer/src/components/browser-pane/ReopenBrowserPageOnServerButton.test.tsx": 384, - "src/renderer/src/components/browser-pane/annotate/BrowserAnnotationSendMenuContent.test.tsx": 59, - "src/renderer/src/components/browser-pane/annotate/GrabConfirmationSheet.test.ts": 150, - "src/renderer/src/components/browser-pane/annotate/MarkupOverlay.test.tsx": 606, - "src/renderer/src/components/browser-pane/annotate/browser-annotation-output.test.ts": 105, - "src/renderer/src/components/browser-pane/annotate/browser-grab-toast-owner-cleanup.test.tsx": 921, - "src/renderer/src/components/browser-pane/annotate/browser-page-annotation-tray.test.tsx": 2518, - "src/renderer/src/components/browser-pane/annotate/guest-annotation-viewport-bridge.test.ts": 52, - "src/renderer/src/components/browser-pane/annotate/markup-drawing-model.test.ts": 69, - "src/renderer/src/components/browser-pane/annotate/markup-gesture.test.ts": 110, - "src/renderer/src/components/browser-pane/annotate/markup-screenshot-compose.test.ts": 78, - "src/renderer/src/components/browser-pane/annotate/markup-shape-hit-test.test.ts": 57, - "src/renderer/src/components/browser-pane/annotate/markup-shape-render.test.ts": 55, - "src/renderer/src/components/browser-pane/annotate/use-browser-page-annotation-send.test.tsx": 883, - "src/renderer/src/components/browser-pane/annotate/use-browser-page-grab-annotations.test.tsx": 1134, - "src/renderer/src/components/browser-pane/annotate/use-markup-draw-hint.test.ts": 281, - "src/renderer/src/components/browser-pane/annotate/useGrabMode.test.ts": 497, - "src/renderer/src/components/browser-pane/annotate/useMarkupEditor.test.ts": 290, - "src/renderer/src/components/browser-pane/annotate/useMarkupPointerHandlers.test.ts": 246, - "src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.test.tsx": 545, - "src/renderer/src/components/browser-pane/assemble-chrome/BrowserFind.test.tsx": 413, - "src/renderer/src/components/browser-pane/assemble-chrome/BrowserPane.webview-preferences.test.ts": 404, - "src/renderer/src/components/browser-pane/assemble-chrome/BrowserPaneOverlayLayer.test.tsx": 1274, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-expansion.test.ts": 50, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-suggestions.test.ts": 348, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-chrome-folded-tools.test.tsx": 350, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-chrome-toolbar.test.tsx": 456, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-deferred-lifecycle.test.tsx": 1800, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-detected-browsers-summary.test.ts": 38, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-egress-indicator.test.tsx": 2010, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-import-hint-visibility.test.ts": 64, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-navigation-control-row.test.tsx": 421, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-page-zoom-indicator.test.tsx": 276, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-pane-page-selection.test.ts": 48, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.adoption-chrome.test.tsx": 16057, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.retention-props.test.tsx": 1906, - "src/renderer/src/components/browser-pane/assemble-chrome/ssh-routed-browser-page-gate.test.tsx": 1675, + "src/main/updater-prerelease-feed.test.ts": 219, + "src/main/updater-release-api-token.test.ts": 47, + "src/main/updater-release-build-cache.test.ts": 46, + "src/main/updater-release-builds.test.ts": 113, + "src/main/updater-sqlite-profile-downgrade.test.ts": 2081, + "src/main/updater.build-channel-selection.test.ts": 3230, + "src/main/updater.check-failure.test.ts": 3474, + "src/main/updater.check-preflight.test.ts": 4950, + "src/main/updater.check-settlement.test.ts": 3854, + "src/main/updater.fallback.test.ts": 39, + "src/main/updater.feed-attempt-lifetime.test.ts": 2718, + "src/main/updater.headless-serve-install.test.ts": 604, + "src/main/updater.install-failure-cause.test.ts": 4875, + "src/main/updater.linux-externally-managed.test.ts": 2325, + "src/main/updater.linux-root-package-install.test.ts": 3113, + "src/main/updater.mac-install.test.ts": 1719, + "src/main/updater.nudge-campaign.test.ts": 3409, + "src/main/updater.prerelease-fallback.test.ts": 4456, + "src/main/updater.publishing-window-feed.test.ts": 4837, + "src/main/updater.quit-and-install.test.ts": 4994, + "src/main/updater.startup-scheduling.test.ts": 3378, + "src/main/updater/mac-browser-sidecar-recovery.test.ts": 81, + "src/main/updater/rollout-flags.test.ts": 285, + "src/main/updater/serve-supervisor-install-park.test.ts": 151, + "src/main/updater/shipit-leftover-recovery.test.ts": 103, + "src/main/usage-worktree-canonicalizer.test.ts": 32, + "src/main/usage-worktree-metadata.test.ts": 44, + "src/main/usage/agent-token-usage-reporter.test.ts": 335, + "src/main/usage/agent-token-usage.test.ts": 34, + "src/main/usage/analytics-session-id-store.test.ts": 343, + "src/main/usage/highest-usage-key.test.ts": 41, + "src/main/usage/jsonl-file-reader.test.ts": 81, + "src/main/usage/jsonl-file-snapshot.test.ts": 52, + "src/main/usage/jsonl-line-offsets.test.ts": 55, + "src/main/usage/usage-breakdown-scaling.test.ts": 90, + "src/main/usage/usage-calendar-range.test.ts": 44, + "src/main/usage/usage-provider-store-initialization.test.ts": 669, + "src/main/usage/usage-provider-store-lifecycle.test.ts": 868, + "src/main/usage/usage-scan-worker-cache-cleanup.test.ts": 139, + "src/main/usage/usage-scan-worker-client.test.ts": 83, + "src/main/usage/usage-scan-worker-event-loop.test.ts": 2396, + "src/main/usage/usage-source-cache-file.test.ts": 76, + "src/main/usage/usage-worktree-refs.test.ts": 33, + "src/main/usage/usage-worktree-resolver.test.ts": 120, + "src/main/warp-themes/discovery.test.ts": 55, + "src/main/warp-themes/index.test.ts": 625, + "src/main/warp-themes/manual-warp-theme-files.test.ts": 60, + "src/main/warp-themes/parser-runner.test.ts": 45, + "src/main/warp-themes/parser.test.ts": 91, + "src/main/warp-themes/theme-file-scanner.test.ts": 46, + "src/main/win32-utils-async-acl.test.ts": 53, + "src/main/win32-utils.test.ts": 75, + "src/main/window/attach-main-window-services.test.ts": 741, + "src/main/window/clipboard-agent-session-attachment-paste.test.ts": 582, + "src/main/window/clipboard-copied-file-paths.test.ts": 59, + "src/main/window/clipboard-dashboard-popout-access.test.ts": 1186, + "src/main/window/clipboard-file-copy.test.ts": 46, + "src/main/window/clipboard-image-source.test.ts": 61, + "src/main/window/clipboard-image-temp-file.test.ts": 46, + "src/main/window/clipboard-image-thumbnail.test.ts": 29, + "src/main/window/clipboard-ipc-handlers.test.ts": 553, + "src/main/window/clipboard-macos-image-paste.test.ts": 932, + "src/main/window/clipboard-platform-routing.test.ts": 59, + "src/main/window/clipboard-publisher-executable.test.ts": 48, + "src/main/window/clipboard-remote-file-cleanup.bench.test.ts": 26, + "src/main/window/clipboard-remote-file-staging-fs.test.ts": 44, + "src/main/window/clipboard-remote-file-staging.test.ts": 100, + "src/main/window/clipboard-runtime-owned-ssh-paste.test.ts": 303, + "src/main/window/clipboard-text-publisher.test.ts": 344, + "src/main/window/clipboard-text-write-verify.test.ts": 38, + "src/main/window/clipboard-windows-image-file.test.ts": 92, + "src/main/window/createMainWindow-close-confirmation.test.ts": 1826, + "src/main/window/createMainWindow-low-commit-oom-recovery.test.ts": 1701, + "src/main/window/createMainWindow-markdown-editor-focus.test.ts": 1756, + "src/main/window/createMainWindow-recovery-reload-watchdog.test.ts": 2387, + "src/main/window/createMainWindow-renderer-crash-recovery.test.ts": 1674, + "src/main/window/createMainWindow-renderer-launch-failure.test.ts": 1750, + "src/main/window/createMainWindow-startup-reveal.test.ts": 1903, + "src/main/window/createMainWindow-system-resume-relay.test.ts": 1871, + "src/main/window/createMainWindow-terminal-focus-shortcuts.test.ts": 2155, + "src/main/window/createMainWindow-tray-minimize-close.test.ts": 1657, + "src/main/window/createMainWindow-user-quit-close.test.ts": 1615, + "src/main/window/createMainWindow-zoom-and-tab-switch-shortcuts.test.ts": 1676, + "src/main/window/createMainWindow.test.ts": 1790, + "src/main/window/darwin-user-temp-dir.test.ts": 54, + "src/main/window/dashboard-popout-window.test.ts": 111, + "src/main/window/dragged-temp-file-copy.test.ts": 220, + "src/main/window/dropped-path-preparation-ipc.test.ts": 70, + "src/main/window/dropped-path-preparation.test.ts": 65, + "src/main/window/editable-context-menu.test.ts": 60, + "src/main/window/focus-existing-window.test.ts": 62, + "src/main/window/foreground-activation-policy.test.ts": 46, + "src/main/window/history-gc-worktree-ids.test.ts": 421, + "src/main/window/host-frame-navigation-guard.test.ts": 268, + "src/main/window/macos-app-activation.test.ts": 52, + "src/main/window/macos-tahoe-release.test.ts": 34, + "src/main/window/main-window-state-lifecycle.test.ts": 36, + "src/main/window/main-window-updater-deferred-setup.test.ts": 176, + "src/main/window/main-window-updater-headless-serve.test.ts": 41, + "src/main/window/main-window-visibility.test.ts": 32, + "src/main/window/main-window-webview-security.test.ts": 277, + "src/main/window/mobile-markdown-request-relay.test.ts": 35, + "src/main/window/native-chat-paste-files.test.ts": 815, + "src/main/window/privileged-window-navigation.test.ts": 240, + "src/main/window/renderer-document-navigation.test.ts": 59, + "src/main/window/renderer-launch-failure-backoff.test.ts": 21, + "src/main/window/renderer-launch-failure-probe.spawn.test.ts": 77, + "src/main/window/renderer-launch-failure-probe.test.ts": 48, + "src/main/window/renderer-publication-throttle.test.ts": 34, + "src/main/window/renderer-recovery-prompt.test.ts": 80, + "src/main/window/renderer-recovery-reload-watchdog.test.ts": 45, + "src/main/window/runtime-renderer-notification-sender.test.ts": 36, + "src/main/window/runtime-window-lifecycle-file-open.test.ts": 46, + "src/main/window/session-tab-close-request-relay.test.ts": 57, + "src/main/window/terminal-tab-close-request-relay.test.ts": 51, + "src/main/window/updater-package-recovery-ipc.test.ts": 3623, + "src/main/window/window-close-decision.test.ts": 31, + "src/main/window/worktree-sleep-request-relay.test.ts": 67, + "src/main/windows-live-tree-kill.win32.test.ts": 78, + "src/main/windows-process-tree-kill.test.ts": 68, + "src/main/windows-pty-root-identity.test.ts": 120, + "src/main/windows-registry-addon.test.ts": 32, + "src/main/windows/windows-administrator-restart.test.ts": 37, + "src/main/windows/windows-administrator-restart.win32.test.ts": 59, + "src/main/windows/windows-administrator-status.test.ts": 56, + "src/main/windows/windows-administrator-status.win32.test.ts": 47, + "src/main/windows/windows-command-line-recovery-health.test.ts": 36, + "src/main/windows/windows-host-job.win32.test.ts": 30, + "src/main/windows/windows-msys-job.win32.test.ts": 50, + "src/main/windows/windows-process-creation-time.test.ts": 83, + "src/main/windows/windows-process-table-cim-scan.test.ts": 48, + "src/main/windows/windows-process-table-native-addon.win32.test.ts": 63, + "src/main/windows/windows-process-table.test.ts": 142, + "src/main/windows/windows-process-tree-command-line-patch.test.ts": 40, + "src/main/windows/windows-pty-job.test.ts": 48, + "src/main/windows/windows-pty-job.win32.test.ts": 74, + "src/main/worker-thread-child-process/worker-thread-child-process.test.ts": 452, + "src/main/worker-thread-request-queue.test.ts": 83, + "src/main/workspace-cleanup-removal-snapshot-prune.test.ts": 37, + "src/main/workspace-cleanup-scan-snapshot.test.ts": 156, + "src/main/workspace-create-event-fields.test.ts": 358, + "src/main/workspace-snapshot-pruning.bench.test.ts": 50, + "src/main/workspace-space-analysis-capacity.test.ts": 488, + "src/main/workspace-space-analysis-du-timeout.test.ts": 770, + "src/main/workspace-space-analysis-snapshot.test.ts": 140, + "src/main/workspace-space-analysis.test.ts": 492, + "src/main/workspace-space-repo-scan.test.ts": 675, + "src/main/workspace-space-scan-control.test.ts": 36, + "src/main/worktree-background-removal-records.test.ts": 501, + "src/main/worktree-background-removal.test.ts": 360, + "src/main/worktree-create-base-prefetch.test.ts": 286, + "src/main/worktree-create-base.test.ts": 33, + "src/main/worktree-create-candidates.test.ts": 40, + "src/main/worktree-create-concurrency.test.ts": 38, + "src/main/worktree-create-execution-host-route.test.ts": 525, + "src/main/worktree-create-preparation-activity.test.ts": 31, + "src/main/worktree-create-preparation-cancellation.test.ts": 325, + "src/main/worktree-create-preparation-claim.test.ts": 74, + "src/main/worktree-create-preparation-contention.test.ts": 519, + "src/main/worktree-create-preparation-timing.test.ts": 335, + "src/main/worktree-create-preparation-wsl-root.test.ts": 314, + "src/main/worktree-create-preparation.test.ts": 264, + "src/main/worktree-create-timing.test.ts": 106, + "src/main/worktree-failed-removal.test.ts": 614, + "src/main/worktree-identity-persistence.test.ts": 1812, + "src/main/worktree-lineage-pruning.test.ts": 388, + "src/main/worktree-metadata-ownership.test.ts": 27, + "src/main/worktree-name-retirement.test.ts": 275, + "src/main/worktree-preparation-discard-retry.test.ts": 315, + "src/main/worktree-preparation-fetch-barrier.test.ts": 322, + "src/main/worktree-preparation-tip-refresh.test.ts": 585, + "src/main/worktree-prunable-git-file.test.ts": 55, + "src/main/worktree-removal-authority.test.ts": 92, + "src/main/worktree-removal-close-records.test.ts": 1345, + "src/main/worktree-removal-execution-host-route.test.ts": 534, + "src/main/worktree-removal-home-guard.test.ts": 69, + "src/main/worktree-removal-repo-owner.test.ts": 40, + "src/main/worktree-removal-safety.test.ts": 114, + "src/main/worktree-removal-session-partition-fencing.test.ts": 1472, + "src/main/worktree-retirement-backfill-scan.test.ts": 65, + "src/main/worktree-retirement-backfill-stall.test.ts": 303, + "src/main/worktree-retirement-discovery-wsl.test.ts": 256, + "src/main/worktree-retirement-discovery.test.ts": 319, + "src/main/worktree-retirement-namespace.test.ts": 71, + "src/main/worktree-root-preparation.test.ts": 310, + "src/main/worktree-trash.test.ts": 108, + "src/main/wsl-approved-root-race.wsl.test.ts": 61, + "src/main/wsl-availability-missing-kernel.test.ts": 51, + "src/main/wsl-background-distro-gate.test.ts": 95, + "src/main/wsl-bash-command.test.ts": 30, + "src/main/wsl-distro-list-output.test.ts": 33, + "src/main/wsl-distro-list-single-flight.test.ts": 75, + "src/main/wsl-fish-history-cleanup.test.ts": 125, + "src/main/wsl-interop-spawn-directory.test.ts": 44, + "src/main/wsl-managed-cli-path-restore.test.ts": 68, + "src/main/wsl-move-incidental-writes.test.ts": 1045, + "src/main/wsl-move-scan-pruning.test.ts": 1921, + "src/main/wsl-running-distro-stop.test.ts": 39, + "src/main/wsl-running-distros.test.ts": 171, + "src/main/wsl-running-path-filter.test.ts": 37, + "src/main/wsl-unc-delete-symlink-repro.test.ts": 52, + "src/main/wsl-unc-delete.test.ts": 66, + "src/main/wsl-unc-delete.wsl.test.ts": 74, + "src/main/wsl.test.ts": 110, + "src/main/wsl/wsl-command-loop.test.ts": 6064, + "src/main/wsl/wsl-directory-upload.test.ts": 15460, + "src/main/wsl/wsl-distro-holder.test.ts": 5207, + "src/main/wsl/wsl-distro-registry-id.test.ts": 61, + "src/main/wsl/wsl-executable-path.win32.test.ts": 29, + "src/main/wsl/wsl-guest-environment.test.ts": 99, + "src/main/wsl/wsl-guest-process-identity.test.ts": 85, + "src/main/wsl/wsl-host-distro-list.test.ts": 78, + "src/main/wsl/wsl-host-server-stop.test.ts": 482, + "src/main/wsl/wsl-host-service.test.ts": 978, + "src/main/wsl/wsl-interop-health.test.ts": 224, + "src/main/wsl/wsl-interop-registration.test.ts": 45, + "src/main/wsl/wsl-orcad-account-env.test.ts": 189, + "src/main/wsl/wsl-orcad-connection.test.ts": 776, + "src/main/wsl/wsl-orcad-deploy.test.ts": 666, + "src/main/wsl/wsl-orcad-host-account-push.test.ts": 235, + "src/main/wsl/wsl-orcad-host-environments.test.ts": 33, + "src/main/wsl/wsl-orcad-host-transport-loops.test.ts": 223, + "src/main/wsl/wsl-orcad-host-transport.test.ts": 197, + "src/main/wsl/wsl-orcad-incumbent.test.ts": 540, + "src/main/wsl/wsl-orcad-instance-adoption.test.ts": 406, + "src/main/wsl/wsl-orcad-liveness-supervisor.test.ts": 132, + "src/main/wsl/wsl-orcad-liveness-verdict.test.ts": 33, + "src/main/wsl/wsl-orcad-real-distro-lane.test.ts": 639, + "src/main/wsl/wsl-orcad-reconnect-backoff.test.ts": 44, + "src/main/wsl/wsl-orcad-stdio-bridge.test.ts": 546, + "src/main/wsl/wsl-orcad-watch.test.ts": 551, + "src/main/wsl/wsl-pinned-runtime.test.ts": 294, + "src/main/wsl/wsl-runner.test.ts": 80, + "src/main/wsl/wsl-runner.wsl.test.ts": 53, + "src/main/wsl/wsl-w1-w3-contract.test.ts": 57, + "src/main/zcode/hook-service.test.ts": 176, + "src/main/zcode/interactive-capability.live.test.ts": 55, + "src/main/zcode/zcode-plan-api-key-store-native-windows.test.ts": 30, + "src/main/zcode/zcode-plan-api-key-store-plaintext-rollback.test.ts": 113, + "src/main/zcode/zcode-plan-api-key-store-publication-race.test.ts": 78, + "src/main/zcode/zcode-plan-api-key-store.test.ts": 96, + "src/main/zcode/zcode-v2-personal-overlay-raw-json.test.ts": 339, + "src/main/zcode/zcode-v2-plan-credentials.test.ts": 997, + "src/main/zod-schema-compilation.test.ts": 166, + "src/packages/process-host/src/bounded-output-sink.test.ts": 70, + "src/packages/process-host/src/close-process-registry.test.ts": 25, + "src/packages/process-host/src/growing-byte-buffer.test.ts": 91, + "src/packages/process-host/src/posix-lsof-probe.test.ts": 4280, + "src/packages/process-host/src/process-tree-termination.test.ts": 58, + "src/packages/process-host/src/retryable-process-exit-proof.test.ts": 37, + "src/packages/process-host/src/run-process-bytes.test.ts": 2236, + "src/packages/process-host/src/run-process-termination-failure.test.ts": 67, + "src/packages/process-host/src/run-process.test.ts": 7365, + "src/packages/process-host/src/spawn-observer.test.ts": 258, + "src/packages/process-host/src/spawn-process-stdio.test.ts": 257, + "src/packages/process-host/src/windows-command-line.test.ts": 60, + "src/preload/api/notification-sound-playback.test.ts": 56, + "src/preload/api/platform-bridge.test.ts": 47, + "src/preload/api/wsl-hosts-api.test.ts": 31, + "src/preload/app-restart-checkpoint-routing.test.ts": 1729, + "src/preload/browser-client-page-renderer-requests.test.ts": 263, + "src/preload/browser-find-subscriptions.test.ts": 41, + "src/preload/browser-window-close.test.ts": 29, + "src/preload/close-active-tab-payload-admission.test.ts": 30, + "src/preload/doc-preview-link-interception.test.ts": 208, + "src/preload/opencode-go-credentials.test.ts": 28, + "src/preload/preload-file-drop-absence.test.ts": 349, + "src/preload/pty-snapshot-capability-ipc.test.ts": 868, + "src/preload/renderer-heap-statistics-reader.test.ts": 27, + "src/preload/renderer-process-memory-reader.test.ts": 51, + "src/preload/renderer-restart-wiring.test.ts": 51, + "src/preload/runtime-environment-subscriptions.test.ts": 47, + "src/preload/ssh-authority-forwarding.test.ts": 1277, + "src/preload/updater-package-recovery.test.ts": 491, + "src/preload/usage-provider-api.test.ts": 37, + "src/renderer/src/app-shell/app-command-handlers-child-workspaces.test.ts": 1123, + "src/renderer/src/app-shell/app-command-handlers-tab-rename.test.ts": 1224, + "src/renderer/src/app-shell/app-command-handlers-workspace-delete.test.ts": 514, + "src/renderer/src/app-shell/app-root-surface-settings.test.tsx": 515, + "src/renderer/src/app-shell/app-window-chrome.test.ts": 32, + "src/renderer/src/app-shell/reconcile-hydrated-workspace-tab-models.test.ts": 48, + "src/renderer/src/app-shell/shutdown-checkpoint-persist.test.ts": 61, + "src/renderer/src/app-shell/shutdown-checkpoint-restart-lifecycle.test.ts": 190, + "src/renderer/src/app-shell/startup-actions-selector.test.ts": 65, + "src/renderer/src/app-shell/startup-window-parity.test.ts": 1484, + "src/renderer/src/app-shell/use-document-appearance.test.tsx": 1441, + "src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx": 333, + "src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx": 276, + "src/renderer/src/app-shell/use-terminal-viewer-color-publication.test.tsx": 1679, + "src/renderer/src/app-shell/use-visible-hosted-review-refresh.test.tsx": 339, + "src/renderer/src/app-shell/visible-review-reporting.test.ts": 1158, + "src/renderer/src/app-shell/window-visibility-actions-selector.test.ts": 250, + "src/renderer/src/app-shell/workspace-view-cross-client-sync.test.tsx": 938, + "src/renderer/src/attention/agent-attention-policy.test.ts": 37, + "src/renderer/src/attention/notification-sources.test.ts": 1558, + "src/renderer/src/components/AgentStateDot.test.ts": 113, + "src/renderer/src/components/AgentWorkingSpinner.test.tsx": 220, + "src/renderer/src/components/ButtonKeyHint.test.tsx": 267, + "src/renderer/src/components/JiraIssueWorkspace.assignee-source.test.tsx": 1928, + "src/renderer/src/components/JiraIssueWorkspace.optimistic-edit-reopen.test.tsx": 1828, + "src/renderer/src/components/JiraIssueWorkspace.optimistic-edit-rollback.test.tsx": 949, + "src/renderer/src/components/JiraIssueWorkspace.optimistic-edit.test.tsx": 1551, + "src/renderer/src/components/JiraIssueWorkspace.status-queue.test.tsx": 1181, + "src/renderer/src/components/JiraIssueWorkspace.transition-source.test.tsx": 1146, + "src/renderer/src/components/LinuxPackageInstallRecoveryCard.test.tsx": 888, + "src/renderer/src/components/NativeChatResumeOnRestartGroups.selection.test.tsx": 2871, + "src/renderer/src/components/NativeChatResumeOnRestartGroups.test.tsx": 2266, + "src/renderer/src/components/NativeChatResumeOnRestartModal.list.test.tsx": 2270, + "src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx": 3190, + "src/renderer/src/components/NewWorkspaceComposerCard.file-drop.test.tsx": 630, + "src/renderer/src/components/NewWorkspaceComposerCard.quick-file-drop.test.tsx": 3819, + "src/renderer/src/components/NewWorkspaceComposerCard.set-location-warm.test.tsx": 1037, + "src/renderer/src/components/NewWorkspaceComposerCard.set-location.test.tsx": 1164, + "src/renderer/src/components/NewWorkspaceComposerCard.start-from.test.tsx": 1008, + "src/renderer/src/components/NewWorkspaceComposerCard.test.tsx": 1575, + "src/renderer/src/components/QuickOpen.mount-gating.test.tsx": 1589, + "src/renderer/src/components/QuickOpen.selection.test.tsx": 866, + "src/renderer/src/components/SelectedTextCopyMenu.test.tsx": 347, + "src/renderer/src/components/StarNagCard.test.tsx": 277, + "src/renderer/src/components/StateIndicatorTooltip.test.tsx": 43, + "src/renderer/src/components/TaskPageJiraSavedFilters.test.tsx": 1317, + "src/renderer/src/components/TaskPageJiraStatusFilters.test.tsx": 664, + "src/renderer/src/components/TerminalSearch.test.tsx": 485, + "src/renderer/src/components/TerminalTitlebarTabs.test.tsx": 188, + "src/renderer/src/components/TerminalWorkbenchContainer.test.tsx": 279, + "src/renderer/src/components/UpdateCard.error-card.test.tsx": 2563, + "src/renderer/src/components/UpdateCard.test.ts": 529, + "src/renderer/src/components/WorktreeBaseFallbackDialog.test.tsx": 1562, + "src/renderer/src/components/WorktreeJumpPalette.linear-url.test.tsx": 3255, + "src/renderer/src/components/WorktreeJumpPalette.mount-gating.test.tsx": 3045, + "src/renderer/src/components/WorktreeJumpPalette.recent-tabs.behavior.test.tsx": 2766, + "src/renderer/src/components/WorktreeJumpPalette.recent-tabs.test.tsx": 4413, + "src/renderer/src/components/WorktreeJumpPalette.task-search.test.tsx": 4356, + "src/renderer/src/components/WorktreeJumpPalette.test.tsx": 3062, + "src/renderer/src/components/activity/ActivityPrototypePage.filter-focus-shortcut.test.ts": 42, + "src/renderer/src/components/activity/ActivityPrototypePage.test.ts": 532, + "src/renderer/src/components/activity/ActivityPrototypePage.thread-grouping.test.ts": 1405, + "src/renderer/src/components/activity/ActivityThreadOptionsMenu.test.tsx": 2278, + "src/renderer/src/components/activity/activity-answered-ask-timeline.test.tsx": 1793, + "src/renderer/src/components/activity/activity-auto-mark-read-loop.react185.test.tsx": 3027, + "src/renderer/src/components/activity/activity-clear-completed-hmr.test.ts": 579, + "src/renderer/src/components/activity/activity-clear-completed-state-lifetime.test.ts": 2320, + "src/renderer/src/components/activity/activity-clear-completed.test.ts": 428, + "src/renderer/src/components/activity/activity-event-builder.identity-reuse.test.ts": 445, + "src/renderer/src/components/activity/activity-portal-churn-budget.test.ts": 45, + "src/renderer/src/components/activity/activity-portal-readiness-decay.react185.test.tsx": 236, + "src/renderer/src/components/activity/activity-portal-readiness-loop.react185.test.tsx": 365, + "src/renderer/src/components/activity/activity-portal-readiness-oscillation.test.ts": 54, + "src/renderer/src/components/activity/activity-portal-readiness-subscription-churn.react185.test.tsx": 324, + "src/renderer/src/components/activity/activity-portal-thread-reconciliation.test.ts": 40, + "src/renderer/src/components/activity/activity-scope-filter.test.ts": 442, + "src/renderer/src/components/activity/activity-search-query-work-budget.test.tsx": 1763, + "src/renderer/src/components/activity/activity-terminal-portal-publication-loop.react185.test.tsx": 191, + "src/renderer/src/components/activity/activity-terminal-portal.test.tsx": 174, + "src/renderer/src/components/activity/activity-thread-actions.test.ts": 602, + "src/renderer/src/components/activity/activity-thread-context-menu.test.tsx": 2316, + "src/renderer/src/components/activity/activity-thread-grouping.status-order.test.ts": 530, + "src/renderer/src/components/activity/activity-thread-hover-card-intent.test.tsx": 476, + "src/renderer/src/components/activity/activity-thread-hover-card.test.tsx": 2348, + "src/renderer/src/components/activity/activity-thread-list-pane-collapsible.test.tsx": 2783, + "src/renderer/src/components/activity/activity-thread-list-pane.multi-select.test.tsx": 4208, + "src/renderer/src/components/activity/activity-thread-list-pane.virtualization.test.tsx": 3196, + "src/renderer/src/components/activity/dev-activity-fixture.test.ts": 43, + "src/renderer/src/components/activity/event-time-clock-refresh.test.tsx": 1622, + "src/renderer/src/components/activity/use-activity-thread-action-bindings.test.tsx": 1305, + "src/renderer/src/components/activity/useActivityUnreadCount.freshness.test.tsx": 1542, + "src/renderer/src/components/activity/useActivityUnreadCount.test.ts": 1418, + "src/renderer/src/components/agent-child-row-parity.test.tsx": 547, + "src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.test.tsx": 279, + "src/renderer/src/components/agent-session-continuation/agent-session-continuation-selection.test.ts": 40, + "src/renderer/src/components/agent/AgentCombobox.test.tsx": 1534, + "src/renderer/src/components/agent/AgentSettingsDialog.test.tsx": 585, + "src/renderer/src/components/agent/agent-combobox-command-state.test.ts": 29, + "src/renderer/src/components/artifacts/ArtifactCollection.test.tsx": 1447, + "src/renderer/src/components/artifacts/ArtifactPreview.test.tsx": 316, + "src/renderer/src/components/artifacts/ArtifactPublishButton.test.tsx": 605, + "src/renderer/src/components/artifacts/ArtifactsPage.test.tsx": 1594, + "src/renderer/src/components/artifacts/artifact-display-labels.test.ts": 71, + "src/renderer/src/components/artifacts/artifact-list-search.test.ts": 76, + "src/renderer/src/components/artifacts/artifact-list-windowing.test.tsx": 3481, + "src/renderer/src/components/artifacts/artifact-publish-flow.test.ts": 86, + "src/renderer/src/components/artifacts/artifact-published-link-client.test.ts": 40, + "src/renderer/src/components/automations/AutomationDestinationField.test.tsx": 577, + "src/renderer/src/components/automations/AutomationDetail.host.test.tsx": 608, + "src/renderer/src/components/automations/AutomationDetail.test.tsx": 716, + "src/renderer/src/components/automations/AutomationEditorPromptEditor.test.tsx": 306, + "src/renderer/src/components/automations/AutomationEditorSettingsSidebar.test.tsx": 796, + "src/renderer/src/components/automations/AutomationHostBadges.test.tsx": 382, + "src/renderer/src/components/automations/AutomationHostFilterNotice.test.tsx": 358, + "src/renderer/src/components/automations/AutomationListEmptyView.test.tsx": 278, + "src/renderer/src/components/automations/AutomationListHostGroups.test.tsx": 707, + "src/renderer/src/components/automations/AutomationListLocalRows.test.tsx": 1082, + "src/renderer/src/components/automations/AutomationListSearchField.test.tsx": 322, + "src/renderer/src/components/automations/AutomationListTableHeader.test.tsx": 434, + "src/renderer/src/components/automations/AutomationOwnerConflictNotice.test.tsx": 278, + "src/renderer/src/components/automations/AutomationPromptDisclosure.test.tsx": 770, + "src/renderer/src/components/automations/AutomationRunDetailsPage.test.tsx": 1763, + "src/renderer/src/components/automations/AutomationRunHistory.test.tsx": 1111, + "src/renderer/src/components/automations/AutomationRunsDashboardSurface.test.tsx": 505, + "src/renderer/src/components/automations/AutomationRunsTable.test.tsx": 535, + "src/renderer/src/components/automations/AutomationSchedulePicker.render.test.tsx": 1042, + "src/renderer/src/components/automations/AutomationSchedulePicker.test.ts": 946, + "src/renderer/src/components/automations/AutomationTimeField.test.tsx": 556, + "src/renderer/src/components/automations/AutomationsDetailPane.run-count.test.tsx": 786, + "src/renderer/src/components/automations/AutomationsDetailPane.test.tsx": 1040, + "src/renderer/src/components/automations/AutomationsListPanel.test.tsx": 1103, + "src/renderer/src/components/automations/AutomationsPage.create-destination.test.tsx": 2462, + "src/renderer/src/components/automations/AutomationsPage.cross-authority-actions.test.tsx": 2016, + "src/renderer/src/components/automations/AutomationsPage.escape-precedence.test.tsx": 1322, + "src/renderer/src/components/automations/AutomationsPage.external-scope.test.tsx": 2202, + "src/renderer/src/components/automations/AutomationsPage.notice-recovery.test.tsx": 2264, + "src/renderer/src/components/automations/AutomationsPage.refresh-selection.test.tsx": 1648, + "src/renderer/src/components/automations/AutomationsPage.run-visibility.test.tsx": 1240, + "src/renderer/src/components/automations/AutomationsPage.save-visibility.test.tsx": 1505, + "src/renderer/src/components/automations/AutomationsPage.strict-mode-save-visibility.test.tsx": 1331, + "src/renderer/src/components/automations/AutomationsPage.test.tsx": 2709, + "src/renderer/src/components/automations/AutomationsPageBreadcrumb.test.tsx": 265, + "src/renderer/src/components/automations/ExternalAutomationManagers.test.tsx": 785, + "src/renderer/src/components/automations/automation-authority-identity.test.ts": 42, + "src/renderer/src/components/automations/automation-captured-owner.test.ts": 335, + "src/renderer/src/components/automations/automation-create-destination.test.ts": 113, + "src/renderer/src/components/automations/automation-create-projects.test.ts": 31, + "src/renderer/src/components/automations/automation-detail-tab-navigation.test.ts": 187, + "src/renderer/src/components/automations/automation-editor-prompt-options.test.ts": 43, + "src/renderer/src/components/automations/automation-extra-agent-args-capability.test.ts": 292, + "src/renderer/src/components/automations/automation-host-cache-controller.test.ts": 366, + "src/renderer/src/components/automations/automation-host-cache-health.test.ts": 43, + "src/renderer/src/components/automations/automation-host-cache.test.ts": 62, + "src/renderer/src/components/automations/automation-host-catalog-generation.test.ts": 109, + "src/renderer/src/components/automations/automation-host-catalog-order.test.ts": 39, + "src/renderer/src/components/automations/automation-host-catalog-source.test.ts": 205, + "src/renderer/src/components/automations/automation-host-catalog.test.ts": 58, + "src/renderer/src/components/automations/automation-host-client.test.ts": 228, + "src/renderer/src/components/automations/automation-host-detail-display.test.ts": 39, + "src/renderer/src/components/automations/automation-host-diagnostics.test.ts": 405, + "src/renderer/src/components/automations/automation-host-filter-resolution.test.ts": 71, + "src/renderer/src/components/automations/automation-host-health.test.ts": 36, + "src/renderer/src/components/automations/automation-host-invalidation-window-events.test.ts": 59, + "src/renderer/src/components/automations/automation-host-invalidation.test.ts": 41, + "src/renderer/src/components/automations/automation-host-list-rows.test.ts": 393, + "src/renderer/src/components/automations/automation-host-orphan-entry.test.ts": 446, + "src/renderer/src/components/automations/automation-host-picker-groups.test.ts": 44, + "src/renderer/src/components/automations/automation-host-recovery.test.ts": 50, + "src/renderer/src/components/automations/automation-host-scale-gates.test.ts": 570, + "src/renderer/src/components/automations/automation-host-scheduler.test.ts": 381, + "src/renderer/src/components/automations/automation-host-status-descriptors.test.ts": 105, + "src/renderer/src/components/automations/automation-legacy-schedule-editing.test.ts": 1599, + "src/renderer/src/components/automations/automation-list-empty-state.test.ts": 107, + "src/renderer/src/components/automations/automation-list-focus-recovery.test.ts": 46, + "src/renderer/src/components/automations/automation-list-keyboard-navigation.test.ts": 60, + "src/renderer/src/components/automations/automation-list-last-run.test.ts": 88, + "src/renderer/src/components/automations/automation-list-row-identity.test.ts": 576, + "src/renderer/src/components/automations/automation-list-search-rows.test.ts": 446, + "src/renderer/src/components/automations/automation-list-search.test.ts": 51, + "src/renderer/src/components/automations/automation-list-view-sort.test.ts": 501, + "src/renderer/src/components/automations/automation-list-view.test.ts": 100, + "src/renderer/src/components/automations/automation-project-groups.test.ts": 55, + "src/renderer/src/components/automations/automation-run-completion-evidence.test.ts": 331, + "src/renderer/src/components/automations/automation-run-context.test.ts": 39, + "src/renderer/src/components/automations/automation-run-history-keyboard-navigation.test.ts": 423, + "src/renderer/src/components/automations/automation-run-open-target.test.ts": 316, + "src/renderer/src/components/automations/automation-run-output-snapshot-equivalence.test.ts": 131, + "src/renderer/src/components/automations/automation-run-output-snapshot.test.ts": 86, + "src/renderer/src/components/automations/automation-run-view-state.test.ts": 51, + "src/renderer/src/components/automations/automation-run-workspace-action.paired-server.test.ts": 396, + "src/renderer/src/components/automations/automation-runs-dashboard-model.test.ts": 33, + "src/renderer/src/components/automations/automation-scoped-list-client.test.ts": 282, + "src/renderer/src/components/automations/automation-setup-decision.test.ts": 1337, + "src/renderer/src/components/automations/automation-source-display.test.ts": 40, + "src/renderer/src/components/automations/automation-target-availability.test.ts": 243, + "src/renderer/src/components/automations/automation-usage-model.test.ts": 35, + "src/renderer/src/components/automations/automation-write-invalidation.test.ts": 48, + "src/renderer/src/components/automations/external-automation-list-entries.test.ts": 49, + "src/renderer/src/components/automations/external-automation-scope-gating.test.ts": 53, + "src/renderer/src/components/automations/external-automation-scope-keys.test.ts": 41, + "src/renderer/src/components/automations/external-automation-source-availability.test.ts": 40, + "src/renderer/src/components/automations/use-automation-host-catalog-readoption.test.tsx": 641, + "src/renderer/src/components/automations/use-automation-host-catalog.test.tsx": 551, + "src/renderer/src/components/automations/use-automation-list-focus-recovery.test.tsx": 201, + "src/renderer/src/components/automations/use-automation-list-search.test.tsx": 627, + "src/renderer/src/components/automations/use-automation-runs-dashboard-payload.test.tsx": 1297, + "src/renderer/src/components/automations/use-automation-runs-dashboard.test.tsx": 667, + "src/renderer/src/components/automations/use-automations-page-setup-state.test.tsx": 1694, + "src/renderer/src/components/automations/use-automations-page-store-state.folder.test.tsx": 1325, + "src/renderer/src/components/automations/use-external-automation-scope-retention.test.tsx": 167, + "src/renderer/src/components/automations/use-selected-automation-run-history.test.tsx": 572, + "src/renderer/src/components/browser-cookie-import-google-disclosure.test.tsx": 713, + "src/renderer/src/components/browser-favicon.test.tsx": 288, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.chrome-parity.test.tsx": 2840, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.dead-guest.test.tsx": 2567, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.download-notices.test.tsx": 2156, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.drag-focus.test.tsx": 2322, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.failure-remount.test.tsx": 2055, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.markup.test.tsx": 3462, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.popup-notices.test.tsx": 2096, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.test.tsx": 1921, + "src/renderer/src/components/browser-pane/ReopenBrowserPageOnServerButton.test.tsx": 365, + "src/renderer/src/components/browser-pane/annotate/BrowserAnnotationSendMenuContent.test.tsx": 33, + "src/renderer/src/components/browser-pane/annotate/GrabConfirmationSheet.test.ts": 109, + "src/renderer/src/components/browser-pane/annotate/MarkupOverlay.test.tsx": 627, + "src/renderer/src/components/browser-pane/annotate/browser-annotation-output.test.ts": 62, + "src/renderer/src/components/browser-pane/annotate/browser-grab-toast-owner-cleanup.test.tsx": 885, + "src/renderer/src/components/browser-pane/annotate/browser-page-annotation-tray.test.tsx": 2284, + "src/renderer/src/components/browser-pane/annotate/guest-annotation-viewport-bridge.test.ts": 44, + "src/renderer/src/components/browser-pane/annotate/markup-drawing-model.test.ts": 52, + "src/renderer/src/components/browser-pane/annotate/markup-gesture.test.ts": 75, + "src/renderer/src/components/browser-pane/annotate/markup-screenshot-compose.test.ts": 33, + "src/renderer/src/components/browser-pane/annotate/markup-shape-hit-test.test.ts": 55, + "src/renderer/src/components/browser-pane/annotate/markup-shape-render.test.ts": 44, + "src/renderer/src/components/browser-pane/annotate/use-browser-page-annotation-send.test.tsx": 773, + "src/renderer/src/components/browser-pane/annotate/use-browser-page-grab-annotations.test.tsx": 875, + "src/renderer/src/components/browser-pane/annotate/use-markup-draw-hint.test.ts": 209, + "src/renderer/src/components/browser-pane/annotate/useGrabMode.test.ts": 467, + "src/renderer/src/components/browser-pane/annotate/useMarkupEditor.test.ts": 327, + "src/renderer/src/components/browser-pane/annotate/useMarkupPointerHandlers.test.ts": 236, + "src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.test.tsx": 649, + "src/renderer/src/components/browser-pane/assemble-chrome/BrowserFind.test.tsx": 398, + "src/renderer/src/components/browser-pane/assemble-chrome/BrowserPane.webview-preferences.test.ts": 375, + "src/renderer/src/components/browser-pane/assemble-chrome/BrowserPaneOverlayLayer.test.tsx": 1151, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-expansion.test.ts": 31, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-suggestions.test.ts": 323, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-chrome-folded-tools.test.tsx": 324, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-chrome-toolbar.test.tsx": 317, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-deferred-lifecycle.test.tsx": 1345, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-detected-browsers-summary.test.ts": 25, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-egress-indicator.test.tsx": 1917, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-import-hint-visibility.test.ts": 37, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-navigation-control-row.test.tsx": 382, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-page-zoom-indicator.test.tsx": 262, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-pane-page-selection.test.ts": 28, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.adoption-chrome.test.tsx": 17595, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.retention-props.test.tsx": 2189, + "src/renderer/src/components/browser-pane/assemble-chrome/ssh-routed-browser-page-gate.test.tsx": 1630, "src/renderer/src/components/browser-pane/assemble-chrome/use-browser-chrome-tool-fold.test.tsx": 319, - "src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-chrome-focus.test.tsx": 1619, - "src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-find-shortcuts.test.tsx": 1680, - "src/renderer/src/components/browser-pane/browser-client-page-attachment-scan-budget.test.ts": 382, - "src/renderer/src/components/browser-pane/browser-client-page-metadata-publisher.test.ts": 317, - "src/renderer/src/components/browser-pane/browser-client-page-position-driver.test.ts": 398, - "src/renderer/src/components/browser-pane/browser-client-page-renderer-installation.test.ts": 333, - "src/renderer/src/components/browser-pane/browser-client-page-retained-drag-passthrough.test.ts": 394, - "src/renderer/src/components/browser-pane/browser-client-page-retained-registry.test.ts": 435, - "src/renderer/src/components/browser-pane/browser-download-destination-toast.test.ts": 53, - "src/renderer/src/components/browser-pane/browser-reopen-on-server.test.ts": 287, - "src/renderer/src/components/browser-pane/client-hosted-browser-unavailable-notice.test.tsx": 359, - "src/renderer/src/components/browser-pane/describe-page/browser-annotation-geometry.test.ts": 119, - "src/renderer/src/components/browser-pane/describe-page/browser-artifact-upload.test.ts": 1571, - "src/renderer/src/components/browser-pane/describe-page/browser-favicon-url.test.ts": 48, - "src/renderer/src/components/browser-pane/describe-page/browser-overlay-shortcut-target.test.ts": 1332, - "src/renderer/src/components/browser-pane/describe-page/browser-page-url-display.test.ts": 1382, - "src/renderer/src/components/browser-pane/describe-page/live-browser-url-registry.test.ts": 42, - "src/renderer/src/components/browser-pane/host-guest/browser-automation-visibility.test.ts": 52, - "src/renderer/src/components/browser-pane/host-guest/browser-focus.test.ts": 149, - "src/renderer/src/components/browser-pane/host-guest/browser-guest-eviction-veto.test.ts": 1390, - "src/renderer/src/components/browser-pane/host-guest/browser-guest-page-id-identity.test.ts": 1745, - "src/renderer/src/components/browser-pane/host-guest/browser-guest-paint-retention.test.ts": 2307, - "src/renderer/src/components/browser-pane/host-guest/browser-guest-worktree-retention.test.ts": 2078, - "src/renderer/src/components/browser-pane/host-guest/browser-keyboard.test.ts": 151, - "src/renderer/src/components/browser-pane/host-guest/browser-page-annotation-document.test.ts": 1878, - "src/renderer/src/components/browser-pane/host-guest/browser-page-evicted-guest-recovery.test.ts": 553, - "src/renderer/src/components/browser-pane/host-guest/browser-page-favicon-retention.test.ts": 1737, - "src/renderer/src/components/browser-pane/host-guest/browser-page-guest-recovery.test.ts": 514, - "src/renderer/src/components/browser-pane/host-guest/browser-page-paintability.test.ts": 48, - "src/renderer/src/components/browser-pane/host-guest/browser-page-registration-ownership.test.ts": 684, - "src/renderer/src/components/browser-pane/host-guest/browser-page-viewport.test.ts": 179, - "src/renderer/src/components/browser-pane/host-guest/browser-page-webview-surface.test.ts": 397, - "src/renderer/src/components/browser-pane/host-guest/browser-page-webview.test.ts": 388, - "src/renderer/src/components/browser-pane/host-guest/browser-page-zoom.test.ts": 74, - "src/renderer/src/components/browser-pane/host-guest/browser-system-resume.test.ts": 155, - "src/renderer/src/components/browser-pane/host-guest/browser-worktree-surface-paintability.test.ts": 55, - "src/renderer/src/components/browser-pane/host-guest/use-browser-page-keyboard-shortcuts.split.test.tsx": 2135, - "src/renderer/src/components/browser-pane/host-guest/use-browser-page-slot-viewport.test.ts": 316, - "src/renderer/src/components/browser-pane/host-guest/use-browser-page-viewport-scroll-reporting.test.tsx": 243, - "src/renderer/src/components/browser-pane/host-guest/use-webview-drag-passthrough-active.test.tsx": 246, - "src/renderer/src/components/browser-pane/host-guest/webview-registry.test.ts": 107, - "src/renderer/src/components/browser-pane/navigate/browser-address-bar-navigation.test.ts": 319, - "src/renderer/src/components/browser-pane/navigate/browser-download-progress.test.ts": 147, - "src/renderer/src/components/browser-pane/navigate/browser-load-failure-overlay.test.tsx": 808, - "src/renderer/src/components/browser-pane/navigate/browser-notices.test.ts": 101, - "src/renderer/src/components/browser-pane/navigate/browser-page-download-activity.test.ts": 64, - "src/renderer/src/components/browser-pane/navigate/browser-reload-action.test.ts": 54, - "src/renderer/src/components/browser-pane/navigate/browser-webview-load-failure.test.ts": 290, - "src/renderer/src/components/browser-pane/navigate/chromium-error-page-poll-visibility.test.ts": 1762, - "src/renderer/src/components/browser-pane/navigate/chromium-error-page-polling.test.ts": 48, - "src/renderer/src/components/browser-pane/navigate/navigate-browser-page-notebook-link.test.ts": 510, - "src/renderer/src/components/browser-pane/navigate/use-browser-page-navigation-downloads.file-drop.test.tsx": 1699, - "src/renderer/src/components/browser-pane/navigate/use-browser-page-reload-actions.test.tsx": 1593, - "src/renderer/src/components/browser-pane/restored-client-hosted-recovery-window.test.tsx": 2547, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-frame-style.test.ts": 49, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-keyboard.test.ts": 41, + "src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-chrome-focus.test.tsx": 1676, + "src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-find-shortcuts.test.tsx": 1557, + "src/renderer/src/components/browser-pane/browser-client-page-attachment-scan-budget.test.ts": 494, + "src/renderer/src/components/browser-pane/browser-client-page-metadata-publisher.test.ts": 297, + "src/renderer/src/components/browser-pane/browser-client-page-position-driver.test.ts": 447, + "src/renderer/src/components/browser-pane/browser-client-page-renderer-installation.test.ts": 400, + "src/renderer/src/components/browser-pane/browser-client-page-retained-drag-passthrough.test.ts": 405, + "src/renderer/src/components/browser-pane/browser-client-page-retained-registry.test.ts": 445, + "src/renderer/src/components/browser-pane/browser-download-destination-toast.test.ts": 37, + "src/renderer/src/components/browser-pane/browser-reopen-on-server.test.ts": 232, + "src/renderer/src/components/browser-pane/client-hosted-browser-unavailable-notice.test.tsx": 553, + "src/renderer/src/components/browser-pane/describe-page/browser-annotation-geometry.test.ts": 92, + "src/renderer/src/components/browser-pane/describe-page/browser-artifact-upload.test.ts": 1417, + "src/renderer/src/components/browser-pane/describe-page/browser-favicon-url.test.ts": 30, + "src/renderer/src/components/browser-pane/describe-page/browser-overlay-shortcut-target.test.ts": 1497, + "src/renderer/src/components/browser-pane/describe-page/browser-page-url-display.test.ts": 1509, + "src/renderer/src/components/browser-pane/describe-page/live-browser-url-registry.test.ts": 45, + "src/renderer/src/components/browser-pane/host-guest/agent-input-focus-guard.test.ts": 159, + "src/renderer/src/components/browser-pane/host-guest/browser-automation-visibility.test.ts": 33, + "src/renderer/src/components/browser-pane/host-guest/browser-focus.test.ts": 157, + "src/renderer/src/components/browser-pane/host-guest/browser-guest-eviction-veto.test.ts": 1259, + "src/renderer/src/components/browser-pane/host-guest/browser-guest-page-id-identity.test.ts": 1300, + "src/renderer/src/components/browser-pane/host-guest/browser-guest-paint-retention.test.ts": 3557, + "src/renderer/src/components/browser-pane/host-guest/browser-guest-worktree-retention.test.ts": 1937, + "src/renderer/src/components/browser-pane/host-guest/browser-keyboard.test.ts": 190, + "src/renderer/src/components/browser-pane/host-guest/browser-page-annotation-document.test.ts": 1555, + "src/renderer/src/components/browser-pane/host-guest/browser-page-evicted-guest-recovery.test.ts": 804, + "src/renderer/src/components/browser-pane/host-guest/browser-page-favicon-retention.test.ts": 2623, + "src/renderer/src/components/browser-pane/host-guest/browser-page-guest-recovery.test.ts": 496, + "src/renderer/src/components/browser-pane/host-guest/browser-page-mount-admission.test.ts": 1485, + "src/renderer/src/components/browser-pane/host-guest/browser-page-paintability.test.ts": 40, + "src/renderer/src/components/browser-pane/host-guest/browser-page-registration-ownership.test.ts": 688, + "src/renderer/src/components/browser-pane/host-guest/browser-page-viewport.test.ts": 209, + "src/renderer/src/components/browser-pane/host-guest/browser-page-webview-surface.test.ts": 454, + "src/renderer/src/components/browser-pane/host-guest/browser-page-webview.test.ts": 404, + "src/renderer/src/components/browser-pane/host-guest/browser-page-zoom.test.ts": 65, + "src/renderer/src/components/browser-pane/host-guest/browser-system-resume.test.ts": 163, + "src/renderer/src/components/browser-pane/host-guest/browser-worktree-surface-paintability.test.ts": 48, + "src/renderer/src/components/browser-pane/host-guest/use-browser-page-keyboard-shortcuts.split.test.tsx": 1419, + "src/renderer/src/components/browser-pane/host-guest/use-browser-page-slot-viewport.test.ts": 231, + "src/renderer/src/components/browser-pane/host-guest/use-browser-page-viewport-scroll-reporting.test.tsx": 244, + "src/renderer/src/components/browser-pane/host-guest/use-webview-drag-passthrough-active.test.tsx": 258, + "src/renderer/src/components/browser-pane/host-guest/webview-registry.test.ts": 110, + "src/renderer/src/components/browser-pane/navigate/browser-address-bar-navigation.test.ts": 262, + "src/renderer/src/components/browser-pane/navigate/browser-download-progress.test.ts": 49, + "src/renderer/src/components/browser-pane/navigate/browser-file-editor-open.test.ts": 1310, + "src/renderer/src/components/browser-pane/navigate/browser-load-failure-overlay.test.tsx": 725, + "src/renderer/src/components/browser-pane/navigate/browser-notices.test.ts": 92, + "src/renderer/src/components/browser-pane/navigate/browser-page-download-activity.test.ts": 44, + "src/renderer/src/components/browser-pane/navigate/browser-reload-action.test.ts": 37, + "src/renderer/src/components/browser-pane/navigate/browser-webview-load-failure.test.ts": 276, + "src/renderer/src/components/browser-pane/navigate/chromium-error-page-poll-visibility.test.ts": 1449, + "src/renderer/src/components/browser-pane/navigate/chromium-error-page-polling.test.ts": 33, + "src/renderer/src/components/browser-pane/navigate/navigate-browser-page-notebook-link.test.ts": 526, + "src/renderer/src/components/browser-pane/navigate/use-browser-page-navigation-downloads.file-drop.test.tsx": 1590, + "src/renderer/src/components/browser-pane/navigate/use-browser-page-reload-actions.test.tsx": 1545, + "src/renderer/src/components/browser-pane/restored-client-hosted-recovery-window.test.tsx": 1767, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-frame-style.test.ts": 31, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-keyboard.test.ts": 37, "src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-input-model.test.ts": 49, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-pane.address-bar.test.tsx": 2342, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-pane.chrome-chords.test.tsx": 2187, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-service-failure.test.ts": 390, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-errors.test.ts": 132, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-lifecycle.test.ts": 409, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-restart-scheduler.test.ts": 69, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-status.test.ts": 80, - "src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-page-navigation.test.ts": 1962, - "src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-stream-activation.test.ts": 338, - "src/renderer/src/components/browser-pane/use-ssh-workspace-browser-route.host-connection.test.tsx": 1836, - "src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx": 3852, - "src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx": 3045, - "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.test.ts": 280, - "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-identity.test.ts": 62, - "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-external-link-confirmation.test.ts": 146, - "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-webview-attach.test.ts": 154, - "src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.lazy-ref.test.tsx": 272, - "src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts": 191, - "src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.test.tsx": 210, - "src/renderer/src/components/browser-webauthn-account-dialog.test.tsx": 541, - "src/renderer/src/components/cmd-j/palette-duplicate-key-ghost-rows.test.tsx": 191, - "src/renderer/src/components/cmd-j/palette-filter-option-list.test.ts": 49, - "src/renderer/src/components/cmd-j/palette-filter-options.test.ts": 144, - "src/renderer/src/components/cmd-j/palette-filter.test.ts": 92, - "src/renderer/src/components/cmd-j/palette-focus-restore-target.test.ts": 162, - "src/renderer/src/components/cmd-j/palette-host-badge.test.ts": 120, - "src/renderer/src/components/cmd-j/palette-list-entry-render-keys.test.ts": 53, - "src/renderer/src/components/cmd-j/palette-live-status.test.tsx": 1819, - "src/renderer/src/components/cmd-j/palette-query-tokens.test.ts": 66, - "src/renderer/src/components/cmd-j/palette-results.test.ts": 266, - "src/renderer/src/components/cmd-j/palette-section-render-cap.test.ts": 79, - "src/renderer/src/components/cmd-j/palette-session-age.test.ts": 79, - "src/renderer/src/components/cmd-j/plugin-quick-actions.test.ts": 123, - "src/renderer/src/components/cmd-j/quick-action-context.test.ts": 1708, - "src/renderer/src/components/cmd-j/worktree-checks-review-index.test.ts": 163, - "src/renderer/src/components/cmd-j/worktree-palette-cache-inputs.test.ts": 58, - "src/renderer/src/components/codex-restart-chip.test.tsx": 1738, - "src/renderer/src/components/comment-code-context-state.test.ts": 56, - "src/renderer/src/components/comment-reply-target-state.test.ts": 54, - "src/renderer/src/components/confirmation-dialog.test.tsx": 833, - "src/renderer/src/components/confirmation-skip-preference.test.ts": 155, - "src/renderer/src/components/connect-dialog-outside-dismiss.test.tsx": 839, - "src/renderer/src/components/contextual-tours/ContextualTourControl.test.ts": 1374, - "src/renderer/src/components/contextual-tours/ContextualTourOverlay.remeasure.test.tsx": 2051, - "src/renderer/src/components/contextual-tours/ContextualTourOverlay.test.tsx": 1496, - "src/renderer/src/components/contextual-tours/ContextualTourOverlay.visibility.test.tsx": 4049, - "src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.localization.test.tsx": 1962, - "src/renderer/src/components/contextual-tours/contextual-tour-floating-position.test.ts": 2054, - "src/renderer/src/components/contextual-tours/contextual-tour-gate.test.ts": 69, - "src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts": 1941, - "src/renderer/src/components/contextual-tours/contextual-tour-step-actions.test.ts": 68, - "src/renderer/src/components/contextual-tours/request-contextual-tour-when-ready.test.ts": 1339, - "src/renderer/src/components/contextual-tours/use-contextual-tour.test.ts": 1428, - "src/renderer/src/components/contextual-tours/workspace-creation-tour-handoff.test.ts": 1400, - "src/renderer/src/components/crash-report/CrashReportDialogSurface.overflow.test.tsx": 544, - "src/renderer/src/components/crash-report/crash-report-submit-notice.test.ts": 103, - "src/renderer/src/components/crash-report/use-crash-report-copy.test.tsx": 301, - "src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx": 1462, - "src/renderer/src/components/dashboard-popout/AgentKanbanCard.test.tsx": 630, - "src/renderer/src/components/dashboard-popout/AgentTerminalDialog.test.tsx": 732, - "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.clipboard-routes.test.tsx": 2011, - "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.frame-lifecycle.test.tsx": 2133, - "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.option-dead-key.test.tsx": 1271, - "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-geometry.test.tsx": 1908, - "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-lifetime.test.tsx": 2148, - "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.test.tsx": 2784, - "src/renderer/src/components/dashboard-popout/DashboardHostBadge.test.tsx": 470, - "src/renderer/src/components/dashboard-popout/DashboardPopoutRoot.test.tsx": 323, - "src/renderer/src/components/dashboard-popout/agent-board-filtering.test.ts": 56, - "src/renderer/src/components/dashboard-popout/dashboard-agent-status-patch.test.ts": 67, - "src/renderer/src/components/dashboard-popout/preview-grid-claim.test.ts": 275, - "src/renderer/src/components/dashboard-popout/preview-terminal-box-fit-lifecycle.test.ts": 189, - "src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts": 439, - "src/renderer/src/components/dashboard-popout/preview-terminal-options.test.ts": 155, - "src/renderer/src/components/dashboard-popout/preview-terminal-right-click-paste.test.ts": 1585, - "src/renderer/src/components/dashboard-popout/preview-terminal-shortcuts.test.ts": 175, - "src/renderer/src/components/dashboard-popout/preview-terminal-snapshot-replay.test.ts": 58, - "src/renderer/src/components/dashboard-popout/remote-runtime-terminal-preview-api.test.ts": 107, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-pane.address-bar.test.tsx": 1659, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-pane.chrome-chords.test.tsx": 2326, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-service-failure.test.ts": 342, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-errors.test.ts": 87, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-lifecycle.test.ts": 422, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-restart-scheduler.test.ts": 52, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-status.test.ts": 60, + "src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-page-navigation.test.ts": 1472, + "src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-stream-activation.test.ts": 274, + "src/renderer/src/components/browser-pane/use-ssh-workspace-browser-route.host-connection.test.tsx": 1677, + "src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx": 2588, + "src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx": 3892, + "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-artifact-share.test.ts": 360, + "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.test.ts": 254, + "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-identity.test.ts": 31, + "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-external-link-confirmation.test.ts": 123, + "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-share-key-parity.test.ts": 1124, + "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-webview-attach.test.ts": 198, + "src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.lazy-ref.test.tsx": 275, + "src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts": 256, + "src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.test.tsx": 194, + "src/renderer/src/components/browser-webauthn-account-dialog.test.tsx": 461, + "src/renderer/src/components/cmd-j/palette-duplicate-key-ghost-rows.test.tsx": 216, + "src/renderer/src/components/cmd-j/palette-filter-option-list.test.ts": 46, + "src/renderer/src/components/cmd-j/palette-filter-options.test.ts": 338, + "src/renderer/src/components/cmd-j/palette-filter.test.ts": 75, + "src/renderer/src/components/cmd-j/palette-focus-restore-target.test.ts": 167, + "src/renderer/src/components/cmd-j/palette-host-badge.test.ts": 313, + "src/renderer/src/components/cmd-j/palette-list-entry-render-keys.test.ts": 35, + "src/renderer/src/components/cmd-j/palette-live-status.test.tsx": 1575, + "src/renderer/src/components/cmd-j/palette-query-tokens.test.ts": 40, + "src/renderer/src/components/cmd-j/palette-results.test.ts": 185, + "src/renderer/src/components/cmd-j/palette-section-render-cap.test.ts": 48, + "src/renderer/src/components/cmd-j/palette-session-age.test.ts": 67, + "src/renderer/src/components/cmd-j/plugin-quick-actions.test.ts": 101, + "src/renderer/src/components/cmd-j/quick-action-context.test.ts": 1337, + "src/renderer/src/components/cmd-j/task-search-activation.test.ts": 1085, + "src/renderer/src/components/cmd-j/task-search-requests.test.ts": 1413, + "src/renderer/src/components/cmd-j/worktree-checks-review-index.test.ts": 145, + "src/renderer/src/components/cmd-j/worktree-palette-cache-inputs.test.ts": 32, + "src/renderer/src/components/codex-restart-chip.test.tsx": 1731, + "src/renderer/src/components/comment-code-context-state.test.ts": 36, + "src/renderer/src/components/comment-reply-target-state.test.ts": 29, + "src/renderer/src/components/confirmation-dialog.test.tsx": 780, + "src/renderer/src/components/confirmation-skip-preference.test.ts": 149, + "src/renderer/src/components/connect-dialog-outside-dismiss.test.tsx": 1070, + "src/renderer/src/components/contextual-tours/ContextualTourControl.test.ts": 1266, + "src/renderer/src/components/contextual-tours/ContextualTourOverlay.remeasure.test.tsx": 1744, + "src/renderer/src/components/contextual-tours/ContextualTourOverlay.test.tsx": 1645, + "src/renderer/src/components/contextual-tours/ContextualTourOverlay.visibility.test.tsx": 3997, + "src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.localization.test.tsx": 1757, + "src/renderer/src/components/contextual-tours/contextual-tour-floating-position.test.ts": 2095, + "src/renderer/src/components/contextual-tours/contextual-tour-gate.test.ts": 49, + "src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts": 1641, + "src/renderer/src/components/contextual-tours/contextual-tour-step-actions.test.ts": 46, + "src/renderer/src/components/contextual-tours/request-contextual-tour-when-ready.test.ts": 1364, + "src/renderer/src/components/contextual-tours/use-contextual-tour.test.ts": 1349, + "src/renderer/src/components/contextual-tours/workspace-creation-tour-handoff.test.ts": 1377, + "src/renderer/src/components/crash-report/CrashReportDialogSurface.overflow.test.tsx": 400, + "src/renderer/src/components/crash-report/crash-report-submit-notice.test.ts": 89, + "src/renderer/src/components/crash-report/use-crash-report-copy.test.tsx": 290, + "src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx": 1126, + "src/renderer/src/components/dashboard-popout/AgentKanbanCard.test.tsx": 745, + "src/renderer/src/components/dashboard-popout/AgentTerminalDialog.test.tsx": 687, + "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.clipboard-routes.test.tsx": 2694, + "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.frame-lifecycle.test.tsx": 1920, + "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.option-dead-key.test.tsx": 929, + "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-geometry.test.tsx": 1839, + "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-lifetime.test.tsx": 2118, + "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.test.tsx": 2357, + "src/renderer/src/components/dashboard-popout/DashboardHostBadge.test.tsx": 398, + "src/renderer/src/components/dashboard-popout/DashboardPopoutRoot.test.tsx": 290, + "src/renderer/src/components/dashboard-popout/agent-board-filtering.test.ts": 42, + "src/renderer/src/components/dashboard-popout/dashboard-agent-status-patch.test.ts": 63, + "src/renderer/src/components/dashboard-popout/preview-grid-claim.test.ts": 200, + "src/renderer/src/components/dashboard-popout/preview-terminal-box-fit-lifecycle.test.ts": 221, + "src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts": 424, + "src/renderer/src/components/dashboard-popout/preview-terminal-options.test.ts": 185, + "src/renderer/src/components/dashboard-popout/preview-terminal-right-click-paste.test.ts": 1528, + "src/renderer/src/components/dashboard-popout/preview-terminal-shortcuts.test.ts": 213, + "src/renderer/src/components/dashboard-popout/preview-terminal-snapshot-replay.test.ts": 53, + "src/renderer/src/components/dashboard-popout/remote-runtime-terminal-preview-api.test.ts": 59, "src/renderer/src/components/dashboard-popout/terminal-preview-unavailable-message.test.ts": 77, - "src/renderer/src/components/dashboard-popout/useDashboardSnapshot.test.tsx": 290, - "src/renderer/src/components/dashboard/AgentDashboardDrawer.test.tsx": 2233, - "src/renderer/src/components/dashboard/AgentDashboardSettingsMenu.test.tsx": 595, - "src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.test.tsx": 1822, - "src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx": 1741, - "src/renderer/src/components/dashboard/agent-dashboard-open-mode.test.ts": 48, - "src/renderer/src/components/dashboard/agent-finished-timestamp.test.ts": 58, - "src/renderer/src/components/dashboard/agent-row-lineage-model.test.ts": 134, - "src/renderer/src/components/dashboard/agent-row-pane-live-title.test.ts": 52, - "src/renderer/src/components/dashboard/build-dashboard-bucket-counts.allocation.test.ts": 381, - "src/renderer/src/components/dashboard/build-dashboard-bucket-counts.cache.test.ts": 230, - "src/renderer/src/components/dashboard/build-dashboard-bucket-counts.equivalence.test.ts": 296, - "src/renderer/src/components/dashboard/build-dashboard-snapshot-folder-workspace.test.ts": 1527, - "src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts": 1528, - "src/renderer/src/components/dashboard/build-dashboard-snapshot.rows-cache.test.ts": 1542, - "src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts": 1528, - "src/renderer/src/components/dashboard/dashboard-card-context.test.ts": 76, - "src/renderer/src/components/dashboard/dashboard-card-labels.test.ts": 77, - "src/renderer/src/components/dashboard/dashboard-card-terminal-input.test.ts": 1338, - "src/renderer/src/components/dashboard/dashboard-worktree-launch-options.test.ts": 86, - "src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts": 60, - "src/renderer/src/components/dashboard/reveal-dashboard-agent.test.ts": 56, - "src/renderer/src/components/dashboard/use-agent-row-conversation-name.test.ts": 106, - "src/renderer/src/components/dashboard/useAgentBucketCounts.gate.test.ts": 346, - "src/renderer/src/components/dashboard/useAgentBucketCounts.test.tsx": 273, - "src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx": 1283, - "src/renderer/src/components/dashboard/useLiveDashboardSnapshot.test.ts": 1818, - "src/renderer/src/components/dashboard/useRetainedAgents.test.ts": 1671, - "src/renderer/src/components/dashboard/useRetainedAgentsSync.test.ts": 1484, - "src/renderer/src/components/dictation/DictationIndicator.localization.test.ts": 257, - "src/renderer/src/components/dictation/DictationIndicator.test.tsx": 461, - "src/renderer/src/components/dictation/dictation-audio-meter.test.ts": 44, - "src/renderer/src/components/dictation/dictation-final-segments.test.ts": 48, - "src/renderer/src/components/dictation/dictation-insertion-target.test.ts": 337, - "src/renderer/src/components/dictation/dictation-meter-store.test.tsx": 305, - "src/renderer/src/components/dictation/dictation-stopped-sessions.test.ts": 54, - "src/renderer/src/components/dictation/microphone-devices.test.ts": 309, - "src/renderer/src/components/dictation/use-hold-dictation-gesture.test.tsx": 261, - "src/renderer/src/components/diff-comments/DiffCommentCard.resize.test.tsx": 460, - "src/renderer/src/components/diff-comments/DiffCommentDraftCard.test.tsx": 2015, - "src/renderer/src/components/diff-comments/diff-comment-draft-zone.test.tsx": 2153, - "src/renderer/src/components/diff-comments/diff-comment-drag-auto-scroll.test.ts": 47, - "src/renderer/src/components/diff-comments/diff-comment-line-range.test.ts": 61, - "src/renderer/src/components/diff-comments/diff-comment-popover-outside-click.test.tsx": 1638, - "src/renderer/src/components/diff-comments/diff-comment-popover-position.test.ts": 52, - "src/renderer/src/components/diff-comments/diff-comment-range-drag.test.ts": 9586, - "src/renderer/src/components/diff-comments/diff-comment-zone-mouse-events.test.ts": 54, - "src/renderer/src/components/diff-comments/use-visible-worktree-diff-comments.test.ts": 263, - "src/renderer/src/components/diff-comments/useDiffCommentDecorator.commentable-lines.test.tsx": 2092, - "src/renderer/src/components/diff-comments/useDiffCommentDecorator.model-lifecycle.test.tsx": 8367, - "src/renderer/src/components/diff-comments/useDiffCommentDecorator.range-selection.test.tsx": 5475, - "src/renderer/src/components/editor/ChangesModeView.test.tsx": 345, - "src/renderer/src/components/editor/CheckRunCopyButton.test.tsx": 362, - "src/renderer/src/components/editor/CheckRunDetailsPanel.copy.test.tsx": 1906, - "src/renderer/src/components/editor/ConflictComponents.test.tsx": 213, - "src/renderer/src/components/editor/DiffSectionBody.word-wrap-lifecycle.test.tsx": 405, - "src/renderer/src/components/editor/DiffViewer.word-wrap-lifecycle.test.tsx": 376, - "src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx": 1799, - "src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx": 2378, - "src/renderer/src/components/editor/EditorContent.test.tsx": 2816, - "src/renderer/src/components/editor/EditorEditFileSurface.monaco-containment.test.tsx": 1937, - "src/renderer/src/components/editor/EditorFileLoadErrorView.test.tsx": 388, - "src/renderer/src/components/editor/EditorPanel.markdown-classification-memoization.test.tsx": 3015, - "src/renderer/src/components/editor/EditorPanel.markdown-review-tools-setting.test.tsx": 2316, - "src/renderer/src/components/editor/EditorPanelHeader.test.tsx": 1367, - "src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx": 2121, - "src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.test.tsx": 177, - "src/renderer/src/components/editor/EditorPanelShell.header.test.tsx": 193, - "src/renderer/src/components/editor/ExternalFileChangeBanner.test.tsx": 521, - "src/renderer/src/components/editor/ExternalFileChangeCompareDialog.test.tsx": 719, - "src/renderer/src/components/editor/ImageViewer.test.tsx": 148, - "src/renderer/src/components/editor/IpynbCellEditor.external-sync.test.tsx": 1507, - "src/renderer/src/components/editor/IpynbKernelToolbar.test.tsx": 676, - "src/renderer/src/components/editor/IpynbRendering.test.tsx": 2202, - "src/renderer/src/components/editor/LargeDiffLoadPrompt.test.tsx": 338, - "src/renderer/src/components/editor/MarkdownPreview.image-access.test.tsx": 1100, - "src/renderer/src/components/editor/MarkdownPreview.link-routing.interaction.test.tsx": 1077, - "src/renderer/src/components/editor/MarkdownPreview.test.ts": 2492, - "src/renderer/src/components/editor/MarkdownPreview.toc-visibility-gate.test.tsx": 967, - "src/renderer/src/components/editor/MarkdownPreviewBody.test.tsx": 342, - "src/renderer/src/components/editor/MarkdownTableOfContentsPanel.test.tsx": 1650, - "src/renderer/src/components/editor/MediaViewer.test.tsx": 221, - "src/renderer/src/components/editor/MermaidBlock.test.tsx": 522, - "src/renderer/src/components/editor/MonacoEditor.content-owner.test.tsx": 5776, - "src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx": 6247, - "src/renderer/src/components/editor/MonacoEditor.owner-view-state.test.tsx": 3008, - "src/renderer/src/components/editor/MonacoEditor.system-theme.test.tsx": 2679, - "src/renderer/src/components/editor/NotesSendMenu.test.tsx": 212, - "src/renderer/src/components/editor/PdfFind.test.tsx": 582, - "src/renderer/src/components/editor/ReviewNotesSendMenuContent.test.tsx": 215, - "src/renderer/src/components/editor/RichMarkdownErrorBoundary.lazy-chunk.test.tsx": 344, - "src/renderer/src/components/editor/RichMarkdownLinkBubble.render.test.tsx": 375, - "src/renderer/src/components/editor/RichMarkdownLinkBubble.test.ts": 151, - "src/renderer/src/components/editor/RichMarkdownSearchBar.ime-enter.test.tsx": 423, - "src/renderer/src/components/editor/RichMarkdownSlashMenu.test.tsx": 168, - "src/renderer/src/components/editor/RichMarkdownTableControls.test.tsx": 2455, - "src/renderer/src/components/editor/check-annotation-open.test.ts": 110, - "src/renderer/src/components/editor/check-annotation-path.test.ts": 59, - "src/renderer/src/components/editor/check-job-step-status.test.ts": 49, - "src/renderer/src/components/editor/check-run-clipboard-text.test.ts": 48, - "src/renderer/src/components/editor/check-run-details-fix-with-ai.test.ts": 549, - "src/renderer/src/components/editor/check-run-details-tab.test.ts": 43, - "src/renderer/src/components/editor/closed-editor-model-lifetime.test.ts": 5487, - "src/renderer/src/components/editor/closed-editor-model-registry.test.ts": 12691, - "src/renderer/src/components/editor/closed-editor-model-shell.test.tsx": 3197, - "src/renderer/src/components/editor/closed-editor-tab-cache-sweep.test.ts": 51, - "src/renderer/src/components/editor/closed-editor-tab-disposal.test.ts": 111, - "src/renderer/src/components/editor/closed-editor-view-state-retention.test.ts": 5815, - "src/renderer/src/components/editor/combined-diff-on-demand-load.test.ts": 60, - "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-collapsed-work.test.tsx": 693, - "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-render.test.tsx": 1027, - "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-row-drag.test.tsx": 287, - "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-windowing.test.tsx": 775, - "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree.test.ts": 71, - "src/renderer/src/components/editor/combined-diff/browse-files/use-combined-diff-file-tree-resize.test.tsx": 273, - "src/renderer/src/components/editor/combined-diff/browse-files/use-combined-diff-tree-navigation.test.ts": 255, - "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-initial-section-load.test.ts": 49, - "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-load-scheduler.test.ts": 55, - "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-section-connection.test.ts": 1148, - "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-section-load-state.test.ts": 58, - "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-stale-analysis-work-budget.test.tsx": 769, - "src/renderer/src/components/editor/combined-diff/remember-view/use-combined-diff-view-restore.test.tsx": 731, - "src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-entries.test.ts": 71, - "src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-section-cache-match.test.ts": 55, - "src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-section-scaling.test.tsx": 681, - "src/renderer/src/components/editor/combined-diff/resolve-changes/use-combined-diff-section-index-map.test.tsx": 265, - "src/renderer/src/components/editor/combined-diff/review-controls/combined-diff-commit-message.test.ts": 59, - "src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-render-range.test.ts": 47, - "src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-restore-signal-equivalence.test.tsx": 740, - "src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-scrollbar-drag.test.ts": 68, - "src/renderer/src/components/editor/conflict-review-file-tree-windowing.test.tsx": 1135, - "src/renderer/src/components/editor/csv/CsvGrid.test.tsx": 765, - "src/renderer/src/components/editor/csv/CsvViewer.delimiter.test.tsx": 2535, - "src/renderer/src/components/editor/csv/CsvViewer.editing.test.tsx": 3037, - "src/renderer/src/components/editor/csv/csv-byte-index.test.ts": 1733, - "src/renderer/src/components/editor/csv/csv-cell-links.test.tsx": 294, - "src/renderer/src/components/editor/csv/csv-column-width-preferences.test.ts": 144, - "src/renderer/src/components/editor/csv/csv-paged-preview.test.ts": 94, - "src/renderer/src/components/editor/csv/csv-parse-budget.test.ts": 100, - "src/renderer/src/components/editor/csv/csv-parse.test.ts": 125, - "src/renderer/src/components/editor/csv/csv-pending-save.test.ts": 1349, - "src/renderer/src/components/editor/csv/csv-preview-save-safety.test.ts": 1483, - "src/renderer/src/components/editor/csv/csv-table-interactions.test.ts": 118, - "src/renderer/src/components/editor/csv/csv-text-document.test.ts": 139, - "src/renderer/src/components/editor/csv/useCsvPagedPreview.test.tsx": 396, - "src/renderer/src/components/editor/details-markdown-html.test.ts": 78, - "src/renderer/src/components/editor/diff-editor-hide-unchanged-options.test.ts": 54, - "src/renderer/src/components/editor/diff-editor-line-number-options.test.ts": 73, - "src/renderer/src/components/editor/diff-editor-shift-wheel-scroll.test.ts": 155, - "src/renderer/src/components/editor/diff-editor-whitespace-options.test.ts": 48, - "src/renderer/src/components/editor/diff-editor-word-wrap-options.test.ts": 180, - "src/renderer/src/components/editor/diff-line-stats.test.ts": 58, - "src/renderer/src/components/editor/diff-model-swap-view-state.test.ts": 145, - "src/renderer/src/components/editor/diff-monaco-model-disposal.test.ts": 51, - "src/renderer/src/components/editor/diff-navigation-context.test.tsx": 1732, - "src/renderer/src/components/editor/diff-section-comment-submit.test.ts": 99, - "src/renderer/src/components/editor/diff-section-layout.test.ts": 87, - "src/renderer/src/components/editor/diff-section-preview.test.ts": 1498, - "src/renderer/src/components/editor/diff-viewer-large-diff-save-action.test.ts": 93, - "src/renderer/src/components/editor/drag-selection-guard-lifetime.test.ts": 563, - "src/renderer/src/components/editor/editor-autosave-conflict-flow.test.ts": 634, - "src/renderer/src/components/editor/editor-autosave-controller.test.ts": 652, - "src/renderer/src/components/editor/editor-autosave.test.ts": 279, - "src/renderer/src/components/editor/editor-cmd-save-target.test.ts": 258, - "src/renderer/src/components/editor/editor-content-dirty-state.test.ts": 83, - "src/renderer/src/components/editor/editor-dropped-file-open.escaping-link.test.ts": 362, - "src/renderer/src/components/editor/editor-dropped-file-open.test.ts": 1242, - "src/renderer/src/components/editor/editor-external-watch-path-index.test.ts": 1264, - "src/renderer/src/components/editor/editor-file-save-attempt.test.ts": 57, - "src/renderer/src/components/editor/editor-header-file-rename.test.ts": 1748, - "src/renderer/src/components/editor/editor-header.test.ts": 59, - "src/renderer/src/components/editor/editor-labels.test.ts": 54, - "src/renderer/src/components/editor/editor-model-host-isolation.test.ts": 2625, - "src/renderer/src/components/editor/editor-model-owner.test.ts": 309, - "src/renderer/src/components/editor/editor-model-uri.test.ts": 131, - "src/renderer/src/components/editor/editor-panel-diff-reload.test.ts": 46, - "src/renderer/src/components/editor/editor-panel-draft-selector.test.ts": 64, - "src/renderer/src/components/editor/editor-panel-git-entry-selector.test.ts": 54, - "src/renderer/src/components/editor/editor-panel-render-model.test.ts": 1971, - "src/renderer/src/components/editor/editor-path-display.test.ts": 52, - "src/renderer/src/components/editor/editor-path-move-inflight.test.ts": 54, - "src/renderer/src/components/editor/editor-pending-flush.test.ts": 38, - "src/renderer/src/components/editor/editor-restored-tab-conflict-scan.test.ts": 624, - "src/renderer/src/components/editor/editor-save-user-named-tab.test.ts": 547, - "src/renderer/src/components/editor/editor-self-write-registry.test.ts": 93, - "src/renderer/src/components/editor/editor-shortcuts.test.ts": 225, - "src/renderer/src/components/editor/editor-untitled-dont-save-dialogs.test.tsx": 668, - "src/renderer/src/components/editor/editor-untitled-save-lifecycle.test.ts": 837, - "src/renderer/src/components/editor/export-active-markdown.test.ts": 128, - "src/renderer/src/components/editor/file-editor-word-wrap-options.test.ts": 55, - "src/renderer/src/components/editor/image-viewer-zoom.test.ts": 63, - "src/renderer/src/components/editor/ipynb-ansi.test.ts": 70, - "src/renderer/src/components/editor/ipynb-code-cell-lines.test.ts": 142, - "src/renderer/src/components/editor/ipynb-kernel-outputs.test.ts": 318, - "src/renderer/src/components/editor/ipynb-kernel-session-start-lifetime.test.ts": 113, - "src/renderer/src/components/editor/ipynb-kernel-session.test.ts": 182, - "src/renderer/src/components/editor/ipynb-kernel-setup-commands.test.ts": 58, - "src/renderer/src/components/editor/ipynb-parse.test.ts": 80, - "src/renderer/src/components/editor/large-diff-render-limit.test.ts": 168, - "src/renderer/src/components/editor/large-markdown-render-freeze.test.tsx": 2232, - "src/renderer/src/components/editor/line-copy-path.test.ts": 45, - "src/renderer/src/components/editor/local-log-tail-decoder.test.ts": 113, - "src/renderer/src/components/editor/markdown-artifact-upload.test.ts": 68, - "src/renderer/src/components/editor/markdown-dirty-state.test.ts": 274, - "src/renderer/src/components/editor/markdown-doc-completions.test.ts": 59, - "src/renderer/src/components/editor/markdown-doc-links.test.ts": 139, - "src/renderer/src/components/editor/markdown-document-list-request.test.ts": 379, - "src/renderer/src/components/editor/markdown-document-worktree-path-selector.test.ts": 1527, - "src/renderer/src/components/editor/markdown-export-extract.test.ts": 218, - "src/renderer/src/components/editor/markdown-export-html.test.ts": 58, - "src/renderer/src/components/editor/markdown-frontmatter.test.ts": 53, - "src/renderer/src/components/editor/markdown-heading-slug.test.ts": 42, - "src/renderer/src/components/editor/markdown-internal-links.test.ts": 74, - "src/renderer/src/components/editor/markdown-preview-anchor-navigation.test.ts": 135, - "src/renderer/src/components/editor/markdown-preview-annotation-shortcut.test.ts": 177, - "src/renderer/src/components/editor/markdown-preview-controls.test.ts": 91, - "src/renderer/src/components/editor/markdown-preview-document-client.test.ts": 58, - "src/renderer/src/components/editor/markdown-preview-document-search-budget.test.ts": 357, - "src/renderer/src/components/editor/markdown-preview-document-search-cache.test.ts": 552, - "src/renderer/src/components/editor/markdown-preview-document-search-scope.test.ts": 385, - "src/renderer/src/components/editor/markdown-preview-document.test.ts": 1525, - "src/renderer/src/components/editor/markdown-preview-links.test.ts": 67, - "src/renderer/src/components/editor/markdown-preview-local-images.test.ts": 1715, - "src/renderer/src/components/editor/markdown-preview-review-frame-lifecycle.test.tsx": 325, - "src/renderer/src/components/editor/markdown-preview-row-measurements.test.ts": 167, - "src/renderer/src/components/editor/markdown-preview-search-crash.test.tsx": 230, - "src/renderer/src/components/editor/markdown-preview-search-memory.test.ts": 171, - "src/renderer/src/components/editor/markdown-preview-search-offsets.test.ts": 518, - "src/renderer/src/components/editor/markdown-preview-search.test.ts": 79, - "src/renderer/src/components/editor/markdown-preview-table-chunks.test.ts": 6270, - "src/renderer/src/components/editor/markdown-preview-url-transform.test.ts": 95, - "src/renderer/src/components/editor/markdown-reference-link-normalization.test.ts": 77, - "src/renderer/src/components/editor/markdown-render-mode.test.ts": 50, - "src/renderer/src/components/editor/markdown-rich-comment-scan.test.ts": 323, - "src/renderer/src/components/editor/markdown-rich-line-scan.test.ts": 1628, - "src/renderer/src/components/editor/markdown-rich-mode-eligibility-cache.test.ts": 3909, - "src/renderer/src/components/editor/markdown-rich-mode.test.ts": 2831, - "src/renderer/src/components/editor/markdown-rich-size-limit.test.ts": 340, - "src/renderer/src/components/editor/markdown-round-trip.test.ts": 2101, - "src/renderer/src/components/editor/markdown-scanner-boundaries.test.ts": 100, - "src/renderer/src/components/editor/markdown-table-of-contents.test.ts": 166, - "src/renderer/src/components/editor/markdown-toc-collapse-state.test.ts": 53, - "src/renderer/src/components/editor/markdown-toc-visibility-gate.test.ts": 105, - "src/renderer/src/components/editor/markdown-tokenizer-start.test.ts": 97, - "src/renderer/src/components/editor/mermaid-config.test.ts": 44, - "src/renderer/src/components/editor/monaco-auto-height.test.ts": 114, - "src/renderer/src/components/editor/monaco-codebase-search.test.ts": 52, - "src/renderer/src/components/editor/monaco-conflict-decorations.test.ts": 68, - "src/renderer/src/components/editor/monaco-content-sync.test.ts": 67, - "src/renderer/src/components/editor/monaco-content-sync.undo-history.test.ts": 1193, - "src/renderer/src/components/editor/monaco-context-menu-paste.test.ts": 208, - "src/renderer/src/components/editor/monaco-cursor-line-lifetime.test.ts": 1102, - "src/renderer/src/components/editor/monaco-find-options.test.ts": 44, - "src/renderer/src/components/editor/monaco-find-widget.test.ts": 167, - "src/renderer/src/components/editor/monaco-large-text-paste.test.ts": 208, - "src/renderer/src/components/editor/monaco-markdown-doc-completions-retention.test.ts": 64, - "src/renderer/src/components/editor/monaco-markdown-doc-link-decorations.offset-scan.test.ts": 267, - "src/renderer/src/components/editor/monaco-markdown-doc-link-decorations.test.ts": 82, - "src/renderer/src/components/editor/monaco-markdown-selection-annotation.test.ts": 49, - "src/renderer/src/components/editor/monaco-programmatic-sync.test.ts": 54, - "src/renderer/src/components/editor/monaco-reveal-range.test.ts": 45, - "src/renderer/src/components/editor/monaco-view-state-persistence.test.ts": 55, - "src/renderer/src/components/editor/pdf-js-document-options.test.ts": 46, - "src/renderer/src/components/editor/pdf-scale-preference-storage.test.ts": 65, - "src/renderer/src/components/editor/pdf-scale-preference.test.ts": 71, - "src/renderer/src/components/editor/pdf-view-position.test.ts": 73, - "src/renderer/src/components/editor/pdf-viewer-failed-load-recovery.test.tsx": 2246, - "src/renderer/src/components/editor/pdf-viewer-zoom-ownership.test.tsx": 933, - "src/renderer/src/components/editor/pdf-zoom-request.test.ts": 137, - "src/renderer/src/components/editor/pending-editor-focus-request.test.ts": 34, - "src/renderer/src/components/editor/position-stable-node-view-update.test.ts": 46, - "src/renderer/src/components/editor/raw-markdown-comment-scan.test.ts": 82, - "src/renderer/src/components/editor/raw-markdown-html-line-start.test.ts": 62, - "src/renderer/src/components/editor/raw-markdown-html.blank-run.test.ts": 195, - "src/renderer/src/components/editor/raw-markdown-html.details-fences.test.ts": 64, - "src/renderer/src/components/editor/restored-editor-owner-save-lifecycle.test.ts": 2235, - "src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.test.tsx": 1466, - "src/renderer/src/components/editor/rich-markdown-async-image-paste.test.ts": 1484, - "src/renderer/src/components/editor/rich-markdown-async-image-table.test.ts": 1133, - "src/renderer/src/components/editor/rich-markdown-auto-focus.test.ts": 197, - "src/renderer/src/components/editor/rich-markdown-code-block-languages.test.ts": 92, - "src/renderer/src/components/editor/rich-markdown-commands.test.ts": 1846, - "src/renderer/src/components/editor/rich-markdown-comment-blocks.test.ts": 2049, - "src/renderer/src/components/editor/rich-markdown-context-command-routing.test.ts": 1887, - "src/renderer/src/components/editor/rich-markdown-cut.test.ts": 293, - "src/renderer/src/components/editor/rich-markdown-details-keyboard.test.ts": 3545, - "src/renderer/src/components/editor/rich-markdown-doc-link-keyboard.test.ts": 306, - "src/renderer/src/components/editor/rich-markdown-doc-link-resolution.test.ts": 248, - "src/renderer/src/components/editor/rich-markdown-doc-link.test.ts": 51, - "src/renderer/src/components/editor/rich-markdown-editor-click-routing.test.ts": 695, - "src/renderer/src/components/editor/rich-markdown-editor-config.test.ts": 1952, - "src/renderer/src/components/editor/rich-markdown-empty-paragraph-delete.test.ts": 208, - "src/renderer/src/components/editor/rich-markdown-escape-fidelity.test.ts": 1526, - "src/renderer/src/components/editor/rich-markdown-extension.test.ts": 260, - "src/renderer/src/components/editor/rich-markdown-html-superscript-link.test.ts": 3213, - "src/renderer/src/components/editor/rich-markdown-image-context.test.ts": 675, - "src/renderer/src/components/editor/rich-markdown-image-cut.test.ts": 2226, - "src/renderer/src/components/editor/rich-markdown-image-insert-code-block.test.ts": 1165, - "src/renderer/src/components/editor/rich-markdown-image-insert.test.ts": 770, - "src/renderer/src/components/editor/rich-markdown-image-insertion-target.test.ts": 2190, - "src/renderer/src/components/editor/rich-markdown-inline-source-tokenization.test.ts": 2521, - "src/renderer/src/components/editor/rich-markdown-key-handler.test.ts": 2196, - "src/renderer/src/components/editor/rich-markdown-large-html.test.ts": 3625, - "src/renderer/src/components/editor/rich-markdown-large-text-paste.fidelity.test.ts": 1997, - "src/renderer/src/components/editor/rich-markdown-large-text-paste.selection.test.ts": 2208, - "src/renderer/src/components/editor/rich-markdown-large-text-paste.test.ts": 1824, - "src/renderer/src/components/editor/rich-markdown-link-clipboard.test.ts": 140, - "src/renderer/src/components/editor/rich-markdown-link-shortcut.test.ts": 1507, - "src/renderer/src/components/editor/rich-markdown-list-continuation.test.ts": 150, - "src/renderer/src/components/editor/rich-markdown-list-tokenizers.test.ts": 87, - "src/renderer/src/components/editor/rich-markdown-literal-serialization.test.ts": 160, - "src/renderer/src/components/editor/rich-markdown-local-image.test.ts": 1491, - "src/renderer/src/components/editor/rich-markdown-lowlight-cache.test.ts": 149, - "src/renderer/src/components/editor/rich-markdown-lowlight.test.ts": 1932, - "src/renderer/src/components/editor/rich-markdown-normalize.test.ts": 138, - "src/renderer/src/components/editor/rich-markdown-paragraph.test.ts": 90, - "src/renderer/src/components/editor/rich-markdown-paste-handler.test.ts": 74, - "src/renderer/src/components/editor/rich-markdown-paste-image.test.ts": 494, - "src/renderer/src/components/editor/rich-markdown-range-bounds.test.ts": 94, - "src/renderer/src/components/editor/rich-markdown-review-annotations.test.ts": 109, - "src/renderer/src/components/editor/rich-markdown-review-note-layout.test.ts": 58, - "src/renderer/src/components/editor/rich-markdown-review-rail-benchmark.test.ts": 1687, - "src/renderer/src/components/editor/rich-markdown-review-rail-blocks.test.ts": 2390, - "src/renderer/src/components/editor/rich-markdown-review-text-ranges.test.ts": 74, - "src/renderer/src/components/editor/rich-markdown-save-benchmark.test.ts": 1604, - "src/renderer/src/components/editor/rich-markdown-search-focus.adversarial.test.ts": 316, - "src/renderer/src/components/editor/rich-markdown-search-focus.test.ts": 226, + "src/renderer/src/components/dashboard-popout/useDashboardSnapshot.test.tsx": 268, + "src/renderer/src/components/dashboard/AgentDashboardDrawer.test.tsx": 1576, + "src/renderer/src/components/dashboard/AgentDashboardSettingsMenu.test.tsx": 554, + "src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.test.tsx": 2019, + "src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx": 1603, + "src/renderer/src/components/dashboard/agent-dashboard-open-mode.test.ts": 39, + "src/renderer/src/components/dashboard/agent-finished-timestamp.test.ts": 43, + "src/renderer/src/components/dashboard/agent-row-lineage-model.test.ts": 73, + "src/renderer/src/components/dashboard/agent-row-pane-live-title.test.ts": 43, + "src/renderer/src/components/dashboard/build-dashboard-bucket-counts.allocation.test.ts": 388, + "src/renderer/src/components/dashboard/build-dashboard-bucket-counts.cache.test.ts": 179, + "src/renderer/src/components/dashboard/build-dashboard-bucket-counts.equivalence.test.ts": 261, + "src/renderer/src/components/dashboard/build-dashboard-snapshot-folder-workspace.test.ts": 1517, + "src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts": 1465, + "src/renderer/src/components/dashboard/build-dashboard-snapshot.rows-cache.test.ts": 1574, + "src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts": 1506, + "src/renderer/src/components/dashboard/dashboard-card-context.test.ts": 49, + "src/renderer/src/components/dashboard/dashboard-card-labels.test.ts": 76, + "src/renderer/src/components/dashboard/dashboard-card-terminal-input.test.ts": 2024, + "src/renderer/src/components/dashboard/dashboard-worktree-launch-options.test.ts": 49, + "src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts": 46, + "src/renderer/src/components/dashboard/reveal-dashboard-agent.test.ts": 41, + "src/renderer/src/components/dashboard/use-agent-row-conversation-name.test.ts": 69, + "src/renderer/src/components/dashboard/useAgentBucketCounts.gate.test.ts": 297, + "src/renderer/src/components/dashboard/useAgentBucketCounts.test.tsx": 225, + "src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx": 1392, + "src/renderer/src/components/dashboard/useLiveDashboardSnapshot.test.ts": 1789, + "src/renderer/src/components/dashboard/useRetainedAgents.test.ts": 1551, + "src/renderer/src/components/dashboard/useRetainedAgentsSync.test.ts": 1326, + "src/renderer/src/components/dictation/DictationIndicator.localization.test.ts": 294, + "src/renderer/src/components/dictation/DictationIndicator.test.tsx": 406, + "src/renderer/src/components/dictation/dictation-audio-meter.test.ts": 38, + "src/renderer/src/components/dictation/dictation-final-segments.test.ts": 32, + "src/renderer/src/components/dictation/dictation-insertion-target.test.ts": 297, + "src/renderer/src/components/dictation/dictation-meter-store.test.tsx": 249, + "src/renderer/src/components/dictation/dictation-stopped-sessions.test.ts": 39, + "src/renderer/src/components/dictation/microphone-devices.test.ts": 279, + "src/renderer/src/components/dictation/use-hold-dictation-gesture.test.tsx": 225, + "src/renderer/src/components/diff-comments/DiffCommentCard.resize.test.tsx": 537, + "src/renderer/src/components/diff-comments/DiffCommentDraftCard.test.tsx": 2007, + "src/renderer/src/components/diff-comments/diff-comment-draft-zone.test.tsx": 2250, + "src/renderer/src/components/diff-comments/diff-comment-drag-auto-scroll.test.ts": 41, + "src/renderer/src/components/diff-comments/diff-comment-line-range.test.ts": 47, + "src/renderer/src/components/diff-comments/diff-comment-popover-outside-click.test.tsx": 1433, + "src/renderer/src/components/diff-comments/diff-comment-popover-position.test.ts": 41, + "src/renderer/src/components/diff-comments/diff-comment-range-drag.test.ts": 5126, + "src/renderer/src/components/diff-comments/diff-comment-zone-mouse-events.test.ts": 40, + "src/renderer/src/components/diff-comments/use-visible-worktree-diff-comments.test.ts": 225, + "src/renderer/src/components/diff-comments/useDiffCommentDecorator.commentable-lines.test.tsx": 1316, + "src/renderer/src/components/diff-comments/useDiffCommentDecorator.model-lifecycle.test.tsx": 6697, + "src/renderer/src/components/diff-comments/useDiffCommentDecorator.range-selection.test.tsx": 3708, + "src/renderer/src/components/editor/ChangesModeView.test.tsx": 283, + "src/renderer/src/components/editor/CheckRunCopyButton.test.tsx": 338, + "src/renderer/src/components/editor/CheckRunDetailsPanel.copy.test.tsx": 2165, + "src/renderer/src/components/editor/ConflictComponents.test.tsx": 150, + "src/renderer/src/components/editor/DiagramViewerModal.test.tsx": 2151, + "src/renderer/src/components/editor/DiffSectionBody.word-wrap-lifecycle.test.tsx": 385, + "src/renderer/src/components/editor/DiffViewer.word-wrap-lifecycle.test.tsx": 345, + "src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx": 1788, + "src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx": 1679, + "src/renderer/src/components/editor/EditorContent.test.tsx": 2324, + "src/renderer/src/components/editor/EditorEditFileSurface.monaco-containment.test.tsx": 1661, + "src/renderer/src/components/editor/EditorFileLoadErrorView.test.tsx": 451, + "src/renderer/src/components/editor/EditorPanel.markdown-classification-memoization.test.tsx": 2435, + "src/renderer/src/components/editor/EditorPanel.markdown-review-tools-setting.test.tsx": 2232, + "src/renderer/src/components/editor/EditorPanelHeader.test.tsx": 1207, + "src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx": 2241, + "src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.test.tsx": 88, + "src/renderer/src/components/editor/EditorPanelShell.header.test.tsx": 178, + "src/renderer/src/components/editor/ExternalFileChangeBanner.test.tsx": 467, + "src/renderer/src/components/editor/ExternalFileChangeCompareDialog.test.tsx": 852, + "src/renderer/src/components/editor/ImageViewer.test.tsx": 141, + "src/renderer/src/components/editor/IpynbCellEditor.external-sync.test.tsx": 1548, + "src/renderer/src/components/editor/IpynbKernelToolbar.test.tsx": 713, + "src/renderer/src/components/editor/IpynbRendering.test.tsx": 2222, + "src/renderer/src/components/editor/LargeDiffLoadPrompt.test.tsx": 311, + "src/renderer/src/components/editor/MarkdownPreview.image-access.test.tsx": 1020, + "src/renderer/src/components/editor/MarkdownPreview.link-routing.interaction.test.tsx": 1040, + "src/renderer/src/components/editor/MarkdownPreview.test.ts": 1339, + "src/renderer/src/components/editor/MarkdownPreview.toc-visibility-gate.test.tsx": 936, + "src/renderer/src/components/editor/MarkdownPreviewBody.test.tsx": 247, + "src/renderer/src/components/editor/MarkdownTableOfContentsPanel.test.tsx": 1741, + "src/renderer/src/components/editor/MediaViewer.test.tsx": 195, + "src/renderer/src/components/editor/MermaidBlock.test.tsx": 673, + "src/renderer/src/components/editor/MonacoEditor.content-owner.test.tsx": 8350, + "src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx": 5368, + "src/renderer/src/components/editor/MonacoEditor.owner-view-state.test.tsx": 2904, + "src/renderer/src/components/editor/MonacoEditor.system-theme.test.tsx": 2265, + "src/renderer/src/components/editor/NotesSendMenu.test.tsx": 145, + "src/renderer/src/components/editor/PdfFind.test.tsx": 509, + "src/renderer/src/components/editor/ReviewNotesSendMenuContent.test.tsx": 182, + "src/renderer/src/components/editor/RichMarkdownErrorBoundary.lazy-chunk.test.tsx": 373, + "src/renderer/src/components/editor/RichMarkdownLinkBubble.render.test.tsx": 355, + "src/renderer/src/components/editor/RichMarkdownLinkBubble.test.ts": 96, + "src/renderer/src/components/editor/RichMarkdownSearchBar.ime-enter.test.tsx": 364, + "src/renderer/src/components/editor/RichMarkdownSlashMenu.test.tsx": 148, + "src/renderer/src/components/editor/RichMarkdownTableControls.test.tsx": 2038, + "src/renderer/src/components/editor/check-annotation-open.test.ts": 80, + "src/renderer/src/components/editor/check-annotation-path.test.ts": 46, + "src/renderer/src/components/editor/check-job-step-status.test.ts": 36, + "src/renderer/src/components/editor/check-run-clipboard-text.test.ts": 32, + "src/renderer/src/components/editor/check-run-details-fix-with-ai.test.ts": 601, + "src/renderer/src/components/editor/check-run-details-tab.test.ts": 35, + "src/renderer/src/components/editor/closed-editor-model-lifetime.test.ts": 8979, + "src/renderer/src/components/editor/closed-editor-model-shell.test.tsx": 4156, + "src/renderer/src/components/editor/closed-editor-tab-cache-sweep.test.ts": 43, + "src/renderer/src/components/editor/closed-editor-tab-disposal.test.ts": 120, + "src/renderer/src/components/editor/closed-editor-view-state-retention.test.ts": 3232, + "src/renderer/src/components/editor/combined-diff-on-demand-load.test.ts": 47, + "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-collapsed-work.test.tsx": 617, + "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-render.test.tsx": 867, + "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-row-drag.test.tsx": 256, + "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-windowing.test.tsx": 743, + "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree.test.ts": 94, + "src/renderer/src/components/editor/combined-diff/browse-files/use-combined-diff-file-tree-resize.test.tsx": 228, + "src/renderer/src/components/editor/combined-diff/browse-files/use-combined-diff-tree-navigation.test.ts": 297, + "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-initial-section-load.test.ts": 41, + "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-load-scheduler.test.ts": 43, + "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-section-connection.test.ts": 1457, + "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-section-load-state.test.ts": 40, + "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-stale-analysis-work-budget.test.tsx": 853, + "src/renderer/src/components/editor/combined-diff/remember-view/use-combined-diff-view-restore.test.tsx": 530, + "src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-entries.test.ts": 53, + "src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-section-cache-match.test.ts": 47, + "src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-section-scaling.test.tsx": 648, + "src/renderer/src/components/editor/combined-diff/resolve-changes/use-combined-diff-section-index-map.test.tsx": 240, + "src/renderer/src/components/editor/combined-diff/review-controls/combined-diff-commit-message.test.ts": 41, + "src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-render-range.test.ts": 36, + "src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-restore-signal-equivalence.test.tsx": 718, + "src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-scrollbar-drag.test.ts": 38, + "src/renderer/src/components/editor/conflict-review-file-tree-windowing.test.tsx": 1460, + "src/renderer/src/components/editor/csv/CsvGrid.test.tsx": 736, + "src/renderer/src/components/editor/csv/CsvViewer.delimiter.test.tsx": 2556, + "src/renderer/src/components/editor/csv/CsvViewer.editing.test.tsx": 3100, + "src/renderer/src/components/editor/csv/csv-byte-index.test.ts": 1486, + "src/renderer/src/components/editor/csv/csv-cell-links.test.tsx": 296, + "src/renderer/src/components/editor/csv/csv-column-width-preferences.test.ts": 191, + "src/renderer/src/components/editor/csv/csv-paged-preview.test.ts": 106, + "src/renderer/src/components/editor/csv/csv-parse-budget.test.ts": 82, + "src/renderer/src/components/editor/csv/csv-parse.test.ts": 123, + "src/renderer/src/components/editor/csv/csv-pending-save.test.ts": 1407, + "src/renderer/src/components/editor/csv/csv-preview-save-safety.test.ts": 1578, + "src/renderer/src/components/editor/csv/csv-table-interactions.test.ts": 142, + "src/renderer/src/components/editor/csv/csv-text-document.test.ts": 118, + "src/renderer/src/components/editor/csv/useCsvPagedPreview.test.tsx": 406, + "src/renderer/src/components/editor/details-markdown-html.test.ts": 66, + "src/renderer/src/components/editor/diagram-pan-zoom.test.ts": 45, + "src/renderer/src/components/editor/diff-editor-hide-unchanged-options.test.ts": 39, + "src/renderer/src/components/editor/diff-editor-line-number-options.test.ts": 33, + "src/renderer/src/components/editor/diff-editor-shift-wheel-scroll.test.ts": 192, + "src/renderer/src/components/editor/diff-editor-whitespace-options.test.ts": 31, + "src/renderer/src/components/editor/diff-editor-word-wrap-options.test.ts": 179, + "src/renderer/src/components/editor/diff-line-stats.test.ts": 34, + "src/renderer/src/components/editor/diff-model-swap-view-state.test.ts": 149, + "src/renderer/src/components/editor/diff-monaco-model-disposal.test.ts": 46, + "src/renderer/src/components/editor/diff-navigation-context.test.tsx": 1605, + "src/renderer/src/components/editor/diff-section-comment-submit.test.ts": 63, + "src/renderer/src/components/editor/diff-section-layout.test.ts": 77, + "src/renderer/src/components/editor/diff-section-preview.test.ts": 1232, + "src/renderer/src/components/editor/diff-viewer-large-diff-save-action.test.ts": 60, + "src/renderer/src/components/editor/drag-selection-guard-lifetime.test.ts": 615, + "src/renderer/src/components/editor/editor-artifact-upload.test.ts": 35, + "src/renderer/src/components/editor/editor-autosave-conflict-flow.test.ts": 626, + "src/renderer/src/components/editor/editor-autosave-controller.test.ts": 794, + "src/renderer/src/components/editor/editor-autosave.test.ts": 265, + "src/renderer/src/components/editor/editor-cmd-save-target.test.ts": 272, + "src/renderer/src/components/editor/editor-content-dirty-state.test.ts": 44, + "src/renderer/src/components/editor/editor-dropped-file-open.escaping-link.test.ts": 394, + "src/renderer/src/components/editor/editor-dropped-file-open.test.ts": 1367, + "src/renderer/src/components/editor/editor-external-watch-path-index.test.ts": 1270, + "src/renderer/src/components/editor/editor-file-save-attempt.test.ts": 39, + "src/renderer/src/components/editor/editor-header-file-rename.test.ts": 1642, + "src/renderer/src/components/editor/editor-header.test.ts": 52, + "src/renderer/src/components/editor/editor-labels.test.ts": 40, + "src/renderer/src/components/editor/editor-model-host-isolation.test.ts": 2119, + "src/renderer/src/components/editor/editor-model-owner.test.ts": 322, + "src/renderer/src/components/editor/editor-model-uri.test.ts": 62, + "src/renderer/src/components/editor/editor-panel-diff-reload.test.ts": 44, + "src/renderer/src/components/editor/editor-panel-draft-selector.test.ts": 51, + "src/renderer/src/components/editor/editor-panel-git-entry-selector.test.ts": 39, + "src/renderer/src/components/editor/editor-panel-render-model.test.ts": 1484, + "src/renderer/src/components/editor/editor-path-display.test.ts": 40, + "src/renderer/src/components/editor/editor-path-move-inflight.test.ts": 33, + "src/renderer/src/components/editor/editor-pending-flush.test.ts": 48, + "src/renderer/src/components/editor/editor-restored-tab-conflict-scan.test.ts": 525, + "src/renderer/src/components/editor/editor-save-user-named-tab.test.ts": 603, + "src/renderer/src/components/editor/editor-self-write-registry.test.ts": 62, + "src/renderer/src/components/editor/editor-shortcuts.test.ts": 227, + "src/renderer/src/components/editor/editor-untitled-dont-save-dialogs.test.tsx": 733, + "src/renderer/src/components/editor/editor-untitled-save-lifecycle.test.ts": 946, + "src/renderer/src/components/editor/export-active-markdown.test.ts": 164, + "src/renderer/src/components/editor/file-editor-word-wrap-options.test.ts": 38, + "src/renderer/src/components/editor/image-viewer-zoom.test.ts": 49, + "src/renderer/src/components/editor/ipynb-ansi.test.ts": 56, + "src/renderer/src/components/editor/ipynb-code-cell-lines.test.ts": 70, + "src/renderer/src/components/editor/ipynb-kernel-outputs.test.ts": 302, + "src/renderer/src/components/editor/ipynb-kernel-session-start-lifetime.test.ts": 99, + "src/renderer/src/components/editor/ipynb-kernel-session.test.ts": 197, + "src/renderer/src/components/editor/ipynb-kernel-setup-commands.test.ts": 36, + "src/renderer/src/components/editor/ipynb-parse.test.ts": 67, + "src/renderer/src/components/editor/large-diff-render-limit.test.ts": 106, + "src/renderer/src/components/editor/large-markdown-render-freeze.test.tsx": 2035, + "src/renderer/src/components/editor/line-copy-path.test.ts": 32, + "src/renderer/src/components/editor/local-log-tail-decoder.test.ts": 85, + "src/renderer/src/components/editor/markdown-dirty-state.test.ts": 217, + "src/renderer/src/components/editor/markdown-doc-completions.test.ts": 39, + "src/renderer/src/components/editor/markdown-doc-links.test.ts": 149, + "src/renderer/src/components/editor/markdown-document-list-request.test.ts": 361, + "src/renderer/src/components/editor/markdown-document-worktree-path-selector.test.ts": 1743, + "src/renderer/src/components/editor/markdown-export-extract.test.ts": 254, + "src/renderer/src/components/editor/markdown-export-html.test.ts": 76, + "src/renderer/src/components/editor/markdown-frontmatter.test.ts": 40, + "src/renderer/src/components/editor/markdown-heading-slug.test.ts": 32, + "src/renderer/src/components/editor/markdown-internal-links.test.ts": 44, + "src/renderer/src/components/editor/markdown-preview-anchor-navigation.test.ts": 145, + "src/renderer/src/components/editor/markdown-preview-annotation-shortcut.test.ts": 230, + "src/renderer/src/components/editor/markdown-preview-controls.test.ts": 73, + "src/renderer/src/components/editor/markdown-preview-document-client.test.ts": 57, + "src/renderer/src/components/editor/markdown-preview-document-search-budget.test.ts": 391, + "src/renderer/src/components/editor/markdown-preview-document-search-cache.test.ts": 593, + "src/renderer/src/components/editor/markdown-preview-document-search-scope.test.ts": 462, + "src/renderer/src/components/editor/markdown-preview-document.test.ts": 1549, + "src/renderer/src/components/editor/markdown-preview-links.test.ts": 53, + "src/renderer/src/components/editor/markdown-preview-local-images.test.ts": 1443, + "src/renderer/src/components/editor/markdown-preview-review-frame-lifecycle.test.tsx": 384, + "src/renderer/src/components/editor/markdown-preview-row-measurements.test.ts": 184, + "src/renderer/src/components/editor/markdown-preview-search-crash.test.tsx": 224, + "src/renderer/src/components/editor/markdown-preview-search-memory.test.ts": 163, + "src/renderer/src/components/editor/markdown-preview-search-offsets.test.ts": 609, + "src/renderer/src/components/editor/markdown-preview-search.test.ts": 83, + "src/renderer/src/components/editor/markdown-preview-table-chunks.test.ts": 6179, + "src/renderer/src/components/editor/markdown-preview-url-transform.test.ts": 78, + "src/renderer/src/components/editor/markdown-reference-link-normalization.test.ts": 64, + "src/renderer/src/components/editor/markdown-render-mode.test.ts": 49, + "src/renderer/src/components/editor/markdown-rich-comment-scan.test.ts": 351, + "src/renderer/src/components/editor/markdown-rich-mode-eligibility-cache.test.ts": 3887, + "src/renderer/src/components/editor/markdown-rich-mode.test.ts": 2727, + "src/renderer/src/components/editor/markdown-rich-size-limit.test.ts": 244, + "src/renderer/src/components/editor/markdown-round-trip.test.ts": 1977, + "src/renderer/src/components/editor/markdown-scanner-boundaries.test.ts": 69, + "src/renderer/src/components/editor/markdown-table-of-contents.test.ts": 121, + "src/renderer/src/components/editor/markdown-toc-collapse-state.test.ts": 39, + "src/renderer/src/components/editor/markdown-toc-visibility-gate.test.ts": 98, + "src/renderer/src/components/editor/markdown-tokenizer-start.test.ts": 69, + "src/renderer/src/components/editor/mermaid-config.test.ts": 512, + "src/renderer/src/components/editor/monaco-auto-height.test.ts": 95, + "src/renderer/src/components/editor/monaco-codebase-search.test.ts": 37, + "src/renderer/src/components/editor/monaco-conflict-decorations.test.ts": 50, + "src/renderer/src/components/editor/monaco-content-sync.test.ts": 37, + "src/renderer/src/components/editor/monaco-content-sync.undo-history.test.ts": 1119, + "src/renderer/src/components/editor/monaco-context-menu-paste.test.ts": 213, + "src/renderer/src/components/editor/monaco-cursor-line-lifetime.test.ts": 862, + "src/renderer/src/components/editor/monaco-find-options.test.ts": 33, + "src/renderer/src/components/editor/monaco-find-widget.test.ts": 183, + "src/renderer/src/components/editor/monaco-large-text-paste.test.ts": 178, + "src/renderer/src/components/editor/monaco-markdown-doc-completions-retention.test.ts": 60, + "src/renderer/src/components/editor/monaco-markdown-doc-link-decorations.offset-scan.test.ts": 194, + "src/renderer/src/components/editor/monaco-markdown-doc-link-decorations.test.ts": 87, + "src/renderer/src/components/editor/monaco-markdown-selection-annotation.test.ts": 42, + "src/renderer/src/components/editor/monaco-programmatic-sync.test.ts": 34, + "src/renderer/src/components/editor/monaco-reveal-range.test.ts": 31, + "src/renderer/src/components/editor/monaco-view-state-persistence.test.ts": 43, + "src/renderer/src/components/editor/pdf-js-document-options.test.ts": 35, + "src/renderer/src/components/editor/pdf-scale-preference-storage.test.ts": 43, + "src/renderer/src/components/editor/pdf-scale-preference.test.ts": 44, + "src/renderer/src/components/editor/pdf-view-position.test.ts": 54, + "src/renderer/src/components/editor/pdf-viewer-failed-load-recovery.test.tsx": 2210, + "src/renderer/src/components/editor/pdf-viewer-zoom-ownership.test.tsx": 726, + "src/renderer/src/components/editor/pdf-zoom-request.test.ts": 192, + "src/renderer/src/components/editor/pending-editor-focus-request.test.ts": 27, + "src/renderer/src/components/editor/position-stable-node-view-update.test.ts": 33, + "src/renderer/src/components/editor/raw-markdown-comment-scan.test.ts": 68, + "src/renderer/src/components/editor/raw-markdown-html-line-start.test.ts": 37, + "src/renderer/src/components/editor/raw-markdown-html.blank-run.test.ts": 172, + "src/renderer/src/components/editor/raw-markdown-html.details-fences.test.ts": 75, + "src/renderer/src/components/editor/restored-editor-owner-save-lifecycle.test.ts": 2045, + "src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.test.tsx": 1559, + "src/renderer/src/components/editor/rich-markdown-async-image-paste.test.ts": 1363, + "src/renderer/src/components/editor/rich-markdown-async-image-table.test.ts": 999, + "src/renderer/src/components/editor/rich-markdown-auto-focus.test.ts": 180, + "src/renderer/src/components/editor/rich-markdown-code-block-languages.test.ts": 89, + "src/renderer/src/components/editor/rich-markdown-commands.test.ts": 1799, + "src/renderer/src/components/editor/rich-markdown-comment-blocks.test.ts": 1955, + "src/renderer/src/components/editor/rich-markdown-context-command-routing.test.ts": 1769, + "src/renderer/src/components/editor/rich-markdown-cut.test.ts": 308, + "src/renderer/src/components/editor/rich-markdown-details-keyboard.test.ts": 3294, + "src/renderer/src/components/editor/rich-markdown-doc-link-keyboard.test.ts": 381, + "src/renderer/src/components/editor/rich-markdown-doc-link-resolution.test.ts": 274, + "src/renderer/src/components/editor/rich-markdown-doc-link.test.ts": 34, + "src/renderer/src/components/editor/rich-markdown-editor-click-routing.test.ts": 841, + "src/renderer/src/components/editor/rich-markdown-editor-config.test.ts": 2004, + "src/renderer/src/components/editor/rich-markdown-empty-paragraph-delete.test.ts": 177, + "src/renderer/src/components/editor/rich-markdown-escape-fidelity.test.ts": 2084, + "src/renderer/src/components/editor/rich-markdown-extension.test.ts": 206, + "src/renderer/src/components/editor/rich-markdown-html-superscript-link.test.ts": 2646, + "src/renderer/src/components/editor/rich-markdown-image-context.test.ts": 481, + "src/renderer/src/components/editor/rich-markdown-image-cut.test.ts": 1999, + "src/renderer/src/components/editor/rich-markdown-image-insert-code-block.test.ts": 990, + "src/renderer/src/components/editor/rich-markdown-image-insert.test.ts": 930, + "src/renderer/src/components/editor/rich-markdown-image-insertion-target.test.ts": 1953, + "src/renderer/src/components/editor/rich-markdown-inline-source-tokenization.test.ts": 2383, + "src/renderer/src/components/editor/rich-markdown-key-handler.test.ts": 2146, + "src/renderer/src/components/editor/rich-markdown-large-html.test.ts": 4403, + "src/renderer/src/components/editor/rich-markdown-large-text-paste.fidelity.test.ts": 1802, + "src/renderer/src/components/editor/rich-markdown-large-text-paste.selection.test.ts": 1894, + "src/renderer/src/components/editor/rich-markdown-large-text-paste.test.ts": 1462, + "src/renderer/src/components/editor/rich-markdown-link-clipboard.test.ts": 159, + "src/renderer/src/components/editor/rich-markdown-link-shortcut.test.ts": 1814, + "src/renderer/src/components/editor/rich-markdown-list-continuation.test.ts": 121, + "src/renderer/src/components/editor/rich-markdown-list-tokenizers.test.ts": 64, + "src/renderer/src/components/editor/rich-markdown-literal-serialization.test.ts": 167, + "src/renderer/src/components/editor/rich-markdown-local-image.test.ts": 1802, + "src/renderer/src/components/editor/rich-markdown-lowlight-cache.test.ts": 171, + "src/renderer/src/components/editor/rich-markdown-lowlight.test.ts": 2336, + "src/renderer/src/components/editor/rich-markdown-normalize.test.ts": 198, + "src/renderer/src/components/editor/rich-markdown-paragraph.test.ts": 61, + "src/renderer/src/components/editor/rich-markdown-paste-handler.test.ts": 51, + "src/renderer/src/components/editor/rich-markdown-paste-image.test.ts": 559, + "src/renderer/src/components/editor/rich-markdown-range-bounds.test.ts": 58, + "src/renderer/src/components/editor/rich-markdown-review-annotations.test.ts": 108, + "src/renderer/src/components/editor/rich-markdown-review-note-layout.test.ts": 36, + "src/renderer/src/components/editor/rich-markdown-review-rail-benchmark.test.ts": 1794, + "src/renderer/src/components/editor/rich-markdown-review-rail-blocks.test.ts": 2001, + "src/renderer/src/components/editor/rich-markdown-review-text-ranges.test.ts": 62, + "src/renderer/src/components/editor/rich-markdown-save-benchmark.test.ts": 1466, + "src/renderer/src/components/editor/rich-markdown-search-focus.adversarial.test.ts": 279, + "src/renderer/src/components/editor/rich-markdown-search-focus.test.ts": 239, "src/renderer/src/components/editor/rich-markdown-search-matches-cache.test.ts": 116, - "src/renderer/src/components/editor/rich-markdown-search.test.ts": 103, - "src/renderer/src/components/editor/rich-markdown-serialization-commit.test.ts": 1408, - "src/renderer/src/components/editor/rich-markdown-signed-images.test.ts": 1568, - "src/renderer/src/components/editor/rich-markdown-slash-command-filter.test.ts": 47, - "src/renderer/src/components/editor/rich-markdown-source-eol-allocation.test.ts": 285, - "src/renderer/src/components/editor/rich-markdown-source-reconcile.test.ts": 1937, - "src/renderer/src/components/editor/rich-markdown-tab-key-handler.test.ts": 2528, - "src/renderer/src/components/editor/rich-markdown-table-actions.test.ts": 2439, - "src/renderer/src/components/editor/rich-markdown-table-control-layout.test.ts": 51, - "src/renderer/src/components/editor/rich-markdown-table-keyboard.test.ts": 1783, - "src/renderer/src/components/editor/rich-markdown-table.test.ts": 84, - "src/renderer/src/components/editor/rich-markdown-terminal-path-paste.test.ts": 150, - "src/renderer/src/components/editor/rich-markdown-toc-heading-target.test.ts": 168, - "src/renderer/src/components/editor/rich-markdown-trailing-paragraph.test.ts": 2081, - "src/renderer/src/components/editor/selection-copy.test.ts": 63, - "src/renderer/src/components/editor/setup-contextual-copy.test.ts": 93, - "src/renderer/src/components/editor/untitled-file-rename-path.test.ts": 48, - "src/renderer/src/components/editor/use-editor-content-change-handler.test.tsx": 1784, - "src/renderer/src/components/editor/use-editor-group-file-drop-owner.remote.test.tsx": 2275, - "src/renderer/src/components/editor/use-editor-group-file-drop-owner.test.tsx": 1048, - "src/renderer/src/components/editor/use-markdown-preview-document-search.test.tsx": 228, - "src/renderer/src/components/editor/use-markdown-preview-document.test.tsx": 290, - "src/renderer/src/components/editor/use-markdown-preview-navigation.test.tsx": 512, - "src/renderer/src/components/editor/use-markdown-preview-search-reveal.test.tsx": 372, - "src/renderer/src/components/editor/use-monaco-editor-decorations.doc-link-refresh.test.tsx": 206, - "src/renderer/src/components/editor/use-monaco-editor-mount.cursor-owner.test.tsx": 1084, - "src/renderer/src/components/editor/use-pdf-viewer-find-shortcut.test.tsx": 740, - "src/renderer/src/components/editor/use-quick-open-history-visit.test.tsx": 225, - "src/renderer/src/components/editor/use-rich-markdown-table-context-menu.test.tsx": 2188, - "src/renderer/src/components/editor/use-rich-markdown-table-control-target.test.tsx": 2157, - "src/renderer/src/components/editor/useDiffViewerLargeDiffLifecycle.test.tsx": 273, - "src/renderer/src/components/editor/useEditorCmdSaveRequest.test.tsx": 455, - "src/renderer/src/components/editor/useEditorPanelContentState.test.tsx": 1443, - "src/renderer/src/components/editor/useEditorPanelExternalContentEvents.test.tsx": 461, - "src/renderer/src/components/editor/useEditorPanelFileLoadRetry.host-connection.test.tsx": 1687, - "src/renderer/src/components/editor/useEditorPanelFileLoadRetry.test.tsx": 1705, - "src/renderer/src/components/editor/useEditorPanelFloatingTabContentState.test.tsx": 615, - "src/renderer/src/components/editor/useEditorPanelHostUnresolvedContentState.test.tsx": 784, - "src/renderer/src/components/editor/useEditorPanelRemoteSiblingContentState.test.tsx": 579, - "src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx": 660, - "src/renderer/src/components/editor/useIpynbCellExecution.test.tsx": 431, - "src/renderer/src/components/editor/useIpynbDocumentEditing.test.tsx": 349, - "src/renderer/src/components/editor/useLinkBubble.test.tsx": 356, - "src/renderer/src/components/editor/useLocalImagePick.test.tsx": 1197, - "src/renderer/src/components/editor/useLocalImageSrc.test.ts": 587, - "src/renderer/src/components/editor/useLocalLogTail.test.tsx": 511, - "src/renderer/src/components/editor/useMarkdownDocuments.navigation.test.ts": 573, - "src/renderer/src/components/editor/useMarkdownDocuments.ownership.test.tsx": 1885, - "src/renderer/src/components/editor/useMarkdownDocuments.test.ts": 1520, - "src/renderer/src/components/editor/useMarkdownDocuments.watch.test.ts": 438, - "src/renderer/src/components/editor/useRichMarkdownEditorInstance.integration.test.tsx": 340, - "src/renderer/src/components/editor/useRichMarkdownEditorInstance.test.tsx": 265, - "src/renderer/src/components/editor/useRichMarkdownPendingFocus.test.ts": 249, - "src/renderer/src/components/editor/useRichMarkdownProgrammaticSync.test.ts": 1933, - "src/renderer/src/components/editor/useRichMarkdownReviewController.open-guard.test.ts": 418, - "src/renderer/src/components/editor/useRichMarkdownReviewData.test.ts": 302, - "src/renderer/src/components/editor/useRichMarkdownSearch.adversarial.test.tsx": 617, - "src/renderer/src/components/editor/useRichMarkdownSearch.editing.test.tsx": 577, - "src/renderer/src/components/editor/useRichMarkdownSearch.reuse.test.tsx": 355, - "src/renderer/src/components/editor/useRichMarkdownSearch.test.tsx": 1817, - "src/renderer/src/components/emulator-pane/MobileEmulatorTabIntroCallout.test.tsx": 386, - "src/renderer/src/components/emulator-pane/emulator-device-frame-layout.test.ts": 54, - "src/renderer/src/components/emulator-pane/emulator-device-frame-visibility.test.tsx": 843, - "src/renderer/src/components/emulator-pane/emulator-device-frame.input.test.tsx": 577, - "src/renderer/src/components/emulator-pane/emulator-device-state.test.ts": 55, - "src/renderer/src/components/emulator-pane/emulator-keyboard-paste.test.ts": 198, - "src/renderer/src/components/emulator-pane/emulator-screen-gesture.test.ts": 83, - "src/renderer/src/components/emulator-pane/emulator-screen-stream-content.test.tsx": 371, - "src/renderer/src/components/emulator-pane/mobile-emulator-agent-setup-visibility.test.ts": 46, - "src/renderer/src/components/emulator-pane/mobile-emulator-hidden-toast.test.tsx": 214, - "src/renderer/src/components/emulator-pane/mobile-emulator-tab-intro-visibility.test.ts": 45, - "src/renderer/src/components/emulator-pane/use-emulator-pane-session.test.tsx": 1737, - "src/renderer/src/components/emulator-pane/use-mobile-emulator-tab-intro-actions.test.tsx": 1585, - "src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.lazy-chunk.test.tsx": 289, - "src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.never-landed-reload.test.tsx": 296, - "src/renderer/src/components/error-boundaries/react-185-bystander-attribution.test.tsx": 391, - "src/renderer/src/components/feature-tips/CliSkillSetupTerminal.test.tsx": 1870, - "src/renderer/src/components/feature-tips/CmdJPaletteFeatureTipVisual.test.tsx": 325, - "src/renderer/src/components/feature-tips/CmdJPaletteTipDialog.test.tsx": 169, - "src/renderer/src/components/feature-tips/NativeChatUpgradeTipDialog.test.tsx": 1996, - "src/renderer/src/components/feature-tips/SessionSearchTipDialog.test.tsx": 374, - "src/renderer/src/components/feature-tips/VoiceDictationFeatureTipVisual.test.tsx": 338, - "src/renderer/src/components/feature-tips/VoiceDictationTipDialog.test.tsx": 269, - "src/renderer/src/components/feature-tips/feature-tip-cli-install-action.test.ts": 56, - "src/renderer/src/components/feature-tips/feature-tip-modal-state.test.ts": 269, - "src/renderer/src/components/feature-tips/feature-tip-startup-gate.test.ts": 299, - "src/renderer/src/components/feature-tips/feature-tip-telemetry.test.ts": 66, - "src/renderer/src/components/feature-tips/use-session-search-tip-setup.test.tsx": 441, - "src/renderer/src/components/feature-wall/AgentCapabilitiesSetupAction.test.ts": 1436, - "src/renderer/src/components/feature-wall/ConnectIntegrationsList.test.tsx": 749, - "src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.test.tsx": 1754, - "src/renderer/src/components/feature-wall/FullDiskAccessSetupPrompt.test.ts": 330, - "src/renderer/src/components/feature-wall/KeepAwakeCard.test.tsx": 641, - "src/renderer/src/components/feature-wall/agents-orchestration/UsageAccountsCard.test.tsx": 383, - "src/renderer/src/components/feature-wall/browser-animated-visual-sequence.test.ts": 62, - "src/renderer/src/components/feature-wall/feature-wall-animation-visibility.test.tsx": 638, - "src/renderer/src/components/feature-wall/feature-wall-rail-navigation.test.ts": 57, - "src/renderer/src/components/feature-wall/feature-wall-setup-checklist-localized-copy.test.ts": 552, - "src/renderer/src/components/feature-wall/feature-wall-setup-progress.test.ts": 61, - "src/renderer/src/components/feature-wall/feature-wall-shortcut-labels.test.tsx": 1718, - "src/renderer/src/components/feature-wall/feature-wall-usage-tracking.test.ts": 91, - "src/renderer/src/components/feature-wall/use-feature-wall-completion.test.ts": 85, - "src/renderer/src/components/feature-wall/use-feature-wall-tour-telemetry.test.ts": 55, - "src/renderer/src/components/feature-wall/use-integration-connection-status.test.ts": 2054, - "src/renderer/src/components/feature-wall/workbench-terminal-storyboard-sequence.test.ts": 54, - "src/renderer/src/components/file-path-cursor-tooltip.test.ts": 177, - "src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.freshness.test.tsx": 491, - "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.bounds.test.tsx": 4024, - "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.focus.test.tsx": 7004, - "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.shortcuts.test.tsx": 2710, - "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tab-lifecycle.test.tsx": 4440, - "src/renderer/src/components/floating-terminal/FloatingTerminalToggleButton.test.tsx": 126, - "src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.test.tsx": 433, - "src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.test.ts": 57, - "src/renderer/src/components/floating-terminal/floating-terminal-panel-inputs.test.ts": 313, - "src/renderer/src/components/floating-terminal/floating-terminal-panel-view-state.test.ts": 172, - "src/renderer/src/components/floating-terminal/floating-terminal-trigger-position.test.ts": 68, - "src/renderer/src/components/floating-terminal/floating-workspace-surface-model.test.ts": 228, - "src/renderer/src/components/floating-terminal/floating-workspace-tab-reorder.test.ts": 1744, - "src/renderer/src/components/floating-terminal/use-floating-terminal-close-actions.test.tsx": 2068, - "src/renderer/src/components/github-body-draft-state.test.ts": 54, - "src/renderer/src/components/github-checks-tab-state.test.ts": 61, - "src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section.assignee-identity.test.tsx": 414, - "src/renderer/src/components/github-item-dialog/inspect-pull-request/pr-files-combined-diff-section-index.test.tsx": 330, - "src/renderer/src/components/github-item-dialog/load-item-details/work-item-details-issue-target.test.ts": 69, - "src/renderer/src/components/github-link-copy-state.test.ts": 45, - "src/renderer/src/components/github-pr-merge-state.test.ts": 99, - "src/renderer/src/components/github-pr-reviewer-display.test.ts": 61, - "src/renderer/src/components/github-project/GhAuthErrorHelp.test.ts": 180, - "src/renderer/src/components/github-project/ProjectBoard.test.tsx": 554, - "src/renderer/src/components/github-project/ProjectRoadmap.test.tsx": 2087, - "src/renderer/src/components/github-project/ProjectViewStates.test.tsx": 459, - "src/renderer/src/components/github-project/github-project-picker-filter.test.ts": 67, - "src/renderer/src/components/github-project/group-sort.test.ts": 89, - "src/renderer/src/components/github-project/project-dialog-state.test.ts": 46, - "src/renderer/src/components/github-project/project-picker-browse-cache.test.ts": 52, - "src/renderer/src/components/github-project/project-picker-input.test.ts": 1461, - "src/renderer/src/components/github-project/project-row-filtering.test.ts": 68, - "src/renderer/src/components/github-project/project-view-wrapper-source-context-boundary.test.ts": 64, - "src/renderer/src/components/github-project/project-visible-table-cache.test.ts": 57, - "src/renderer/src/components/github/PRFilterPickers.test.ts": 173, - "src/renderer/src/components/github/github-markdown-image-url.test.ts": 53, - "src/renderer/src/components/github/github-mention-option-filter.test.ts": 75, - "src/renderer/src/components/github/github-pr-reviewer-candidate-filter.test.ts": 66, - "src/renderer/src/components/github/github-user-avatar.test.tsx": 238, - "src/renderer/src/components/github/github-work-item-assignee-filter.test.ts": 70, - "src/renderer/src/components/github/github-work-item-issue-target.test.ts": 163, - "src/renderer/src/components/github/github-work-item-label-filter.test.ts": 75, - "src/renderer/src/components/github/github-work-item-mutation-target.test.ts": 144, - "src/renderer/src/components/github/pr-comment-code-context.test.ts": 79, - "src/renderer/src/components/github/pr-file-content-size.test.ts": 57, - "src/renderer/src/components/github/use-image-input.test.ts": 2248, - "src/renderer/src/components/jira-create-adf.test.ts": 59, - "src/renderer/src/components/jira-issue-workspace-request-lifetime.test.tsx": 444, - "src/renderer/src/components/jira-project-picker-filter.test.ts": 59, - "src/renderer/src/components/jira-self-user.test.ts": 58, - "src/renderer/src/components/landing-preflight-dismissal.test.ts": 66, - "src/renderer/src/components/landing-preflight-issues.test.ts": 107, - "src/renderer/src/components/landing-preflight-runtime-boundary.test.ts": 1928, - "src/renderer/src/components/landing-preflight-unverifiable-host.test.tsx": 1541, - "src/renderer/src/components/linear-api-key-dialog-state.test.ts": 49, - "src/renderer/src/components/linear-issue-attribute-filter-coverage-agreement.test.ts": 633, - "src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx": 1593, - "src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts": 63, - "src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts": 83, - "src/renderer/src/components/linear-issue-project-selector.test.tsx": 453, - "src/renderer/src/components/linear-issue-text-draft-state.test.ts": 54, - "src/renderer/src/components/linear-issue-text-save-plan.test.ts": 52, - "src/renderer/src/components/linear-issue-view-storage.test.ts": 174, - "src/renderer/src/components/linear-issue-workspace-detail-state.test.tsx": 222, - "src/renderer/src/components/linear-issue-workspace-text.test.ts": 109, - "src/renderer/src/components/linear-project-presentation.test.ts": 65, - "src/renderer/src/components/linear-project-search-query.test.ts": 49, - "src/renderer/src/components/linear-project-view-surfaces.test.tsx": 504, - "src/renderer/src/components/linear-scope-selector.test.ts": 211, - "src/renderer/src/components/link-actions/LinkActionPopover.test.tsx": 545, - "src/renderer/src/components/maintenance/update-card/update-card-error-model.test.ts": 121, - "src/renderer/src/components/mobile/MobileHero.test.tsx": 2284, - "src/renderer/src/components/mobile/MobilePage.test.tsx": 1610, - "src/renderer/src/components/mobile/MobilePageToolbar.test.tsx": 313, - "src/renderer/src/components/mobile/NetworkInterfacePicker.test.tsx": 516, - "src/renderer/src/components/mobile/WindowsFirewallNotice.test.tsx": 461, - "src/renderer/src/components/mobile/mobile-page-stage.test.ts": 39, - "src/renderer/src/components/mobile/paired-mobile-devices.test.ts": 155, - "src/renderer/src/components/mobile/use-mobile-page-paired-devices.test.ts": 298, - "src/renderer/src/components/mobile/use-mobile-pairing-address-preference.test.tsx": 309, - "src/renderer/src/components/native-chat-launch-resume-decision.test.tsx": 1492, - "src/renderer/src/components/native-chat-restart-action-notifications.test.ts": 129, - "src/renderer/src/components/native-chat-resume-activity-label.test.ts": 107, - "src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx": 2316, - "src/renderer/src/components/native-chat-resume-failed-chat-watch.test.ts": 1826, - "src/renderer/src/components/native-chat-resume-failure-guidance.test.ts": 102, - "src/renderer/src/components/native-chat-resume-finished-run.test.tsx": 2925, - "src/renderer/src/components/native-chat-resume-on-restart-grouping.test.ts": 106, - "src/renderer/src/components/native-chat-resume-on-restart-outcome-toast.test.ts": 1502, - "src/renderer/src/components/native-chat-resume-run.test.ts": 99, - "src/renderer/src/components/native-chat-resume-skipped-progress.test.tsx": 2367, - "src/renderer/src/components/native-chat/NativeChatAgentMessageRow.test.tsx": 1911, - "src/renderer/src/components/native-chat/NativeChatAgentMessageSenders.test.tsx": 1766, - "src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx": 2314, - "src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.test.tsx": 428, - "src/renderer/src/components/native-chat/NativeChatBackgroundTaskRun.test.tsx": 456, - "src/renderer/src/components/native-chat/NativeChatBackgroundTasksStatus.test.tsx": 682, - "src/renderer/src/components/native-chat/NativeChatCodeBlock.load-failure.test.tsx": 482, - "src/renderer/src/components/native-chat/NativeChatCodeBlock.test.tsx": 4855, - "src/renderer/src/components/native-chat/NativeChatComposer.paste-remount.test.tsx": 2164, - "src/renderer/src/components/native-chat/NativeChatComposer.test.tsx": 1040, - "src/renderer/src/components/native-chat/NativeChatComposerActions.test.tsx": 494, - "src/renderer/src/components/native-chat/NativeChatComposerNotices.test.tsx": 450, - "src/renderer/src/components/native-chat/NativeChatContextUsageRing.test.tsx": 681, - "src/renderer/src/components/native-chat/NativeChatEmptyState.test.tsx": 399, - "src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.test.tsx": 1768, - "src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx": 1710, - "src/renderer/src/components/native-chat/NativeChatInterruptedContinue.test.tsx": 673, - "src/renderer/src/components/native-chat/NativeChatJumpToLatest.test.tsx": 363, - "src/renderer/src/components/native-chat/NativeChatMarkdown.mermaid.test.tsx": 1267, - "src/renderer/src/components/native-chat/NativeChatMarkdown.test.tsx": 1968, - "src/renderer/src/components/native-chat/NativeChatMarkdown.visual.test.tsx": 2370, - "src/renderer/src/components/native-chat/NativeChatMessageList.growth-windowing.test.tsx": 5931, - "src/renderer/src/components/native-chat/NativeChatMessageList.live-reasoning.test.tsx": 4308, - "src/renderer/src/components/native-chat/NativeChatMessageList.older-history.test.tsx": 4124, - "src/renderer/src/components/native-chat/NativeChatMessageList.opening-send.test.tsx": 3124, - "src/renderer/src/components/native-chat/NativeChatMessageList.provider-frame.test.tsx": 1738, - "src/renderer/src/components/native-chat/NativeChatMessageList.queued-turn-order.test.tsx": 3019, - "src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx": 16129, - "src/renderer/src/components/native-chat/NativeChatMessageList.reader-navigation.test.tsx": 5316, - "src/renderer/src/components/native-chat/NativeChatMessageList.rewind-rows.test.tsx": 2450, - "src/renderer/src/components/native-chat/NativeChatMessageList.stream-render.perf.test.tsx": 3416, - "src/renderer/src/components/native-chat/NativeChatMessageList.subagent-sections.test.tsx": 4465, - "src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx": 3209, - "src/renderer/src/components/native-chat/NativeChatMessageList.tool-stream-cost.test.tsx": 6211, - "src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx": 2860, - "src/renderer/src/components/native-chat/NativeChatMessageList.turn-membership.test.tsx": 3458, - "src/renderer/src/components/native-chat/NativeChatMessageList.turn-status.test.tsx": 3074, - "src/renderer/src/components/native-chat/NativeChatMessageList.unsent-message.test.tsx": 2773, - "src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx": 7809, - "src/renderer/src/components/native-chat/NativeChatMessageList.work-run-memo.test.tsx": 2520, - "src/renderer/src/components/native-chat/NativeChatMessageRail.test.tsx": 975, - "src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx": 2763, - "src/renderer/src/components/native-chat/NativeChatMessageRow.work-run.test.tsx": 2752, - "src/renderer/src/components/native-chat/NativeChatMessageTimestamp.test.tsx": 530, - "src/renderer/src/components/native-chat/NativeChatModelCombobox.test.tsx": 922, - "src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx": 2999, - "src/renderer/src/components/native-chat/NativeChatPacedMarkdown.file-links.test.tsx": 2066, - "src/renderer/src/components/native-chat/NativeChatPacedMarkdown.mermaid.test.tsx": 2028, - "src/renderer/src/components/native-chat/NativeChatPromptEditor.test.tsx": 670, - "src/renderer/src/components/native-chat/NativeChatQuestionCard.test.tsx": 565, - "src/renderer/src/components/native-chat/NativeChatQueuedMessageEditor.test.tsx": 2144, - "src/renderer/src/components/native-chat/NativeChatQueuedMessageList.inline-edit.test.tsx": 1044, - "src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx": 1795, - "src/renderer/src/components/native-chat/NativeChatReasoningRow.test.tsx": 2689, - "src/renderer/src/components/native-chat/NativeChatResolutionReceipt.empty-answer.test.tsx": 397, - "src/renderer/src/components/native-chat/NativeChatResolutionReceipt.test.tsx": 1020, - "src/renderer/src/components/native-chat/NativeChatResolvedView.context-menu.test.tsx": 3415, - "src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx": 3672, - "src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx": 3511, - "src/renderer/src/components/native-chat/NativeChatResolvedView.reveal-latest.test.tsx": 2755, - "src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx": 3611, - "src/renderer/src/components/native-chat/NativeChatRewindAction.target.test.tsx": 3255, - "src/renderer/src/components/native-chat/NativeChatRewindAction.test.tsx": 2974, - "src/renderer/src/components/native-chat/NativeChatSelectionQuote.test.tsx": 589, - "src/renderer/src/components/native-chat/NativeChatSessionOptionPickers.focus-return.test.tsx": 1366, - "src/renderer/src/components/native-chat/NativeChatSessionOptionPickers.test.tsx": 1137, - "src/renderer/src/components/native-chat/NativeChatStructuredQuestionCard.test.tsx": 491, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.approval-and-start-failure.test.tsx": 2905, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.availability-notice.test.tsx": 2847, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.goal-dock.test.tsx": 3314, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.history-read-state.test.tsx": 2758, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.host-outage.test.tsx": 3114, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsx": 3169, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.notice-card.test.tsx": 2787, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.refusal-notices.test.tsx": 2576, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.stopping.test.tsx": 2687, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx": 2836, - "src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.test.tsx": 459, - "src/renderer/src/components/native-chat/NativeChatSubagentRun.test.tsx": 1152, - "src/renderer/src/components/native-chat/NativeChatTaskList.test.tsx": 489, - "src/renderer/src/components/native-chat/NativeChatThreadGoalBanner.test.tsx": 594, - "src/renderer/src/components/native-chat/NativeChatToolFileTarget.test.tsx": 502, - "src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx": 1353, - "src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx": 1214, - "src/renderer/src/components/native-chat/NativeChatToolRun.identity.test.tsx": 1029, - "src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx": 1212, - "src/renderer/src/components/native-chat/NativeChatToolRunCallCounts.test.tsx": 841, - "src/renderer/src/components/native-chat/NativeChatToolRunMemberList.identity.test.tsx": 786, - "src/renderer/src/components/native-chat/NativeChatTranscriptChrome.test.tsx": 1913, - "src/renderer/src/components/native-chat/NativeChatTranscriptRow.work-run.test.tsx": 2772, - "src/renderer/src/components/native-chat/NativeChatTurnActivityLine.test.tsx": 2341, - "src/renderer/src/components/native-chat/NativeChatUserMessageFold.test.tsx": 2829, - "src/renderer/src/components/native-chat/NativeChatView.test.tsx": 481, - "src/renderer/src/components/native-chat/NativeChatVisualFrame.test.tsx": 869, - "src/renderer/src/components/native-chat/NativeChatVisualTab.test.tsx": 1828, - "src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.test.tsx": 2038, - "src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.test.tsx": 378, - "src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.launch-directory.test.tsx": 1686, - "src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsx": 1946, - "src/renderer/src/components/native-chat/agent-session-failure-words-text.test.ts": 1046, - "src/renderer/src/components/native-chat/agent-session-write-notice-text.test.ts": 684, - "src/renderer/src/components/native-chat/background-task-words-text.test.ts": 158, - "src/renderer/src/components/native-chat/claude-model-switch-confirmation.test.ts": 1797, - "src/renderer/src/components/native-chat/claude-terminal-session-options.test.ts": 84, - "src/renderer/src/components/native-chat/native-chat-active-rail-item.test.ts": 53, - "src/renderer/src/components/native-chat/native-chat-agent-message-sender-host.test.tsx": 3468, - "src/renderer/src/components/native-chat/native-chat-agent-message-sender-identity.test.tsx": 1721, - "src/renderer/src/components/native-chat/native-chat-appearance-palette.test.ts": 592, - "src/renderer/src/components/native-chat/native-chat-appearance-style.test.ts": 324, - "src/renderer/src/components/native-chat/native-chat-appearance-write.test.ts": 346, - "src/renderer/src/components/native-chat/native-chat-attachment-upload.test.ts": 1810, - "src/renderer/src/components/native-chat/native-chat-autoscroll.test.ts": 66, - "src/renderer/src/components/native-chat/native-chat-availability.test.ts": 62, - "src/renderer/src/components/native-chat/native-chat-claude-sign-in.test.tsx": 2665, - "src/renderer/src/components/native-chat/native-chat-command-language.test.ts": 58, - "src/renderer/src/components/native-chat/native-chat-compaction-visibility.test.ts": 450, - "src/renderer/src/components/native-chat/native-chat-composer-autogrow.test.tsx": 1883, - "src/renderer/src/components/native-chat/native-chat-composer-composition.test.tsx": 2116, - "src/renderer/src/components/native-chat/native-chat-composer-draft-addition.test.ts": 223, - "src/renderer/src/components/native-chat/native-chat-composer-draft-comparison.test.ts": 48, - "src/renderer/src/components/native-chat/native-chat-composer-draft-indexeddb.test.ts": 118, - "src/renderer/src/components/native-chat/native-chat-composer-draft-lifecycle.test.tsx": 1926, - "src/renderer/src/components/native-chat/native-chat-composer-draft-store.test.ts": 1553, - "src/renderer/src/components/native-chat/native-chat-composer-drop-scope.test.tsx": 1112, - "src/renderer/src/components/native-chat/native-chat-composer-field-resume.test.tsx": 1838, - "src/renderer/src/components/native-chat/native-chat-composer-ime-file-drop.test.tsx": 1111, - "src/renderer/src/components/native-chat/native-chat-composer-placeholder.test.ts": 54, - "src/renderer/src/components/native-chat/native-chat-composer-primary-action.test.ts": 40, - "src/renderer/src/components/native-chat/native-chat-composer-prompt-recall.test.tsx": 1254, - "src/renderer/src/components/native-chat/native-chat-composer-reveal-focus.test.tsx": 256, - "src/renderer/src/components/native-chat/native-chat-composer-state.test.ts": 123, - "src/renderer/src/components/native-chat/native-chat-composer-upload-completion.test.tsx": 1909, - "src/renderer/src/components/native-chat/native-chat-composer-workspace-file-drop.test.tsx": 954, - "src/renderer/src/components/native-chat/native-chat-context-command.test.tsx": 501, - "src/renderer/src/components/native-chat/native-chat-context-usage-summary.test.ts": 52, - "src/renderer/src/components/native-chat/native-chat-diff.test.ts": 69, - "src/renderer/src/components/native-chat/native-chat-dismiss-key.test.ts": 50, - "src/renderer/src/components/native-chat/native-chat-draft-cache.test.ts": 92, - "src/renderer/src/components/native-chat/native-chat-element-file-drop.test.tsx": 1865, - "src/renderer/src/components/native-chat/native-chat-external-attachments-destination.test.tsx": 594, - "src/renderer/src/components/native-chat/native-chat-file-link-existence.test.ts": 1427, - "src/renderer/src/components/native-chat/native-chat-file-link.test.ts": 366, - "src/renderer/src/components/native-chat/native-chat-find-visibility.test.ts": 135, - "src/renderer/src/components/native-chat/native-chat-floating-file-drop.test.tsx": 773, - "src/renderer/src/components/native-chat/native-chat-font-size.test.ts": 82, - "src/renderer/src/components/native-chat/native-chat-http-link-source-owner.test.ts": 56, - "src/renderer/src/components/native-chat/native-chat-image-paste.test.ts": 69, - "src/renderer/src/components/native-chat/native-chat-image-runtime-context.test.ts": 1523, - "src/renderer/src/components/native-chat/native-chat-incremental-assembler.test.ts": 301, - "src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts": 115, - "src/renderer/src/components/native-chat/native-chat-launch-default-adoption.test.ts": 129, + "src/renderer/src/components/editor/rich-markdown-search.test.ts": 71, + "src/renderer/src/components/editor/rich-markdown-serialization-commit.test.ts": 1507, + "src/renderer/src/components/editor/rich-markdown-signed-images.test.ts": 1642, + "src/renderer/src/components/editor/rich-markdown-slash-command-filter.test.ts": 37, + "src/renderer/src/components/editor/rich-markdown-source-eol-allocation.test.ts": 302, + "src/renderer/src/components/editor/rich-markdown-source-reconcile.test.ts": 2146, + "src/renderer/src/components/editor/rich-markdown-tab-key-handler.test.ts": 1914, + "src/renderer/src/components/editor/rich-markdown-table-actions.test.ts": 2341, + "src/renderer/src/components/editor/rich-markdown-table-control-layout.test.ts": 29, + "src/renderer/src/components/editor/rich-markdown-table-keyboard.test.ts": 1642, + "src/renderer/src/components/editor/rich-markdown-table.test.ts": 58, + "src/renderer/src/components/editor/rich-markdown-terminal-path-paste.test.ts": 159, + "src/renderer/src/components/editor/rich-markdown-toc-heading-target.test.ts": 175, + "src/renderer/src/components/editor/rich-markdown-trailing-paragraph.test.ts": 1992, + "src/renderer/src/components/editor/selection-copy.test.ts": 42, + "src/renderer/src/components/editor/setup-contextual-copy.test.ts": 53, + "src/renderer/src/components/editor/untitled-file-rename-path.test.ts": 39, + "src/renderer/src/components/editor/use-editor-content-change-handler.test.tsx": 1646, + "src/renderer/src/components/editor/use-editor-group-file-drop-owner.remote.test.tsx": 2011, + "src/renderer/src/components/editor/use-editor-group-file-drop-owner.test.tsx": 891, + "src/renderer/src/components/editor/use-markdown-preview-document-search.test.tsx": 220, + "src/renderer/src/components/editor/use-markdown-preview-document.test.tsx": 340, + "src/renderer/src/components/editor/use-markdown-preview-navigation.test.tsx": 461, + "src/renderer/src/components/editor/use-markdown-preview-search-reveal.test.tsx": 307, + "src/renderer/src/components/editor/use-monaco-editor-decorations.doc-link-refresh.test.tsx": 266, + "src/renderer/src/components/editor/use-monaco-editor-mount.cursor-owner.test.tsx": 996, + "src/renderer/src/components/editor/use-monaco-reveal-scheduler.test.tsx": 195, + "src/renderer/src/components/editor/use-pdf-viewer-find-shortcut.test.tsx": 801, + "src/renderer/src/components/editor/use-quick-open-history-visit.test.tsx": 202, + "src/renderer/src/components/editor/use-rich-markdown-table-context-menu.test.tsx": 1891, + "src/renderer/src/components/editor/use-rich-markdown-table-control-target.test.tsx": 1887, + "src/renderer/src/components/editor/useDiffViewerLargeDiffLifecycle.test.tsx": 237, + "src/renderer/src/components/editor/useEditorCmdSaveRequest.test.tsx": 412, + "src/renderer/src/components/editor/useEditorPanelContentState.test.tsx": 1655, + "src/renderer/src/components/editor/useEditorPanelExternalContentEvents.test.tsx": 446, + "src/renderer/src/components/editor/useEditorPanelFileLoadRetry.host-connection.test.tsx": 1574, + "src/renderer/src/components/editor/useEditorPanelFileLoadRetry.test.tsx": 1575, + "src/renderer/src/components/editor/useEditorPanelFloatingTabContentState.test.tsx": 632, + "src/renderer/src/components/editor/useEditorPanelHostUnresolvedContentState.test.tsx": 808, + "src/renderer/src/components/editor/useEditorPanelRemoteSiblingContentState.test.tsx": 537, + "src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx": 911, + "src/renderer/src/components/editor/useIpynbCellExecution.test.tsx": 427, + "src/renderer/src/components/editor/useIpynbDocumentEditing.test.tsx": 255, + "src/renderer/src/components/editor/useLinkBubble.test.tsx": 288, + "src/renderer/src/components/editor/useLocalImagePick.test.tsx": 1051, + "src/renderer/src/components/editor/useLocalImageSrc.test.ts": 551, + "src/renderer/src/components/editor/useLocalLogTail.test.tsx": 466, + "src/renderer/src/components/editor/useMarkdownDocuments.navigation.test.ts": 566, + "src/renderer/src/components/editor/useMarkdownDocuments.ownership.test.tsx": 1603, + "src/renderer/src/components/editor/useMarkdownDocuments.test.ts": 1294, + "src/renderer/src/components/editor/useMarkdownDocuments.watch.test.ts": 525, + "src/renderer/src/components/editor/useRichMarkdownEditorInstance.integration.test.tsx": 335, + "src/renderer/src/components/editor/useRichMarkdownEditorInstance.test.tsx": 235, + "src/renderer/src/components/editor/useRichMarkdownPendingFocus.test.ts": 253, + "src/renderer/src/components/editor/useRichMarkdownProgrammaticSync.test.ts": 1892, + "src/renderer/src/components/editor/useRichMarkdownReviewController.open-guard.test.ts": 355, + "src/renderer/src/components/editor/useRichMarkdownReviewData.test.ts": 292, + "src/renderer/src/components/editor/useRichMarkdownSearch.adversarial.test.tsx": 751, + "src/renderer/src/components/editor/useRichMarkdownSearch.editing.test.tsx": 439, + "src/renderer/src/components/editor/useRichMarkdownSearch.reuse.test.tsx": 324, + "src/renderer/src/components/editor/useRichMarkdownSearch.test.tsx": 1597, + "src/renderer/src/components/emulator-pane/MobileEmulatorTabIntroCallout.test.tsx": 295, + "src/renderer/src/components/emulator-pane/emulator-device-frame-layout.test.ts": 52, + "src/renderer/src/components/emulator-pane/emulator-device-frame-visibility.test.tsx": 719, + "src/renderer/src/components/emulator-pane/emulator-device-frame.input.test.tsx": 551, + "src/renderer/src/components/emulator-pane/emulator-device-state.test.ts": 34, + "src/renderer/src/components/emulator-pane/emulator-keyboard-paste.test.ts": 204, + "src/renderer/src/components/emulator-pane/emulator-screen-gesture.test.ts": 57, + "src/renderer/src/components/emulator-pane/emulator-screen-stream-content.test.tsx": 295, + "src/renderer/src/components/emulator-pane/mobile-emulator-agent-setup-visibility.test.ts": 38, + "src/renderer/src/components/emulator-pane/mobile-emulator-hidden-toast.test.tsx": 216, + "src/renderer/src/components/emulator-pane/mobile-emulator-tab-intro-visibility.test.ts": 27, + "src/renderer/src/components/emulator-pane/use-emulator-pane-session.test.tsx": 1678, + "src/renderer/src/components/emulator-pane/use-mobile-emulator-tab-intro-actions.test.tsx": 1366, + "src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.lazy-chunk.test.tsx": 320, + "src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.never-landed-reload.test.tsx": 264, + "src/renderer/src/components/error-boundaries/react-185-bystander-attribution.test.tsx": 337, + "src/renderer/src/components/feature-tips/CliSkillSetupTerminal.test.tsx": 1476, + "src/renderer/src/components/feature-tips/CmdJPaletteFeatureTipVisual.test.tsx": 313, + "src/renderer/src/components/feature-tips/CmdJPaletteTipDialog.test.tsx": 116, + "src/renderer/src/components/feature-tips/NativeChatUpgradeTipDialog.test.tsx": 2110, + "src/renderer/src/components/feature-tips/SessionSearchTipDialog.test.tsx": 370, + "src/renderer/src/components/feature-tips/VoiceDictationFeatureTipVisual.test.tsx": 330, + "src/renderer/src/components/feature-tips/VoiceDictationTipDialog.test.tsx": 325, + "src/renderer/src/components/feature-tips/feature-tip-cli-install-action.test.ts": 34, + "src/renderer/src/components/feature-tips/feature-tip-modal-state.test.ts": 321, + "src/renderer/src/components/feature-tips/feature-tip-startup-gate.test.ts": 270, + "src/renderer/src/components/feature-tips/feature-tip-telemetry.test.ts": 51, + "src/renderer/src/components/feature-tips/use-session-search-tip-setup.test.tsx": 422, + "src/renderer/src/components/feature-wall/AgentCapabilitiesSetupAction.test.ts": 1407, + "src/renderer/src/components/feature-wall/ConnectIntegrationsList.test.tsx": 767, + "src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.test.tsx": 1588, + "src/renderer/src/components/feature-wall/FullDiskAccessSetupPrompt.test.ts": 351, + "src/renderer/src/components/feature-wall/KeepAwakeCard.test.tsx": 710, + "src/renderer/src/components/feature-wall/agents-orchestration/UsageAccountsCard.test.tsx": 345, + "src/renderer/src/components/feature-wall/browser-animated-visual-sequence.test.ts": 41, + "src/renderer/src/components/feature-wall/feature-wall-animation-visibility.test.tsx": 724, + "src/renderer/src/components/feature-wall/feature-wall-rail-navigation.test.ts": 36, + "src/renderer/src/components/feature-wall/feature-wall-setup-checklist-localized-copy.test.ts": 861, + "src/renderer/src/components/feature-wall/feature-wall-setup-progress.test.ts": 51, + "src/renderer/src/components/feature-wall/feature-wall-shortcut-labels.test.tsx": 1548, + "src/renderer/src/components/feature-wall/feature-wall-usage-tracking.test.ts": 67, + "src/renderer/src/components/feature-wall/use-feature-wall-completion.test.ts": 64, + "src/renderer/src/components/feature-wall/use-feature-wall-tour-telemetry.test.ts": 30, + "src/renderer/src/components/feature-wall/use-integration-connection-status.test.ts": 1373, + "src/renderer/src/components/feature-wall/workbench-terminal-storyboard-sequence.test.ts": 55, + "src/renderer/src/components/file-path-cursor-tooltip.test.ts": 184, + "src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.freshness.test.tsx": 561, + "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.bounds.test.tsx": 8255, + "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.focus.test.tsx": 11451, + "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.shortcuts.test.tsx": 8034, + "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tab-lifecycle.test.tsx": 9448, + "src/renderer/src/components/floating-terminal/FloatingTerminalToggleButton.test.tsx": 121, + "src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.test.tsx": 327, + "src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.test.ts": 45, + "src/renderer/src/components/floating-terminal/floating-terminal-panel-inputs.test.ts": 268, + "src/renderer/src/components/floating-terminal/floating-terminal-panel-view-state.test.ts": 161, + "src/renderer/src/components/floating-terminal/floating-terminal-trigger-position.test.ts": 58, + "src/renderer/src/components/floating-terminal/floating-workspace-surface-model.test.ts": 266, + "src/renderer/src/components/floating-terminal/floating-workspace-tab-reorder.test.ts": 1521, + "src/renderer/src/components/floating-terminal/use-floating-terminal-close-actions.test.tsx": 1731, + "src/renderer/src/components/github-body-draft-state.test.ts": 34, + "src/renderer/src/components/github-checks-tab-state.test.ts": 33, + "src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section.assignee-identity.test.tsx": 461, + "src/renderer/src/components/github-item-dialog/inspect-pull-request/pr-files-combined-diff-section-index.test.tsx": 373, + "src/renderer/src/components/github-item-dialog/load-item-details/work-item-details-issue-target.test.ts": 62, + "src/renderer/src/components/github-link-copy-state.test.ts": 29, + "src/renderer/src/components/github-pr-merge-state.test.ts": 80, + "src/renderer/src/components/github-pr-reviewer-display.test.ts": 52, + "src/renderer/src/components/github-project/GhAuthErrorHelp.test.ts": 126, + "src/renderer/src/components/github-project/ProjectBoard.test.tsx": 595, + "src/renderer/src/components/github-project/ProjectRoadmap.test.tsx": 1861, + "src/renderer/src/components/github-project/ProjectViewStates.test.tsx": 360, + "src/renderer/src/components/github-project/github-project-picker-filter.test.ts": 52, + "src/renderer/src/components/github-project/group-sort.test.ts": 52, + "src/renderer/src/components/github-project/project-dialog-state.test.ts": 37, + "src/renderer/src/components/github-project/project-picker-browse-cache.test.ts": 51, + "src/renderer/src/components/github-project/project-picker-input.test.ts": 235, + "src/renderer/src/components/github-project/project-row-filtering.test.ts": 55, + "src/renderer/src/components/github-project/project-view-wrapper-source-context-boundary.test.ts": 38, + "src/renderer/src/components/github-project/project-visible-table-cache.test.ts": 36, + "src/renderer/src/components/github/PRFilterPickers.test.ts": 133, + "src/renderer/src/components/github/github-markdown-image-url.test.ts": 43, + "src/renderer/src/components/github/github-mention-option-filter.test.ts": 52, + "src/renderer/src/components/github/github-pr-reviewer-candidate-filter.test.ts": 37, + "src/renderer/src/components/github/github-user-avatar.test.tsx": 216, + "src/renderer/src/components/github/github-work-item-assignee-filter.test.ts": 50, + "src/renderer/src/components/github/github-work-item-issue-target.test.ts": 352, + "src/renderer/src/components/github/github-work-item-label-filter.test.ts": 49, + "src/renderer/src/components/github/github-work-item-mutation-target.test.ts": 252, + "src/renderer/src/components/github/pr-comment-code-context.test.ts": 48, + "src/renderer/src/components/github/pr-file-content-size.test.ts": 40, + "src/renderer/src/components/github/use-image-input.test.ts": 1923, + "src/renderer/src/components/jira-attachment-image.test.tsx": 400, + "src/renderer/src/components/jira-connect-scoped-token.test.tsx": 811, + "src/renderer/src/components/jira-create-adf.test.ts": 37, + "src/renderer/src/components/jira-issue-assignee-control.test.tsx": 2518, + "src/renderer/src/components/jira-issue-transition-control.test.tsx": 633, + "src/renderer/src/components/jira-issue-workspace-actions.test.ts": 68, + "src/renderer/src/components/jira-issue-workspace-image-owner.test.tsx": 317, + "src/renderer/src/components/jira-issue-workspace-request-lifetime.test.tsx": 588, + "src/renderer/src/components/jira-list-columns.test.ts": 197, + "src/renderer/src/components/jira-project-picker-filter.test.ts": 45, + "src/renderer/src/components/jira-project-picker.test.tsx": 463, + "src/renderer/src/components/jira-prompt-templates.test.ts": 46, + "src/renderer/src/components/jira-self-user.test.ts": 43, + "src/renderer/src/components/landing-preflight-dismissal.test.ts": 41, + "src/renderer/src/components/landing-preflight-issues.test.ts": 90, + "src/renderer/src/components/landing-preflight-runtime-boundary.test.ts": 1620, + "src/renderer/src/components/landing-preflight-unverifiable-host.test.tsx": 1679, + "src/renderer/src/components/linear-api-key-dialog-state.test.ts": 40, + "src/renderer/src/components/linear-issue-attribute-filter-coverage-agreement.test.ts": 688, + "src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx": 1684, + "src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts": 37, + "src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts": 60, + "src/renderer/src/components/linear-issue-project-selector.test.tsx": 476, + "src/renderer/src/components/linear-issue-text-draft-state.test.ts": 33, + "src/renderer/src/components/linear-issue-text-save-plan.test.ts": 32, + "src/renderer/src/components/linear-issue-view-storage.test.ts": 181, + "src/renderer/src/components/linear-issue-workspace-detail-state.test.tsx": 223, + "src/renderer/src/components/linear-issue-workspace-text.test.ts": 99, + "src/renderer/src/components/linear-project-presentation.test.ts": 29, + "src/renderer/src/components/linear-project-search-query.test.ts": 42, + "src/renderer/src/components/linear-project-view-surfaces.test.tsx": 422, + "src/renderer/src/components/linear-scope-selector.test.ts": 133, + "src/renderer/src/components/link-actions/LinkActionPopover.test.tsx": 562, + "src/renderer/src/components/maintenance/update-card/update-card-error-model.test.ts": 94, + "src/renderer/src/components/mobile/MobileHero.test.tsx": 2453, + "src/renderer/src/components/mobile/MobilePage.test.tsx": 1616, + "src/renderer/src/components/mobile/MobilePageToolbar.test.tsx": 257, + "src/renderer/src/components/mobile/NetworkInterfacePicker.test.tsx": 413, + "src/renderer/src/components/mobile/WindowsFirewallNotice.test.tsx": 456, + "src/renderer/src/components/mobile/mobile-page-stage.test.ts": 36, + "src/renderer/src/components/mobile/paired-mobile-devices.test.ts": 187, + "src/renderer/src/components/mobile/use-mobile-page-paired-devices.test.ts": 302, + "src/renderer/src/components/mobile/use-mobile-pairing-address-preference.test.tsx": 243, + "src/renderer/src/components/native-chat-launch-resume-decision.test.tsx": 1509, + "src/renderer/src/components/native-chat-restart-action-notifications.test.ts": 107, + "src/renderer/src/components/native-chat-resume-activity-label.test.ts": 73, + "src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx": 2577, + "src/renderer/src/components/native-chat-resume-failed-chat-watch.test.ts": 1492, + "src/renderer/src/components/native-chat-resume-failure-guidance.test.ts": 67, + "src/renderer/src/components/native-chat-resume-finished-run.test.tsx": 3568, + "src/renderer/src/components/native-chat-resume-on-restart-grouping.test.ts": 92, + "src/renderer/src/components/native-chat-resume-on-restart-outcome-toast.test.ts": 1413, + "src/renderer/src/components/native-chat-resume-result-view.test.tsx": 2313, + "src/renderer/src/components/native-chat-resume-run.test.ts": 45, + "src/renderer/src/components/native-chat-resume-skipped-progress.test.tsx": 2665, + "src/renderer/src/components/native-chat/NativeChatAgentMessageRow.test.tsx": 2507, + "src/renderer/src/components/native-chat/NativeChatAgentMessageSenders.test.tsx": 1661, + "src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx": 2282, + "src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.test.tsx": 468, + "src/renderer/src/components/native-chat/NativeChatBackgroundTaskRun.test.tsx": 396, + "src/renderer/src/components/native-chat/NativeChatBackgroundTasksStatus.test.tsx": 693, + "src/renderer/src/components/native-chat/NativeChatCodeBlock.load-failure.test.tsx": 431, + "src/renderer/src/components/native-chat/NativeChatCodeBlock.test.tsx": 4272, + "src/renderer/src/components/native-chat/NativeChatComposer.paste-remount.test.tsx": 1842, + "src/renderer/src/components/native-chat/NativeChatComposer.test.tsx": 1112, + "src/renderer/src/components/native-chat/NativeChatComposerActions.test.tsx": 695, + "src/renderer/src/components/native-chat/NativeChatComposerNotices.test.tsx": 370, + "src/renderer/src/components/native-chat/NativeChatContextUsageRing.test.tsx": 927, + "src/renderer/src/components/native-chat/NativeChatEmptyState.test.tsx": 420, + "src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.test.tsx": 1836, + "src/renderer/src/components/native-chat/NativeChatInlineVisual.open-as-file.test.tsx": 1984, + "src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx": 1564, + "src/renderer/src/components/native-chat/NativeChatInterruptedContinue.test.tsx": 786, + "src/renderer/src/components/native-chat/NativeChatJumpToLatest.test.tsx": 313, + "src/renderer/src/components/native-chat/NativeChatMarkdown.code-wrap.test.tsx": 2290, + "src/renderer/src/components/native-chat/NativeChatMarkdown.mermaid.test.tsx": 1634, + "src/renderer/src/components/native-chat/NativeChatMarkdown.test.tsx": 1822, + "src/renderer/src/components/native-chat/NativeChatMarkdown.visual.test.tsx": 2953, + "src/renderer/src/components/native-chat/NativeChatMessageList.growth-windowing.test.tsx": 5930, + "src/renderer/src/components/native-chat/NativeChatMessageList.live-reasoning.test.tsx": 9840, + "src/renderer/src/components/native-chat/NativeChatMessageList.older-history.test.tsx": 4463, + "src/renderer/src/components/native-chat/NativeChatMessageList.opening-send.test.tsx": 3299, + "src/renderer/src/components/native-chat/NativeChatMessageList.provider-frame.test.tsx": 1653, + "src/renderer/src/components/native-chat/NativeChatMessageList.queued-turn-order.test.tsx": 3018, + "src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx": 17796, + "src/renderer/src/components/native-chat/NativeChatMessageList.reader-navigation.test.tsx": 5187, + "src/renderer/src/components/native-chat/NativeChatMessageList.rewind-rows.test.tsx": 2401, + "src/renderer/src/components/native-chat/NativeChatMessageList.stream-render.perf.test.tsx": 3720, + "src/renderer/src/components/native-chat/NativeChatMessageList.subagent-sections.test.tsx": 4144, + "src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx": 3031, + "src/renderer/src/components/native-chat/NativeChatMessageList.tool-stream-cost.test.tsx": 8326, + "src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx": 3007, + "src/renderer/src/components/native-chat/NativeChatMessageList.turn-membership.test.tsx": 3090, + "src/renderer/src/components/native-chat/NativeChatMessageList.turn-status.test.tsx": 3275, + "src/renderer/src/components/native-chat/NativeChatMessageList.unsent-message.test.tsx": 2272, + "src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx": 5544, + "src/renderer/src/components/native-chat/NativeChatMessageList.work-run-memo.test.tsx": 3104, + "src/renderer/src/components/native-chat/NativeChatMessageRail.test.tsx": 1052, + "src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx": 2306, + "src/renderer/src/components/native-chat/NativeChatMessageRow.work-run.test.tsx": 2250, + "src/renderer/src/components/native-chat/NativeChatMessageTimestamp.test.tsx": 527, + "src/renderer/src/components/native-chat/NativeChatModelCombobox.test.tsx": 608, + "src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx": 3357, + "src/renderer/src/components/native-chat/NativeChatPacedMarkdown.file-links.test.tsx": 1757, + "src/renderer/src/components/native-chat/NativeChatPacedMarkdown.mermaid.test.tsx": 2152, + "src/renderer/src/components/native-chat/NativeChatPermissionModePicker.test.tsx": 672, + "src/renderer/src/components/native-chat/NativeChatPromptEditor.paragraph-selection.test.tsx": 387, + "src/renderer/src/components/native-chat/NativeChatPromptEditor.test.tsx": 598, + "src/renderer/src/components/native-chat/NativeChatQuestionCard.test.tsx": 582, + "src/renderer/src/components/native-chat/NativeChatQueuedMessageEditor.test.tsx": 2160, + "src/renderer/src/components/native-chat/NativeChatQueuedMessageList.inline-edit.test.tsx": 1003, + "src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx": 2098, + "src/renderer/src/components/native-chat/NativeChatReasoningRow.test.tsx": 2024, + "src/renderer/src/components/native-chat/NativeChatResolutionReceipt.empty-answer.test.tsx": 466, + "src/renderer/src/components/native-chat/NativeChatResolutionReceipt.test.tsx": 941, + "src/renderer/src/components/native-chat/NativeChatResolvedView.context-menu.test.tsx": 3343, + "src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx": 3683, + "src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx": 3003, + "src/renderer/src/components/native-chat/NativeChatResolvedView.reveal-latest.test.tsx": 2183, + "src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx": 3452, + "src/renderer/src/components/native-chat/NativeChatRewindAction.target.test.tsx": 2869, + "src/renderer/src/components/native-chat/NativeChatRewindAction.test.tsx": 2594, + "src/renderer/src/components/native-chat/NativeChatSelectionQuote.test.tsx": 541, + "src/renderer/src/components/native-chat/NativeChatSessionOptionPickers.focus-return.test.tsx": 2644, + "src/renderer/src/components/native-chat/NativeChatSessionOptionPickers.test.tsx": 1353, + "src/renderer/src/components/native-chat/NativeChatStructuredQuestionCard.test.tsx": 402, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.approval-and-start-failure.test.tsx": 2229, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.availability-notice.test.tsx": 2310, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.goal-dock.test.tsx": 2740, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.history-read-state.test.tsx": 1974, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.host-outage.test.tsx": 2864, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsx": 3067, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.notice-card.test.tsx": 2620, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.orca-stop-continue.test.tsx": 2142, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.refusal-notices.test.tsx": 2657, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.stopping.test.tsx": 2060, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx": 2965, + "src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.test.tsx": 405, + "src/renderer/src/components/native-chat/NativeChatSubagentRun.test.tsx": 801, + "src/renderer/src/components/native-chat/NativeChatTaskList.test.tsx": 430, + "src/renderer/src/components/native-chat/NativeChatThreadGoalBanner.test.tsx": 545, + "src/renderer/src/components/native-chat/NativeChatToolFileTarget.test.tsx": 487, + "src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx": 1198, + "src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx": 1299, + "src/renderer/src/components/native-chat/NativeChatToolRun.identity.test.tsx": 1011, + "src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx": 1259, + "src/renderer/src/components/native-chat/NativeChatToolRunCallCounts.test.tsx": 810, + "src/renderer/src/components/native-chat/NativeChatToolRunMemberList.identity.test.tsx": 899, + "src/renderer/src/components/native-chat/NativeChatTranscriptChrome.test.tsx": 1962, + "src/renderer/src/components/native-chat/NativeChatTranscriptRow.work-run.test.tsx": 2712, + "src/renderer/src/components/native-chat/NativeChatTurnActivityLine.test.tsx": 2044, + "src/renderer/src/components/native-chat/NativeChatUserMessageFold.test.tsx": 2446, + "src/renderer/src/components/native-chat/NativeChatView.test.tsx": 440, + "src/renderer/src/components/native-chat/NativeChatVisualFrame.test.tsx": 804, + "src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.test.tsx": 2280, + "src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.test.tsx": 326, + "src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.launch-directory.test.tsx": 1487, + "src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsx": 2246, + "src/renderer/src/components/native-chat/agent-session-failure-words-text.test.ts": 1624, + "src/renderer/src/components/native-chat/agent-session-write-notice-text.test.ts": 858, + "src/renderer/src/components/native-chat/background-task-words-text.test.ts": 137, + "src/renderer/src/components/native-chat/claude-model-switch-confirmation.test.ts": 1457, + "src/renderer/src/components/native-chat/claude-terminal-session-options.test.ts": 63, + "src/renderer/src/components/native-chat/code-fence-language-label.test.ts": 61, + "src/renderer/src/components/native-chat/native-chat-active-rail-item.test.ts": 49, + "src/renderer/src/components/native-chat/native-chat-agent-message-sender-host.test.tsx": 3120, + "src/renderer/src/components/native-chat/native-chat-agent-message-sender-identity.test.tsx": 1661, + "src/renderer/src/components/native-chat/native-chat-appearance-palette.test.ts": 534, + "src/renderer/src/components/native-chat/native-chat-appearance-style.test.ts": 307, + "src/renderer/src/components/native-chat/native-chat-appearance-write.test.ts": 310, + "src/renderer/src/components/native-chat/native-chat-attachment-upload.test.ts": 1396, + "src/renderer/src/components/native-chat/native-chat-autoscroll.test.ts": 34, + "src/renderer/src/components/native-chat/native-chat-availability.test.ts": 55, + "src/renderer/src/components/native-chat/native-chat-claude-sign-in.test.tsx": 2233, + "src/renderer/src/components/native-chat/native-chat-command-language.test.ts": 47, + "src/renderer/src/components/native-chat/native-chat-compaction-visibility.test.ts": 422, + "src/renderer/src/components/native-chat/native-chat-composer-autogrow.test.tsx": 1741, + "src/renderer/src/components/native-chat/native-chat-composer-composition.test.tsx": 1995, + "src/renderer/src/components/native-chat/native-chat-composer-draft-addition.test.ts": 314, + "src/renderer/src/components/native-chat/native-chat-composer-draft-comparison.test.ts": 29, + "src/renderer/src/components/native-chat/native-chat-composer-draft-indexeddb.test.ts": 123, + "src/renderer/src/components/native-chat/native-chat-composer-draft-lifecycle.test.tsx": 2070, + "src/renderer/src/components/native-chat/native-chat-composer-draft-store.test.ts": 1584, + "src/renderer/src/components/native-chat/native-chat-composer-drop-scope.test.tsx": 1138, + "src/renderer/src/components/native-chat/native-chat-composer-field-resume.test.tsx": 2040, + "src/renderer/src/components/native-chat/native-chat-composer-ime-file-drop.test.tsx": 1077, + "src/renderer/src/components/native-chat/native-chat-composer-placeholder.test.ts": 40, + "src/renderer/src/components/native-chat/native-chat-composer-primary-action.test.ts": 35, + "src/renderer/src/components/native-chat/native-chat-composer-prompt-recall.test.tsx": 1622, + "src/renderer/src/components/native-chat/native-chat-composer-reveal-focus.test.tsx": 265, + "src/renderer/src/components/native-chat/native-chat-composer-state.test.ts": 82, + "src/renderer/src/components/native-chat/native-chat-composer-upload-completion.test.tsx": 1869, + "src/renderer/src/components/native-chat/native-chat-composer-workspace-file-drop.test.tsx": 1012, + "src/renderer/src/components/native-chat/native-chat-context-command.test.tsx": 584, + "src/renderer/src/components/native-chat/native-chat-context-usage-summary.test.ts": 29, + "src/renderer/src/components/native-chat/native-chat-diff.test.ts": 73, + "src/renderer/src/components/native-chat/native-chat-dismiss-key.test.ts": 40, + "src/renderer/src/components/native-chat/native-chat-draft-cache.test.ts": 96, + "src/renderer/src/components/native-chat/native-chat-element-file-drop.test.tsx": 1759, + "src/renderer/src/components/native-chat/native-chat-external-attachments-destination.test.tsx": 660, + "src/renderer/src/components/native-chat/native-chat-file-link-existence.test.ts": 1384, + "src/renderer/src/components/native-chat/native-chat-file-link.test.ts": 301, + "src/renderer/src/components/native-chat/native-chat-find-visibility.test.ts": 176, + "src/renderer/src/components/native-chat/native-chat-floating-file-drop.test.tsx": 630, + "src/renderer/src/components/native-chat/native-chat-font-size.test.ts": 77, + "src/renderer/src/components/native-chat/native-chat-http-link-source-owner.test.ts": 40, + "src/renderer/src/components/native-chat/native-chat-image-paste.test.ts": 50, + "src/renderer/src/components/native-chat/native-chat-image-runtime-context.test.ts": 1265, + "src/renderer/src/components/native-chat/native-chat-incremental-assembler.test.ts": 309, + "src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts": 114, + "src/renderer/src/components/native-chat/native-chat-launch-default-adoption.test.ts": 103, "src/renderer/src/components/native-chat/native-chat-launch-draft-resolution.test.ts": 51, - "src/renderer/src/components/native-chat/native-chat-launch-draft-send.test.ts": 63, - "src/renderer/src/components/native-chat/native-chat-layout-actions.test.ts": 1604, - "src/renderer/src/components/native-chat/native-chat-leaf-routing.test.ts": 60, - "src/renderer/src/components/native-chat/native-chat-live-message-preparation.test.ts": 314, - "src/renderer/src/components/native-chat/native-chat-live-status.test.ts": 79, - "src/renderer/src/components/native-chat/native-chat-message-grouping.test.ts": 280, - "src/renderer/src/components/native-chat/native-chat-message-list-projection.test.ts": 298, - "src/renderer/src/components/native-chat/native-chat-message-rail-items.test.ts": 388, - "src/renderer/src/components/native-chat/native-chat-observed-send.test.ts": 82, - "src/renderer/src/components/native-chat/native-chat-owner-selectors.test.tsx": 505, - "src/renderer/src/components/native-chat/native-chat-pagination.test.ts": 43, - "src/renderer/src/components/native-chat/native-chat-pane-file-drop.test.tsx": 435, - "src/renderer/src/components/native-chat/native-chat-pane-resolution.test.ts": 65, - "src/renderer/src/components/native-chat/native-chat-paste-routing.test.tsx": 1916, - "src/renderer/src/components/native-chat/native-chat-pending-attachment-cache.test.ts": 83, - "src/renderer/src/components/native-chat/native-chat-pending-occurrence.test.ts": 63, - "src/renderer/src/components/native-chat/native-chat-pending-pasted-content.test.ts": 74, - "src/renderer/src/components/native-chat/native-chat-pending.test.ts": 133, - "src/renderer/src/components/native-chat/native-chat-pinned-rows.test.ts": 60, - "src/renderer/src/components/native-chat/native-chat-preassembled-session-parity.test.ts": 471, - "src/renderer/src/components/native-chat/native-chat-prompt-document-text.test.ts": 57, - "src/renderer/src/components/native-chat/native-chat-prompt-editor-store-value.test.tsx": 2153, - "src/renderer/src/components/native-chat/native-chat-pty-retired-model.test.ts": 118, - "src/renderer/src/components/native-chat/native-chat-pty-session-options.test.ts": 435, - "src/renderer/src/components/native-chat/native-chat-queue-send-confirm.test.tsx": 2050, - "src/renderer/src/components/native-chat/native-chat-rail-outline-parity.test.ts": 363, - "src/renderer/src/components/native-chat/native-chat-reader-scroll-input.test.tsx": 276, - "src/renderer/src/components/native-chat/native-chat-resolved-path-ownership.test.ts": 51, - "src/renderer/src/components/native-chat/native-chat-retire-persisted-model.test.ts": 346, - "src/renderer/src/components/native-chat/native-chat-row-height-estimate.test.ts": 101, - "src/renderer/src/components/native-chat/native-chat-runtime-owner-selector.test.ts": 371, - "src/renderer/src/components/native-chat/native-chat-runtime-owner.test.ts": 326, - "src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts": 1348, - "src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts": 222, - "src/renderer/src/components/native-chat/native-chat-scrape-fallback.test.ts": 45, - "src/renderer/src/components/native-chat/native-chat-send-eligibility.test.ts": 38, - "src/renderer/src/components/native-chat/native-chat-send.test.ts": 54, - "src/renderer/src/components/native-chat/native-chat-sent-prompt-history.test.ts": 252, - "src/renderer/src/components/native-chat/native-chat-session-assembler.test.ts": 321, - "src/renderer/src/components/native-chat/native-chat-session-option-discovery.test.ts": 1181, - "src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts": 1681, - "src/renderer/src/components/native-chat/native-chat-session-option-labels.test.ts": 60, - "src/renderer/src/components/native-chat/native-chat-session-transport.test.ts": 123, - "src/renderer/src/components/native-chat/native-chat-shared-copy-matches-catalog.test.ts": 246, - "src/renderer/src/components/native-chat/native-chat-shortcut.test.ts": 60, - "src/renderer/src/components/native-chat/native-chat-split-shortcut.test.ts": 68, - "src/renderer/src/components/native-chat/native-chat-structured-send-composition-clear.test.tsx": 1857, - "src/renderer/src/components/native-chat/native-chat-structured-session-notices.command-failure.test.tsx": 1836, - "src/renderer/src/components/native-chat/native-chat-structured-session-notices.create-message.test.tsx": 1788, - "src/renderer/src/components/native-chat/native-chat-subagent-delegation.test.ts": 61, - "src/renderer/src/components/native-chat/native-chat-subagent-group-words-text.test.ts": 159, - "src/renderer/src/components/native-chat/native-chat-subagent-handoff-copy-budget.test.ts": 339, - "src/renderer/src/components/native-chat/native-chat-subagent-section-work-runs.test.ts": 377, - "src/renderer/src/components/native-chat/native-chat-subagent-sections-retention.test.ts": 774, - "src/renderer/src/components/native-chat/native-chat-subagent-sections.test.ts": 444, - "src/renderer/src/components/native-chat/native-chat-submission-navigation.test.tsx": 789, - "src/renderer/src/components/native-chat/native-chat-tab-agent-entry.test.ts": 47, - "src/renderer/src/components/native-chat/native-chat-tab-directory.test.ts": 280, - "src/renderer/src/components/native-chat/native-chat-task-list-history.test.ts": 69, - "src/renderer/src/components/native-chat/native-chat-task-list-state.test.ts": 68, - "src/renderer/src/components/native-chat/native-chat-terminal-turn.test.ts": 73, - "src/renderer/src/components/native-chat/native-chat-thread-goal-rows.test.ts": 54, - "src/renderer/src/components/native-chat/native-chat-tool-fold.test.ts": 72, - "src/renderer/src/components/native-chat/native-chat-tool-summary.test.ts": 54, - "src/renderer/src/components/native-chat/native-chat-transcript-slots.test.ts": 355, - "src/renderer/src/components/native-chat/native-chat-transcript-work-runs.test.ts": 328, + "src/renderer/src/components/native-chat/native-chat-launch-draft-send.test.ts": 32, + "src/renderer/src/components/native-chat/native-chat-layout-actions.test.ts": 1455, + "src/renderer/src/components/native-chat/native-chat-leaf-routing.test.ts": 63, + "src/renderer/src/components/native-chat/native-chat-live-message-preparation.test.ts": 267, + "src/renderer/src/components/native-chat/native-chat-live-status.test.ts": 49, + "src/renderer/src/components/native-chat/native-chat-message-grouping.test.ts": 289, + "src/renderer/src/components/native-chat/native-chat-message-list-projection.test.ts": 288, + "src/renderer/src/components/native-chat/native-chat-message-rail-items.test.ts": 314, + "src/renderer/src/components/native-chat/native-chat-observed-send.test.ts": 62, + "src/renderer/src/components/native-chat/native-chat-owner-selectors.test.tsx": 611, + "src/renderer/src/components/native-chat/native-chat-pagination.test.ts": 42, + "src/renderer/src/components/native-chat/native-chat-pane-file-drop.test.tsx": 353, + "src/renderer/src/components/native-chat/native-chat-pane-resolution.test.ts": 44, + "src/renderer/src/components/native-chat/native-chat-paste-routing.test.tsx": 1918, + "src/renderer/src/components/native-chat/native-chat-pending-attachment-cache.test.ts": 70, + "src/renderer/src/components/native-chat/native-chat-pending-occurrence.test.ts": 53, + "src/renderer/src/components/native-chat/native-chat-pending-pasted-content.test.ts": 51, + "src/renderer/src/components/native-chat/native-chat-pending.test.ts": 109, + "src/renderer/src/components/native-chat/native-chat-pinned-rows.test.ts": 33, + "src/renderer/src/components/native-chat/native-chat-preassembled-session-parity.test.ts": 511, + "src/renderer/src/components/native-chat/native-chat-prompt-document-text.test.ts": 45, + "src/renderer/src/components/native-chat/native-chat-prompt-document.test.ts": 132, + "src/renderer/src/components/native-chat/native-chat-prompt-editor-store-value.test.tsx": 691, + "src/renderer/src/components/native-chat/native-chat-pty-retired-model.test.ts": 95, + "src/renderer/src/components/native-chat/native-chat-pty-session-options.test.ts": 438, + "src/renderer/src/components/native-chat/native-chat-queue-send-confirm.test.tsx": 2051, + "src/renderer/src/components/native-chat/native-chat-rail-outline-parity.test.ts": 550, + "src/renderer/src/components/native-chat/native-chat-reader-scroll-input.test.tsx": 293, + "src/renderer/src/components/native-chat/native-chat-reply-reveals.test.tsx": 206, + "src/renderer/src/components/native-chat/native-chat-resolved-path-ownership.test.ts": 38, + "src/renderer/src/components/native-chat/native-chat-retire-persisted-model.test.ts": 313, + "src/renderer/src/components/native-chat/native-chat-row-height-estimate.test.ts": 88, + "src/renderer/src/components/native-chat/native-chat-runtime-owner-selector.test.ts": 301, + "src/renderer/src/components/native-chat/native-chat-runtime-owner.test.ts": 300, + "src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts": 1277, + "src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts": 234, + "src/renderer/src/components/native-chat/native-chat-scrape-fallback.test.ts": 34, + "src/renderer/src/components/native-chat/native-chat-send-eligibility.test.ts": 28, + "src/renderer/src/components/native-chat/native-chat-send.test.ts": 45, + "src/renderer/src/components/native-chat/native-chat-sent-prompt-history.test.ts": 283, + "src/renderer/src/components/native-chat/native-chat-session-assembler.test.ts": 265, + "src/renderer/src/components/native-chat/native-chat-session-option-discovery.test.ts": 1142, + "src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts": 1735, + "src/renderer/src/components/native-chat/native-chat-session-option-labels.test.ts": 40, + "src/renderer/src/components/native-chat/native-chat-session-transport.test.ts": 272, + "src/renderer/src/components/native-chat/native-chat-shared-copy-matches-catalog.test.ts": 123, + "src/renderer/src/components/native-chat/native-chat-shortcut.test.ts": 32, + "src/renderer/src/components/native-chat/native-chat-split-shortcut.test.ts": 63, + "src/renderer/src/components/native-chat/native-chat-structured-send-composition-clear.test.tsx": 1367, + "src/renderer/src/components/native-chat/native-chat-structured-session-notices.command-failure.test.tsx": 1588, + "src/renderer/src/components/native-chat/native-chat-structured-session-notices.create-message.test.tsx": 1640, + "src/renderer/src/components/native-chat/native-chat-subagent-delegation.test.ts": 37, + "src/renderer/src/components/native-chat/native-chat-subagent-group-words-text.test.ts": 133, + "src/renderer/src/components/native-chat/native-chat-subagent-handoff-copy-budget.test.ts": 371, + "src/renderer/src/components/native-chat/native-chat-subagent-section-work-runs.test.ts": 291, + "src/renderer/src/components/native-chat/native-chat-subagent-sections-retention.test.ts": 712, + "src/renderer/src/components/native-chat/native-chat-subagent-sections.test.ts": 429, + "src/renderer/src/components/native-chat/native-chat-submission-navigation.test.tsx": 868, + "src/renderer/src/components/native-chat/native-chat-tab-agent-entry.test.ts": 38, + "src/renderer/src/components/native-chat/native-chat-tab-directory.test.ts": 325, + "src/renderer/src/components/native-chat/native-chat-task-list-history.test.ts": 48, + "src/renderer/src/components/native-chat/native-chat-task-list-state.test.ts": 46, + "src/renderer/src/components/native-chat/native-chat-terminal-turn.test.ts": 63, + "src/renderer/src/components/native-chat/native-chat-thread-goal-rows.test.ts": 45, + "src/renderer/src/components/native-chat/native-chat-tool-fold.test.ts": 75, + "src/renderer/src/components/native-chat/native-chat-tool-summary.test.ts": 40, + "src/renderer/src/components/native-chat/native-chat-transcript-slots.test.ts": 363, + "src/renderer/src/components/native-chat/native-chat-transcript-work-runs.test.ts": 389, "src/renderer/src/components/native-chat/native-chat-turn-diffs.test.ts": 328, - "src/renderer/src/components/native-chat/native-chat-typing-redirect.test.ts": 62, - "src/renderer/src/components/native-chat/native-chat-view-file-drop.test.tsx": 1879, - "src/renderer/src/components/native-chat/native-chat-view-state.test.ts": 54, - "src/renderer/src/components/native-chat/native-chat-visual-markdown-syntax.test.tsx": 1741, - "src/renderer/src/components/native-chat/native-chat-visual-read-client.test.ts": 258, - "src/renderer/src/components/native-chat/native-chat-waiting-partition-budget.test.ts": 653, + "src/renderer/src/components/native-chat/native-chat-typing-redirect.test.ts": 47, + "src/renderer/src/components/native-chat/native-chat-view-file-drop.test.tsx": 1553, + "src/renderer/src/components/native-chat/native-chat-view-state.test.ts": 46, + "src/renderer/src/components/native-chat/native-chat-visual-markdown-syntax.test.tsx": 1581, + "src/renderer/src/components/native-chat/native-chat-visual-read-client.test.ts": 259, + "src/renderer/src/components/native-chat/native-chat-waiting-partition-budget.test.ts": 912, "src/renderer/src/components/native-chat/native-chat-web-link-actions.test.ts": 330, - "src/renderer/src/components/native-chat/native-chat-work-run-pairing.test.ts": 419, - "src/renderer/src/components/native-chat/native-chat-working-status-shared-clock.test.tsx": 300, - "src/renderer/src/components/native-chat/native-chat-working-suppression.test.ts": 53, - "src/renderer/src/components/native-chat/omp-model-switch-capability.test.ts": 130, - "src/renderer/src/components/native-chat/serialized-screen-background-rows.test.ts": 124, - "src/renderer/src/components/native-chat/structured-agent-question-projection.test.ts": 306, - "src/renderer/src/components/native-chat/structured-agent-registered-agent-surface.test.ts": 435, - "src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.kept-card.test.ts": 369, - "src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.test.ts": 392, - "src/renderer/src/components/native-chat/structured-agent-session-legacy-outbox.test.ts": 360, - "src/renderer/src/components/native-chat/structured-agent-session-message-hand-back.test.ts": 218, - "src/renderer/src/components/native-chat/structured-agent-session-message-projection.rejected-in-place.test.ts": 405, - "src/renderer/src/components/native-chat/structured-agent-session-message-projection.test.ts": 325, - "src/renderer/src/components/native-chat/structured-agent-session-message-sender.test.ts": 419, - "src/renderer/src/components/native-chat/structured-agent-session-queue-request.test.ts": 52, - "src/renderer/src/components/native-chat/structured-agent-session-queued-cards.test.ts": 219, - "src/renderer/src/components/native-chat/structured-agent-session-rail-outline.test.ts": 49, - "src/renderer/src/components/native-chat/structured-agent-session-read-transport.test.ts": 294, - "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-clock.test.tsx": 1549, - "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-failed-start.test.tsx": 1883, - "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-stopping.test.tsx": 1608, - "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-unconfirmed-start.test.tsx": 1818, + "src/renderer/src/components/native-chat/native-chat-work-run-pairing.test.ts": 300, + "src/renderer/src/components/native-chat/native-chat-working-status-shared-clock.test.tsx": 237, + "src/renderer/src/components/native-chat/native-chat-working-suppression.test.ts": 28, + "src/renderer/src/components/native-chat/omp-model-switch-capability.test.ts": 118, + "src/renderer/src/components/native-chat/serialized-screen-background-rows.test.ts": 100, + "src/renderer/src/components/native-chat/structured-agent-question-projection.test.ts": 300, + "src/renderer/src/components/native-chat/structured-agent-registered-agent-surface.test.ts": 378, + "src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.kept-card.test.ts": 345, + "src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.test.ts": 346, + "src/renderer/src/components/native-chat/structured-agent-session-legacy-outbox.test.ts": 655, + "src/renderer/src/components/native-chat/structured-agent-session-message-hand-back.test.ts": 184, + "src/renderer/src/components/native-chat/structured-agent-session-message-projection.rejected-in-place.test.ts": 355, + "src/renderer/src/components/native-chat/structured-agent-session-message-projection.test.ts": 320, + "src/renderer/src/components/native-chat/structured-agent-session-message-sender.test.ts": 449, + "src/renderer/src/components/native-chat/structured-agent-session-queue-request.test.ts": 32, + "src/renderer/src/components/native-chat/structured-agent-session-queued-cards.test.ts": 274, + "src/renderer/src/components/native-chat/structured-agent-session-rail-outline.test.ts": 37, + "src/renderer/src/components/native-chat/structured-agent-session-read-transport.test.ts": 326, + "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-clock.test.tsx": 1445, + "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-failed-start.test.tsx": 1749, + "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-stopping.test.tsx": 1514, + "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-unconfirmed-start.test.tsx": 1493, "src/renderer/src/components/native-chat/structured-agent-session-transcript-order.test.ts": 364, - "src/renderer/src/components/native-chat/structured-attention-dispatch.test.ts": 1277, - "src/renderer/src/components/native-chat/structured-attention-surface.test.ts": 1681, - "src/renderer/src/components/native-chat/structured-child-records-switch.test.tsx": 1858, - "src/renderer/src/components/native-chat/structured-conversation-command-send.test.ts": 785, - "src/renderer/src/components/native-chat/structured-session-takeover-report.test.tsx": 588, - "src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsx": 1720, - "src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.unavailable.test.tsx": 1718, - "src/renderer/src/components/native-chat/use-native-chat-appearance-style.test.tsx": 564, - "src/renderer/src/components/native-chat/use-native-chat-availability-notice.test.tsx": 602, - "src/renderer/src/components/native-chat/use-native-chat-composer-app-menu-selection.test.tsx": 297, - "src/renderer/src/components/native-chat/use-native-chat-composer-attachment-previews.test.tsx": 189, - "src/renderer/src/components/native-chat/use-native-chat-composer-attachments.test.tsx": 1797, + "src/renderer/src/components/native-chat/structured-attention-dispatch.test.ts": 1534, + "src/renderer/src/components/native-chat/structured-attention-surface.test.ts": 1328, + "src/renderer/src/components/native-chat/structured-child-records-switch.test.tsx": 1952, + "src/renderer/src/components/native-chat/structured-conversation-command-send.test.ts": 550, + "src/renderer/src/components/native-chat/structured-session-takeover-report.test.tsx": 568, + "src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsx": 1628, + "src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.unavailable.test.tsx": 1634, + "src/renderer/src/components/native-chat/use-native-chat-appearance-style.test.tsx": 454, + "src/renderer/src/components/native-chat/use-native-chat-availability-notice.test.tsx": 588, + "src/renderer/src/components/native-chat/use-native-chat-composer-app-menu-selection.test.tsx": 275, + "src/renderer/src/components/native-chat/use-native-chat-composer-attachment-previews.test.tsx": 205, + "src/renderer/src/components/native-chat/use-native-chat-composer-attachments.test.tsx": 1752, "src/renderer/src/components/native-chat/use-native-chat-composer-catalog.test.tsx": 523, - "src/renderer/src/components/native-chat/use-native-chat-composer-image-input.test.tsx": 1613, - "src/renderer/src/components/native-chat/use-native-chat-composer-keydown.steer.test.tsx": 457, - "src/renderer/src/components/native-chat/use-native-chat-composer-keydown.test.tsx": 614, - "src/renderer/src/components/native-chat/use-native-chat-composer-notice.test.tsx": 309, - "src/renderer/src/components/native-chat/use-native-chat-composer-paste-session-attachments.test.tsx": 279, - "src/renderer/src/components/native-chat/use-native-chat-composer-paste.test.tsx": 446, - "src/renderer/src/components/native-chat/use-native-chat-composer-preview-lifetime.test.tsx": 1616, - "src/renderer/src/components/native-chat/use-native-chat-composer-recall.test.tsx": 466, - "src/renderer/src/components/native-chat/use-native-chat-composer-submit.test.tsx": 332, - "src/renderer/src/components/native-chat/use-native-chat-context-menu.test.tsx": 2301, - "src/renderer/src/components/native-chat/use-native-chat-external-attachments.test.tsx": 702, - "src/renderer/src/components/native-chat/use-native-chat-file-attachment-actions.test.tsx": 259, - "src/renderer/src/components/native-chat/use-native-chat-file-link-click.test.tsx": 1131, - "src/renderer/src/components/native-chat/use-native-chat-file-link-existence.test.tsx": 706, - "src/renderer/src/components/native-chat/use-native-chat-find.test.tsx": 1343, - "src/renderer/src/components/native-chat/use-native-chat-font-size.test.tsx": 660, - "src/renderer/src/components/native-chat/use-native-chat-hook-status.test.ts": 1604, - "src/renderer/src/components/native-chat/use-native-chat-host-outage.test.tsx": 1609, - "src/renderer/src/components/native-chat/use-native-chat-interactive-prompt-card.test.ts": 1685, - "src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx": 388, - "src/renderer/src/components/native-chat/use-native-chat-launch-draft-adoption.test.tsx": 368, - "src/renderer/src/components/native-chat/use-native-chat-link-actions.test.tsx": 844, - "src/renderer/src/components/native-chat/use-native-chat-live-session-pending.test.ts": 1561, - "src/renderer/src/components/native-chat/use-native-chat-live-session-visibility.test.ts": 1650, - "src/renderer/src/components/native-chat/use-native-chat-live-session.test.ts": 1567, - "src/renderer/src/components/native-chat/use-native-chat-mention-files.test.tsx": 1739, - "src/renderer/src/components/native-chat/use-native-chat-menu-paste-lifetime.test.tsx": 1722, - "src/renderer/src/components/native-chat/use-native-chat-message-rail.test.ts": 778, - "src/renderer/src/components/native-chat/use-native-chat-paced-text.remount.test.tsx": 288, - "src/renderer/src/components/native-chat/use-native-chat-paced-text.test.ts": 45, - "src/renderer/src/components/native-chat/use-native-chat-paste-lifetime.test.tsx": 317, - "src/renderer/src/components/native-chat/use-native-chat-pending-delivery.test.tsx": 350, - "src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.test.tsx": 1548, - "src/renderer/src/components/native-chat/use-native-chat-picker-state.test.tsx": 295, - "src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts": 1743, - "src/renderer/src/components/native-chat/use-native-chat-provisional-launch.test.tsx": 1968, - "src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.test.tsx": 431, - "src/renderer/src/components/native-chat/use-native-chat-rail-history-jump.test.ts": 586, - "src/renderer/src/components/native-chat/use-native-chat-retained-session.test.ts": 217, - "src/renderer/src/components/native-chat/use-native-chat-rewind.test.tsx": 725, - "src/renderer/src/components/native-chat/use-native-chat-row-typography.test.tsx": 1793, - "src/renderer/src/components/native-chat/use-native-chat-send-lifecycle.test.tsx": 252, - "src/renderer/src/components/native-chat/use-native-chat-session-option-command.test.tsx": 1640, - "src/renderer/src/components/native-chat/use-native-chat-session-options.test.ts": 805, - "src/renderer/src/components/native-chat/use-native-chat-skills.react.test.tsx": 972, - "src/renderer/src/components/native-chat/use-native-chat-skills.test.ts": 1523, - "src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.test.tsx": 983, - "src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.surface-owner.test.ts": 590, - "src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.test.ts": 1442, - "src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.first-reply.test.tsx": 261, - "src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.test.tsx": 355, - "src/renderer/src/components/native-chat/use-native-chat-transcript-window.options.test.tsx": 684, - "src/renderer/src/components/native-chat/use-structured-agent-session-command-send-gate.test.tsx": 1939, - "src/renderer/src/components/native-chat/use-structured-agent-session-command-start-failure.test.tsx": 2211, - "src/renderer/src/components/native-chat/use-structured-agent-session-context-usage.test.tsx": 2614, - "src/renderer/src/components/native-chat/use-structured-agent-session-delivery-notices.test.tsx": 575, - "src/renderer/src/components/native-chat/use-structured-agent-session-hold.test.tsx": 533, - "src/renderer/src/components/native-chat/use-structured-agent-session-legacy-recovery.test.tsx": 2508, - "src/renderer/src/components/native-chat/use-structured-agent-session-messages.test.tsx": 547, - "src/renderer/src/components/native-chat/use-structured-agent-session-mutate.test.tsx": 876, - "src/renderer/src/components/native-chat/use-structured-agent-session-options.first-frame.test.tsx": 2125, - "src/renderer/src/components/native-chat/use-structured-agent-session-options.learned-default.test.tsx": 1942, - "src/renderer/src/components/native-chat/use-structured-agent-session-options.preloaded-agents.test.tsx": 1799, - "src/renderer/src/components/native-chat/use-structured-agent-session-options.test.tsx": 2745, - "src/renderer/src/components/native-chat/use-structured-agent-session-paired-runtime.test.tsx": 2294, - "src/renderer/src/components/native-chat/use-structured-agent-session-prompt-cancel.test.tsx": 2071, - "src/renderer/src/components/native-chat/use-structured-agent-session-prompt-state.test.tsx": 2502, - "src/renderer/src/components/native-chat/use-structured-agent-session-provisional.test.tsx": 2577, - "src/renderer/src/components/native-chat/use-structured-agent-session-question-answers.test.tsx": 1996, - "src/renderer/src/components/native-chat/use-structured-agent-session-queued-edit.test.tsx": 2429, - "src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.clear.test.tsx": 1709, - "src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.resume.test.tsx": 1659, - "src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.test.tsx": 1691, - "src/renderer/src/components/native-chat/use-structured-agent-session-rail-outline.test.tsx": 564, - "src/renderer/src/components/native-chat/use-structured-agent-session-read.test.tsx": 2334, - "src/renderer/src/components/native-chat/use-structured-agent-session-sends.test.tsx": 553, - "src/renderer/src/components/native-chat/use-structured-agent-session-start-failure-facts.test.ts": 523, - "src/renderer/src/components/native-chat/use-structured-agent-session-stop-press.test.tsx": 247, - "src/renderer/src/components/native-chat/use-structured-agent-session-stop.test.tsx": 2147, - "src/renderer/src/components/native-chat/use-structured-agent-session-thread-goal.test.tsx": 312, - "src/renderer/src/components/native-chat/use-structured-agent-session-transport.test.tsx": 1868, - "src/renderer/src/components/native-chat/use-structured-agent-session-write-refusal.test.tsx": 2124, - "src/renderer/src/components/native-chat/use-structured-agent-session.clear-send.test.tsx": 1896, - "src/renderer/src/components/native-chat/use-structured-agent-session.cut-turn-notice.test.tsx": 2024, - "src/renderer/src/components/native-chat/use-structured-agent-session.queue-while-stopping.test.tsx": 2061, - "src/renderer/src/components/native-chat/use-structured-agent-session.queued-gating.test.tsx": 2287, - "src/renderer/src/components/native-chat/use-structured-agent-session.rejected-card.test.tsx": 2042, - "src/renderer/src/components/native-chat/use-structured-agent-session.rewind.test.tsx": 2472, - "src/renderer/src/components/native-chat/use-structured-agent-session.test.tsx": 3082, - "src/renderer/src/components/native-chat/use-structured-agent-turn-timing.test.tsx": 471, - "src/renderer/src/components/native-chat/use-structured-native-chat-pane-commands.test.tsx": 540, - "src/renderer/src/components/native-chat/use-structured-native-chat-submit-reveal.test.ts": 295, - "src/renderer/src/components/network/CustomAddressDialog.test.tsx": 516, - "src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.test.tsx": 596, - "src/renderer/src/components/new-workspace/NewWorkspaceComposerAgentSection.test.tsx": 773, - "src/renderer/src/components/new-workspace/ProjectCombobox.dialog-handoff.test.tsx": 926, - "src/renderer/src/components/new-workspace/ProjectCombobox.test.tsx": 563, - "src/renderer/src/components/new-workspace/ProjectComboboxRow.test.tsx": 324, - "src/renderer/src/components/new-workspace/SetProjectLocationDialog.test.tsx": 1306, - "src/renderer/src/components/new-workspace/SmartWorkspaceNameField-provider-boundaries.test.ts": 54, - "src/renderer/src/components/new-workspace/SmartWorkspaceNameField-repo-slug-routing.test.ts": 97, - "src/renderer/src/components/new-workspace/SmartWorkspaceNameField.ime-enter.test.tsx": 635, - "src/renderer/src/components/new-workspace/SmartWorkspaceNameField.jira-accessibility.test.tsx": 1078, - "src/renderer/src/components/new-workspace/base-ref-picker-visibility.test.ts": 53, - "src/renderer/src/components/new-workspace/project-combobox-matching.test.ts": 76, - "src/renderer/src/components/new-workspace/smart-workspace-command-value.test.ts": 53, - "src/renderer/src/components/new-workspace/smart-workspace-localized-options.test.ts": 185, - "src/renderer/src/components/new-workspace/smart-workspace-source-popover-focus.test.ts": 179, - "src/renderer/src/components/new-workspace/smart-workspace-source-results.test.ts": 125, - "src/renderer/src/components/new-workspace/use-jira-source-connection.test.tsx": 323, - "src/renderer/src/components/new-workspace/use-jira-url-source.test.tsx": 293, - "src/renderer/src/components/new-workspace/use-linear-source-connection.test.tsx": 878, - "src/renderer/src/components/new-workspace/use-recent-project-ids.test.ts": 1462, - "src/renderer/src/components/new-workspace/use-smart-workspace-field-focus-controls.test.tsx": 272, - "src/renderer/src/components/new-workspace/use-smart-workspace-name-field-foundation.source.test.tsx": 663, - "src/renderer/src/components/new-workspace/work-item-source-selection.test.ts": 1480, - "src/renderer/src/components/onboarding/AgentStep.test.tsx": 289, - "src/renderer/src/components/onboarding/FeatureSetupInlineTerminal.test.tsx": 1700, - "src/renderer/src/components/onboarding/NotificationStep.test.tsx": 1612, - "src/renderer/src/components/onboarding/OnboardingFlow.test.tsx": 14464, - "src/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.command-finished.test.tsx": 369, - "src/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.test.ts": 9982, - "src/renderer/src/components/onboarding/ThemeStep.test.ts": 132, - "src/renderer/src/components/onboarding/WindowsTerminalStep.test.tsx": 558, - "src/renderer/src/components/onboarding/agent-picked-payload.test.ts": 68, - "src/renderer/src/components/onboarding/onboarding-dismiss-target.test.ts": 51, - "src/renderer/src/components/onboarding/onboarding-feature-setup.test.ts": 1391, - "src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts": 332, - "src/renderer/src/components/onboarding/onboarding-settings-hydration.test.ts": 60, - "src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.ts": 1673, - "src/renderer/src/components/onboarding/use-onboarding-flow.test.ts": 159, - "src/renderer/src/components/onboarding/windows-terminal-onboarding-telemetry.test.ts": 42, - "src/renderer/src/components/orca-profiles/OrcaProfileSignOutConfirmDialog.test.tsx": 250, - "src/renderer/src/components/pet/PetOverlay.frame-durations.test.tsx": 269, - "src/renderer/src/components/pet/PetOverlay.keyframes.test.tsx": 372, - "src/renderer/src/components/pet/PetOverlay.pet-switch.test.tsx": 245, - "src/renderer/src/components/pet/PetOverlay.pointer-interaction.test.tsx": 348, - "src/renderer/src/components/pet/PetOverlay.test.ts": 1488, - "src/renderer/src/components/pet/pet-agent-state.test.ts": 95, - "src/renderer/src/components/pet/pet-blob-cache.test.ts": 66, - "src/renderer/src/components/pet/pet-overlay-hit-area.test.tsx": 283, - "src/renderer/src/components/pet/pet-overlay-position.test.ts": 1490, - "src/renderer/src/components/pet/pet-overlay-visibility.test.ts": 49, - "src/renderer/src/components/pet/sprite-animation-css.test.ts": 57, - "src/renderer/src/components/pet/usePetPointerInteraction.test.ts": 319, - "src/renderer/src/components/pet/usePetUrl.test.tsx": 355, - "src/renderer/src/components/ports/WorkspacePortScanner.test.tsx": 2253, - "src/renderer/src/components/pr-check-counts.test.ts": 84, - "src/renderer/src/components/pr-checks-fix-prompt.test.ts": 61, - "src/renderer/src/components/pr-comments-resolution-prompt.test.ts": 48, - "src/renderer/src/components/provider-check-classification-parity.test.ts": 129, - "src/renderer/src/components/pull-request-page/cache/file-content.test.ts": 129, - "src/renderer/src/components/pull-request-page/files/combined-diff-section-index.test.tsx": 495, - "src/renderer/src/components/pull-request-page/mentions/options.test.ts": 59, - "src/renderer/src/components/pull-request-page/mentions/query.test.ts": 54, - "src/renderer/src/components/pull-request-page/presentation/state-badge.test.ts": 155, - "src/renderer/src/components/quick-open-file-history.test.ts": 1540, - "src/renderer/src/components/quick-open-file-list-host-name-filter.react.test.tsx": 1753, - "src/renderer/src/components/quick-open-file-list-remote-catalog.react.test.tsx": 1792, - "src/renderer/src/components/quick-open-file-list.react.test.tsx": 1934, - "src/renderer/src/components/quick-open-file-list.test.ts": 1601, - "src/renderer/src/components/quick-open-file-navigation.test.ts": 335, - "src/renderer/src/components/quick-open-history-ranking.test.ts": 59, - "src/renderer/src/components/quick-open-install-rg-guidance.render.test.tsx": 314, - "src/renderer/src/components/quick-open-install-rg-guidance.test.ts": 133, - "src/renderer/src/components/quick-open-recent-lifetime.react.test.tsx": 1679, - "src/renderer/src/components/quick-open-recent-validation.test.ts": 109, - "src/renderer/src/components/quick-open-search.test.ts": 275, - "src/renderer/src/components/quick-open-selection-owner.react.test.tsx": 1849, - "src/renderer/src/components/repo/CreateFromPicker.test.tsx": 677, - "src/renderer/src/components/repo/NestedRepoChecklist.test.tsx": 186, - "src/renderer/src/components/repo/NestedRepoScanLimitNotice.test.ts": 177, - "src/renderer/src/components/repo/repo-icon.emoji-centering.test.tsx": 333, - "src/renderer/src/components/repo/repo-icon.test.tsx": 399, - "src/renderer/src/components/right-sidebar/ActionButton.test.tsx": 143, - "src/renderer/src/components/right-sidebar/AiVaultPanel.legacy-filter.test.tsx": 3070, - "src/renderer/src/components/right-sidebar/AiVaultPanelHeader.focus.test.tsx": 385, - "src/renderer/src/components/right-sidebar/AiVaultPanelSearch.test.tsx": 541, - "src/renderer/src/components/right-sidebar/AiVaultSearchEvidence.test.tsx": 2086, - "src/renderer/src/components/right-sidebar/AiVaultSessionActionMenuItems.test.tsx": 482, - "src/renderer/src/components/right-sidebar/AiVaultSessionListBar.test.tsx": 696, - "src/renderer/src/components/right-sidebar/AiVaultSessionRow.structured-title.test.tsx": 2145, - "src/renderer/src/components/right-sidebar/AiVaultSessionRow.test.tsx": 2248, - "src/renderer/src/components/right-sidebar/AiVaultSessionSubagents.test.tsx": 2264, - "src/renderer/src/components/right-sidebar/AiVaultShowMoreSessionsRow.test.tsx": 376, - "src/renderer/src/components/right-sidebar/AiVaultVirtualRow.test.tsx": 2999, - "src/renderer/src/components/right-sidebar/ChecksPanel.review-header.test.tsx": 5337, - "src/renderer/src/components/right-sidebar/ChecksPanel.updated-at-metadata.test.tsx": 249, - "src/renderer/src/components/right-sidebar/CommitArea.chevron-spinner.test.tsx": 2229, - "src/renderer/src/components/right-sidebar/CommitArea.generate.test.tsx": 1890, - "src/renderer/src/components/right-sidebar/CommitArea.primary-icons.test.tsx": 3363, - "src/renderer/src/components/right-sidebar/CommitArea.test.tsx": 6748, - "src/renderer/src/components/right-sidebar/FileExplorerFilesTreePane.errors.test.tsx": 1674, - "src/renderer/src/components/right-sidebar/FileExplorerNameFilter.test.tsx": 156, - "src/renderer/src/components/right-sidebar/FileExplorerRow.actions.test.tsx": 432, - "src/renderer/src/components/right-sidebar/FileExplorerRow.load-error.test.tsx": 1657, - "src/renderer/src/components/right-sidebar/FileExplorerScopeNotice.test.tsx": 538, - "src/renderer/src/components/right-sidebar/FileExplorerToolbar.test.tsx": 1512, - "src/renderer/src/components/right-sidebar/FileExplorerViewSwitch.test.tsx": 98, - "src/renderer/src/components/right-sidebar/FileExplorerVirtualRows.row-handlers.test.tsx": 1604, - "src/renderer/src/components/right-sidebar/FileExplorerVirtualRowsAddProject.test.tsx": 1525, - "src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.test.tsx": 566, - "src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.test.tsx": 428, - "src/renderer/src/components/right-sidebar/FolderWorkspaceWorktreesPanel.test.tsx": 334, - "src/renderer/src/components/right-sidebar/GitHistoryPanel.test.tsx": 1611, - "src/renderer/src/components/right-sidebar/GitHubPRStackMap.test.tsx": 359, - "src/renderer/src/components/right-sidebar/HostedReviewActions.draft.test.tsx": 1176, + "src/renderer/src/components/native-chat/use-native-chat-composer-image-input.test.tsx": 1658, + "src/renderer/src/components/native-chat/use-native-chat-composer-keydown.steer.test.tsx": 482, + "src/renderer/src/components/native-chat/use-native-chat-composer-keydown.test.tsx": 622, + "src/renderer/src/components/native-chat/use-native-chat-composer-notice.test.tsx": 197, + "src/renderer/src/components/native-chat/use-native-chat-composer-paste-session-attachments.test.tsx": 278, + "src/renderer/src/components/native-chat/use-native-chat-composer-paste.test.tsx": 427, + "src/renderer/src/components/native-chat/use-native-chat-composer-preview-lifetime.test.tsx": 1298, + "src/renderer/src/components/native-chat/use-native-chat-composer-recall.test.tsx": 509, + "src/renderer/src/components/native-chat/use-native-chat-composer-submit.test.tsx": 300, + "src/renderer/src/components/native-chat/use-native-chat-context-menu.test.tsx": 2180, + "src/renderer/src/components/native-chat/use-native-chat-external-attachments.test.tsx": 609, + "src/renderer/src/components/native-chat/use-native-chat-file-attachment-actions.test.tsx": 237, + "src/renderer/src/components/native-chat/use-native-chat-file-link-click.test.tsx": 1025, + "src/renderer/src/components/native-chat/use-native-chat-file-link-existence.test.tsx": 603, + "src/renderer/src/components/native-chat/use-native-chat-find.test.tsx": 1353, + "src/renderer/src/components/native-chat/use-native-chat-font-size.test.tsx": 658, + "src/renderer/src/components/native-chat/use-native-chat-hook-status.test.ts": 1369, + "src/renderer/src/components/native-chat/use-native-chat-host-outage.test.tsx": 1473, + "src/renderer/src/components/native-chat/use-native-chat-interactive-prompt-card.test.ts": 1729, + "src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx": 367, + "src/renderer/src/components/native-chat/use-native-chat-launch-draft-adoption.test.tsx": 264, + "src/renderer/src/components/native-chat/use-native-chat-link-actions.test.tsx": 728, + "src/renderer/src/components/native-chat/use-native-chat-live-session-pending.test.ts": 1702, + "src/renderer/src/components/native-chat/use-native-chat-live-session-visibility.test.ts": 1616, + "src/renderer/src/components/native-chat/use-native-chat-live-session.test.ts": 1863, + "src/renderer/src/components/native-chat/use-native-chat-mention-files.test.tsx": 1474, + "src/renderer/src/components/native-chat/use-native-chat-menu-paste-lifetime.test.tsx": 1809, + "src/renderer/src/components/native-chat/use-native-chat-message-rail.test.ts": 768, + "src/renderer/src/components/native-chat/use-native-chat-paced-text.remount.test.tsx": 331, + "src/renderer/src/components/native-chat/use-native-chat-paced-text.test.ts": 42, + "src/renderer/src/components/native-chat/use-native-chat-paste-lifetime.test.tsx": 328, + "src/renderer/src/components/native-chat/use-native-chat-pending-delivery.test.tsx": 349, + "src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.test.tsx": 1635, + "src/renderer/src/components/native-chat/use-native-chat-picker-state.test.tsx": 236, + "src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts": 1666, + "src/renderer/src/components/native-chat/use-native-chat-provisional-launch.test.tsx": 1846, + "src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.test.tsx": 519, + "src/renderer/src/components/native-chat/use-native-chat-rail-history-jump.test.ts": 554, + "src/renderer/src/components/native-chat/use-native-chat-retained-session.test.ts": 181, + "src/renderer/src/components/native-chat/use-native-chat-rewind.test.tsx": 721, + "src/renderer/src/components/native-chat/use-native-chat-row-typography.test.tsx": 1671, + "src/renderer/src/components/native-chat/use-native-chat-send-lifecycle.test.tsx": 251, + "src/renderer/src/components/native-chat/use-native-chat-session-option-command.test.tsx": 1521, + "src/renderer/src/components/native-chat/use-native-chat-session-options.test.ts": 814, + "src/renderer/src/components/native-chat/use-native-chat-skills.react.test.tsx": 884, + "src/renderer/src/components/native-chat/use-native-chat-skills.test.ts": 1322, + "src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.test.tsx": 857, + "src/renderer/src/components/native-chat/use-native-chat-structured-composer-transport.test.tsx": 211, + "src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.surface-owner.test.ts": 690, + "src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.test.ts": 1283, + "src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.first-reply.test.tsx": 290, + "src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.test.tsx": 275, + "src/renderer/src/components/native-chat/use-native-chat-transcript-window.options.test.tsx": 673, + "src/renderer/src/components/native-chat/use-structured-agent-session-command-send-gate.test.tsx": 1764, + "src/renderer/src/components/native-chat/use-structured-agent-session-command-start-failure.test.tsx": 1954, + "src/renderer/src/components/native-chat/use-structured-agent-session-context-usage.test.tsx": 1734, + "src/renderer/src/components/native-chat/use-structured-agent-session-delivery-notices.test.tsx": 629, + "src/renderer/src/components/native-chat/use-structured-agent-session-hold.test.tsx": 517, + "src/renderer/src/components/native-chat/use-structured-agent-session-legacy-recovery.test.tsx": 2183, + "src/renderer/src/components/native-chat/use-structured-agent-session-messages.test.tsx": 620, + "src/renderer/src/components/native-chat/use-structured-agent-session-mutate.test.tsx": 811, + "src/renderer/src/components/native-chat/use-structured-agent-session-options.first-frame.test.tsx": 2060, + "src/renderer/src/components/native-chat/use-structured-agent-session-options.learned-default.test.tsx": 1700, + "src/renderer/src/components/native-chat/use-structured-agent-session-options.permission-mode.test.tsx": 1882, + "src/renderer/src/components/native-chat/use-structured-agent-session-options.preloaded-agents.test.tsx": 1688, + "src/renderer/src/components/native-chat/use-structured-agent-session-options.test.tsx": 2989, + "src/renderer/src/components/native-chat/use-structured-agent-session-paired-runtime.test.tsx": 2276, + "src/renderer/src/components/native-chat/use-structured-agent-session-prompt-cancel.test.tsx": 1790, + "src/renderer/src/components/native-chat/use-structured-agent-session-prompt-state.test.tsx": 1860, + "src/renderer/src/components/native-chat/use-structured-agent-session-provisional.test.tsx": 1464, + "src/renderer/src/components/native-chat/use-structured-agent-session-question-answers.test.tsx": 1897, + "src/renderer/src/components/native-chat/use-structured-agent-session-queued-edit.test.tsx": 2428, + "src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.clear.test.tsx": 1786, + "src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.resume.test.tsx": 2036, + "src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.test.tsx": 1570, + "src/renderer/src/components/native-chat/use-structured-agent-session-rail-outline.test.tsx": 516, + "src/renderer/src/components/native-chat/use-structured-agent-session-read.test.tsx": 2516, + "src/renderer/src/components/native-chat/use-structured-agent-session-send-options.test.tsx": 302, + "src/renderer/src/components/native-chat/use-structured-agent-session-sends.test.tsx": 563, + "src/renderer/src/components/native-chat/use-structured-agent-session-start-failure-facts.test.ts": 549, + "src/renderer/src/components/native-chat/use-structured-agent-session-stop-press.test.tsx": 239, + "src/renderer/src/components/native-chat/use-structured-agent-session-stop.test.tsx": 2037, + "src/renderer/src/components/native-chat/use-structured-agent-session-thread-goal.test.tsx": 222, + "src/renderer/src/components/native-chat/use-structured-agent-session-transport.test.tsx": 1648, + "src/renderer/src/components/native-chat/use-structured-agent-session-write-refusal.test.tsx": 1956, + "src/renderer/src/components/native-chat/use-structured-agent-session.clear-send.test.tsx": 2029, + "src/renderer/src/components/native-chat/use-structured-agent-session.cut-turn-notice.test.tsx": 1712, + "src/renderer/src/components/native-chat/use-structured-agent-session.queue-while-stopping.test.tsx": 1881, + "src/renderer/src/components/native-chat/use-structured-agent-session.queued-gating.test.tsx": 2336, + "src/renderer/src/components/native-chat/use-structured-agent-session.rejected-card.test.tsx": 1871, + "src/renderer/src/components/native-chat/use-structured-agent-session.rewind.test.tsx": 2345, + "src/renderer/src/components/native-chat/use-structured-agent-session.test.tsx": 3405, + "src/renderer/src/components/native-chat/use-structured-agent-turn-timing.test.tsx": 441, + "src/renderer/src/components/native-chat/use-structured-native-chat-pane-commands.test.tsx": 502, + "src/renderer/src/components/native-chat/use-structured-native-chat-submit-reveal.test.ts": 238, + "src/renderer/src/components/network/CustomAddressDialog.test.tsx": 389, + "src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.test.tsx": 621, + "src/renderer/src/components/new-workspace/NewWorkspaceComposerAgentSection.test.tsx": 725, + "src/renderer/src/components/new-workspace/ProjectCombobox.dialog-handoff.test.tsx": 860, + "src/renderer/src/components/new-workspace/ProjectCombobox.test.tsx": 647, + "src/renderer/src/components/new-workspace/ProjectComboboxRow.test.tsx": 254, + "src/renderer/src/components/new-workspace/RunTargetCombobox.host-identity.test.tsx": 590, + "src/renderer/src/components/new-workspace/SetProjectLocationDialog.host-identity.test.tsx": 820, + "src/renderer/src/components/new-workspace/SetProjectLocationDialog.test.tsx": 1460, + "src/renderer/src/components/new-workspace/SmartWorkspaceNameField-provider-boundaries.test.ts": 39, + "src/renderer/src/components/new-workspace/SmartWorkspaceNameField-repo-slug-routing.test.ts": 282, + "src/renderer/src/components/new-workspace/SmartWorkspaceNameField.ime-enter.test.tsx": 918, + "src/renderer/src/components/new-workspace/SmartWorkspaceNameField.jira-accessibility.test.tsx": 1007, + "src/renderer/src/components/new-workspace/base-ref-picker-visibility.test.ts": 38, + "src/renderer/src/components/new-workspace/project-combobox-matching.test.ts": 82, + "src/renderer/src/components/new-workspace/smart-workspace-command-value.test.ts": 58, + "src/renderer/src/components/new-workspace/smart-workspace-localized-options.test.ts": 165, + "src/renderer/src/components/new-workspace/smart-workspace-source-popover-focus.test.ts": 190, + "src/renderer/src/components/new-workspace/smart-workspace-source-results.test.ts": 85, + "src/renderer/src/components/new-workspace/use-jira-composer-project.test.tsx": 230, + "src/renderer/src/components/new-workspace/use-jira-source-connection.test.tsx": 249, + "src/renderer/src/components/new-workspace/use-jira-url-source.test.tsx": 438, + "src/renderer/src/components/new-workspace/use-linear-source-connection.test.tsx": 850, + "src/renderer/src/components/new-workspace/use-recent-project-ids.test.ts": 1492, + "src/renderer/src/components/new-workspace/use-smart-workspace-field-focus-controls.test.tsx": 223, + "src/renderer/src/components/new-workspace/use-smart-workspace-name-field-foundation.source.test.tsx": 816, + "src/renderer/src/components/new-workspace/work-item-source-selection.test.ts": 1410, + "src/renderer/src/components/onboarding/AgentStep.test.tsx": 199, + "src/renderer/src/components/onboarding/FeatureSetupInlineTerminal.test.tsx": 1420, + "src/renderer/src/components/onboarding/NotificationStep.test.tsx": 1599, + "src/renderer/src/components/onboarding/OnboardingFlow.test.tsx": 11176, + "src/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.command-finished.test.tsx": 337, + "src/renderer/src/components/onboarding/ThemeStep.test.ts": 98, + "src/renderer/src/components/onboarding/WindowsTerminalStep.test.tsx": 487, + "src/renderer/src/components/onboarding/agent-picked-payload.test.ts": 34, + "src/renderer/src/components/onboarding/onboarding-dismiss-target.test.ts": 37, + "src/renderer/src/components/onboarding/onboarding-feature-setup.test.ts": 1183, + "src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts": 345, + "src/renderer/src/components/onboarding/onboarding-settings-hydration.test.ts": 42, + "src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.ts": 1521, + "src/renderer/src/components/onboarding/use-onboarding-flow.test.ts": 79, + "src/renderer/src/components/onboarding/windows-terminal-onboarding-telemetry.test.ts": 33, + "src/renderer/src/components/orca-profiles/OrcaProfileSignOutConfirmDialog.test.tsx": 228, + "src/renderer/src/components/pet/PetOverlay.frame-durations.test.tsx": 238, + "src/renderer/src/components/pet/PetOverlay.keyframes.test.tsx": 388, + "src/renderer/src/components/pet/PetOverlay.pet-switch.test.tsx": 282, + "src/renderer/src/components/pet/PetOverlay.pointer-interaction.test.tsx": 316, + "src/renderer/src/components/pet/PetOverlay.test.ts": 1373, + "src/renderer/src/components/pet/pet-agent-state.test.ts": 94, + "src/renderer/src/components/pet/pet-blob-cache.test.ts": 42, + "src/renderer/src/components/pet/pet-overlay-hit-area.test.tsx": 289, + "src/renderer/src/components/pet/pet-overlay-position.test.ts": 1466, + "src/renderer/src/components/pet/pet-overlay-visibility.test.ts": 34, + "src/renderer/src/components/pet/sprite-animation-css.test.ts": 38, + "src/renderer/src/components/pet/usePetPointerInteraction.test.ts": 309, + "src/renderer/src/components/pet/usePetUrl.test.tsx": 246, + "src/renderer/src/components/ports/WorkspacePortScanner.test.tsx": 1880, + "src/renderer/src/components/pr-check-counts.test.ts": 53, + "src/renderer/src/components/pr-checks-fix-prompt.test.ts": 60, + "src/renderer/src/components/pr-comments-resolution-prompt.test.ts": 36, + "src/renderer/src/components/provider-check-classification-parity.test.ts": 89, + "src/renderer/src/components/pull-request-page/cache/file-content.test.ts": 274, + "src/renderer/src/components/pull-request-page/files/combined-diff-section-index.test.tsx": 603, + "src/renderer/src/components/pull-request-page/mentions/options.test.ts": 37, + "src/renderer/src/components/pull-request-page/mentions/query.test.ts": 38, + "src/renderer/src/components/pull-request-page/presentation/state-badge.test.ts": 83, + "src/renderer/src/components/quick-open-file-history.test.ts": 1431, + "src/renderer/src/components/quick-open-file-list-host-name-filter.react.test.tsx": 1521, + "src/renderer/src/components/quick-open-file-list-remote-catalog.react.test.tsx": 1527, + "src/renderer/src/components/quick-open-file-list.react.test.tsx": 1687, + "src/renderer/src/components/quick-open-file-list.test.ts": 1696, + "src/renderer/src/components/quick-open-file-navigation.test.ts": 443, + "src/renderer/src/components/quick-open-history-ranking.test.ts": 36, + "src/renderer/src/components/quick-open-install-rg-guidance.render.test.tsx": 294, + "src/renderer/src/components/quick-open-install-rg-guidance.test.ts": 76, + "src/renderer/src/components/quick-open-recent-lifetime.react.test.tsx": 1413, + "src/renderer/src/components/quick-open-recent-validation.test.ts": 76, + "src/renderer/src/components/quick-open-search.test.ts": 263, + "src/renderer/src/components/quick-open-selection-owner.react.test.tsx": 1714, + "src/renderer/src/components/repo/CreateFromPicker.test.tsx": 706, + "src/renderer/src/components/repo/NestedRepoChecklist.test.tsx": 126, + "src/renderer/src/components/repo/NestedRepoScanLimitNotice.test.ts": 114, + "src/renderer/src/components/repo/repo-icon.emoji-centering.test.tsx": 314, + "src/renderer/src/components/repo/repo-icon.test.tsx": 312, + "src/renderer/src/components/right-sidebar/ActionButton.test.tsx": 82, + "src/renderer/src/components/right-sidebar/AiVaultPanel.legacy-filter.test.tsx": 3333, + "src/renderer/src/components/right-sidebar/AiVaultPanelHeader.focus.test.tsx": 326, + "src/renderer/src/components/right-sidebar/AiVaultPanelSearch.test.tsx": 632, + "src/renderer/src/components/right-sidebar/AiVaultSearchEvidence.test.tsx": 1823, + "src/renderer/src/components/right-sidebar/AiVaultSessionActionMenuItems.test.tsx": 511, + "src/renderer/src/components/right-sidebar/AiVaultSessionListBar.test.tsx": 627, + "src/renderer/src/components/right-sidebar/AiVaultSessionRow.structured-title.test.tsx": 1980, + "src/renderer/src/components/right-sidebar/AiVaultSessionRow.test.tsx": 2285, + "src/renderer/src/components/right-sidebar/AiVaultSessionSubagents.test.tsx": 2067, + "src/renderer/src/components/right-sidebar/AiVaultShowMoreSessionsRow.test.tsx": 363, + "src/renderer/src/components/right-sidebar/AiVaultVirtualRow.test.tsx": 2985, + "src/renderer/src/components/right-sidebar/ChecksPanel.review-header.test.tsx": 6315, + "src/renderer/src/components/right-sidebar/ChecksPanel.updated-at-metadata.test.tsx": 204, + "src/renderer/src/components/right-sidebar/CommitArea.chevron-spinner.test.tsx": 2448, + "src/renderer/src/components/right-sidebar/CommitArea.generate.test.tsx": 1621, + "src/renderer/src/components/right-sidebar/CommitArea.primary-icons.test.tsx": 2452, + "src/renderer/src/components/right-sidebar/CommitArea.test.tsx": 10499, + "src/renderer/src/components/right-sidebar/FileExplorerFilesTreePane.errors.test.tsx": 1777, + "src/renderer/src/components/right-sidebar/FileExplorerNameFilter.test.tsx": 103, + "src/renderer/src/components/right-sidebar/FileExplorerRow.actions.test.tsx": 353, + "src/renderer/src/components/right-sidebar/FileExplorerRow.load-error.test.tsx": 1966, + "src/renderer/src/components/right-sidebar/FileExplorerScopeNotice.test.tsx": 558, + "src/renderer/src/components/right-sidebar/FileExplorerToolbar.test.tsx": 1314, + "src/renderer/src/components/right-sidebar/FileExplorerViewSwitch.test.tsx": 76, + "src/renderer/src/components/right-sidebar/FileExplorerVirtualRows.row-handlers.test.tsx": 1853, + "src/renderer/src/components/right-sidebar/FileExplorerVirtualRowsAddProject.test.tsx": 1440, + "src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.test.tsx": 674, + "src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.test.tsx": 312, + "src/renderer/src/components/right-sidebar/FolderWorkspaceWorktreesPanel.test.tsx": 356, + "src/renderer/src/components/right-sidebar/GitHistoryPanel.test.tsx": 1304, + "src/renderer/src/components/right-sidebar/GitHubPRStackMap.test.tsx": 314, + "src/renderer/src/components/right-sidebar/HostedReviewActions.draft.test.tsx": 1486, "src/renderer/src/components/right-sidebar/PluginPanel.test.tsx": 559, - "src/renderer/src/components/right-sidebar/PortsPanel.test.tsx": 2438, - "src/renderer/src/components/right-sidebar/PullRequestComposer.generate-tooltip.test.tsx": 902, - "src/renderer/src/components/right-sidebar/SearchResultItems.test.tsx": 197, - "src/renderer/src/components/right-sidebar/SessionRowTrailingActions.test.tsx": 280, - "src/renderer/src/components/right-sidebar/SourceControl.branch-line-total.test.tsx": 2329, - "src/renderer/src/components/right-sidebar/SourceControl.commit-drafts.test.ts": 133, - "src/renderer/src/components/right-sidebar/SourceControl.commit-failure-recovery.test.ts": 70, - "src/renderer/src/components/right-sidebar/SourceControl.commit-generation-records.test.ts": 63, - "src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts": 182, - "src/renderer/src/components/right-sidebar/SourceControl.hosted-review-header-link.test.tsx": 146, - "src/renderer/src/components/right-sidebar/SourceControl.open-file-highlight.test.tsx": 3578, - "src/renderer/src/components/right-sidebar/SourceControl.pr-generation-records.test.ts": 70, - "src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx": 3147, - "src/renderer/src/components/right-sidebar/SourceControl.push-failure-recovery.test.ts": 45, - "src/renderer/src/components/right-sidebar/SourceControl.remote-action-errors.test.ts": 46, - "src/renderer/src/components/right-sidebar/SourceControl.resolve-conflicts-prompt.test.ts": 98, - "src/renderer/src/components/right-sidebar/SourceControl.virtual-file-list.test.tsx": 3961, - "src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx": 2686, - "src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.test.tsx": 258, - "src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialog.test.ts": 376, - "src/renderer/src/components/right-sidebar/active-checks-status.test.ts": 1468, - "src/renderer/src/components/right-sidebar/activity-bar-buttons.test.tsx": 328, - "src/renderer/src/components/right-sidebar/activity-bar-overflow.test.ts": 53, - "src/renderer/src/components/right-sidebar/ai-vault-first-prompt-card.test.tsx": 419, - "src/renderer/src/components/right-sidebar/ai-vault-host-scope.test.ts": 201, - "src/renderer/src/components/right-sidebar/ai-vault-original-pane-index.test.ts": 79, - "src/renderer/src/components/right-sidebar/ai-vault-original-pane.test.ts": 66, - "src/renderer/src/components/right-sidebar/ai-vault-panel-session-list-request.test.tsx": 2271, - "src/renderer/src/components/right-sidebar/ai-vault-scan-issue-state.test.ts": 69, - "src/renderer/src/components/right-sidebar/ai-vault-scope-paths.test.ts": 72, - "src/renderer/src/components/right-sidebar/ai-vault-scope-state.test.ts": 52, - "src/renderer/src/components/right-sidebar/ai-vault-search-scope-identity.test.tsx": 448, - "src/renderer/src/components/right-sidebar/ai-vault-search-session.test.ts": 268, - "src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.test.ts": 77, - "src/renderer/src/components/right-sidebar/ai-vault-session-continuation.test.ts": 69, - "src/renderer/src/components/right-sidebar/ai-vault-session-deletability.test.ts": 306, - "src/renderer/src/components/right-sidebar/ai-vault-session-display.test.ts": 59, - "src/renderer/src/components/right-sidebar/ai-vault-session-filters.test.ts": 103, - "src/renderer/src/components/right-sidebar/ai-vault-session-identity.test.ts": 57, - "src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.test.ts": 1623, - "src/renderer/src/components/right-sidebar/ai-vault-session-log-open.test.ts": 337, - "src/renderer/src/components/right-sidebar/ai-vault-session-path-actions.test.ts": 330, - "src/renderer/src/components/right-sidebar/ai-vault-session-projects.test.ts": 101, - "src/renderer/src/components/right-sidebar/ai-vault-session-publication-gate.test.ts": 156, - "src/renderer/src/components/right-sidebar/ai-vault-session-refresh.test.ts": 1511, - "src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.test.ts": 1041, - "src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.test.ts": 519, - "src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat.test.ts": 84, - "src/renderer/src/components/right-sidebar/ai-vault-session-resume.test.ts": 1709, - "src/renderer/src/components/right-sidebar/ai-vault-session-surface-switch.test.ts": 1609, - "src/renderer/src/components/right-sidebar/ai-vault-session-worktree-map.test.tsx": 1626, - "src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts": 109, - "src/renderer/src/components/right-sidebar/ai-vault-view-defaults.test.ts": 70, - "src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.test.ts": 68, - "src/renderer/src/components/right-sidebar/branch-line-total-request-gate.test.ts": 42, - "src/renderer/src/components/right-sidebar/check-details-resize.test.ts": 48, - "src/renderer/src/components/right-sidebar/checks-entry-refresh.test.ts": 72, - "src/renderer/src/components/right-sidebar/checks-list-expanded-details.test.tsx": 701, - "src/renderer/src/components/right-sidebar/checks-panel-async-result-key.test.ts": 55, - "src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx": 1908, - "src/renderer/src/components/right-sidebar/checks-panel-empty-state.test.ts": 138, - "src/renderer/src/components/right-sidebar/checks-panel-git-status-snapshot.test.ts": 66, - "src/renderer/src/components/right-sidebar/checks-panel-hosted-review-click-routing.test.ts": 56, - "src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-breadcrumb.test.ts": 61, - "src/renderer/src/components/right-sidebar/checks-panel-review-creation.test.ts": 72, - "src/renderer/src/components/right-sidebar/checks-panel-review-lookup-authority.test.ts": 74, - "src/renderer/src/components/right-sidebar/checks-panel-review.test.ts": 58, - "src/renderer/src/components/right-sidebar/checks-panel-terminal-worktree.test.ts": 72, - "src/renderer/src/components/right-sidebar/checks-panel/checks-detail-polling-policy.test.ts": 49, - "src/renderer/src/components/right-sidebar/checks-panel/gitlab-review-client.test.ts": 103, - "src/renderer/src/components/right-sidebar/checks-panel/panel-content-rendering.test.tsx": 6495, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-check-and-review-actions.test.tsx": 2148, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-create-review.test.tsx": 1647, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-foreground-effects.test.tsx": 228, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-generation.test.tsx": 1760, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-git-status-effects.test.tsx": 241, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-manual-refresh.test.tsx": 1661, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-polling-stability.test.tsx": 1756, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-polling.test.tsx": 1865, - "src/renderer/src/components/right-sidebar/coalesced-poll-runner.test.ts": 65, - "src/renderer/src/components/right-sidebar/commit-failure-dialog-state.test.ts": 56, - "src/renderer/src/components/right-sidebar/commit-failure-summary.test.ts": 66, - "src/renderer/src/components/right-sidebar/create-review-draft-title.test.ts": 60, - "src/renderer/src/components/right-sidebar/diff-comments-clear-dialog-state.test.ts": 59, - "src/renderer/src/components/right-sidebar/discard-all-sequence.test.ts": 76, - "src/renderer/src/components/right-sidebar/file-explorer-add-project-action.test.ts": 56, - "src/renderer/src/components/right-sidebar/file-explorer-batch-deletion.test.ts": 136, - "src/renderer/src/components/right-sidebar/file-explorer-copy-file-owner.test.ts": 76, - "src/renderer/src/components/right-sidebar/file-explorer-delete-editor-owner.test.tsx": 1672, - "src/renderer/src/components/right-sidebar/file-explorer-dir-load-tracker.test.ts": 50, - "src/renderer/src/components/right-sidebar/file-explorer-dir-toggle-timing.test.ts": 167, - "src/renderer/src/components/right-sidebar/file-explorer-directory-filename-identity.test.ts": 1416, - "src/renderer/src/components/right-sidebar/file-explorer-directory-listing.test.ts": 1622, - "src/renderer/src/components/right-sidebar/file-explorer-display-root.test.ts": 1628, - "src/renderer/src/components/right-sidebar/file-explorer-drag-scroll-marker.test.tsx": 2169, - "src/renderer/src/components/right-sidebar/file-explorer-entries.test.ts": 58, - "src/renderer/src/components/right-sidebar/file-explorer-expanded-dirs-refresh.test.ts": 1581, - "src/renderer/src/components/right-sidebar/file-explorer-inline-input-ime-enter.test.tsx": 361, - "src/renderer/src/components/right-sidebar/file-explorer-inline-input-outside-click.test.tsx": 348, - "src/renderer/src/components/right-sidebar/file-explorer-inline-rename-flow.test.tsx": 910, - "src/renderer/src/components/right-sidebar/file-explorer-instant-dir-toggle.test.ts": 389, - "src/renderer/src/components/right-sidebar/file-explorer-keyboard-navigation.test.ts": 68, - "src/renderer/src/components/right-sidebar/file-explorer-linked-recovery.test.tsx": 1794, - "src/renderer/src/components/right-sidebar/file-explorer-linked-watch.test.ts": 1465, - "src/renderer/src/components/right-sidebar/file-explorer-name-filter-projection.test.ts": 70, - "src/renderer/src/components/right-sidebar/file-explorer-operation-generation.test.ts": 1460, - "src/renderer/src/components/right-sidebar/file-explorer-operation-owner.test.ts": 349, - "src/renderer/src/components/right-sidebar/file-explorer-paths.test.ts": 61, - "src/renderer/src/components/right-sidebar/file-explorer-refresh-concurrency.test.ts": 49, - "src/renderer/src/components/right-sidebar/file-explorer-replace-selection-order-budget.test.tsx": 338, - "src/renderer/src/components/right-sidebar/file-explorer-reset.test.ts": 52, - "src/renderer/src/components/right-sidebar/file-explorer-row-context-menu.test.tsx": 433, - "src/renderer/src/components/right-sidebar/file-explorer-row-projection.test.ts": 53, - "src/renderer/src/components/right-sidebar/file-explorer-selection.test.ts": 55, - "src/renderer/src/components/right-sidebar/file-explorer-stale-dir-cache.test.ts": 48, - "src/renderer/src/components/right-sidebar/file-explorer-stale-retry.test.tsx": 1729, - "src/renderer/src/components/right-sidebar/file-explorer-watch-drive-root.test.ts": 1350, - "src/renderer/src/components/right-sidebar/file-explorer-watch-filename-identity.test.ts": 1562, - "src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.test.ts": 1632, - "src/renderer/src/components/right-sidebar/file-explorer-watch-refresh-scheduler.test.ts": 99, - "src/renderer/src/components/right-sidebar/file-explorer-watch-selected-host.test.ts": 1457, - "src/renderer/src/components/right-sidebar/file-search-include-pattern.test.ts": 61, - "src/renderer/src/components/right-sidebar/folder-workspace-attached-worktrees.test.ts": 104, - "src/renderer/src/components/right-sidebar/fork-push-target-label.test.ts": 61, - "src/renderer/src/components/right-sidebar/git-status-push-signal-refresh.test.ts": 76, - "src/renderer/src/components/right-sidebar/git-status-refresh-branch-line-total.test.ts": 318, - "src/renderer/src/components/right-sidebar/git-status-refresh-scheduler.test.ts": 80, - "src/renderer/src/components/right-sidebar/git-status-refresh.test.ts": 430, - "src/renderer/src/components/right-sidebar/github-pr-link-modal.test.ts": 48, - "src/renderer/src/components/right-sidebar/github-pr-stack-merge.test.ts": 91, - "src/renderer/src/components/right-sidebar/gitlab-mr-merge-state.test.ts": 112, - "src/renderer/src/components/right-sidebar/local-workspace-port-sections.test.ts": 55, - "src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts": 224, - "src/renderer/src/components/right-sidebar/parent-pr-checks-refresh.test.ts": 129, - "src/renderer/src/components/right-sidebar/parent-pr-checks-rows.test.ts": 133, - "src/renderer/src/components/right-sidebar/plugin-panel-activity-items.test.ts": 111, - "src/renderer/src/components/right-sidebar/plugin-panel-bridge-host.test.ts": 354, - "src/renderer/src/components/right-sidebar/plugin-panel-watchdog-visibility.test.ts": 404, - "src/renderer/src/components/right-sidebar/plugin-panel-watchdog.test.ts": 296, - "src/renderer/src/components/right-sidebar/ports-panel-host.test.ts": 319, - "src/renderer/src/components/right-sidebar/pr-comment-ack-summary.test.ts": 49, - "src/renderer/src/components/right-sidebar/pr-comment-presentation.test.ts": 167, - "src/renderer/src/components/right-sidebar/pr-comment-snapshotted-thread-resolver.test.ts": 58, - "src/renderer/src/components/right-sidebar/pr-comment-thread-resolution.test.ts": 51, - "src/renderer/src/components/right-sidebar/pr-comments-ai-launch-ack.test.ts": 113, - "src/renderer/src/components/right-sidebar/pr-comments-list-selection.test.tsx": 2618, - "src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.test.ts": 390, - "src/renderer/src/components/right-sidebar/review-cache-entry-selection.test.ts": 49, - "src/renderer/src/components/right-sidebar/right-panel-comment-composer.ime-enter.test.tsx": 297, - "src/renderer/src/components/right-sidebar/right-panel-comment-focus-timers.test.ts": 45, - "src/renderer/src/components/right-sidebar/right-sidebar-activity-visibility.test.ts": 106, - "src/renderer/src/components/right-sidebar/right-sidebar-effective-tab.test.ts": 50, - "src/renderer/src/components/right-sidebar/right-sidebar-primary-action-layout.test.ts": 52, - "src/renderer/src/components/right-sidebar/right-sidebar-titlebar-drag-regions.render.test.tsx": 718, - "src/renderer/src/components/right-sidebar/right-sidebar-titlebar-drag-regions.test.ts": 44, - "src/renderer/src/components/right-sidebar/right-sidebar-width.test.ts": 46, - "src/renderer/src/components/right-sidebar/runSourceControlAgentActionStart.test.ts": 218, - "src/renderer/src/components/right-sidebar/search-match-open.test.ts": 64, - "src/renderer/src/components/right-sidebar/search-rows.test.ts": 56, - "src/renderer/src/components/right-sidebar/source-control-action-recipe-match.test.ts": 270, - "src/renderer/src/components/right-sidebar/source-control-actions.test.ts": 53, - "src/renderer/src/components/right-sidebar/source-control-active-open-file-keys.test.ts": 47, - "src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.test.ts": 134, - "src/renderer/src/components/right-sidebar/source-control-branch-context-row.test.tsx": 259, - "src/renderer/src/components/right-sidebar/source-control-branch-context-stats.test.ts": 86, - "src/renderer/src/components/right-sidebar/source-control-branch-section-heading.test.tsx": 1703, - "src/renderer/src/components/right-sidebar/source-control-commit-eligibility.test.ts": 51, - "src/renderer/src/components/right-sidebar/source-control-commit-message-rows.test.ts": 110, - "src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.test.ts": 78, - "src/renderer/src/components/right-sidebar/source-control-create-review-blocked-action.test.ts": 93, - "src/renderer/src/components/right-sidebar/source-control-created-review-link.test.ts": 49, - "src/renderer/src/components/right-sidebar/source-control-discard-confirmation.test.ts": 87, - "src/renderer/src/components/right-sidebar/source-control-discard-dialog.test.tsx": 194, - "src/renderer/src/components/right-sidebar/source-control-discard-localization.test.ts": 353, - "src/renderer/src/components/right-sidebar/source-control-dropdown-items.create-pr-intent.test.ts": 131, - "src/renderer/src/components/right-sidebar/source-control-dropdown-items.test.ts": 160, - "src/renderer/src/components/right-sidebar/source-control-entry-actions.test.ts": 48, - "src/renderer/src/components/right-sidebar/source-control-entry-context-menu.test.tsx": 420, - "src/renderer/src/components/right-sidebar/source-control-file-filter.test.ts": 61, - "src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx": 372, - "src/renderer/src/components/right-sidebar/source-control-header-toolbar.test.ts": 211, - "src/renderer/src/components/right-sidebar/source-control-hosted-review-creation-eligibility-snapshot.test.ts": 71, - "src/renderer/src/components/right-sidebar/source-control-hosted-review-push-target.test.ts": 79, - "src/renderer/src/components/right-sidebar/source-control-huge-repo-warning-dismissals.test.ts": 154, - "src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.test.ts": 434, - "src/renderer/src/components/right-sidebar/source-control-manual-review-url.test.ts": 71, - "src/renderer/src/components/right-sidebar/source-control-primary-action-tooltip.test.ts": 113, - "src/renderer/src/components/right-sidebar/source-control-primary-action.create-pr-intent.test.ts": 143, - "src/renderer/src/components/right-sidebar/source-control-primary-action.test.ts": 148, - "src/renderer/src/components/right-sidebar/source-control-push-recovery.test.ts": 59, - "src/renderer/src/components/right-sidebar/source-control-section-order.test.ts": 67, - "src/renderer/src/components/right-sidebar/source-control-split-open.test.ts": 53, - "src/renderer/src/components/right-sidebar/source-control-status-sort.test.ts": 56, - "src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts": 72, - "src/renderer/src/components/right-sidebar/source-control-text-generation-defaults.test.ts": 270, - "src/renderer/src/components/right-sidebar/source-control-too-many-changes-banner.test.tsx": 421, - "src/renderer/src/components/right-sidebar/source-control-tree.test.ts": 75, - "src/renderer/src/components/right-sidebar/source-control/commit/discard-all-failure-description.test.tsx": 1485, - "src/renderer/src/components/right-sidebar/source-control/commit/rename-mutation-paths.test.ts": 55, - "src/renderer/src/components/right-sidebar/source-control/commit/source-control-discard-editor-owner.test.tsx": 315, - "src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts": 226, - "src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-mutation-failures.test.tsx": 323, - "src/renderer/src/components/right-sidebar/source-control/commit/use-bulk-actions-capped-stage.test.tsx": 320, - "src/renderer/src/components/right-sidebar/source-control/listing/section-action-buttons.test.tsx": 2110, - "src/renderer/src/components/right-sidebar/source-control/listing/section-header.test.tsx": 417, - "src/renderer/src/components/right-sidebar/source-control/listing/use-file-projection-work.test.tsx": 331, - "src/renderer/src/components/right-sidebar/source-control/listing/use-store-actions.store-subscriptions.test.tsx": 1360, - "src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.test.tsx": 300, - "src/renderer/src/components/right-sidebar/source-control/review/suppressed-github-pr.test.ts": 50, - "src/renderer/src/components/right-sidebar/source-control/review/use-action-model.test.tsx": 303, - "src/renderer/src/components/right-sidebar/source-control/review/use-hosted-review-creation.test.tsx": 1779, - "src/renderer/src/components/right-sidebar/source-control/review/use-hosted-review-state.test.tsx": 306, - "src/renderer/src/components/right-sidebar/source-control/review/use-pull-request-generation.test.tsx": 1604, - "src/renderer/src/components/right-sidebar/source-control/review/use-review-push-target.test.tsx": 251, - "src/renderer/src/components/right-sidebar/source-control/sync/git-history-format.test.ts": 69, - "src/renderer/src/components/right-sidebar/source-control/sync/git-history-graph-svg.test.tsx": 98, - "src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.local-ignore.test.tsx": 164, - "src/renderer/src/components/right-sidebar/status-display.test.ts": 51, - "src/renderer/src/components/right-sidebar/use-ai-vault-search-focus-request.test.tsx": 1381, - "src/renderer/src/components/right-sidebar/use-ai-vault-search.test.tsx": 453, - "src/renderer/src/components/right-sidebar/use-checks-panel-terminal-worktree.test.ts": 1614, - "src/renderer/src/components/right-sidebar/use-file-explorer-name-filter.runtime-listing.test.tsx": 1666, - "src/renderer/src/components/right-sidebar/use-file-explorer-name-filter.test.ts": 1698, - "src/renderer/src/components/right-sidebar/use-file-explorer-root-navigation.test.tsx": 297, - "src/renderer/src/components/right-sidebar/use-git-status-upstream-ref-watch.test.ts": 53, - "src/renderer/src/components/right-sidebar/use-hosted-review-actions.test.tsx": 297, - "src/renderer/src/components/right-sidebar/use-installed-plugin-route-reconciliation.test.tsx": 357, - "src/renderer/src/components/right-sidebar/use-persisted-ai-vault-view-options.test.tsx": 341, - "src/renderer/src/components/right-sidebar/use-plugin-panel-theme-revision.test.tsx": 480, - "src/renderer/src/components/right-sidebar/use-right-sidebar-tab-routing.effective-tab.test.tsx": 266, - "src/renderer/src/components/right-sidebar/use-source-control-ai.test.ts": 1903, - "src/renderer/src/components/right-sidebar/use-source-control-branch-compare.test.tsx": 421, - "src/renderer/src/components/right-sidebar/use-source-control-git-history.test.tsx": 303, - "src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.repo-default.test.ts": 1571, - "src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.test.ts": 1533, - "src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.os-file-drop.test.tsx": 2146, - "src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.test.ts": 52, - "src/renderer/src/components/right-sidebar/useFileExplorerHandlers.test.ts": 1433, - "src/renderer/src/components/right-sidebar/useFileExplorerImport.committed-scope.test.tsx": 291, - "src/renderer/src/components/right-sidebar/useFileExplorerImport.os-file-drop.test.tsx": 514, - "src/renderer/src/components/right-sidebar/useFileExplorerImport.remote.test.tsx": 1732, - "src/renderer/src/components/right-sidebar/useFileExplorerKeys.test.ts": 1412, - "src/renderer/src/components/right-sidebar/useFileExplorerReveal.scoped-root.test.tsx": 316, - "src/renderer/src/components/right-sidebar/useFileExplorerRowDrag.test.ts": 64, - "src/renderer/src/components/right-sidebar/useFileExplorerTree.lifecycle.test.tsx": 1983, - "src/renderer/src/components/right-sidebar/useFileExplorerTree.refresh-projection-churn.test.tsx": 1825, - "src/renderer/src/components/right-sidebar/useFileExplorerTree.scoped-root.test.tsx": 1837, - "src/renderer/src/components/right-sidebar/useFileExplorerTree.stale-dirs.test.tsx": 1690, - "src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.debounce.test.tsx": 1954, - "src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.test.ts": 1433, - "src/renderer/src/components/right-sidebar/useFileExplorerWatch.broker.test.tsx": 286, - "src/renderer/src/components/right-sidebar/useFileExplorerWatch.host-root.test.tsx": 569, - "src/renderer/src/components/right-sidebar/useFileExplorerWatch.pending-refresh.test.tsx": 346, - "src/renderer/src/components/right-sidebar/useFileExplorerWatch.test.ts": 1444, - "src/renderer/src/components/right-sidebar/useFileSearchPanel.lifecycle.test.tsx": 1892, - "src/renderer/src/components/right-sidebar/useFileSearchRunner.test.tsx": 582, - "src/renderer/src/components/right-sidebar/useGitStatusPolling.rerender.test.ts": 1604, - "src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts": 2655, - "src/renderer/src/components/right-sidebar/useHostedReviewStackParent.test.tsx": 296, - "src/renderer/src/components/right-sidebar/useSourceControlSelection.test.ts": 63, - "src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.test.tsx": 254, - "src/renderer/src/components/settings/AccountsPane.owner-scope.test.tsx": 1292, - "src/renderer/src/components/settings/AccountsPane.section-lifetime.test.tsx": 2440, - "src/renderer/src/components/settings/AccountsPane.test.tsx": 2349, - "src/renderer/src/components/settings/AdvancedNetworkSettingsSection.interaction.test.tsx": 1600, - "src/renderer/src/components/settings/AdvancedNetworkSettingsSection.test.ts": 1496, - "src/renderer/src/components/settings/AdvancedPane.test.tsx": 496, - "src/renderer/src/components/settings/AgentSkillSetupPanel.freshness.test.tsx": 1728, - "src/renderer/src/components/settings/AgentSkillSetupPanel.test.tsx": 2128, - "src/renderer/src/components/settings/AgentsPane.test.tsx": 2229, - "src/renderer/src/components/settings/AntigravityAccountsSection.test.tsx": 743, - "src/renderer/src/components/settings/AppearanceChatContrastControls.draft.test.tsx": 697, - "src/renderer/src/components/settings/AppearanceChatContrastControls.test.tsx": 1066, - "src/renderer/src/components/settings/AppearanceChatSection.test.tsx": 1335, - "src/renderer/src/components/settings/AppearancePane.test.tsx": 1175, - "src/renderer/src/components/settings/ArtifactsSettingsPane.test.tsx": 859, - "src/renderer/src/components/settings/AutomationsSettingsPane.test.tsx": 735, - "src/renderer/src/components/settings/BranchPrefixFeedback.test.tsx": 116, - "src/renderer/src/components/settings/BrowserClientHostedRemoteSetting.test.tsx": 710, - "src/renderer/src/components/settings/BrowserPane.search-catalog.test.tsx": 564, - "src/renderer/src/components/settings/BrowserPane.test.ts": 49, - "src/renderer/src/components/settings/BrowserSshWorkspaceRoutingSetting.test.tsx": 619, - "src/renderer/src/components/settings/BrowserUseSkillStep.test.tsx": 162, - "src/renderer/src/components/settings/BrowserUserAgentSetting.test.tsx": 1795, - "src/renderer/src/components/settings/ChatNamingSetting.test.tsx": 1784, - "src/renderer/src/components/settings/ChatSettingsSection.inline-visuals.test.tsx": 1723, - "src/renderer/src/components/settings/ChatSettingsSection.navigation.test.tsx": 1343, - "src/renderer/src/components/settings/ChatSettingsSection.test.tsx": 1939, - "src/renderer/src/components/settings/CliSection.install-failure.test.tsx": 2113, - "src/renderer/src/components/settings/CliSection.test.tsx": 1847, - "src/renderer/src/components/settings/CliSkillRuntimeSetup.test.tsx": 1626, - "src/renderer/src/components/settings/CloudVmSetupGuide.test.tsx": 1517, - "src/renderer/src/components/settings/CodexLoginLinkNotice.test.tsx": 463, - "src/renderer/src/components/settings/CommitMessageAiPane.test.tsx": 1859, - "src/renderer/src/components/settings/DefaultWindowsProjectRuntimeSetting.test.tsx": 459, - "src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx": 1979, - "src/renderer/src/components/settings/DiffShowWhitespaceSetting.test.tsx": 558, - "src/renderer/src/components/settings/EditorWordWrapSetting.test.tsx": 685, - "src/renderer/src/components/settings/EphemeralVmRuntimesSection.test.tsx": 786, - "src/renderer/src/components/settings/EphemeralVmsPane.test.tsx": 7418, - "src/renderer/src/components/settings/ExperimentalPane.test.tsx": 1697, - "src/renderer/src/components/settings/FloatingWorkspacePane.test.tsx": 1674, - "src/renderer/src/components/settings/GeneralPane.section-lifetime.test.tsx": 1367, - "src/renderer/src/components/settings/GeneralPane.test.ts": 9929, - "src/renderer/src/components/settings/GeneralRemoteServerUpdates.test.tsx": 306, - "src/renderer/src/components/settings/GeneralUpdateSettingsSection.test.tsx": 1953, - "src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.test.tsx": 1880, - "src/renderer/src/components/settings/GhosttyImportModal.test.ts": 213, - "src/renderer/src/components/settings/GitPane.test.ts": 1505, - "src/renderer/src/components/settings/GitPane.test.tsx": 1506, - "src/renderer/src/components/settings/GrokAccountsSection.test.tsx": 384, - "src/renderer/src/components/settings/HiddenExperimentalGroup.test.tsx": 326, - "src/renderer/src/components/settings/KagiSessionLinkForm.test.ts": 1476, - "src/renderer/src/components/settings/LinearAgentSkillGuide.test.tsx": 227, - "src/renderer/src/components/settings/LinearAgentSkillNotes.test.tsx": 152, - "src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx": 672, - "src/renderer/src/components/settings/LocalNetworkConnectionTest.test.tsx": 432, - "src/renderer/src/components/settings/MachineNameField.test.tsx": 531, - "src/renderer/src/components/settings/ManagedServerRow.test.tsx": 401, - "src/renderer/src/components/settings/McpConfigSection.host-owner.test.tsx": 1848, - "src/renderer/src/components/settings/MobileEmulatorAgentControlRow.test.tsx": 1509, - "src/renderer/src/components/settings/MobilePairingConnectionOptions.test.tsx": 551, - "src/renderer/src/components/settings/MobilePairingQrSection.test.tsx": 539, - "src/renderer/src/components/settings/MobilePairingSetupSection.test.tsx": 1031, - "src/renderer/src/components/settings/MobilePane.test.tsx": 1213, - "src/renderer/src/components/settings/MobilePaneAddressSearch.test.tsx": 635, - "src/renderer/src/components/settings/NativeChatExperimentalSetting.test.tsx": 2559, - "src/renderer/src/components/settings/NativeChatSupportedAgents.test.tsx": 564, - "src/renderer/src/components/settings/NotificationHostToggles.test.tsx": 610, - "src/renderer/src/components/settings/NotificationsPane.test.tsx": 2062, - "src/renderer/src/components/settings/OpenInMenuSetting.test.ts": 233, - "src/renderer/src/components/settings/OrcaAccountSettingsPane.test.tsx": 516, - "src/renderer/src/components/settings/OrchestrationPane.test.tsx": 1871, - "src/renderer/src/components/settings/OrchestrationSkillAgentCoverage.test.tsx": 207, - "src/renderer/src/components/settings/PluginConsentDialog.test.tsx": 2096, - "src/renderer/src/components/settings/PluginInstallDialog.test.tsx": 803, - "src/renderer/src/components/settings/PluginKeybindingConsentPreview.test.ts": 1327, - "src/renderer/src/components/settings/PluginMarketplaceBrowser.test.tsx": 804, - "src/renderer/src/components/settings/PluginMarketplaceSourceDialog.test.tsx": 533, - "src/renderer/src/components/settings/PluginSettingsRow.test.tsx": 642, - "src/renderer/src/components/settings/PluginsSettingsSection.lifecycle.test.tsx": 1128, - "src/renderer/src/components/settings/PrivacyPane.test.ts": 1570, - "src/renderer/src/components/settings/ProjectWindowsRuntimeSetting.test.tsx": 709, - "src/renderer/src/components/settings/QuickCommandsList.test.tsx": 604, - "src/renderer/src/components/settings/QuickCommandsPane.test.ts": 1851, - "src/renderer/src/components/settings/RemoteServerUpdateDialog.test.tsx": 335, - "src/renderer/src/components/settings/RepositoryForkSyncSection.test.tsx": 685, - "src/renderer/src/components/settings/RepositoryGitHubAccountSection.inventory-lifetime.test.tsx": 475, - "src/renderer/src/components/settings/RepositoryGitHubAccountSection.test.tsx": 479, - "src/renderer/src/components/settings/RepositoryHooksSection.test.ts": 799, - "src/renderer/src/components/settings/RepositoryHostSetupsSection.settings-entry.test.tsx": 1925, - "src/renderer/src/components/settings/RepositoryHostSetupsSection.test.tsx": 2009, - "src/renderer/src/components/settings/RepositoryHostSetupsSection.workspace-window.test.tsx": 1574, - "src/renderer/src/components/settings/RepositoryIconEmojiPicker.test.tsx": 262, - "src/renderer/src/components/settings/RepositoryIconPicker.github-avatar-refresh.test.tsx": 542, - "src/renderer/src/components/settings/RepositoryPane.test.ts": 2984, - "src/renderer/src/components/settings/RepositoryPaneDraftInput.test.tsx": 258, - "src/renderer/src/components/settings/RepositorySourceControlAiSection.test.ts": 466, + "src/renderer/src/components/right-sidebar/PortsPanel.test.tsx": 1357, + "src/renderer/src/components/right-sidebar/PullRequestComposer.generate-tooltip.test.tsx": 982, + "src/renderer/src/components/right-sidebar/SearchResultItems.test.tsx": 144, + "src/renderer/src/components/right-sidebar/SessionRowTrailingActions.test.tsx": 270, + "src/renderer/src/components/right-sidebar/SourceControl.branch-line-total.test.tsx": 3151, + "src/renderer/src/components/right-sidebar/SourceControl.commit-drafts.test.ts": 141, + "src/renderer/src/components/right-sidebar/SourceControl.commit-failure-recovery.test.ts": 56, + "src/renderer/src/components/right-sidebar/SourceControl.commit-generation-records.test.ts": 39, + "src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts": 148, + "src/renderer/src/components/right-sidebar/SourceControl.hosted-review-header-link.test.tsx": 56, + "src/renderer/src/components/right-sidebar/SourceControl.open-file-highlight.test.tsx": 3419, + "src/renderer/src/components/right-sidebar/SourceControl.pr-generation-records.test.ts": 105, + "src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx": 2053, + "src/renderer/src/components/right-sidebar/SourceControl.push-failure-recovery.test.ts": 31, + "src/renderer/src/components/right-sidebar/SourceControl.remote-action-errors.test.ts": 43, + "src/renderer/src/components/right-sidebar/SourceControl.resolve-conflicts-prompt.test.ts": 48, + "src/renderer/src/components/right-sidebar/SourceControl.virtual-file-list.test.tsx": 4347, + "src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx": 2372, + "src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.test.tsx": 181, + "src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialog.test.ts": 410, + "src/renderer/src/components/right-sidebar/active-checks-status.test.ts": 1406, + "src/renderer/src/components/right-sidebar/activity-bar-buttons.test.tsx": 278, + "src/renderer/src/components/right-sidebar/activity-bar-overflow.test.ts": 37, + "src/renderer/src/components/right-sidebar/ai-vault-first-prompt-card.test.tsx": 414, + "src/renderer/src/components/right-sidebar/ai-vault-host-scope.test.ts": 1625, + "src/renderer/src/components/right-sidebar/ai-vault-original-pane-actions.test.ts": 313, + "src/renderer/src/components/right-sidebar/ai-vault-original-pane-index.test.ts": 55, + "src/renderer/src/components/right-sidebar/ai-vault-original-pane.test.ts": 51, + "src/renderer/src/components/right-sidebar/ai-vault-panel-session-list-request.test.tsx": 2030, + "src/renderer/src/components/right-sidebar/ai-vault-scan-issue-state.test.ts": 49, + "src/renderer/src/components/right-sidebar/ai-vault-scope-paths.test.ts": 76, + "src/renderer/src/components/right-sidebar/ai-vault-scope-state.test.ts": 40, + "src/renderer/src/components/right-sidebar/ai-vault-search-scope-identity.test.tsx": 450, + "src/renderer/src/components/right-sidebar/ai-vault-search-session.test.ts": 245, + "src/renderer/src/components/right-sidebar/ai-vault-session-chat-action.test.ts": 1255, + "src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.test.ts": 1496, + "src/renderer/src/components/right-sidebar/ai-vault-session-conditional-refresh-hook.test.tsx": 1235, + "src/renderer/src/components/right-sidebar/ai-vault-session-conditional-refresh.test.ts": 189, + "src/renderer/src/components/right-sidebar/ai-vault-session-continuation.test.ts": 55, + "src/renderer/src/components/right-sidebar/ai-vault-session-deletability.test.ts": 328, + "src/renderer/src/components/right-sidebar/ai-vault-session-display.test.ts": 32, + "src/renderer/src/components/right-sidebar/ai-vault-session-filters.test.ts": 87, + "src/renderer/src/components/right-sidebar/ai-vault-session-identity.test.ts": 35, + "src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.test.ts": 1558, + "src/renderer/src/components/right-sidebar/ai-vault-session-log-open.test.ts": 376, + "src/renderer/src/components/right-sidebar/ai-vault-session-path-actions.test.ts": 271, + "src/renderer/src/components/right-sidebar/ai-vault-session-projects.test.ts": 85, + "src/renderer/src/components/right-sidebar/ai-vault-session-publication-gate.test.ts": 178, + "src/renderer/src/components/right-sidebar/ai-vault-session-refresh.test.ts": 1830, + "src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.test.ts": 1298, + "src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.test.ts": 617, + "src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat.test.ts": 77, + "src/renderer/src/components/right-sidebar/ai-vault-session-resume.test.ts": 1442, + "src/renderer/src/components/right-sidebar/ai-vault-session-surface-switch.test.ts": 1331, + "src/renderer/src/components/right-sidebar/ai-vault-session-worktree-map.test.tsx": 1595, + "src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts": 149, + "src/renderer/src/components/right-sidebar/ai-vault-view-defaults.test.ts": 45, + "src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.test.ts": 57, + "src/renderer/src/components/right-sidebar/branch-line-total-request-gate.test.ts": 29, + "src/renderer/src/components/right-sidebar/check-details-resize.test.ts": 26, + "src/renderer/src/components/right-sidebar/checks-entry-refresh.test.ts": 38, + "src/renderer/src/components/right-sidebar/checks-list-expanded-details.test.tsx": 819, + "src/renderer/src/components/right-sidebar/checks-panel-async-result-key.test.ts": 37, + "src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx": 2011, + "src/renderer/src/components/right-sidebar/checks-panel-empty-state.test.ts": 169, + "src/renderer/src/components/right-sidebar/checks-panel-git-status-snapshot.test.ts": 53, + "src/renderer/src/components/right-sidebar/checks-panel-hosted-review-click-routing.test.ts": 46, + "src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-breadcrumb.test.ts": 55, + "src/renderer/src/components/right-sidebar/checks-panel-review-creation.test.ts": 46, + "src/renderer/src/components/right-sidebar/checks-panel-review-lookup-authority.test.ts": 46, + "src/renderer/src/components/right-sidebar/checks-panel-review.test.ts": 106, + "src/renderer/src/components/right-sidebar/checks-panel-terminal-worktree.test.ts": 67, + "src/renderer/src/components/right-sidebar/checks-panel/checks-detail-polling-policy.test.ts": 44, + "src/renderer/src/components/right-sidebar/checks-panel/gitlab-review-client.test.ts": 260, + "src/renderer/src/components/right-sidebar/checks-panel/panel-content-rendering.test.tsx": 4621, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-check-and-review-actions.test.tsx": 2052, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-create-review.test.tsx": 1694, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-foreground-effects.test.tsx": 278, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-generation.test.tsx": 1614, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-git-status-effects.test.tsx": 220, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-manual-refresh.test.tsx": 1654, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-polling-stability.test.tsx": 1612, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-polling.test.tsx": 1996, + "src/renderer/src/components/right-sidebar/coalesced-poll-runner.test.ts": 50, + "src/renderer/src/components/right-sidebar/commit-failure-dialog-state.test.ts": 38, + "src/renderer/src/components/right-sidebar/commit-failure-summary.test.ts": 44, + "src/renderer/src/components/right-sidebar/create-review-draft-title.test.ts": 86, + "src/renderer/src/components/right-sidebar/diff-comments-clear-dialog-state.test.ts": 29, + "src/renderer/src/components/right-sidebar/discard-all-sequence.test.ts": 54, + "src/renderer/src/components/right-sidebar/file-explorer-add-project-action.test.ts": 50, + "src/renderer/src/components/right-sidebar/file-explorer-batch-deletion.test.ts": 124, + "src/renderer/src/components/right-sidebar/file-explorer-copy-file-owner.test.ts": 39, + "src/renderer/src/components/right-sidebar/file-explorer-delete-editor-owner.test.tsx": 1512, + "src/renderer/src/components/right-sidebar/file-explorer-dir-load-tracker.test.ts": 36, + "src/renderer/src/components/right-sidebar/file-explorer-dir-toggle-timing.test.ts": 171, + "src/renderer/src/components/right-sidebar/file-explorer-directory-filename-identity.test.ts": 1252, + "src/renderer/src/components/right-sidebar/file-explorer-directory-listing.test.ts": 1394, + "src/renderer/src/components/right-sidebar/file-explorer-display-root.test.ts": 1453, + "src/renderer/src/components/right-sidebar/file-explorer-drag-scroll-marker.test.tsx": 1874, + "src/renderer/src/components/right-sidebar/file-explorer-entries.test.ts": 43, + "src/renderer/src/components/right-sidebar/file-explorer-expanded-dirs-refresh.test.ts": 1394, + "src/renderer/src/components/right-sidebar/file-explorer-inline-input-ime-enter.test.tsx": 650, + "src/renderer/src/components/right-sidebar/file-explorer-inline-input-outside-click.test.tsx": 277, + "src/renderer/src/components/right-sidebar/file-explorer-inline-rename-flow.test.tsx": 738, + "src/renderer/src/components/right-sidebar/file-explorer-instant-dir-toggle.test.ts": 386, + "src/renderer/src/components/right-sidebar/file-explorer-keyboard-navigation.test.ts": 55, + "src/renderer/src/components/right-sidebar/file-explorer-linked-recovery.test.tsx": 1586, + "src/renderer/src/components/right-sidebar/file-explorer-linked-watch.test.ts": 1472, + "src/renderer/src/components/right-sidebar/file-explorer-name-filter-projection.test.ts": 74, + "src/renderer/src/components/right-sidebar/file-explorer-operation-generation.test.ts": 1376, + "src/renderer/src/components/right-sidebar/file-explorer-operation-owner.test.ts": 405, + "src/renderer/src/components/right-sidebar/file-explorer-paths.test.ts": 38, + "src/renderer/src/components/right-sidebar/file-explorer-read-error.test.ts": 35, + "src/renderer/src/components/right-sidebar/file-explorer-refresh-concurrency.test.ts": 38, + "src/renderer/src/components/right-sidebar/file-explorer-replace-selection-order-budget.test.tsx": 351, + "src/renderer/src/components/right-sidebar/file-explorer-reset.test.ts": 33, + "src/renderer/src/components/right-sidebar/file-explorer-row-context-menu.test.tsx": 437, + "src/renderer/src/components/right-sidebar/file-explorer-row-projection.test.ts": 42, + "src/renderer/src/components/right-sidebar/file-explorer-selection.test.ts": 40, + "src/renderer/src/components/right-sidebar/file-explorer-stale-dir-cache.test.ts": 39, + "src/renderer/src/components/right-sidebar/file-explorer-stale-retry.test.tsx": 1573, + "src/renderer/src/components/right-sidebar/file-explorer-watch-drive-root.test.ts": 1295, + "src/renderer/src/components/right-sidebar/file-explorer-watch-filename-identity.test.ts": 1279, + "src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.test.ts": 1602, + "src/renderer/src/components/right-sidebar/file-explorer-watch-refresh-scheduler.test.ts": 103, + "src/renderer/src/components/right-sidebar/file-explorer-watch-selected-host.test.ts": 1277, + "src/renderer/src/components/right-sidebar/file-search-include-pattern.test.ts": 49, + "src/renderer/src/components/right-sidebar/folder-workspace-attached-worktrees.test.ts": 59, + "src/renderer/src/components/right-sidebar/fork-push-target-label.test.ts": 28, + "src/renderer/src/components/right-sidebar/git-status-push-signal-refresh.test.ts": 109, + "src/renderer/src/components/right-sidebar/git-status-refresh-branch-line-total.test.ts": 364, + "src/renderer/src/components/right-sidebar/git-status-refresh-scheduler.test.ts": 63, + "src/renderer/src/components/right-sidebar/git-status-refresh.test.ts": 348, + "src/renderer/src/components/right-sidebar/github-pr-link-modal.test.ts": 32, + "src/renderer/src/components/right-sidebar/github-pr-stack-merge.test.ts": 71, + "src/renderer/src/components/right-sidebar/gitlab-mr-merge-state.test.ts": 54, + "src/renderer/src/components/right-sidebar/local-workspace-port-sections.test.ts": 46, + "src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts": 291, + "src/renderer/src/components/right-sidebar/parent-pr-checks-refresh.test.ts": 122, + "src/renderer/src/components/right-sidebar/parent-pr-checks-rows.test.ts": 159, + "src/renderer/src/components/right-sidebar/plugin-panel-activity-items.test.ts": 50, + "src/renderer/src/components/right-sidebar/plugin-panel-bridge-host.test.ts": 300, + "src/renderer/src/components/right-sidebar/plugin-panel-watchdog-visibility.test.ts": 415, + "src/renderer/src/components/right-sidebar/plugin-panel-watchdog.test.ts": 256, + "src/renderer/src/components/right-sidebar/ports-panel-host.test.ts": 293, + "src/renderer/src/components/right-sidebar/pr-comment-ack-summary.test.ts": 47, + "src/renderer/src/components/right-sidebar/pr-comment-presentation.test.ts": 178, + "src/renderer/src/components/right-sidebar/pr-comment-snapshotted-thread-resolver.test.ts": 51, + "src/renderer/src/components/right-sidebar/pr-comment-thread-resolution.test.ts": 35, + "src/renderer/src/components/right-sidebar/pr-comments-ai-launch-ack.test.ts": 84, + "src/renderer/src/components/right-sidebar/pr-comments-list-selection.test.tsx": 2722, + "src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.test.ts": 312, + "src/renderer/src/components/right-sidebar/review-cache-entry-selection.test.ts": 44, + "src/renderer/src/components/right-sidebar/right-panel-comment-composer.ime-enter.test.tsx": 329, + "src/renderer/src/components/right-sidebar/right-panel-comment-focus-timers.test.ts": 28, + "src/renderer/src/components/right-sidebar/right-sidebar-activity-visibility.test.ts": 53, + "src/renderer/src/components/right-sidebar/right-sidebar-effective-tab.test.ts": 51, + "src/renderer/src/components/right-sidebar/right-sidebar-primary-action-layout.test.ts": 34, + "src/renderer/src/components/right-sidebar/right-sidebar-titlebar-drag-regions.render.test.tsx": 946, + "src/renderer/src/components/right-sidebar/right-sidebar-titlebar-drag-regions.test.ts": 34, + "src/renderer/src/components/right-sidebar/right-sidebar-width.test.ts": 35, + "src/renderer/src/components/right-sidebar/runSourceControlAgentActionStart.test.ts": 198, + "src/renderer/src/components/right-sidebar/search-match-open.test.ts": 41, + "src/renderer/src/components/right-sidebar/search-rows.test.ts": 35, + "src/renderer/src/components/right-sidebar/source-control-action-recipe-match.test.ts": 277, + "src/renderer/src/components/right-sidebar/source-control-actions.test.ts": 28, + "src/renderer/src/components/right-sidebar/source-control-active-open-file-keys.test.ts": 42, + "src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.test.ts": 104, + "src/renderer/src/components/right-sidebar/source-control-branch-context-row.test.tsx": 224, + "src/renderer/src/components/right-sidebar/source-control-branch-context-stats.test.ts": 73, + "src/renderer/src/components/right-sidebar/source-control-branch-section-heading.test.tsx": 1485, + "src/renderer/src/components/right-sidebar/source-control-commit-eligibility.test.ts": 35, + "src/renderer/src/components/right-sidebar/source-control-commit-message-rows.test.ts": 75, + "src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.test.ts": 52, + "src/renderer/src/components/right-sidebar/source-control-create-review-blocked-action.test.ts": 77, + "src/renderer/src/components/right-sidebar/source-control-created-review-link.test.ts": 42, + "src/renderer/src/components/right-sidebar/source-control-discard-confirmation.test.ts": 77, + "src/renderer/src/components/right-sidebar/source-control-discard-dialog.test.tsx": 127, + "src/renderer/src/components/right-sidebar/source-control-discard-localization.test.ts": 350, + "src/renderer/src/components/right-sidebar/source-control-dropdown-items.create-pr-intent.test.ts": 98, + "src/renderer/src/components/right-sidebar/source-control-dropdown-items.test.ts": 234, + "src/renderer/src/components/right-sidebar/source-control-entry-actions.test.ts": 40, + "src/renderer/src/components/right-sidebar/source-control-entry-context-menu.test.tsx": 407, + "src/renderer/src/components/right-sidebar/source-control-file-filter.test.ts": 49, + "src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx": 226, + "src/renderer/src/components/right-sidebar/source-control-header-toolbar.test.ts": 156, + "src/renderer/src/components/right-sidebar/source-control-hosted-review-creation-eligibility-snapshot.test.ts": 58, + "src/renderer/src/components/right-sidebar/source-control-hosted-review-push-target.test.ts": 52, + "src/renderer/src/components/right-sidebar/source-control-huge-repo-warning-dismissals.test.ts": 134, + "src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.test.ts": 403, + "src/renderer/src/components/right-sidebar/source-control-manual-review-url.test.ts": 63, + "src/renderer/src/components/right-sidebar/source-control-primary-action-tooltip.test.ts": 95, + "src/renderer/src/components/right-sidebar/source-control-primary-action.create-pr-intent.test.ts": 128, + "src/renderer/src/components/right-sidebar/source-control-primary-action.test.ts": 139, + "src/renderer/src/components/right-sidebar/source-control-push-recovery.test.ts": 49, + "src/renderer/src/components/right-sidebar/source-control-section-order.test.ts": 51, + "src/renderer/src/components/right-sidebar/source-control-split-open.test.ts": 42, + "src/renderer/src/components/right-sidebar/source-control-status-sort.test.ts": 36, + "src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts": 70, + "src/renderer/src/components/right-sidebar/source-control-text-generation-defaults.test.ts": 265, + "src/renderer/src/components/right-sidebar/source-control-too-many-changes-banner.test.tsx": 411, + "src/renderer/src/components/right-sidebar/source-control-tree.test.ts": 51, + "src/renderer/src/components/right-sidebar/source-control/commit/discard-all-failure-description.test.tsx": 1645, + "src/renderer/src/components/right-sidebar/source-control/commit/rename-mutation-paths.test.ts": 44, + "src/renderer/src/components/right-sidebar/source-control/commit/source-control-discard-editor-owner.test.tsx": 534, + "src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts": 209, + "src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-mutation-failures.test.tsx": 601, + "src/renderer/src/components/right-sidebar/source-control/commit/use-bulk-actions-capped-stage.test.tsx": 1610, + "src/renderer/src/components/right-sidebar/source-control/listing/section-action-buttons.test.tsx": 1771, + "src/renderer/src/components/right-sidebar/source-control/listing/section-header.test.tsx": 362, + "src/renderer/src/components/right-sidebar/source-control/listing/use-file-projection-work.test.tsx": 322, + "src/renderer/src/components/right-sidebar/source-control/listing/use-store-actions.store-subscriptions.test.tsx": 1484, + "src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.test.tsx": 272, + "src/renderer/src/components/right-sidebar/source-control/review/suppressed-github-pr.test.ts": 33, + "src/renderer/src/components/right-sidebar/source-control/review/use-action-model.test.tsx": 299, + "src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-jira-title.test.tsx": 1786, + "src/renderer/src/components/right-sidebar/source-control/review/use-hosted-review-creation.test.tsx": 1590, + "src/renderer/src/components/right-sidebar/source-control/review/use-hosted-review-state.test.tsx": 262, + "src/renderer/src/components/right-sidebar/source-control/review/use-pull-request-generation.test.tsx": 1676, + "src/renderer/src/components/right-sidebar/source-control/review/use-review-push-target.test.tsx": 250, + "src/renderer/src/components/right-sidebar/source-control/sync/git-history-format.test.ts": 46, + "src/renderer/src/components/right-sidebar/source-control/sync/git-history-graph-svg.test.tsx": 97, + "src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.local-ignore.test.tsx": 221, + "src/renderer/src/components/right-sidebar/status-display.test.ts": 37, + "src/renderer/src/components/right-sidebar/use-ai-vault-search-focus-request.test.tsx": 1727, + "src/renderer/src/components/right-sidebar/use-ai-vault-search.test.tsx": 509, + "src/renderer/src/components/right-sidebar/use-checks-panel-terminal-worktree.test.ts": 1576, + "src/renderer/src/components/right-sidebar/use-create-jira-review-title.test.tsx": 1440, + "src/renderer/src/components/right-sidebar/use-file-explorer-name-filter.runtime-listing.test.tsx": 1694, + "src/renderer/src/components/right-sidebar/use-file-explorer-name-filter.test.ts": 1548, + "src/renderer/src/components/right-sidebar/use-file-explorer-root-navigation.test.tsx": 312, + "src/renderer/src/components/right-sidebar/use-generate-before-create-pull-request-context.test.tsx": 1281, + "src/renderer/src/components/right-sidebar/use-git-status-upstream-ref-watch.test.ts": 60, + "src/renderer/src/components/right-sidebar/use-hosted-review-actions.test.tsx": 508, + "src/renderer/src/components/right-sidebar/use-installed-plugin-route-reconciliation.test.tsx": 408, + "src/renderer/src/components/right-sidebar/use-persisted-ai-vault-view-options.test.tsx": 296, + "src/renderer/src/components/right-sidebar/use-plugin-panel-theme-revision.test.tsx": 477, + "src/renderer/src/components/right-sidebar/use-right-sidebar-tab-routing.effective-tab.test.tsx": 239, + "src/renderer/src/components/right-sidebar/use-source-control-ai.test.ts": 1543, + "src/renderer/src/components/right-sidebar/use-source-control-branch-compare.test.tsx": 600, + "src/renderer/src/components/right-sidebar/use-source-control-git-history.test.tsx": 1641, + "src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.repo-default.test.ts": 1606, + "src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.test.ts": 1517, + "src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.os-file-drop.test.tsx": 1659, + "src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.test.ts": 32, + "src/renderer/src/components/right-sidebar/useFileExplorerHandlers.test.ts": 1682, + "src/renderer/src/components/right-sidebar/useFileExplorerImport.committed-scope.test.tsx": 372, + "src/renderer/src/components/right-sidebar/useFileExplorerImport.os-file-drop.test.tsx": 398, + "src/renderer/src/components/right-sidebar/useFileExplorerImport.remote.test.tsx": 1727, + "src/renderer/src/components/right-sidebar/useFileExplorerKeys.test.ts": 1454, + "src/renderer/src/components/right-sidebar/useFileExplorerReveal.scoped-root.test.tsx": 323, + "src/renderer/src/components/right-sidebar/useFileExplorerRowDrag.test.ts": 72, + "src/renderer/src/components/right-sidebar/useFileExplorerTree.lifecycle.test.tsx": 1748, + "src/renderer/src/components/right-sidebar/useFileExplorerTree.refresh-projection-churn.test.tsx": 1939, + "src/renderer/src/components/right-sidebar/useFileExplorerTree.scoped-root.test.tsx": 1330, + "src/renderer/src/components/right-sidebar/useFileExplorerTree.stale-dirs.test.tsx": 1625, + "src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.debounce.test.tsx": 1722, + "src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.test.ts": 1421, + "src/renderer/src/components/right-sidebar/useFileExplorerWatch.broker.test.tsx": 317, + "src/renderer/src/components/right-sidebar/useFileExplorerWatch.host-root.test.tsx": 596, + "src/renderer/src/components/right-sidebar/useFileExplorerWatch.pending-refresh.test.tsx": 301, + "src/renderer/src/components/right-sidebar/useFileExplorerWatch.test.ts": 1400, + "src/renderer/src/components/right-sidebar/useFileSearchPanel.lifecycle.test.tsx": 1764, + "src/renderer/src/components/right-sidebar/useFileSearchRunner.test.tsx": 645, + "src/renderer/src/components/right-sidebar/useGitStatusPolling.rerender.test.ts": 1659, + "src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts": 2419, + "src/renderer/src/components/right-sidebar/useHostedReviewStackParent.test.tsx": 282, + "src/renderer/src/components/right-sidebar/useSourceControlSelection.test.ts": 45, + "src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.test.tsx": 1633, + "src/renderer/src/components/settings/AccountsPane.owner-scope.test.tsx": 1189, + "src/renderer/src/components/settings/AccountsPane.section-lifetime.test.tsx": 2420, + "src/renderer/src/components/settings/AccountsPane.test.tsx": 2623, + "src/renderer/src/components/settings/AdvancedNetworkSettingsSection.interaction.test.tsx": 1666, + "src/renderer/src/components/settings/AdvancedNetworkSettingsSection.test.ts": 1263, + "src/renderer/src/components/settings/AdvancedPane.test.tsx": 460, + "src/renderer/src/components/settings/AgentLaunchDefaultsEditor.test.tsx": 568, + "src/renderer/src/components/settings/AgentLaunchSettingsSection.test.tsx": 588, + "src/renderer/src/components/settings/AgentSkillSetupPanel.freshness.test.tsx": 1484, + "src/renderer/src/components/settings/AgentSkillSetupPanel.test.tsx": 1770, + "src/renderer/src/components/settings/AgentsPane.test.tsx": 1928, + "src/renderer/src/components/settings/AntigravityAccountsSection.test.tsx": 661, + "src/renderer/src/components/settings/AppearanceChatContrastControls.draft.test.tsx": 828, + "src/renderer/src/components/settings/AppearanceChatContrastControls.test.tsx": 1178, + "src/renderer/src/components/settings/AppearanceChatSection.test.tsx": 1282, + "src/renderer/src/components/settings/AppearancePane.test.tsx": 946, + "src/renderer/src/components/settings/ArtifactsSettingsPane.test.tsx": 845, + "src/renderer/src/components/settings/AutomationsSettingsPane.test.tsx": 748, + "src/renderer/src/components/settings/BranchPrefixFeedback.test.tsx": 68, + "src/renderer/src/components/settings/BrowserClientHostedRemoteSetting.test.tsx": 601, + "src/renderer/src/components/settings/BrowserPane.search-catalog.test.tsx": 600, + "src/renderer/src/components/settings/BrowserPane.test.ts": 25, + "src/renderer/src/components/settings/BrowserSshWorkspaceRoutingSetting.test.tsx": 558, + "src/renderer/src/components/settings/BrowserUseSkillStep.test.tsx": 116, + "src/renderer/src/components/settings/BrowserUserAgentSetting.test.tsx": 1791, + "src/renderer/src/components/settings/ChatBehaviorSetting.test.tsx": 2255, + "src/renderer/src/components/settings/ChatNamingSetting.test.tsx": 1643, + "src/renderer/src/components/settings/ChatPermissionSetting.test.tsx": 701, + "src/renderer/src/components/settings/ChatSettingsSection.inline-visuals.test.tsx": 1725, + "src/renderer/src/components/settings/ChatSettingsSection.navigation.test.tsx": 1526, + "src/renderer/src/components/settings/ChatSettingsSection.test.tsx": 2327, + "src/renderer/src/components/settings/CliSection.install-failure.test.tsx": 1756, + "src/renderer/src/components/settings/CliSection.test.tsx": 1761, + "src/renderer/src/components/settings/CliSkillRuntimeSetup.test.tsx": 1408, + "src/renderer/src/components/settings/CloudVmSetupGuide.test.tsx": 1506, + "src/renderer/src/components/settings/CodexLoginLinkNotice.test.tsx": 384, + "src/renderer/src/components/settings/CommitMessageAiPane.test.tsx": 1994, + "src/renderer/src/components/settings/DefaultWindowsProjectRuntimeSetting.test.tsx": 409, + "src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx": 1714, + "src/renderer/src/components/settings/DiffShowWhitespaceSetting.test.tsx": 533, + "src/renderer/src/components/settings/EditorWordWrapSetting.test.tsx": 591, + "src/renderer/src/components/settings/EphemeralVmRuntimesSection.test.tsx": 578, + "src/renderer/src/components/settings/EphemeralVmsPane.test.tsx": 1248, + "src/renderer/src/components/settings/ExperimentalPane.test.tsx": 1165, + "src/renderer/src/components/settings/FloatingWorkspacePane.test.tsx": 1528, + "src/renderer/src/components/settings/GeneralPane.section-lifetime.test.tsx": 1377, + "src/renderer/src/components/settings/GeneralPane.test.ts": 1394, + "src/renderer/src/components/settings/GeneralRemoteServerUpdates.test.tsx": 243, + "src/renderer/src/components/settings/GeneralUpdateSettingsSection.test.tsx": 1689, + "src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.test.tsx": 1801, + "src/renderer/src/components/settings/GhosttyImportModal.test.ts": 157, + "src/renderer/src/components/settings/GitPane.test.ts": 1655, + "src/renderer/src/components/settings/GitPane.test.tsx": 1442, + "src/renderer/src/components/settings/GrokAccountsSection.test.tsx": 373, + "src/renderer/src/components/settings/HiddenExperimentalGroup.test.tsx": 361, + "src/renderer/src/components/settings/HostAgentLaunchSettings.test.tsx": 2818, + "src/renderer/src/components/settings/KagiSessionLinkForm.test.ts": 1230, + "src/renderer/src/components/settings/LinearAgentSkillGuide.test.tsx": 173, + "src/renderer/src/components/settings/LinearAgentSkillNotes.test.tsx": 123, + "src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx": 687, + "src/renderer/src/components/settings/LocalNetworkConnectionTest.test.tsx": 374, + "src/renderer/src/components/settings/MachineNameField.test.tsx": 479, + "src/renderer/src/components/settings/ManagedServerRow.test.tsx": 503, + "src/renderer/src/components/settings/McpConfigSection.host-owner.test.tsx": 1738, + "src/renderer/src/components/settings/MobileEmulatorAgentControlRow.test.tsx": 1506, + "src/renderer/src/components/settings/MobilePairingConnectionOptions.test.tsx": 622, + "src/renderer/src/components/settings/MobilePairingQrSection.test.tsx": 477, + "src/renderer/src/components/settings/MobilePairingSetupSection.test.tsx": 1086, + "src/renderer/src/components/settings/MobilePane.test.tsx": 1559, + "src/renderer/src/components/settings/MobilePaneAddressSearch.test.tsx": 551, + "src/renderer/src/components/settings/NativeChatSupportedAgents.test.tsx": 608, + "src/renderer/src/components/settings/NotificationHostToggles.test.tsx": 603, + "src/renderer/src/components/settings/NotificationsPane.test.tsx": 1361, + "src/renderer/src/components/settings/OpenInMenuSetting.test.ts": 179, + "src/renderer/src/components/settings/OrcaAccountSettingsPane.test.tsx": 487, + "src/renderer/src/components/settings/OrchestrationPane.test.tsx": 2064, + "src/renderer/src/components/settings/OrchestrationSkillAgentCoverage.test.tsx": 168, + "src/renderer/src/components/settings/PluginConsentDialog.test.tsx": 2027, + "src/renderer/src/components/settings/PluginInstallDialog.test.tsx": 780, + "src/renderer/src/components/settings/PluginKeybindingConsentPreview.test.ts": 1415, + "src/renderer/src/components/settings/PluginMarketplaceBrowser.test.tsx": 689, + "src/renderer/src/components/settings/PluginMarketplaceSourceDialog.test.tsx": 493, + "src/renderer/src/components/settings/PluginSettingsRow.test.tsx": 582, + "src/renderer/src/components/settings/PluginsSettingsSection.lifecycle.test.tsx": 1212, + "src/renderer/src/components/settings/PrivacyPane.test.ts": 1509, + "src/renderer/src/components/settings/ProjectWindowsRuntimeSetting.test.tsx": 724, + "src/renderer/src/components/settings/QuickCommandsList.test.tsx": 442, + "src/renderer/src/components/settings/QuickCommandsPane.test.ts": 1701, + "src/renderer/src/components/settings/RemoteServerUpdateDialog.test.tsx": 333, + "src/renderer/src/components/settings/RemoteServerUpdateStatus.test.ts": 75, + "src/renderer/src/components/settings/RepositoryForkSyncSection.test.tsx": 750, + "src/renderer/src/components/settings/RepositoryGitHubAccountSection.inventory-lifetime.test.tsx": 578, + "src/renderer/src/components/settings/RepositoryGitHubAccountSection.test.tsx": 506, + "src/renderer/src/components/settings/RepositoryHooksSection.test.ts": 776, + "src/renderer/src/components/settings/RepositoryHostSetupsSection.settings-entry.test.tsx": 1648, + "src/renderer/src/components/settings/RepositoryHostSetupsSection.test.tsx": 1935, + "src/renderer/src/components/settings/RepositoryHostSetupsSection.workspace-window.test.tsx": 1452, + "src/renderer/src/components/settings/RepositoryIconEmojiPicker.test.tsx": 252, + "src/renderer/src/components/settings/RepositoryIconPicker.github-avatar-refresh.test.tsx": 494, + "src/renderer/src/components/settings/RepositoryJiraProjectSection.test.tsx": 309, + "src/renderer/src/components/settings/RepositoryPane.test.ts": 3208, + "src/renderer/src/components/settings/RepositoryPaneDraftInput.test.tsx": 318, + "src/renderer/src/components/settings/RepositorySourceControlAiSection.test.ts": 357, "src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx": 415, - "src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts": 120, - "src/renderer/src/components/settings/RuntimeHostAccessForm.test.tsx": 405, - "src/renderer/src/components/settings/RuntimePairingGeneratorForm.test.tsx": 315, - "src/renderer/src/components/settings/RuntimePairingUrlGenerator.test.tsx": 335, - "src/renderer/src/components/settings/SessionHistoryComputerRow.test.tsx": 967, - "src/renderer/src/components/settings/SessionHistorySettingsPane.test.tsx": 1217, - "src/renderer/src/components/settings/Settings.load-performance.test.ts": 54, - "src/renderer/src/components/settings/SettingsConstants.test.ts": 285, - "src/renderer/src/components/settings/SettingsFormControls.font-autocomplete.test.tsx": 797, - "src/renderer/src/components/settings/SettingsFormControls.number-field.test.tsx": 622, - "src/renderer/src/components/settings/SettingsFormControls.segmented-control.test.tsx": 662, - "src/renderer/src/components/settings/SettingsSidebar.test.tsx": 2372, - "src/renderer/src/components/settings/ShareSkillsSettingsPane.test.tsx": 832, - "src/renderer/src/components/settings/ShortcutCommandBlock.test.tsx": 1771, - "src/renderer/src/components/settings/ShortcutFilterRail.test.ts": 215, - "src/renderer/src/components/settings/SourceControlActionRepoOverrideNote.test.tsx": 399, - "src/renderer/src/components/settings/SparsePresetSettingsSection.test.tsx": 633, - "src/renderer/src/components/settings/SshHostDeltaMoveDialog.test.tsx": 450, - "src/renderer/src/components/settings/SshTargetCard.test.tsx": 430, - "src/renderer/src/components/settings/SshTargetForm.test.tsx": 1002, - "src/renderer/src/components/settings/TaskSourceLinearSetup.test.tsx": 484, - "src/renderer/src/components/settings/TaskSourceProviderCard.test.tsx": 376, - "src/renderer/src/components/settings/TaskSourceShowInTasksStep.test.tsx": 196, - "src/renderer/src/components/settings/TaskSourceSimpleSetup.test.tsx": 355, - "src/renderer/src/components/settings/TasksPane.test.tsx": 926, - "src/renderer/src/components/settings/TerminalAdvancedSection.test.tsx": 1998, - "src/renderer/src/components/settings/TerminalAppearanceSection.ghostty.test.ts": 499, - "src/renderer/src/components/settings/TerminalContrastSetting.test.tsx": 692, - "src/renderer/src/components/settings/TerminalFontSizeSetting.test.tsx": 1643, - "src/renderer/src/components/settings/TerminalPane.pwsh.test.ts": 489, - "src/renderer/src/components/settings/TerminalSettingsPreview.lifecycle.test.tsx": 407, - "src/renderer/src/components/settings/TerminalTccAttributionNotice.test.tsx": 322, - "src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts": 1446, - "src/renderer/src/components/settings/UnsealedCredentialNotice.test.tsx": 313, - "src/renderer/src/components/settings/VoiceMicrophoneSetting.test.tsx": 945, - "src/renderer/src/components/settings/VoicePane.test.tsx": 902, - "src/renderer/src/components/settings/VoiceSpeechModelSection.test.tsx": 552, - "src/renderer/src/components/settings/WorkspaceDirectorySetting.test.tsx": 1671, - "src/renderer/src/components/settings/ZcodePlanAccountsSection.test.tsx": 873, - "src/renderer/src/components/settings/accounts-pane-claude-section.test.tsx": 1765, - "src/renderer/src/components/settings/accounts-pane-opencode-credentials.test.ts": 435, - "src/renderer/src/components/settings/accounts-search.test.ts": 61, - "src/renderer/src/components/settings/agent-availability-settings.test.ts": 297, - "src/renderer/src/components/settings/agent-default-env-draft.test.ts": 60, - "src/renderer/src/components/settings/appearance-interface-summary.test.ts": 321, - "src/renderer/src/components/settings/appearance-search.test.ts": 556, - "src/renderer/src/components/settings/appearance-status-bar-search.test.ts": 117, - "src/renderer/src/components/settings/appearance-usage-percentage-search.test.ts": 109, - "src/renderer/src/components/settings/bitbucket-integration-card.test.tsx": 466, - "src/renderer/src/components/settings/browser-cookie-import-label.test.ts": 150, - "src/renderer/src/components/settings/browser-link-routing-localization.test.ts": 307, - "src/renderer/src/components/settings/browser-search.test.ts": 234, - "src/renderer/src/components/settings/browser-session-host-selection.test.ts": 47, - "src/renderer/src/components/settings/chat-naming-persistence.test.tsx": 3409, - "src/renderer/src/components/settings/chat-settings-unsaved-guard.test.tsx": 1734, - "src/renderer/src/components/settings/cli-install-failure.test.ts": 60, - "src/renderer/src/components/settings/cli-source-control-integration-cards.test.tsx": 414, - "src/renderer/src/components/settings/codex-account-auth-warning.test.ts": 60, - "src/renderer/src/components/settings/codex-config-sync-warning.test.ts": 42, - "src/renderer/src/components/settings/codex-session-source-home-control.test.tsx": 164, - "src/renderer/src/components/settings/developer-permissions-search.test.ts": 318, - "src/renderer/src/components/settings/general-editor-search.test.ts": 140, - "src/renderer/src/components/settings/general-preview-tab-search.test.ts": 101, - "src/renderer/src/components/settings/host-scoped-setting-scope.test.ts": 60, - "src/renderer/src/components/settings/integrations-pane-status.test.ts": 45, - "src/renderer/src/components/settings/jira-integration-card.test.tsx": 467, - "src/renderer/src/components/settings/linear-agent-skill-install-cta.test.tsx": 1710, - "src/renderer/src/components/settings/managed-server-copy.test.ts": 85, - "src/renderer/src/components/settings/mobile-network-interface-selection.test.ts": 71, - "src/renderer/src/components/settings/mobile-pairing-device-polling.test.ts": 67, - "src/renderer/src/components/settings/mobile-pane-search.test.ts": 92, - "src/renderer/src/components/settings/native-chat-experimental-search-entry.test.ts": 112, - "src/renderer/src/components/settings/notification-host-pending-save.test.tsx": 1929, - "src/renderer/src/components/settings/osc52-clipboard-copy.test.ts": 277, - "src/renderer/src/components/settings/plugin-install-source.test.ts": 297, - "src/renderer/src/components/settings/provider-account-scope.test.ts": 86, - "src/renderer/src/components/settings/provider-account-visibility.test.ts": 59, - "src/renderer/src/components/settings/provider-rate-limit-scope-panels.test.tsx": 248, - "src/renderer/src/components/settings/repository-hook-settings-draft.test.ts": 290, - "src/renderer/src/components/settings/repository-host-setup-options.test.ts": 290, - "src/renderer/src/components/settings/repository-icon-github.test.ts": 62, - "src/renderer/src/components/settings/repository-source-control-ai-global-ux.test.ts": 429, - "src/renderer/src/components/settings/repository-source-control-ai-persist-queue.test.ts": 94, - "src/renderer/src/components/settings/runtime-server-row-unverifiable-host.test.tsx": 1730, - "src/renderer/src/components/settings/runtime-server-workflow-sections.test.tsx": 377, - "src/renderer/src/components/settings/session-history-status-copy.test.ts": 282, - "src/renderer/src/components/settings/session-search-computer-rollup.test.ts": 45, - "src/renderer/src/components/settings/setting-ownership.test.ts": 96, - "src/renderer/src/components/settings/settings-deep-link-target-watcher.test.ts": 187, - "src/renderer/src/components/settings/settings-form-option-filter.test.ts": 69, - "src/renderer/src/components/settings/settings-host-scope.test.ts": 94, - "src/renderer/src/components/settings/settings-project-checkout-target.test.ts": 70, - "src/renderer/src/components/settings/settings-project-list.test.ts": 94, - "src/renderer/src/components/settings/settings-project-regrouping.test.ts": 550, - "src/renderer/src/components/settings/settings-search-keywords.test.ts": 137, - "src/renderer/src/components/settings/settings-search.test.ts": 63, - "src/renderer/src/components/settings/settings-setup-guide-progress-hook.test.tsx": 95, - "src/renderer/src/components/settings/settings-setup-guide-progress.test.ts": 1583, - "src/renderer/src/components/settings/shortcut-binding-list-mutations.test.ts": 57, - "src/renderer/src/components/settings/shortcut-definition-catalog.test.ts": 316, - "src/renderer/src/components/settings/shortcut-groups.test.ts": 143, - "src/renderer/src/components/settings/shortcut-recording-state.test.ts": 51, - "src/renderer/src/components/settings/shortcut-row-visibility.test.ts": 179, - "src/renderer/src/components/settings/source-control-ai-settings-persistence.test.tsx": 3192, - "src/renderer/src/components/settings/sparse-preset-date.test.ts": 66, - "src/renderer/src/components/settings/sparse-preset-operation-error.test.ts": 53, - "src/renderer/src/ssh/ssh-host-server-status-copy.test.ts": 107, - "src/renderer/src/components/settings/ssh-target-action-state.test.ts": 47, - "src/renderer/src/components/settings/ssh-target-draft.test.ts": 66, - "src/renderer/src/components/settings/ssh-target-save-payload.test.ts": 114, - "src/renderer/src/components/settings/task-source-setup-state.test.ts": 62, - "src/renderer/src/components/settings/task-tracker-integration-cards.test.tsx": 565, - "src/renderer/src/components/settings/terminal-preview-content.test.ts": 55, - "src/renderer/src/components/settings/terminal-search.test.ts": 304, - "src/renderer/src/components/settings/use-debounced-settings-text-draft.test.ts": 297, - "src/renderer/src/components/settings/use-mac-captured-digit-chords.test.ts": 706, - "src/renderer/src/components/settings/use-published-machine-name.test.tsx": 269, - "src/renderer/src/components/settings/use-repository-hook-settings-draft.test.tsx": 461, - "src/renderer/src/components/settings/use-runtime-environment-catalog.test.tsx": 1802, - "src/renderer/src/components/settings/use-session-search-status.test.tsx": 560, - "src/renderer/src/components/settings/use-task-source-provider-readiness.test.tsx": 245, - "src/renderer/src/components/settings/use-zcode-plan-credentials.test.tsx": 890, - "src/renderer/src/components/settings/useGhosttyImport.test.ts": 84, - "src/renderer/src/components/settings/useWarpThemeImport.test.ts": 111, - "src/renderer/src/components/settings/worktree-symlink-path-filter.scan-budget.test.ts": 104, - "src/renderer/src/components/settings/worktree-symlink-path-filter.test.ts": 56, - "src/renderer/src/components/setup-guide/SetupGuideModal.mount-gating.test.tsx": 1712, - "src/renderer/src/components/setup-guide/use-setup-guide-progress.test.ts": 1636, - "src/renderer/src/components/setup-guide/use-setup-guide-telemetry.test.ts": 57, - "src/renderer/src/components/shared/MacFolderAccessFixDialog.test.tsx": 1061, - "src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts": 2003, - "src/renderer/src/components/shared/useDaemonActions.test.tsx": 448, - "src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx": 195, - "src/renderer/src/components/sidebar/AddRemoteHostDialog.config-picker.test.tsx": 1551, - "src/renderer/src/components/sidebar/AddRemoteHostFields.test.tsx": 541, - "src/renderer/src/components/sidebar/AddRepoCreateStep.test.tsx": 317, - "src/renderer/src/components/sidebar/AddRepoDialog.default-checkout.test.ts": 59, - "src/renderer/src/components/sidebar/AddRepoDialogChrome.test.tsx": 632, - "src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx": 558, - "src/renderer/src/components/sidebar/AddRepoHostSelector.test.tsx": 228, - "src/renderer/src/components/sidebar/AddRepoNestedImportStep.test.tsx": 492, - "src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx": 659, - "src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts": 141, - "src/renderer/src/components/sidebar/AgentDashboardSidebarHost.test.tsx": 1722, - "src/renderer/src/components/sidebar/AutoRenameFailedDialog.test.tsx": 505, - "src/renderer/src/components/sidebar/CacheTimer.test.tsx": 154, - "src/renderer/src/components/sidebar/CommentMarkdown.github-attachment-image.test.tsx": 1722, - "src/renderer/src/components/sidebar/CommentMarkdown.github-callout.test.tsx": 2046, - "src/renderer/src/components/sidebar/CommentMarkdown.growing.test.tsx": 2136, - "src/renderer/src/components/sidebar/CommentMarkdown.link-click.test.tsx": 2032, - "src/renderer/src/components/sidebar/CommentMarkdown.ordered-list.test.tsx": 2756, - "src/renderer/src/components/sidebar/CommentMarkdown.test.tsx": 2088, - "src/renderer/src/components/sidebar/CommentMarkdown.word-fade.test.tsx": 1979, - "src/renderer/src/components/sidebar/CommentMermaidBlock.system-theme.test.tsx": 290, - "src/renderer/src/components/sidebar/DeleteNestedWorktreesDialog.test.tsx": 878, - "src/renderer/src/components/sidebar/DeleteWorktreeDialog.test.tsx": 1146, - "src/renderer/src/components/sidebar/DeleteWorktreeDialogFooter.test.tsx": 613, - "src/renderer/src/components/sidebar/DeleteWorktreeTargetPreview.test.tsx": 699, - "src/renderer/src/components/sidebar/FilterToggleRow.test.tsx": 307, - "src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.test.tsx": 1535, - "src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.reminder-toast.test.tsx": 1917, - "src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.test.tsx": 2535, - "src/renderer/src/components/sidebar/LocalGitToolchainScanBanner.test.tsx": 1490, - "src/renderer/src/components/sidebar/MarkdownImageLightbox.test.tsx": 589, - "src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.test.tsx": 1727, - "src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx": 1115, - "src/renderer/src/components/sidebar/NoticeHostGlyph.test.tsx": 1056, - "src/renderer/src/components/sidebar/OrcaYamlTrustDialog.test.tsx": 94, - "src/renderer/src/components/sidebar/ProjectAddedDialog.test.tsx": 108, - "src/renderer/src/components/sidebar/ProjectGroupDeleteDialog.test.tsx": 561, - "src/renderer/src/components/sidebar/ProjectHeaderActions.test.tsx": 194, - "src/renderer/src/components/sidebar/RemoteFileBrowser.paste.test.tsx": 510, - "src/renderer/src/components/sidebar/RemoveFolderDialog.test.tsx": 425, - "src/renderer/src/components/sidebar/SetupGuideSidebarEntry.test.tsx": 353, - "src/renderer/src/components/sidebar/SetupScriptPromptCard.test.ts": 54, - "src/renderer/src/components/sidebar/SetupScriptPromptCardShell.test.tsx": 484, - "src/renderer/src/components/sidebar/Sidebar.test.tsx": 840, - "src/renderer/src/components/sidebar/SidebarAgentsList.test.tsx": 2753, - "src/renderer/src/components/sidebar/SidebarFeedbackDialog.test.tsx": 3074, - "src/renderer/src/components/sidebar/SidebarGroupByToggle.test.tsx": 456, - "src/renderer/src/components/sidebar/SidebarHeader.test.tsx": 407, - "src/renderer/src/components/sidebar/SidebarNav.test.tsx": 1772, - "src/renderer/src/components/sidebar/SidebarRepositoryFilterSection.test.tsx": 598, - "src/renderer/src/components/sidebar/SidebarSettingsHelpMenu.test.tsx": 605, - "src/renderer/src/components/sidebar/SidebarToolbar.test.tsx": 392, - "src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.test.tsx": 459, - "src/renderer/src/components/sidebar/StatusIndicator.test.ts": 201, - "src/renderer/src/components/sidebar/WorkspaceAttachmentOrigins.test.tsx": 589, - "src/renderer/src/components/sidebar/WorkspaceAttachmentSearch.resolution.test.tsx": 2470, - "src/renderer/src/components/sidebar/WorkspaceKanbanCard.host-identity.test.tsx": 367, - "src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.mount-gating.test.tsx": 1879, - "src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.search.test.tsx": 2008, - "src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.task-status-sync.test.tsx": 1680, - "src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.test.tsx": 1665, - "src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.test.tsx": 304, - "src/renderer/src/components/sidebar/WorkspaceKanbanLaneGrid.test.tsx": 387, - "src/renderer/src/components/sidebar/WorkspaceKanbanSearchField.test.tsx": 379, - "src/renderer/src/components/sidebar/WorkspaceKanbanSettingsMenu.test.tsx": 826, - "src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.test.tsx": 339, - "src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx": 1949, - "src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx": 3248, - "src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx": 1908, - "src/renderer/src/components/sidebar/WorktreeCard.delete-failed.test.tsx": 1127, - "src/renderer/src/components/sidebar/WorktreeCard.host-badge-toggle.test.tsx": 1664, - "src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx": 1698, - "src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx": 1106, - "src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx": 1239, - "src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx": 1332, - "src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx": 1162, - "src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx": 1696, - "src/renderer/src/components/sidebar/WorktreeCard.single-reference-details.test.tsx": 1881, - "src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx": 1238, - "src/renderer/src/components/sidebar/WorktreeCard.test.ts": 3227, - "src/renderer/src/components/sidebar/WorktreeCardAgents.acknowledgement-emphasis.test.tsx": 1133, - "src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx": 965, - "src/renderer/src/components/sidebar/WorktreeCardAgents.expansion-remount.test.tsx": 1237, - "src/renderer/src/components/sidebar/WorktreeCardAgents.send-target.test.tsx": 1319, - "src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx": 977, - "src/renderer/src/components/sidebar/WorktreeCardAutomationDetailSection.test.tsx": 418, - "src/renderer/src/components/sidebar/WorktreeCardDisplayMenuSection.test.tsx": 473, - "src/renderer/src/components/sidebar/WorktreeCardLinkedItemsSection.test.tsx": 2662, - "src/renderer/src/components/sidebar/WorktreeCardMeta.interaction.test.tsx": 2731, - "src/renderer/src/components/sidebar/WorktreeCardMeta.test.tsx": 2991, - "src/renderer/src/components/sidebar/WorktreeCardPorts.test.tsx": 1396, - "src/renderer/src/components/sidebar/WorktreeCardSshHostControl.test.tsx": 1913, - "src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx": 387, - "src/renderer/src/components/sidebar/WorktreeContextMenu.copy-worktree-name.test.tsx": 1913, - "src/renderer/src/components/sidebar/WorktreeContextMenu.delete-shortcut.test.tsx": 1311, - "src/renderer/src/components/sidebar/WorktreeContextMenu.react185-bystander.test.tsx": 2062, - "src/renderer/src/components/sidebar/WorktreeContextMenu.test.ts": 2108, - "src/renderer/src/components/sidebar/WorktreeDeveloperMenu.test.tsx": 146, - "src/renderer/src/components/sidebar/WorktreeDeveloperMenuReveal.test.tsx": 586, - "src/renderer/src/components/sidebar/WorktreeList.card-memo-stability.test.tsx": 2031, - "src/renderer/src/components/sidebar/WorktreeList.empty-project-rows.test.ts": 2233, - "src/renderer/src/components/sidebar/WorktreeList.folder-workspace-rows.test.ts": 2667, - "src/renderer/src/components/sidebar/WorktreeList.group-headers.test.ts": 3532, - "src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx": 4327, - "src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts": 3044, - "src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx": 1805, - "src/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsx": 2913, - "src/renderer/src/components/sidebar/WorktreeMetaDialog.host-scope.test.tsx": 2087, - "src/renderer/src/components/sidebar/WorktreeMetaDialog.test.tsx": 6094, - "src/renderer/src/components/sidebar/WorktreeOpenInMenu.test.tsx": 195, - "src/renderer/src/components/sidebar/WorktreeParentPickerPopover.test.ts": 1624, - "src/renderer/src/components/sidebar/WorktreeReferenceStack.test.tsx": 414, - "src/renderer/src/components/sidebar/WorktreeTitleInlineRename.begin-editing.test.tsx": 120, - "src/renderer/src/components/sidebar/WorktreeTitleInlineRename.editor-lifecycle.test.tsx": 439, - "src/renderer/src/components/sidebar/WorktreeTitleInlineRename.test.tsx": 265, - "src/renderer/src/components/sidebar/WorktreeVisibilityDialog.test.tsx": 2342, - "src/renderer/src/components/sidebar/WorktreeVisibilityHelpPopover.test.tsx": 450, - "src/renderer/src/components/sidebar/WorktreeVisibilitySourceList.test.tsx": 460, - "src/renderer/src/components/sidebar/active-worktree-focus-after-delete.test.ts": 84, - "src/renderer/src/components/sidebar/add-remote-host-ssh-actions.test.ts": 139, - "src/renderer/src/components/sidebar/add-repo-browse-authority.test.ts": 49, - "src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.test.ts": 59, - "src/renderer/src/components/sidebar/add-repo-skip-finalization.test.ts": 81, - "src/renderer/src/components/sidebar/agent-send-picker-collapse.test.tsx": 1815, - "src/renderer/src/components/sidebar/child-workspaces-toggle-target.test.ts": 1879, - "src/renderer/src/components/sidebar/child-workspaces-toggle-visibility.test.ts": 2328, - "src/renderer/src/components/sidebar/clone-defaults.test.ts": 59, - "src/renderer/src/components/sidebar/create-project-defaults.test.ts": 53, - "src/renderer/src/components/sidebar/default-branch-folder-project-root.test.ts": 1426, - "src/renderer/src/components/sidebar/default-branch-visible-under-hide-sleeping.test.ts": 1666, - "src/renderer/src/components/sidebar/delete-worktree-always-force-preference.test.tsx": 2527, - "src/renderer/src/components/sidebar/delete-worktree-dirty-change-counts.test.ts": 81, - "src/renderer/src/components/sidebar/delete-worktree-dirty-change-hydration.test.tsx": 644, - "src/renderer/src/components/sidebar/delete-worktree-failure-toast.test.tsx": 2640, - "src/renderer/src/components/sidebar/delete-worktree-flow.test.ts": 914, - "src/renderer/src/components/sidebar/delete-worktree-parallel-flow.test.ts": 521, - "src/renderer/src/components/sidebar/delete-worktree-toast.test.ts": 81, - "src/renderer/src/components/sidebar/empty-project-placeholder-repos.test.ts": 63, - "src/renderer/src/components/sidebar/focused-agent-row-highlight.test.ts": 1445, - "src/renderer/src/components/sidebar/folder-workspace-card-pr-display.test.ts": 130, - "src/renderer/src/components/sidebar/folder-workspace-composer-expiry.test.tsx": 2194, - "src/renderer/src/components/sidebar/folder-workspace-composer-helpers.test.ts": 91, - "src/renderer/src/components/sidebar/folder-workspace-composer-path-status.test.tsx": 1765, - "src/renderer/src/components/sidebar/folder-workspace-composer-submit-host-platform.test.ts": 2163, - "src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts": 2015, - "src/renderer/src/components/sidebar/folder-workspace-linked-startup-plan.test.ts": 1762, - "src/renderer/src/components/sidebar/host-header-drag.test.tsx": 275, - "src/renderer/src/components/sidebar/host-header-menu-items.test.ts": 53, - "src/renderer/src/components/sidebar/host-rename-remove.test.ts": 53, - "src/renderer/src/components/sidebar/host-section-collapse-folders.test.ts": 184, - "src/renderer/src/components/sidebar/host-section-collapse.test.ts": 195, - "src/renderer/src/components/sidebar/host-section-folder-workspace-counts.test.ts": 53, - "src/renderer/src/components/sidebar/host-section-order.test.ts": 47, - "src/renderer/src/components/sidebar/host-section-reveal.test.tsx": 2061, - "src/renderer/src/components/sidebar/host-section-rows.test.ts": 179, - "src/renderer/src/components/sidebar/hovered-workspace-delete.test.ts": 2113, - "src/renderer/src/components/sidebar/imported-worktrees-card-actions.test.ts": 63, - "src/renderer/src/components/sidebar/imported-worktrees-card-candidates.test.ts": 75, - "src/renderer/src/components/sidebar/kanban-empty-selection-prune-budget.test.tsx": 1098, - "src/renderer/src/components/sidebar/linear-agent-skill-runtime.shell-override.test.ts": 82, - "src/renderer/src/components/sidebar/linear-agent-skill-setup-reminders.test.ts": 44, + "src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts": 280, + "src/renderer/src/components/settings/RuntimeHostAccessForm.test.tsx": 334, + "src/renderer/src/components/settings/RuntimePairingGeneratorForm.test.tsx": 260, + "src/renderer/src/components/settings/RuntimePairingUrlGenerator.test.tsx": 365, + "src/renderer/src/components/settings/SessionHistoryComputerRow.test.tsx": 816, + "src/renderer/src/components/settings/SessionHistorySettingsPane.test.tsx": 1332, + "src/renderer/src/components/settings/Settings.load-performance.test.ts": 43, + "src/renderer/src/components/settings/SettingsConstants.test.ts": 267, + "src/renderer/src/components/settings/SettingsFormControls.font-autocomplete.test.tsx": 850, + "src/renderer/src/components/settings/SettingsFormControls.number-field.test.tsx": 571, + "src/renderer/src/components/settings/SettingsFormControls.segmented-control.test.tsx": 598, + "src/renderer/src/components/settings/SettingsSidebar.test.tsx": 2294, + "src/renderer/src/components/settings/ShareSkillsSettingsPane.test.tsx": 848, + "src/renderer/src/components/settings/ShortcutCommandBlock.test.tsx": 1568, + "src/renderer/src/components/settings/ShortcutFilterRail.test.ts": 139, + "src/renderer/src/components/settings/SourceControlActionRepoOverrideNote.test.tsx": 418, + "src/renderer/src/components/settings/SparsePresetSettingsSection.test.tsx": 653, + "src/renderer/src/components/settings/SshHostDeltaMoveDialog.test.tsx": 364, + "src/renderer/src/components/settings/SshPane.test.tsx": 931, + "src/renderer/src/components/settings/SshTargetCard.test.tsx": 453, + "src/renderer/src/components/settings/SshTargetForm.test.tsx": 975, + "src/renderer/src/components/settings/TaskSourceLinearSetup.test.tsx": 434, + "src/renderer/src/components/settings/TaskSourceProviderCard.test.tsx": 301, + "src/renderer/src/components/settings/TaskSourceShowInTasksStep.test.tsx": 174, + "src/renderer/src/components/settings/TaskSourceSimpleSetup.test.tsx": 321, + "src/renderer/src/components/settings/TasksPane.test.tsx": 694, + "src/renderer/src/components/settings/TerminalAdvancedSection.test.tsx": 1811, + "src/renderer/src/components/settings/TerminalAppearanceSection.ghostty.test.ts": 461, + "src/renderer/src/components/settings/TerminalContrastSetting.test.tsx": 761, + "src/renderer/src/components/settings/TerminalFontSizeSetting.test.tsx": 1648, + "src/renderer/src/components/settings/TerminalPane.pwsh.test.ts": 441, + "src/renderer/src/components/settings/TerminalSettingsPreview.lifecycle.test.tsx": 376, + "src/renderer/src/components/settings/TerminalTccAttributionNotice.test.tsx": 293, + "src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts": 1568, + "src/renderer/src/components/settings/UnsealedCredentialNotice.test.tsx": 270, + "src/renderer/src/components/settings/VoiceDictationSettingsSection.test.tsx": 593, + "src/renderer/src/components/settings/VoiceMicrophoneSetting.test.tsx": 982, + "src/renderer/src/components/settings/VoicePane.test.tsx": 801, + "src/renderer/src/components/settings/VoiceSpeechModelSection.test.tsx": 467, + "src/renderer/src/components/settings/WorkspaceDirectorySetting.test.tsx": 1697, + "src/renderer/src/components/settings/ZcodeCliSetupSection.test.tsx": 609, + "src/renderer/src/components/settings/ZcodePlanAccountsSection.test.tsx": 990, + "src/renderer/src/components/settings/accounts-pane-claude-section.test.tsx": 1317, + "src/renderer/src/components/settings/accounts-pane-opencode-credentials.test.ts": 396, + "src/renderer/src/components/settings/accounts-search.test.ts": 48, + "src/renderer/src/components/settings/agent-availability-settings.test.ts": 287, + "src/renderer/src/components/settings/agent-default-env-draft.test.ts": 31, + "src/renderer/src/components/settings/agent-launch-settings-transport.test.ts": 43, + "src/renderer/src/components/settings/appearance-interface-summary.test.ts": 302, + "src/renderer/src/components/settings/appearance-search.test.ts": 1011, + "src/renderer/src/components/settings/appearance-status-bar-search.test.ts": 52, + "src/renderer/src/components/settings/appearance-usage-percentage-search.test.ts": 57, + "src/renderer/src/components/settings/bitbucket-integration-card.test.tsx": 384, + "src/renderer/src/components/settings/browser-link-routing-localization.test.ts": 268, + "src/renderer/src/components/settings/browser-search.test.ts": 298, + "src/renderer/src/components/settings/browser-session-host-selection.test.ts": 40, + "src/renderer/src/components/settings/chat-naming-persistence.test.tsx": 2985, + "src/renderer/src/components/settings/chat-settings-unsaved-guard.test.tsx": 1585, + "src/renderer/src/components/settings/cli-install-failure.test.ts": 36, + "src/renderer/src/components/settings/cli-source-control-integration-cards.test.tsx": 343, + "src/renderer/src/components/settings/codex-account-auth-warning.test.ts": 30, + "src/renderer/src/components/settings/codex-config-sync-warning.test.ts": 39, + "src/renderer/src/components/settings/codex-session-source-home-control.test.tsx": 132, + "src/renderer/src/components/settings/developer-permissions-search.test.ts": 307, + "src/renderer/src/components/settings/general-editor-search.test.ts": 104, + "src/renderer/src/components/settings/general-preview-tab-search.test.ts": 95, + "src/renderer/src/components/settings/git-description-templates.test.tsx": 2060, + "src/renderer/src/components/settings/host-scoped-setting-scope.test.ts": 35, + "src/renderer/src/components/settings/integrations-pane-status.test.ts": 33, + "src/renderer/src/components/settings/jira-integration-card.test.tsx": 493, + "src/renderer/src/components/settings/linear-agent-skill-install-cta.test.tsx": 1610, + "src/renderer/src/components/settings/managed-server-copy.test.ts": 62, + "src/renderer/src/components/settings/mobile-network-interface-selection.test.ts": 56, + "src/renderer/src/components/settings/mobile-pairing-device-polling.test.ts": 38, + "src/renderer/src/components/settings/mobile-pane-search.test.ts": 80, + "src/renderer/src/components/settings/native-chat-experimental-search-entry.test.ts": 75, + "src/renderer/src/components/settings/notification-host-pending-save.test.tsx": 1629, + "src/renderer/src/components/settings/plugin-install-source.test.ts": 227, + "src/renderer/src/components/settings/provider-account-scope.test.ts": 75, + "src/renderer/src/components/settings/provider-account-visibility.test.ts": 44, + "src/renderer/src/components/settings/provider-rate-limit-scope-panels.test.tsx": 320, + "src/renderer/src/components/settings/repository-description-templates.test.tsx": 783, + "src/renderer/src/components/settings/repository-hook-settings-draft.test.ts": 409, + "src/renderer/src/components/settings/repository-host-setup-options.test.ts": 268, + "src/renderer/src/components/settings/repository-icon-github.test.ts": 49, + "src/renderer/src/components/settings/repository-source-control-ai-global-ux.test.ts": 389, + "src/renderer/src/components/settings/repository-source-control-ai-persist-queue.test.ts": 86, + "src/renderer/src/components/settings/runtime-server-row-unverifiable-host.test.tsx": 1776, + "src/renderer/src/components/settings/runtime-server-workflow-sections.test.tsx": 300, + "src/renderer/src/components/settings/session-history-status-copy.test.ts": 243, + "src/renderer/src/components/settings/session-search-computer-rollup.test.ts": 38, + "src/renderer/src/components/settings/settings-deep-link-target-watcher.test.ts": 210, + "src/renderer/src/components/settings/settings-form-option-filter.test.ts": 46, + "src/renderer/src/components/settings/settings-host-scope.test.ts": 38, + "src/renderer/src/components/settings/settings-project-checkout-target.test.ts": 55, + "src/renderer/src/components/settings/settings-project-list.test.ts": 72, + "src/renderer/src/components/settings/settings-project-regrouping.test.ts": 500, + "src/renderer/src/components/settings/settings-search-keywords.test.ts": 106, + "src/renderer/src/components/settings/settings-search.test.ts": 37, + "src/renderer/src/components/settings/settings-setup-guide-progress-hook.test.tsx": 47, + "src/renderer/src/components/settings/settings-setup-guide-progress.test.ts": 1451, + "src/renderer/src/components/settings/shortcut-binding-list-mutations.test.ts": 42, + "src/renderer/src/components/settings/shortcut-definition-catalog.test.ts": 361, + "src/renderer/src/components/settings/shortcut-groups.test.ts": 84, + "src/renderer/src/components/settings/shortcut-recording-state.test.ts": 37, + "src/renderer/src/components/settings/shortcut-row-visibility.test.ts": 150, + "src/renderer/src/components/settings/source-control-ai-settings-persistence.test.tsx": 3088, + "src/renderer/src/components/settings/sparse-preset-date.test.ts": 42, + "src/renderer/src/components/settings/sparse-preset-operation-error.test.ts": 35, + "src/renderer/src/components/settings/ssh-target-action-state.test.ts": 38, + "src/renderer/src/components/settings/ssh-target-draft.test.ts": 40, + "src/renderer/src/components/settings/ssh-target-save-payload.test.ts": 68, + "src/renderer/src/components/settings/task-source-setup-state.test.ts": 50, + "src/renderer/src/components/settings/task-tracker-integration-cards.test.tsx": 918, + "src/renderer/src/components/settings/terminal-preview-content.test.ts": 26, + "src/renderer/src/components/settings/terminal-search.test.ts": 282, + "src/renderer/src/components/settings/use-debounced-settings-text-draft.test.ts": 287, + "src/renderer/src/components/settings/use-mac-captured-digit-chords.test.ts": 666, + "src/renderer/src/components/settings/use-published-machine-name.test.tsx": 232, + "src/renderer/src/components/settings/use-repository-hook-settings-draft.test.tsx": 483, + "src/renderer/src/components/settings/use-runtime-environment-catalog.test.tsx": 1805, + "src/renderer/src/components/settings/use-session-search-status.test.tsx": 522, + "src/renderer/src/components/settings/use-task-source-provider-readiness.test.tsx": 218, + "src/renderer/src/components/settings/use-zcode-plan-credentials.test.tsx": 1054, + "src/renderer/src/components/settings/useGhosttyImport.test.ts": 60, + "src/renderer/src/components/settings/useWarpThemeImport.test.ts": 115, + "src/renderer/src/components/settings/worktree-symlink-path-filter.scan-budget.test.ts": 68, + "src/renderer/src/components/settings/worktree-symlink-path-filter.test.ts": 50, + "src/renderer/src/components/setup-guide/SetupGuideModal.mount-gating.test.tsx": 1815, + "src/renderer/src/components/setup-guide/use-setup-guide-progress.test.ts": 1323, + "src/renderer/src/components/setup-guide/use-setup-guide-telemetry.test.ts": 56, + "src/renderer/src/components/shared/MacFolderAccessFixDialog.test.tsx": 1298, + "src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts": 1354, + "src/renderer/src/components/shared/useDaemonActions.test.tsx": 434, + "src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx": 153, + "src/renderer/src/components/sidebar/AddRemoteHostDialog.config-picker.test.tsx": 1286, + "src/renderer/src/components/sidebar/AddRemoteHostFields.test.tsx": 376, + "src/renderer/src/components/sidebar/AddRepoCreateStep.test.tsx": 478, + "src/renderer/src/components/sidebar/AddRepoDialog.default-checkout.test.ts": 34, + "src/renderer/src/components/sidebar/AddRepoDialogChrome.test.tsx": 652, + "src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx": 1847, + "src/renderer/src/components/sidebar/AddRepoHostSelector.test.tsx": 324, + "src/renderer/src/components/sidebar/AddRepoNestedImportStep.test.tsx": 473, + "src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx": 814, + "src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts": 147, + "src/renderer/src/components/sidebar/AddWslHostFormPanel.test.tsx": 196, + "src/renderer/src/components/sidebar/AgentDashboardSidebarHost.test.tsx": 1463, + "src/renderer/src/components/sidebar/AutoRenameFailedDialog.test.tsx": 498, + "src/renderer/src/components/sidebar/CacheTimer.test.tsx": 136, + "src/renderer/src/components/sidebar/CommentMarkdown.code-block-renderer.test.tsx": 370, + "src/renderer/src/components/sidebar/CommentMarkdown.file-link-cases.test.tsx": 1548, + "src/renderer/src/components/sidebar/CommentMarkdown.github-attachment-image.test.tsx": 1588, + "src/renderer/src/components/sidebar/CommentMarkdown.github-callout.test.tsx": 1895, + "src/renderer/src/components/sidebar/CommentMarkdown.growing.test.tsx": 1964, + "src/renderer/src/components/sidebar/CommentMarkdown.jira-links.test.tsx": 1557, + "src/renderer/src/components/sidebar/CommentMarkdown.link-click.test.tsx": 1746, + "src/renderer/src/components/sidebar/CommentMarkdown.ordered-list.test.tsx": 2451, + "src/renderer/src/components/sidebar/CommentMarkdown.test.tsx": 1526, + "src/renderer/src/components/sidebar/CommentMarkdown.word-fade.test.tsx": 1644, + "src/renderer/src/components/sidebar/CommentMermaidBlock.system-theme.test.tsx": 212, + "src/renderer/src/components/sidebar/DeleteNestedWorktreesDialog.test.tsx": 956, + "src/renderer/src/components/sidebar/DeleteWorktreeDialog.test.tsx": 871, + "src/renderer/src/components/sidebar/DeleteWorktreeDialogFooter.test.tsx": 482, + "src/renderer/src/components/sidebar/DeleteWorktreeTargetPreview.test.tsx": 863, + "src/renderer/src/components/sidebar/FilterToggleRow.test.tsx": 265, + "src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.test.tsx": 1372, + "src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.reminder-toast.test.tsx": 2023, + "src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.test.tsx": 3036, + "src/renderer/src/components/sidebar/LocalGitToolchainScanBanner.test.tsx": 1649, + "src/renderer/src/components/sidebar/MarkdownImageLightbox.test.tsx": 630, + "src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.test.tsx": 1486, + "src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx": 1043, + "src/renderer/src/components/sidebar/NoticeHostGlyph.test.tsx": 903, + "src/renderer/src/components/sidebar/OrcaYamlTrustDialog.test.tsx": 76, + "src/renderer/src/components/sidebar/ProjectAddedDialog.test.tsx": 77, + "src/renderer/src/components/sidebar/ProjectGroupDeleteDialog.test.tsx": 528, + "src/renderer/src/components/sidebar/ProjectHeaderActions.test.tsx": 184, + "src/renderer/src/components/sidebar/RemoteFileBrowser.paste.test.tsx": 721, + "src/renderer/src/components/sidebar/RemoveFolderDialog.test.tsx": 433, + "src/renderer/src/components/sidebar/SetupGuideSidebarEntry.test.tsx": 288, + "src/renderer/src/components/sidebar/SetupScriptPromptCard.test.ts": 38, + "src/renderer/src/components/sidebar/SetupScriptPromptCardShell.test.tsx": 334, + "src/renderer/src/components/sidebar/SeveredTerminalDaemonBanner.test.tsx": 462, + "src/renderer/src/components/sidebar/Sidebar.test.tsx": 736, + "src/renderer/src/components/sidebar/SidebarAgentsList.test.tsx": 1978, + "src/renderer/src/components/sidebar/SidebarFeedbackDialog.test.tsx": 3348, + "src/renderer/src/components/sidebar/SidebarGroupByToggle.test.tsx": 508, + "src/renderer/src/components/sidebar/SidebarHeader.test.tsx": 467, + "src/renderer/src/components/sidebar/SidebarNav.test.tsx": 1674, + "src/renderer/src/components/sidebar/SidebarRepositoryFilterSection.test.tsx": 837, + "src/renderer/src/components/sidebar/SidebarSettingsHelpMenu.test.tsx": 497, + "src/renderer/src/components/sidebar/SidebarToolbar.test.tsx": 396, + "src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.test.tsx": 419, + "src/renderer/src/components/sidebar/StatusIndicator.test.ts": 171, + "src/renderer/src/components/sidebar/WorkspaceAttachmentOrigins.test.tsx": 552, + "src/renderer/src/components/sidebar/WorkspaceAttachmentSearch.resolution.test.tsx": 2578, + "src/renderer/src/components/sidebar/WorkspaceKanbanCard.host-identity.test.tsx": 320, + "src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.mount-gating.test.tsx": 2063, + "src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.search.test.tsx": 1818, + "src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.task-status-sync.test.tsx": 1917, + "src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.test.tsx": 1575, + "src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.test.tsx": 301, + "src/renderer/src/components/sidebar/WorkspaceKanbanLaneGrid.test.tsx": 391, + "src/renderer/src/components/sidebar/WorkspaceKanbanSearchField.test.tsx": 363, + "src/renderer/src/components/sidebar/WorkspaceKanbanSettingsMenu.test.tsx": 844, + "src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.test.tsx": 374, + "src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx": 2039, + "src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx": 2328, + "src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx": 1608, + "src/renderer/src/components/sidebar/WorktreeCard.delete-failed.test.tsx": 936, + "src/renderer/src/components/sidebar/WorktreeCard.host-badge-toggle.test.tsx": 998, + "src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx": 1720, + "src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx": 1440, + "src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx": 1732, + "src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx": 1170, + "src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx": 1193, + "src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx": 1534, + "src/renderer/src/components/sidebar/WorktreeCard.single-reference-details.test.tsx": 1519, + "src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx": 863, + "src/renderer/src/components/sidebar/WorktreeCard.test.ts": 31, + "src/renderer/src/components/sidebar/WorktreeCardAgents.acknowledgement-emphasis.test.tsx": 1109, + "src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx": 885, + "src/renderer/src/components/sidebar/WorktreeCardAgents.expansion-remount.test.tsx": 1342, + "src/renderer/src/components/sidebar/WorktreeCardAgents.send-target.test.tsx": 1272, + "src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx": 845, + "src/renderer/src/components/sidebar/WorktreeCardAutomationDetailSection.test.tsx": 558, + "src/renderer/src/components/sidebar/WorktreeCardDisplayMenuSection.test.tsx": 402, + "src/renderer/src/components/sidebar/WorktreeCardLinkedItemsSection.test.tsx": 2028, + "src/renderer/src/components/sidebar/WorktreeCardMeta.interaction.test.tsx": 2591, + "src/renderer/src/components/sidebar/WorktreeCardMeta.test.tsx": 2152, + "src/renderer/src/components/sidebar/WorktreeCardPorts.test.tsx": 1152, + "src/renderer/src/components/sidebar/WorktreeCardSshHostControl.test.tsx": 1797, + "src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx": 264, + "src/renderer/src/components/sidebar/WorktreeContextMenu.copy-worktree-name.test.tsx": 1696, + "src/renderer/src/components/sidebar/WorktreeContextMenu.delete-shortcut.test.tsx": 1315, + "src/renderer/src/components/sidebar/WorktreeContextMenu.react185-bystander.test.tsx": 2115, + "src/renderer/src/components/sidebar/WorktreeContextMenu.test.ts": 122, + "src/renderer/src/components/sidebar/WorktreeDeveloperMenu.test.tsx": 75, + "src/renderer/src/components/sidebar/WorktreeDeveloperMenuReveal.test.tsx": 646, + "src/renderer/src/components/sidebar/WorktreeList.card-memo-stability.test.tsx": 2150, + "src/renderer/src/components/sidebar/WorktreeList.empty-project-rows.test.ts": 1561, + "src/renderer/src/components/sidebar/WorktreeList.folder-workspace-rows.test.ts": 1830, + "src/renderer/src/components/sidebar/WorktreeList.group-headers.test.ts": 1966, + "src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx": 3218, + "src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts": 3256, + "src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx": 2091, + "src/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsx": 2729, + "src/renderer/src/components/sidebar/WorktreeMetaDialog.host-scope.test.tsx": 1889, + "src/renderer/src/components/sidebar/WorktreeMetaDialog.test.tsx": 5615, + "src/renderer/src/components/sidebar/WorktreeOpenInMenu.test.tsx": 199, + "src/renderer/src/components/sidebar/WorktreeParentPickerPopover.test.ts": 1699, + "src/renderer/src/components/sidebar/WorktreeReferenceStack.test.tsx": 392, + "src/renderer/src/components/sidebar/WorktreeTitleInlineRename.begin-editing.test.tsx": 95, + "src/renderer/src/components/sidebar/WorktreeTitleInlineRename.editor-lifecycle.test.tsx": 387, + "src/renderer/src/components/sidebar/WorktreeTitleInlineRename.test.tsx": 177, + "src/renderer/src/components/sidebar/WorktreeVisibilityDialog.test.tsx": 2116, + "src/renderer/src/components/sidebar/WorktreeVisibilityHelpPopover.test.tsx": 425, + "src/renderer/src/components/sidebar/WorktreeVisibilitySourceList.test.tsx": 401, + "src/renderer/src/components/sidebar/active-worktree-focus-after-delete.test.ts": 67, + "src/renderer/src/components/sidebar/add-remote-host-ssh-actions.test.ts": 110, + "src/renderer/src/components/sidebar/add-repo-browse-authority.test.ts": 34, + "src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.test.ts": 39, + "src/renderer/src/components/sidebar/add-repo-skip-finalization.test.ts": 50, + "src/renderer/src/components/sidebar/add-repo-wsl-host-folder-picker.test.ts": 80, + "src/renderer/src/components/sidebar/agent-send-picker-collapse.test.tsx": 1453, + "src/renderer/src/components/sidebar/child-workspaces-toggle-target.test.ts": 1788, + "src/renderer/src/components/sidebar/child-workspaces-toggle-visibility.test.ts": 2237, + "src/renderer/src/components/sidebar/clone-defaults.test.ts": 42, + "src/renderer/src/components/sidebar/create-project-defaults.test.ts": 39, + "src/renderer/src/components/sidebar/default-branch-folder-project-root.test.ts": 1259, + "src/renderer/src/components/sidebar/default-branch-visible-under-hide-sleeping.test.ts": 1375, + "src/renderer/src/components/sidebar/delete-worktree-always-force-preference.test.tsx": 2230, + "src/renderer/src/components/sidebar/delete-worktree-dirty-change-counts.test.ts": 71, + "src/renderer/src/components/sidebar/delete-worktree-dirty-change-hydration.test.tsx": 845, + "src/renderer/src/components/sidebar/delete-worktree-failure-toast.test.tsx": 1579, + "src/renderer/src/components/sidebar/delete-worktree-flow.test.ts": 769, + "src/renderer/src/components/sidebar/delete-worktree-parallel-flow.test.ts": 439, + "src/renderer/src/components/sidebar/delete-worktree-toast.test.ts": 77, + "src/renderer/src/components/sidebar/empty-project-placeholder-repos.test.ts": 32, + "src/renderer/src/components/sidebar/focused-agent-row-highlight.test.ts": 1233, + "src/renderer/src/components/sidebar/folder-workspace-card-pr-display.test.ts": 92, + "src/renderer/src/components/sidebar/folder-workspace-composer-expiry.test.tsx": 2002, + "src/renderer/src/components/sidebar/folder-workspace-composer-helpers.test.ts": 94, + "src/renderer/src/components/sidebar/folder-workspace-composer-path-status.test.tsx": 1490, + "src/renderer/src/components/sidebar/folder-workspace-composer-submit-host-platform.test.ts": 2067, + "src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts": 2132, + "src/renderer/src/components/sidebar/folder-workspace-linked-startup-plan.test.ts": 1757, + "src/renderer/src/components/sidebar/host-header-drag.test.tsx": 260, + "src/renderer/src/components/sidebar/host-header-menu-items.test.ts": 31, + "src/renderer/src/components/sidebar/host-rename-remove.test.ts": 30, + "src/renderer/src/components/sidebar/host-section-collapse-folders.test.ts": 187, + "src/renderer/src/components/sidebar/host-section-collapse.test.ts": 146, + "src/renderer/src/components/sidebar/host-section-folder-workspace-counts.test.ts": 35, + "src/renderer/src/components/sidebar/host-section-order.test.ts": 25, + "src/renderer/src/components/sidebar/host-section-reveal.test.tsx": 1645, + "src/renderer/src/components/sidebar/host-section-rows.test.ts": 108, + "src/renderer/src/components/sidebar/hovered-workspace-delete.test.ts": 1636, + "src/renderer/src/components/sidebar/imported-worktrees-card-actions.test.ts": 52, + "src/renderer/src/components/sidebar/imported-worktrees-card-candidates.test.ts": 77, + "src/renderer/src/components/sidebar/kanban-empty-selection-prune-budget.test.tsx": 996, + "src/renderer/src/components/sidebar/linear-agent-skill-runtime.shell-override.test.ts": 60, + "src/renderer/src/components/sidebar/linear-agent-skill-setup-reminders.test.ts": 49, "src/renderer/src/components/sidebar/local-base-ref-suggestion-toast.test.tsx": 246, - "src/renderer/src/components/sidebar/manual-sort-switch-toast.test.ts": 1537, - "src/renderer/src/components/sidebar/markdown-top-level-blocks.test.ts": 466, - "src/renderer/src/components/sidebar/mobile-sidebar-onboarding-badge.test.ts": 234, - "src/renderer/src/components/sidebar/natural-worktree-ids.test.ts": 136, - "src/renderer/src/components/sidebar/new-external-worktrees-inbox-actions.test.ts": 105, - "src/renderer/src/components/sidebar/new-external-worktrees-inbox-candidates.test.ts": 58, - "src/renderer/src/components/sidebar/pinned-section-worktrees.test.ts": 460, - "src/renderer/src/components/sidebar/preserved-branch-batch-toast.test.tsx": 524, - "src/renderer/src/components/sidebar/preserved-branch-toast.test.tsx": 357, - "src/renderer/src/components/sidebar/project-added-default-checkout.test.ts": 92, - "src/renderer/src/components/sidebar/project-filter-reveal.test.ts": 47, - "src/renderer/src/components/sidebar/project-group-header-drag-commit.test.ts": 201, - "src/renderer/src/components/sidebar/project-group-header-drag-start.test.ts": 155, - "src/renderer/src/components/sidebar/project-group-header-drop.test.ts": 136, - "src/renderer/src/components/sidebar/project-header-action-selector-lockstep.test.ts": 172, - "src/renderer/src/components/sidebar/project-header-color.test.ts": 281, - "src/renderer/src/components/sidebar/project-header-drag-commit.test.ts": 154, - "src/renderer/src/components/sidebar/project-header-drag-start.test.ts": 159, - "src/renderer/src/components/sidebar/project-header-drag.test.ts": 201, - "src/renderer/src/components/sidebar/project-header-drop.test.ts": 169, - "src/renderer/src/components/sidebar/project-order-manual-default-notice-visibility.test.ts": 53, - "src/renderer/src/components/sidebar/prompt-cache-countdown-clock.test.ts": 51, - "src/renderer/src/components/sidebar/prompt-cache-timer-selection.test.ts": 54, - "src/renderer/src/components/sidebar/remote-file-browser-drive-paths.test.ts": 56, - "src/renderer/src/components/sidebar/remote-file-browser-helpers.test.ts": 143, - "src/renderer/src/components/sidebar/rendered-sidebar-worktree-order.test.ts": 1479, - "src/renderer/src/components/sidebar/repo-header-create-state.test.ts": 86, - "src/renderer/src/components/sidebar/sidebar-empty-state-gate.test.ts": 53, - "src/renderer/src/components/sidebar/sidebar-filter-state.test.ts": 1362, - "src/renderer/src/components/sidebar/sidebar-host-options.test.ts": 119, - "src/renderer/src/components/sidebar/sidebar-lineage-ancestors.test.ts": 220, - "src/renderer/src/components/sidebar/sidebar-project-drop.test.ts": 93, - "src/renderer/src/components/sidebar/sidebar-workspace-option-items.test.ts": 386, - "src/renderer/src/components/sidebar/sleep-worktree-activation-race.test.ts": 112, - "src/renderer/src/components/sidebar/sleep-worktree-flow.test.ts": 129, - "src/renderer/src/components/sidebar/smart-attention-host-observed-end.test.ts": 93, - "src/renderer/src/components/sidebar/smart-attention.test.ts": 136, - "src/renderer/src/components/sidebar/smart-sort.test.ts": 158, - "src/renderer/src/components/sidebar/ssh-host-remove-resolution.test.ts": 54, - "src/renderer/src/components/sidebar/ssh-target-duplicate.test.ts": 60, - "src/renderer/src/components/sidebar/ssh-workspace-forget-resolution.test.ts": 51, - "src/renderer/src/components/sidebar/stale-agent-row-unverifiable.test.ts": 1936, - "src/renderer/src/components/sidebar/subagent-child-row-fallback-pixels.test.tsx": 650, - "src/renderer/src/components/sidebar/truncated-sidebar-label.test.tsx": 200, - "src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts": 318, - "src/renderer/src/components/sidebar/use-add-repo-hosted-controller.test.ts": 68, - "src/renderer/src/components/sidebar/use-feedback-image-drop.test.tsx": 330, - "src/renderer/src/components/sidebar/use-sidebar-feedback-images.test.tsx": 334, - "src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.test.tsx": 1548, - "src/renderer/src/components/sidebar/use-workspace-kanban-card-pointer-drag.test.ts": 76, - "src/renderer/src/components/sidebar/use-workspace-kanban-drawer-lingering.test.tsx": 257, - "src/renderer/src/components/sidebar/use-workspace-kanban-outside-dismiss.test.ts": 64, - "src/renderer/src/components/sidebar/use-workspace-kanban-selection.test.tsx": 210, - "src/renderer/src/components/sidebar/use-workspace-reference-details.test.tsx": 1076, - "src/renderer/src/components/sidebar/use-workspace-reveal-body-redirect.test.tsx": 194, - "src/renderer/src/components/sidebar/use-workspace-status-drop.test.ts": 140, - "src/renderer/src/components/sidebar/use-worktree-activity-status.test.tsx": 185, - "src/renderer/src/components/sidebar/use-worktree-activity-statuses.test.ts": 1394, - "src/renderer/src/components/sidebar/use-worktree-card-secondary-details.store-subscriptions.test.tsx": 2686, - "src/renderer/src/components/sidebar/use-worktree-sleep-state.test.tsx": 148, - "src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts": 141, - "src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts": 169, - "src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts": 139, - "src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts": 73, - "src/renderer/src/components/sidebar/useCreateProjectDefaults.test.ts": 116, - "src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts": 139, - "src/renderer/src/components/sidebar/useRenderedSetupScriptPromptState.test.ts": 253, - "src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.test.tsx": 1633, - "src/renderer/src/components/sidebar/useSidebarProjectDrop.os-file-drop.test.tsx": 647, - "src/renderer/src/components/sidebar/useWorkspaceBoardPanel.test.tsx": 245, - "src/renderer/src/components/sidebar/useWorktreeAgentRows.test.ts": 176, - "src/renderer/src/components/sidebar/visible-worktree-activity-inputs.test.ts": 58, - "src/renderer/src/components/sidebar/visible-worktree-indexes.test.ts": 1791, - "src/renderer/src/components/sidebar/visible-worktrees-manual-lineage-order.test.ts": 1627, - "src/renderer/src/components/sidebar/visible-worktrees.test.ts": 1757, - "src/renderer/src/components/sidebar/workspace-attachment-source-label.test.ts": 80, - "src/renderer/src/components/sidebar/workspace-attachment-source-result.test.ts": 1721, - "src/renderer/src/components/sidebar/workspace-board-task-status-sync.test.ts": 127, - "src/renderer/src/components/sidebar/workspace-creator-visibility.test.ts": 84, - "src/renderer/src/components/sidebar/workspace-delete-lineage.test.ts": 52, - "src/renderer/src/components/sidebar/workspace-hover-attachments.test.ts": 119, - "src/renderer/src/components/sidebar/workspace-kanban-area-selection.test.ts": 181, - "src/renderer/src/components/sidebar/workspace-kanban-card-pointer-drag-dom.test.ts": 57, - "src/renderer/src/components/sidebar/workspace-kanban-filtered-drop-index.test.ts": 51, - "src/renderer/src/components/sidebar/workspace-kanban-search.test.ts": 153, - "src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.test.ts": 172, - "src/renderer/src/components/sidebar/workspace-kanban-virtual-lane-layout.test.ts": 64, - "src/renderer/src/components/sidebar/workspace-kanban-worktree-groups.test.ts": 164, - "src/renderer/src/components/sidebar/workspace-lineage-menu-actions.test.ts": 59, - "src/renderer/src/components/sidebar/workspace-reference-detail-read.test.ts": 310, - "src/renderer/src/components/sidebar/workspace-reference-refresh-targets.test.ts": 582, - "src/renderer/src/components/sidebar/workspace-status.test.ts": 151, - "src/renderer/src/components/sidebar/worktree-agent-activity-summary.test.ts": 165, - "src/renderer/src/components/sidebar/worktree-agent-freshness-selector.test.ts": 126, - "src/renderer/src/components/sidebar/worktree-agent-host-attribution.test.ts": 64, - "src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts": 79, - "src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts": 120, - "src/renderer/src/components/sidebar/worktree-agent-remote-attribution.test.ts": 953, - "src/renderer/src/components/sidebar/worktree-agent-row-selectors.test.ts": 82, + "src/renderer/src/components/sidebar/manual-sort-switch-toast.test.ts": 1416, + "src/renderer/src/components/sidebar/markdown-top-level-blocks.test.ts": 716, + "src/renderer/src/components/sidebar/mobile-sidebar-onboarding-badge.test.ts": 200, + "src/renderer/src/components/sidebar/natural-worktree-ids.test.ts": 90, + "src/renderer/src/components/sidebar/new-external-worktrees-inbox-actions.test.ts": 74, + "src/renderer/src/components/sidebar/new-external-worktrees-inbox-candidates.test.ts": 44, + "src/renderer/src/components/sidebar/pinned-section-worktrees.test.ts": 408, + "src/renderer/src/components/sidebar/preserved-branch-batch-toast.test.tsx": 514, + "src/renderer/src/components/sidebar/preserved-branch-toast.test.tsx": 252, + "src/renderer/src/components/sidebar/project-added-default-checkout.test.ts": 82, + "src/renderer/src/components/sidebar/project-filter-reveal.test.ts": 26, + "src/renderer/src/components/sidebar/project-group-header-drag-commit.test.ts": 162, + "src/renderer/src/components/sidebar/project-group-header-drag-start.test.ts": 171, + "src/renderer/src/components/sidebar/project-group-header-drop.test.ts": 177, + "src/renderer/src/components/sidebar/project-header-action-selector-lockstep.test.ts": 159, + "src/renderer/src/components/sidebar/project-header-color.test.ts": 235, + "src/renderer/src/components/sidebar/project-header-drag-commit.test.ts": 186, + "src/renderer/src/components/sidebar/project-header-drag-start.test.ts": 181, + "src/renderer/src/components/sidebar/project-header-drag.test.ts": 184, + "src/renderer/src/components/sidebar/project-header-drop.test.ts": 176, + "src/renderer/src/components/sidebar/project-order-manual-default-notice-visibility.test.ts": 32, + "src/renderer/src/components/sidebar/prompt-cache-countdown-clock.test.ts": 37, + "src/renderer/src/components/sidebar/prompt-cache-timer-selection.test.ts": 46, + "src/renderer/src/components/sidebar/remote-file-browser-drive-paths.test.ts": 39, + "src/renderer/src/components/sidebar/remote-file-browser-helpers.test.ts": 137, + "src/renderer/src/components/sidebar/rendered-sidebar-worktree-order.test.ts": 1450, + "src/renderer/src/components/sidebar/repo-header-create-state.test.ts": 90, + "src/renderer/src/components/sidebar/sidebar-empty-state-gate.test.ts": 40, + "src/renderer/src/components/sidebar/sidebar-filter-state.test.ts": 1409, + "src/renderer/src/components/sidebar/sidebar-host-options.test.ts": 334, + "src/renderer/src/components/sidebar/sidebar-lineage-ancestors.test.ts": 143, + "src/renderer/src/components/sidebar/sidebar-project-drop.test.ts": 88, + "src/renderer/src/components/sidebar/sidebar-workspace-option-items.test.ts": 303, + "src/renderer/src/components/sidebar/sleep-worktree-activation-race.test.ts": 92, + "src/renderer/src/components/sidebar/sleep-worktree-flow.test.ts": 102, + "src/renderer/src/components/sidebar/smart-attention-host-observed-end.test.ts": 61, + "src/renderer/src/components/sidebar/smart-attention.test.ts": 145, + "src/renderer/src/components/sidebar/smart-sort.test.ts": 150, + "src/renderer/src/components/sidebar/ssh-host-remove-resolution.test.ts": 41, + "src/renderer/src/components/sidebar/ssh-target-duplicate.test.ts": 34, + "src/renderer/src/components/sidebar/ssh-workspace-forget-resolution.test.ts": 41, + "src/renderer/src/components/sidebar/stale-agent-row-unverifiable.test.ts": 1311, + "src/renderer/src/components/sidebar/subagent-child-row-fallback-pixels.test.tsx": 496, + "src/renderer/src/components/sidebar/truncated-sidebar-label.test.tsx": 231, + "src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts": 308, + "src/renderer/src/components/sidebar/use-add-repo-hosted-controller.test.ts": 41, + "src/renderer/src/components/sidebar/use-add-repo-wsl-host-paths.test.tsx": 388, + "src/renderer/src/components/sidebar/use-feedback-image-drop.test.tsx": 419, + "src/renderer/src/components/sidebar/use-remote-file-browser-listing.test.ts": 200, + "src/renderer/src/components/sidebar/use-sidebar-feedback-images.test.tsx": 283, + "src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.test.tsx": 1676, + "src/renderer/src/components/sidebar/use-workspace-kanban-card-pointer-drag.test.ts": 48, + "src/renderer/src/components/sidebar/use-workspace-kanban-drawer-lingering.test.tsx": 226, + "src/renderer/src/components/sidebar/use-workspace-kanban-outside-dismiss.test.ts": 36, + "src/renderer/src/components/sidebar/use-workspace-kanban-selection.test.tsx": 219, + "src/renderer/src/components/sidebar/use-workspace-reference-details.test.tsx": 983, + "src/renderer/src/components/sidebar/use-workspace-reveal-body-redirect.test.tsx": 176, + "src/renderer/src/components/sidebar/use-workspace-status-drop.test.ts": 94, + "src/renderer/src/components/sidebar/use-worktree-activity-status.test.tsx": 136, + "src/renderer/src/components/sidebar/use-worktree-activity-statuses.test.ts": 1381, + "src/renderer/src/components/sidebar/use-worktree-card-secondary-details.store-subscriptions.test.tsx": 2311, + "src/renderer/src/components/sidebar/use-worktree-sleep-state.test.tsx": 99, + "src/renderer/src/components/sidebar/use-wsl-distro-list.test.ts": 510, + "src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts": 113, + "src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts": 194, + "src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts": 134, + "src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts": 85, + "src/renderer/src/components/sidebar/useCreateProjectDefaults.test.ts": 80, + "src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts": 102, + "src/renderer/src/components/sidebar/useRenderedSetupScriptPromptState.test.ts": 213, + "src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.test.tsx": 1570, + "src/renderer/src/components/sidebar/useSidebarProjectDrop.os-file-drop.test.tsx": 834, + "src/renderer/src/components/sidebar/useWorkspaceBoardPanel.test.tsx": 248, + "src/renderer/src/components/sidebar/useWorktreeAgentRows.test.ts": 180, + "src/renderer/src/components/sidebar/visible-worktree-activity-inputs.test.ts": 55, + "src/renderer/src/components/sidebar/visible-worktree-indexes.test.ts": 1283, + "src/renderer/src/components/sidebar/visible-worktrees-manual-lineage-order.test.ts": 1378, + "src/renderer/src/components/sidebar/visible-worktrees.test.ts": 1260, + "src/renderer/src/components/sidebar/workspace-attachment-source-label.test.ts": 76, + "src/renderer/src/components/sidebar/workspace-attachment-source-result.test.ts": 1483, + "src/renderer/src/components/sidebar/workspace-board-task-status-sync.test.ts": 243, + "src/renderer/src/components/sidebar/workspace-creator-visibility.test.ts": 69, + "src/renderer/src/components/sidebar/workspace-delete-lineage.test.ts": 46, + "src/renderer/src/components/sidebar/workspace-hover-attachments.test.ts": 106, + "src/renderer/src/components/sidebar/workspace-kanban-area-selection.test.ts": 186, + "src/renderer/src/components/sidebar/workspace-kanban-card-pointer-drag-dom.test.ts": 44, + "src/renderer/src/components/sidebar/workspace-kanban-filtered-drop-index.test.ts": 40, + "src/renderer/src/components/sidebar/workspace-kanban-search.test.ts": 162, + "src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.test.ts": 195, + "src/renderer/src/components/sidebar/workspace-kanban-virtual-lane-layout.test.ts": 55, + "src/renderer/src/components/sidebar/workspace-kanban-worktree-groups.test.ts": 105, + "src/renderer/src/components/sidebar/workspace-lineage-menu-actions.test.ts": 49, + "src/renderer/src/components/sidebar/workspace-reference-detail-read.test.ts": 327, + "src/renderer/src/components/sidebar/workspace-reference-refresh-targets.test.ts": 635, + "src/renderer/src/components/sidebar/workspace-status.test.ts": 108, + "src/renderer/src/components/sidebar/worktree-agent-activity-summary.test.ts": 157, + "src/renderer/src/components/sidebar/worktree-agent-freshness-selector.test.ts": 102, + "src/renderer/src/components/sidebar/worktree-agent-host-attribution.test.ts": 45, + "src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts": 82, + "src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts": 121, + "src/renderer/src/components/sidebar/worktree-agent-remote-attribution.test.ts": 862, + "src/renderer/src/components/sidebar/worktree-agent-row-selectors.test.ts": 65, "src/renderer/src/components/sidebar/worktree-attachment-editing.test.ts": 83, - "src/renderer/src/components/sidebar/worktree-card-agent-ack-inputs.test.tsx": 204, - "src/renderer/src/components/sidebar/worktree-card-agent-summary.test.ts": 1707, - "src/renderer/src/components/sidebar/worktree-card-compact-agent-row-stopping.test.ts": 1763, - "src/renderer/src/components/sidebar/worktree-card-compact-agent-row.stable-message.test.tsx": 520, - "src/renderer/src/components/sidebar/worktree-card-details-hover-state.test.tsx": 172, - "src/renderer/src/components/sidebar/worktree-card-dom-events.test.ts": 158, - "src/renderer/src/components/sidebar/worktree-card-jira-issue-display.test.ts": 45, - "src/renderer/src/components/sidebar/worktree-card-pr-display.test.ts": 65, - "src/renderer/src/components/sidebar/worktree-card-send-target-inputs.test.ts": 65, - "src/renderer/src/components/sidebar/worktree-card-status-inputs.test.ts": 64, - "src/renderer/src/components/sidebar/worktree-card-title-display.test.ts": 47, - "src/renderer/src/components/sidebar/worktree-context-menu-delete-intent.test.ts": 72, - "src/renderer/src/components/sidebar/worktree-delete-host-qualification.test.ts": 1033, - "src/renderer/src/components/sidebar/worktree-delete-position-scaling.test.ts": 306, - "src/renderer/src/components/sidebar/worktree-delete-request.test.ts": 52, - "src/renderer/src/components/sidebar/worktree-drag-units.test.ts": 162, - "src/renderer/src/components/sidebar/worktree-filter-visibility.test.ts": 536, - "src/renderer/src/components/sidebar/worktree-header-section-boundaries.test.ts": 171, - "src/renderer/src/components/sidebar/worktree-keyboard-cycle.test.ts": 182, - "src/renderer/src/components/sidebar/worktree-lineage-drag-drop.test.ts": 179, - "src/renderer/src/components/sidebar/worktree-lineage-expansion.performance.test.ts": 126, - "src/renderer/src/components/sidebar/worktree-lineage-projection.test.ts": 52, - "src/renderer/src/components/sidebar/worktree-lineage-toggle-handler-cache.test.ts": 56, - "src/renderer/src/components/sidebar/worktree-list-groups-host-collision.test.ts": 204, - "src/renderer/src/components/sidebar/worktree-list-groups-host-labels.test.ts": 185, - "src/renderer/src/components/sidebar/worktree-list-groups-imported-worktrees.test.ts": 206, - "src/renderer/src/components/sidebar/worktree-list-groups-lineage-nesting.test.ts": 220, - "src/renderer/src/components/sidebar/worktree-list-groups-nested-project-groups.test.ts": 176, - "src/renderer/src/components/sidebar/worktree-list-groups-notice-host-labels.test.ts": 195, - "src/renderer/src/components/sidebar/worktree-list-groups-pending-creations.test.ts": 184, - "src/renderer/src/components/sidebar/worktree-list-groups-pinned-host-labels.test.ts": 208, - "src/renderer/src/components/sidebar/worktree-list-groups-project-groups.test.ts": 209, - "src/renderer/src/components/sidebar/worktree-list-groups-project-host-setups.test.ts": 216, - "src/renderer/src/components/sidebar/worktree-list-groups-project-order.test.ts": 873, - "src/renderer/src/components/sidebar/worktree-list-groups-section-label-disambiguation.test.ts": 190, - "src/renderer/src/components/sidebar/worktree-list/drag/pointer-flush.test.ts": 420, - "src/renderer/src/components/sidebar/worktree-list/drag/row-state.test.ts": 53, - "src/renderer/src/components/sidebar/worktree-list/grouping/build-rows.folder-workspace-lanes.test.ts": 213, - "src/renderer/src/components/sidebar/worktree-list/grouping/build-rows.test.ts": 224, - "src/renderer/src/components/sidebar/worktree-list/grouping/host-labels.test.ts": 53, - "src/renderer/src/components/sidebar/worktree-list/grouping/imported-rows.test.ts": 1443, - "src/renderer/src/components/sidebar/worktree-list/grouping/recent-project-rank-scan-budget.test.ts": 201, - "src/renderer/src/components/sidebar/worktree-list/listing/host-filtering.test.ts": 58, - "src/renderer/src/components/sidebar/worktree-list/listing/pending-worktree-creation-keys.test.ts": 39, - "src/renderer/src/components/sidebar/worktree-list/listing/review-cache-inputs.test.ts": 50, - "src/renderer/src/components/sidebar/worktree-list/listing/use-sort-order.test.tsx": 1814, - "src/renderer/src/components/sidebar/worktree-list/listing/use-visible-worktrees.test.tsx": 1702, - "src/renderer/src/components/sidebar/worktree-list/navigation/active-descendant-option.test.ts": 181, - "src/renderer/src/components/sidebar/worktree-list/navigation/folder-reveal.test.ts": 181, - "src/renderer/src/components/sidebar/worktree-list/navigation/folder-row-host-identity.test.ts": 170, - "src/renderer/src/components/sidebar/worktree-list/navigation/render-row-lookup.folder-workspace.test.ts": 189, - "src/renderer/src/components/sidebar/worktree-list/navigation/use-keyboard.focus.test.tsx": 517, - "src/renderer/src/components/sidebar/worktree-list/navigation/use-keyboard.host-identity.test.tsx": 754, - "src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx": 528, - "src/renderer/src/components/sidebar/worktree-list/navigation/use-selection-host-collision.test.tsx": 1905, - "src/renderer/src/components/sidebar/worktree-list/rows/FolderPathStatusIndicator.test.tsx": 313, - "src/renderer/src/components/sidebar/worktree-list/rows/RepoScanUnavailableIndicator.test.tsx": 1640, - "src/renderer/src/components/sidebar/worktree-list/rows/indentation.test.ts": 56, - "src/renderer/src/components/sidebar/worktree-list/rows/option-dom-host-collision.test.ts": 179, - "src/renderer/src/components/sidebar/worktree-list/rows/use-project-group-dialogs-owner-host.test.tsx": 259, - "src/renderer/src/components/sidebar/worktree-list/viewport/hard-scroll-up.test.ts": 59, - "src/renderer/src/components/sidebar/worktree-list/viewport/scroll-adjustment.test.ts": 2225, - "src/renderer/src/components/sidebar/worktree-list/viewport/sticky-headers.test.ts": 235, - "src/renderer/src/components/sidebar/worktree-list/viewport/use-row-removal-animation.test.ts": 94, - "src/renderer/src/components/sidebar/worktree-list/viewport/use-scroll-to-top.test.ts": 274, - "src/renderer/src/components/sidebar/worktree-list/viewport/use-scroll-to-top.test.tsx": 274, - "src/renderer/src/components/sidebar/worktree-list/viewport/virtual-rows.test.ts": 207, - "src/renderer/src/components/sidebar/worktree-list/viewport/visible-refresh.test.ts": 1414, - "src/renderer/src/components/sidebar/worktree-manual-order-catalog.test.ts": 152, - "src/renderer/src/components/sidebar/worktree-manual-order-store-write.test.ts": 1611, - "src/renderer/src/components/sidebar/worktree-manual-order.test.ts": 306, - "src/renderer/src/components/sidebar/worktree-name-suggestions.test.ts": 164, - "src/renderer/src/components/sidebar/worktree-parent-eligibility.test.ts": 62, - "src/renderer/src/components/sidebar/worktree-review-helpers.test.tsx": 178, - "src/renderer/src/components/sidebar/worktree-scroll-to-current-button.test.ts": 171, - "src/renderer/src/components/sidebar/worktree-sidebar-drag-autoscroll.test.ts": 69, - "src/renderer/src/components/sidebar/worktree-sidebar-drag-geometry.test.ts": 78, - "src/renderer/src/components/sidebar/worktree-sidebar-drop-preview.test.ts": 65, - "src/renderer/src/components/sidebar/worktree-sidebar-pointer-drag-dom.test.ts": 142, - "src/renderer/src/components/sidebar/worktree-sidebar-reveal-scroll-settle.test.ts": 41, - "src/renderer/src/components/sidebar/worktree-sidebar-reveal.test.ts": 207, - "src/renderer/src/components/sidebar/worktree-sidebar-row-preference.test.ts": 199, - "src/renderer/src/components/sidebar/worktree-sidebar-running-children.test.ts": 122, - "src/renderer/src/components/sidebar/worktree-sort-label-ordering.test.ts": 159, - "src/renderer/src/components/sidebar/worktree-subagent-child-rows.test.ts": 62, - "src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.test.ts": 184, - "src/renderer/src/components/sidebar/worktree-unambiguous-id-index.test.ts": 52, - "src/renderer/src/components/sidebar/worktree-unnest.test.ts": 90, - "src/renderer/src/components/sidebar/worktree-visibility-source-provenance.test.ts": 64, - "src/renderer/src/components/skills/SkillFreshnessNudge.test.tsx": 371, - "src/renderer/src/components/skills/SkillFreshnessStatusPill.test.tsx": 343, - "src/renderer/src/components/skills/SkillFreshnessUpdateDialog.test.tsx": 782, - "src/renderer/src/components/skills/SkillInstallAgentPicker.test.tsx": 690, - "src/renderer/src/components/skills/SkillInstallDialog.test.tsx": 3927, - "src/renderer/src/components/skills/SkillInstallManagementDialog.test.tsx": 3175, - "src/renderer/src/components/skills/SkillInstallTargetFields.test.tsx": 903, - "src/renderer/src/components/skills/SkillInstallWorkspaceCombobox.test.tsx": 705, - "src/renderer/src/components/skills/SkillShareDialog.test.tsx": 2543, - "src/renderer/src/components/skills/SkillSharedLinkRow.test.tsx": 665, - "src/renderer/src/components/skills/SkillsPage.test.tsx": 3940, - "src/renderer/src/components/skills/skill-agent-filter.test.ts": 156, - "src/renderer/src/components/skills/skill-bundle-name.test.ts": 233, - "src/renderer/src/components/skills/skill-bundle-retry-selection.test.ts": 55, - "src/renderer/src/components/skills/skill-delete-copy.test.ts": 110, - "src/renderer/src/components/skills/skill-delete-selection.test.ts": 321, - "src/renderer/src/components/skills/skill-description-length.test.ts": 57, - "src/renderer/src/components/skills/skill-freshness-grouping.test.ts": 63, - "src/renderer/src/components/skills/skill-freshness-skipped-reason.test.ts": 93, - "src/renderer/src/components/skills/skill-install-progress-state.test.tsx": 281, - "src/renderer/src/components/skills/skill-install-provider-groups.test.ts": 62, - "src/renderer/src/components/skills/skill-install-workspace-choices.test.ts": 49, - "src/renderer/src/components/skills/skill-package-checklist-items.test.ts": 102, - "src/renderer/src/components/skills/skill-package-digest.test.ts": 44, - "src/renderer/src/components/skills/skill-package-install-risk.test.ts": 50, - "src/renderer/src/components/skills/skill-share-link.test.ts": 48, - "src/renderer/src/components/skills/skill-share-package-selection.test.ts": 63, - "src/renderer/src/components/skills/skill-share-preview-summary.test.ts": 95, - "src/renderer/src/components/skills/skill-share-selection.test.ts": 103, - "src/renderer/src/components/skills/skill-source-inventory.test.ts": 60, - "src/renderer/src/components/skills/skills-filter.test.ts": 87, - "src/renderer/src/components/source-control/SourceControlActionVariableChips.test.tsx": 184, + "src/renderer/src/components/sidebar/worktree-card-agent-ack-inputs.test.tsx": 196, + "src/renderer/src/components/sidebar/worktree-card-agent-summary.test.ts": 1403, + "src/renderer/src/components/sidebar/worktree-card-compact-agent-row-stopping.test.ts": 1555, + "src/renderer/src/components/sidebar/worktree-card-compact-agent-row.stable-message.test.tsx": 413, + "src/renderer/src/components/sidebar/worktree-card-details-hover-state.test.tsx": 171, + "src/renderer/src/components/sidebar/worktree-card-dom-events.test.ts": 161, + "src/renderer/src/components/sidebar/worktree-card-jira-issue-display.test.ts": 38, + "src/renderer/src/components/sidebar/worktree-card-pr-display.test.ts": 48, + "src/renderer/src/components/sidebar/worktree-card-send-target-inputs.test.ts": 42, + "src/renderer/src/components/sidebar/worktree-card-status-inputs.test.ts": 44, + "src/renderer/src/components/sidebar/worktree-card-title-display.test.ts": 39, + "src/renderer/src/components/sidebar/worktree-context-menu-delete-intent.test.ts": 52, + "src/renderer/src/components/sidebar/worktree-delete-host-qualification.test.ts": 1056, + "src/renderer/src/components/sidebar/worktree-delete-position-scaling.test.ts": 235, + "src/renderer/src/components/sidebar/worktree-delete-request.test.ts": 36, + "src/renderer/src/components/sidebar/worktree-drag-units.test.ts": 98, + "src/renderer/src/components/sidebar/worktree-filter-visibility.test.ts": 532, + "src/renderer/src/components/sidebar/worktree-header-section-boundaries.test.ts": 144, + "src/renderer/src/components/sidebar/worktree-keyboard-cycle.test.ts": 133, + "src/renderer/src/components/sidebar/worktree-lineage-drag-drop.test.ts": 205, + "src/renderer/src/components/sidebar/worktree-lineage-expansion.performance.test.ts": 136, + "src/renderer/src/components/sidebar/worktree-lineage-projection.test.ts": 47, + "src/renderer/src/components/sidebar/worktree-lineage-toggle-handler-cache.test.ts": 40, + "src/renderer/src/components/sidebar/worktree-list-groups-host-collision.test.ts": 141, + "src/renderer/src/components/sidebar/worktree-list-groups-host-labels.test.ts": 212, + "src/renderer/src/components/sidebar/worktree-list-groups-imported-worktrees.test.ts": 166, + "src/renderer/src/components/sidebar/worktree-list-groups-lineage-nesting.test.ts": 165, + "src/renderer/src/components/sidebar/worktree-list-groups-nested-project-groups.test.ts": 125, + "src/renderer/src/components/sidebar/worktree-list-groups-notice-host-labels.test.ts": 126, + "src/renderer/src/components/sidebar/worktree-list-groups-pending-creations.test.ts": 125, + "src/renderer/src/components/sidebar/worktree-list-groups-pinned-host-labels.test.ts": 140, + "src/renderer/src/components/sidebar/worktree-list-groups-project-groups.test.ts": 128, + "src/renderer/src/components/sidebar/worktree-list-groups-project-host-setups.test.ts": 159, + "src/renderer/src/components/sidebar/worktree-list-groups-project-order.test.ts": 923, + "src/renderer/src/components/sidebar/worktree-list-groups-section-label-disambiguation.test.ts": 153, + "src/renderer/src/components/sidebar/worktree-list/drag/pointer-flush.test.ts": 343, + "src/renderer/src/components/sidebar/worktree-list/drag/row-state.test.ts": 29, + "src/renderer/src/components/sidebar/worktree-list/grouping/build-rows.folder-workspace-lanes.test.ts": 160, + "src/renderer/src/components/sidebar/worktree-list/grouping/build-rows.test.ts": 177, + "src/renderer/src/components/sidebar/worktree-list/grouping/host-labels.test.ts": 35, + "src/renderer/src/components/sidebar/worktree-list/grouping/imported-rows.test.ts": 1313, + "src/renderer/src/components/sidebar/worktree-list/grouping/recent-project-rank-scan-budget.test.ts": 189, + "src/renderer/src/components/sidebar/worktree-list/listing/host-filtering.test.ts": 84, + "src/renderer/src/components/sidebar/worktree-list/listing/pending-worktree-creation-keys.test.ts": 31, + "src/renderer/src/components/sidebar/worktree-list/listing/review-cache-inputs.test.ts": 38, + "src/renderer/src/components/sidebar/worktree-list/listing/use-sort-order.test.tsx": 1713, + "src/renderer/src/components/sidebar/worktree-list/listing/use-visible-worktrees.test.tsx": 1568, + "src/renderer/src/components/sidebar/worktree-list/navigation/active-descendant-option.test.ts": 151, + "src/renderer/src/components/sidebar/worktree-list/navigation/folder-reveal.test.ts": 138, + "src/renderer/src/components/sidebar/worktree-list/navigation/folder-row-host-identity.test.ts": 179, + "src/renderer/src/components/sidebar/worktree-list/navigation/render-row-lookup.folder-workspace.test.ts": 107, + "src/renderer/src/components/sidebar/worktree-list/navigation/use-keyboard.focus.test.tsx": 605, + "src/renderer/src/components/sidebar/worktree-list/navigation/use-keyboard.host-identity.test.tsx": 315, + "src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx": 634, + "src/renderer/src/components/sidebar/worktree-list/navigation/use-selection-host-collision.test.tsx": 1580, + "src/renderer/src/components/sidebar/worktree-list/rows/FolderPathStatusIndicator.test.tsx": 253, + "src/renderer/src/components/sidebar/worktree-list/rows/RepoScanUnavailableIndicator.test.tsx": 1646, + "src/renderer/src/components/sidebar/worktree-list/rows/indentation.test.ts": 51, + "src/renderer/src/components/sidebar/worktree-list/rows/option-dom-host-collision.test.ts": 156, + "src/renderer/src/components/sidebar/worktree-list/rows/use-project-group-dialogs-owner-host.test.tsx": 223, + "src/renderer/src/components/sidebar/worktree-list/viewport/hard-scroll-up.test.ts": 60, + "src/renderer/src/components/sidebar/worktree-list/viewport/scroll-adjustment.test.ts": 2083, + "src/renderer/src/components/sidebar/worktree-list/viewport/sticky-headers.test.ts": 158, + "src/renderer/src/components/sidebar/worktree-list/viewport/use-row-removal-animation.test.ts": 78, + "src/renderer/src/components/sidebar/worktree-list/viewport/use-scroll-to-top.test.ts": 262, + "src/renderer/src/components/sidebar/worktree-list/viewport/use-scroll-to-top.test.tsx": 260, + "src/renderer/src/components/sidebar/worktree-list/viewport/virtual-rows.test.ts": 163, + "src/renderer/src/components/sidebar/worktree-list/viewport/visible-refresh.test.ts": 1464, + "src/renderer/src/components/sidebar/worktree-manual-order-catalog.test.ts": 179, + "src/renderer/src/components/sidebar/worktree-manual-order-store-write.test.ts": 1565, + "src/renderer/src/components/sidebar/worktree-manual-order.test.ts": 315, + "src/renderer/src/components/sidebar/worktree-name-suggestions.test.ts": 183, + "src/renderer/src/components/sidebar/worktree-parent-eligibility.test.ts": 40, + "src/renderer/src/components/sidebar/worktree-review-helpers.test.tsx": 87, + "src/renderer/src/components/sidebar/worktree-scroll-to-current-button.test.ts": 160, + "src/renderer/src/components/sidebar/worktree-sidebar-drag-autoscroll.test.ts": 91, + "src/renderer/src/components/sidebar/worktree-sidebar-drag-geometry.test.ts": 90, + "src/renderer/src/components/sidebar/worktree-sidebar-drop-preview.test.ts": 42, + "src/renderer/src/components/sidebar/worktree-sidebar-pointer-drag-dom.test.ts": 166, + "src/renderer/src/components/sidebar/worktree-sidebar-reveal-scroll-settle.test.ts": 29, + "src/renderer/src/components/sidebar/worktree-sidebar-reveal.test.ts": 120, + "src/renderer/src/components/sidebar/worktree-sidebar-row-preference.test.ts": 119, + "src/renderer/src/components/sidebar/worktree-sidebar-running-children.test.ts": 78, + "src/renderer/src/components/sidebar/worktree-sort-label-ordering.test.ts": 140, + "src/renderer/src/components/sidebar/worktree-subagent-child-rows.test.ts": 52, + "src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.test.ts": 177, + "src/renderer/src/components/sidebar/worktree-unambiguous-id-index.test.ts": 22, + "src/renderer/src/components/sidebar/worktree-unnest.test.ts": 76, + "src/renderer/src/components/sidebar/worktree-visibility-source-provenance.test.ts": 57, + "src/renderer/src/components/sidebar/wsl-host-removal.test.ts": 84, + "src/renderer/src/components/sidebar/wsl-host-row-status.test.ts": 289, + "src/renderer/src/components/skills/SkillFreshnessNudge.test.tsx": 285, + "src/renderer/src/components/skills/SkillFreshnessStatusPill.test.tsx": 290, + "src/renderer/src/components/skills/SkillFreshnessUpdateDialog.test.tsx": 972, + "src/renderer/src/components/skills/SkillInstallAgentPicker.test.tsx": 644, + "src/renderer/src/components/skills/SkillInstallDialog.test.tsx": 4101, + "src/renderer/src/components/skills/SkillInstallManagementDialog.test.tsx": 3205, + "src/renderer/src/components/skills/SkillInstallTargetFields.test.tsx": 764, + "src/renderer/src/components/skills/SkillInstallWorkspaceCombobox.test.tsx": 867, + "src/renderer/src/components/skills/SkillShareDialog.test.tsx": 2091, + "src/renderer/src/components/skills/SkillSharedLinkRow.test.tsx": 705, + "src/renderer/src/components/skills/SkillUpdateRow.test.tsx": 324, + "src/renderer/src/components/skills/SkillsPage.test.tsx": 3659, + "src/renderer/src/components/skills/skill-agent-filter.test.ts": 164, + "src/renderer/src/components/skills/skill-bundle-name.test.ts": 221, + "src/renderer/src/components/skills/skill-bundle-retry-selection.test.ts": 25, + "src/renderer/src/components/skills/skill-delete-copy.test.ts": 101, + "src/renderer/src/components/skills/skill-delete-selection.test.ts": 260, + "src/renderer/src/components/skills/skill-description-length.test.ts": 35, + "src/renderer/src/components/skills/skill-freshness-grouping.test.ts": 53, + "src/renderer/src/components/skills/skill-freshness-skipped-reason.test.ts": 59, + "src/renderer/src/components/skills/skill-install-progress-state.test.tsx": 275, + "src/renderer/src/components/skills/skill-install-provider-groups.test.ts": 49, + "src/renderer/src/components/skills/skill-install-workspace-choices.test.ts": 41, + "src/renderer/src/components/skills/skill-package-checklist-items.test.ts": 68, + "src/renderer/src/components/skills/skill-package-digest.test.ts": 28, + "src/renderer/src/components/skills/skill-package-install-risk.test.ts": 42, + "src/renderer/src/components/skills/skill-share-link.test.ts": 45, + "src/renderer/src/components/skills/skill-share-package-selection.test.ts": 35, + "src/renderer/src/components/skills/skill-share-preview-summary.test.ts": 77, + "src/renderer/src/components/skills/skill-share-selection.test.ts": 76, + "src/renderer/src/components/skills/skill-source-inventory.test.ts": 41, + "src/renderer/src/components/skills/skills-filter.test.ts": 69, + "src/renderer/src/components/source-control/SourceControlActionVariableChips.test.tsx": 131, "src/renderer/src/components/sparse/SparseCheckoutPresetDraftForm.test.tsx": 559, - "src/renderer/src/components/sparse/SparsePresetChooser.test.tsx": 521, - "src/renderer/src/components/sparse/sparse-directory-browse.test.ts": 59, - "src/renderer/src/components/sparse/sparse-directory-entry-input.test.ts": 123, - "src/renderer/src/components/star-nag/StarNagAgentValueMomentObserver.test.tsx": 1675, - "src/renderer/src/components/star-nag/StarNagToastHost.test.tsx": 373, - "src/renderer/src/components/stats/CodexUsagePane.test.tsx": 615, - "src/renderer/src/components/stats/GrokUsagePane.test.tsx": 430, - "src/renderer/src/components/stats/UsageBreakdownSection.test.tsx": 406, - "src/renderer/src/components/stats/UsageTrackingPaneShell.test.tsx": 566, - "src/renderer/src/components/stats/usage-daily-chart.test.tsx": 231, - "src/renderer/src/components/stats/usage-overview-model.test.ts": 361, - "src/renderer/src/components/status-bar/CaffeinateStatusSegment.localization.test.tsx": 907, - "src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx": 2014, - "src/renderer/src/components/status-bar/PortsStatusSegment.host-routing.test.tsx": 602, - "src/renderer/src/components/status-bar/PortsStatusSegment.render-stability.test.tsx": 2028, - "src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx": 1713, - "src/renderer/src/components/status-bar/RuntimeHostStatusRow.test.tsx": 422, - "src/renderer/src/components/status-bar/SkillUpdateStatusSegment.test.tsx": 270, - "src/renderer/src/components/status-bar/SshStatusSegment.test.ts": 1449, - "src/renderer/src/components/status-bar/StatusBarUsageChangeNotices.test.tsx": 561, - "src/renderer/src/components/status-bar/UpdateStatusSegment.test.tsx": 1978, - "src/renderer/src/components/status-bar/UsagePercentageDisplayChangeNotice.test.tsx": 391, - "src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx": 677, - "src/renderer/src/components/status-bar/claude-status-sign-in.test.tsx": 1075, - "src/renderer/src/components/status-bar/codex-restart-status-summary.test.ts": 46, - "src/renderer/src/components/status-bar/codex-status-sign-in.test.tsx": 1775, - "src/renderer/src/components/status-bar/cursor-usage-headline.test.tsx": 282, - "src/renderer/src/components/status-bar/icons.test.tsx": 81, - "src/renderer/src/components/status-bar/inline-usage-bars.test.tsx": 1440, - "src/renderer/src/components/status-bar/mergeSnapshotAndSessions.test.ts": 133, - "src/renderer/src/components/status-bar/ports-status-popover-rows.test.tsx": 651, - "src/renderer/src/components/status-bar/provider-account-sync-key.test.ts": 49, - "src/renderer/src/components/status-bar/provider-segment-balance.test.tsx": 1233, - "src/renderer/src/components/status-bar/provider-segment-monthly-window.test.tsx": 1225, - "src/renderer/src/components/status-bar/remote-host-connection-status.test.ts": 90, - "src/renderer/src/components/status-bar/resource-manager-exit-render-budget.test.tsx": 1982, - "src/renderer/src/components/status-bar/resource-manager-terminal-copy.test.ts": 58, - "src/renderer/src/components/status-bar/resource-manager-worktree-target.test.ts": 46, - "src/renderer/src/components/status-bar/resource-memory-metric-copy.test.ts": 67, - "src/renderer/src/components/status-bar/resource-session-bindings.test.ts": 52, - "src/renderer/src/components/status-bar/resource-session-inventory.test.ts": 80, - "src/renderer/src/components/status-bar/resource-session-kill-confirmation.test.ts": 62, - "src/renderer/src/components/status-bar/resource-session-navigation.test.ts": 61, - "src/renderer/src/components/status-bar/resource-session-removal-render-budget.test.tsx": 352, - "src/renderer/src/components/status-bar/resource-usage-folder-merge.test.ts": 100, - "src/renderer/src/components/status-bar/resource-usage-open-slices.test.ts": 1354, - "src/renderer/src/components/status-bar/resource-usage-space-scan-ready.test.ts": 62, - "src/renderer/src/components/status-bar/runtime-host-resources.test.ts": 52, - "src/renderer/src/components/status-bar/ssh-status-segment-copy.test.ts": 48, - "src/renderer/src/components/status-bar/status-bar-agent-gating.test.ts": 45, - "src/renderer/src/components/status-bar/status-bar-antigravity-segment.test.tsx": 1543, - "src/renderer/src/components/status-bar/status-bar-context-menu-policy.test.ts": 54, - "src/renderer/src/components/status-bar/status-bar-copy-localization.test.tsx": 961, - "src/renderer/src/components/status-bar/status-bar-cursor-segment.test.tsx": 1640, - "src/renderer/src/components/status-bar/status-bar-density.test.ts": 61, - "src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx": 1470, - "src/renderer/src/components/status-bar/status-bar-provider-visibility.test.ts": 76, - "src/renderer/src/components/status-bar/status-bar-runtime-groups.test.ts": 2360, - "src/renderer/src/components/status-bar/status-bar-usage-collapse.test.ts": 184, - "src/renderer/src/components/status-bar/tooltip.test.ts": 167, - "src/renderer/src/components/status-bar/usage-error-copy.test.ts": 52, - "src/renderer/src/components/status-bar/usage-percentage-label.localization.test.ts": 134, - "src/renderer/src/components/status-bar/usage-percentage-label.test.ts": 44, - "src/renderer/src/components/status-bar/usage-provider-settings-target.test.ts": 45, - "src/renderer/src/components/status-bar/usage-roster-formatting.test.ts": 64, - "src/renderer/src/components/status-bar/usage-roster-row-state.test.ts": 54, - "src/renderer/src/components/status-bar/usage-unit-label-parity.test.tsx": 344, - "src/renderer/src/components/status-bar/usage-unit-label.test.ts": 135, - "src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx": 1000, - "src/renderer/src/components/status-bar/use-resource-usage-actions.test.tsx": 536, - "src/renderer/src/components/status-bar/use-resource-usage-derived-model.test.tsx": 666, - "src/renderer/src/components/status-bar/use-workspace-space-decision-projection.test.tsx": 1770, - "src/renderer/src/components/status-bar/workspace-space-breakdown-list.test.tsx": 403, - "src/renderer/src/components/status-bar/workspace-space-delete-host-routing.test.ts": 52, - "src/renderer/src/components/status-bar/workspace-space-format.test.ts": 103, - "src/renderer/src/components/status-bar/workspace-space-layout.test.ts": 54, - "src/renderer/src/components/status-bar/workspace-space-presentation.test.ts": 320, - "src/renderer/src/components/tab-bar/BrowserTab.test.tsx": 1750, - "src/renderer/src/components/tab-bar/ClientHostedBrowserTabRows.test.tsx": 1122, - "src/renderer/src/components/tab-bar/CopyTabIdMenuItem.test.tsx": 449, - "src/renderer/src/components/tab-bar/EditorFileTab.test.tsx": 1281, - "src/renderer/src/components/tab-bar/EditorFileTabContextMenu.test.tsx": 1810, - "src/renderer/src/components/tab-bar/QuickLaunchButton.launch-status.test.tsx": 746, - "src/renderer/src/components/tab-bar/QuickLaunchButton.test.ts": 437, - "src/renderer/src/components/tab-bar/RecentTabSwitcher.test.tsx": 329, - "src/renderer/src/components/tab-bar/SortableTab.rename-shortcut.test.tsx": 849, - "src/renderer/src/components/tab-bar/SortableTab.update-depth-probe.test.tsx": 2331, - "src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx": 914, - "src/renderer/src/components/tab-bar/TabBar.client-hosted-row-active-state.test.ts": 2769, - "src/renderer/src/components/tab-bar/TabBar.context-menu.test.ts": 4026, - "src/renderer/src/components/tab-bar/TabBar.os-file-drop.test.tsx": 1358, - "src/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.ts": 2501, - "src/renderer/src/components/tab-bar/TabBar.windows-shell-remote-runtime.test.ts": 1701, - "src/renderer/src/components/tab-bar/TabBar.windows-shell-ssh-host.test.ts": 2447, - "src/renderer/src/components/tab-bar/TabBar.worktree-write-gate.test.tsx": 1338, - "src/renderer/src/components/tab-bar/TabBar.worktree-write-gate.windows.test.tsx": 1434, - "src/renderer/src/components/tab-bar/TabBarCreateEntry.history.test.tsx": 1393, - "src/renderer/src/components/tab-bar/TabBarCreateEntry.keyboard.test.tsx": 2288, - "src/renderer/src/components/tab-bar/TabBarCreateEntry.search.test.tsx": 2607, - "src/renderer/src/components/tab-bar/TabBarCreateEntry.tab-results.test.tsx": 2010, - "src/renderer/src/components/tab-bar/TabBarCreateEntryRow.test.tsx": 662, - "src/renderer/src/components/tab-bar/TabBarItemRow.test.tsx": 1712, - "src/renderer/src/components/tab-bar/TabBarQuickCommandItem.test.tsx": 352, - "src/renderer/src/components/tab-bar/TabBarQuickCommandsMenu.keyboard.test.ts": 139, - "src/renderer/src/components/tab-bar/TabSessionSurfaceSwitchMenuItems.test.tsx": 1749, - "src/renderer/src/components/tab-bar/TabStripScrollIndicator.test.tsx": 313, - "src/renderer/src/components/tab-bar/TabStripTooltipProvider.test.tsx": 592, - "src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.state-tooltip.test.tsx": 220, - "src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.test.tsx": 371, - "src/renderer/src/components/tab-bar/client-hosted-browser-row-strip-placement.test.ts": 47, - "src/renderer/src/components/tab-bar/drop-indicator.test.ts": 53, - "src/renderer/src/components/tab-bar/group-tab-order.test.ts": 2175, - "src/renderer/src/components/tab-bar/middle-button-default-guard.test.ts": 45, - "src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.test.ts": 92, - "src/renderer/src/components/tab-bar/open-tab-entry-dedupe.test.ts": 301, - "src/renderer/src/components/tab-bar/open-tab-search-retention.test.ts": 535, - "src/renderer/src/components/tab-bar/open-tab-search.test.ts": 408, - "src/renderer/src/components/tab-bar/open-tab-selection-routing.test.ts": 211, - "src/renderer/src/components/tab-bar/query-token-match.test.ts": 46, - "src/renderer/src/components/tab-bar/recent-tab-switching.test.ts": 142, - "src/renderer/src/components/tab-bar/reconcile-order.test.ts": 52, - "src/renderer/src/components/tab-bar/tab-agent-launch-options.test.ts": 190, - "src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts": 66, - "src/renderer/src/components/tab-bar/tab-bar-item-model.git-status.test.ts": 1809, - "src/renderer/src/components/tab-bar/tab-create-entry-action.test.ts": 1664, - "src/renderer/src/components/tab-bar/tab-create-entry-classifier.runtime-listing.test.tsx": 1788, - "src/renderer/src/components/tab-bar/tab-create-entry-classifier.test.ts": 1647, - "src/renderer/src/components/tab-bar/tab-create-entry-file-matches.test.ts": 226, - "src/renderer/src/components/tab-bar/tab-create-entry-forced-search.test.ts": 53, - "src/renderer/src/components/tab-bar/tab-create-entry-history-placement.test.ts": 312, - "src/renderer/src/components/tab-bar/tab-create-entry-local-path.test.ts": 1511, - "src/renderer/src/components/tab-bar/tab-create-entry-search-ranking.test.ts": 313, - "src/renderer/src/components/tab-bar/tab-create-entry-url-classification.test.ts": 324, - "src/renderer/src/components/tab-bar/tab-create-menu-options.test.ts": 125, - "src/renderer/src/components/tab-bar/tab-move-to-pane-column.test.ts": 1394, - "src/renderer/src/components/tab-bar/tab-session-history-switch.parity.test.ts": 1955, - "src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts": 1808, - "src/renderer/src/components/tab-bar/tab-strip-content-resize-observers.test.ts": 171, - "src/renderer/src/components/tab-bar/tab-strip-growth-scroll.test.tsx": 407, - "src/renderer/src/components/tab-bar/tab-strip-overflow-navigation.test.ts": 60, - "src/renderer/src/components/tab-bar/tab-strip-pointer-activation.test.tsx": 1868, - "src/renderer/src/components/tab-bar/tab-strip-scroll-metrics.test.ts": 67, - "src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx": 1644, - "src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts": 202, + "src/renderer/src/components/sparse/SparsePresetChooser.test.tsx": 556, + "src/renderer/src/components/sparse/sparse-directory-browse.test.ts": 35, + "src/renderer/src/components/sparse/sparse-directory-entry-input.test.ts": 66, + "src/renderer/src/components/star-nag/StarNagAgentValueMomentObserver.test.tsx": 1527, + "src/renderer/src/components/star-nag/StarNagToastHost.test.tsx": 461, + "src/renderer/src/components/stats/CodexUsagePane.test.tsx": 457, + "src/renderer/src/components/stats/GrokUsagePane.test.tsx": 387, + "src/renderer/src/components/stats/UsageBreakdownSection.test.tsx": 328, + "src/renderer/src/components/stats/UsageTrackingPaneShell.test.tsx": 644, + "src/renderer/src/components/stats/usage-daily-chart.test.tsx": 229, + "src/renderer/src/components/stats/usage-overview-model.test.ts": 294, + "src/renderer/src/components/status-bar/CaffeinateStatusSegment.localization.test.tsx": 738, + "src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx": 1741, + "src/renderer/src/components/status-bar/PortsStatusSegment.host-routing.test.tsx": 686, + "src/renderer/src/components/status-bar/PortsStatusSegment.render-stability.test.tsx": 1808, + "src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx": 1634, + "src/renderer/src/components/status-bar/RuntimeHostStatusRow.test.tsx": 401, + "src/renderer/src/components/status-bar/SkillUpdateStatusSegment.test.tsx": 258, + "src/renderer/src/components/status-bar/SshStatusSegment.managed-hosts.test.tsx": 713, + "src/renderer/src/components/status-bar/SshStatusSegment.test.ts": 1278, + "src/renderer/src/components/status-bar/StatusBar.language-change.test.tsx": 1867, + "src/renderer/src/components/status-bar/StatusBarUsageChangeNotices.test.tsx": 510, + "src/renderer/src/components/status-bar/UpdateStatusSegment.test.tsx": 1734, + "src/renderer/src/components/status-bar/UsagePercentageDisplayChangeNotice.test.tsx": 415, + "src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx": 703, + "src/renderer/src/components/status-bar/claude-status-sign-in.test.tsx": 1108, + "src/renderer/src/components/status-bar/codex-restart-status-summary.test.ts": 34, + "src/renderer/src/components/status-bar/codex-status-sign-in.test.tsx": 1914, + "src/renderer/src/components/status-bar/cursor-usage-headline.test.tsx": 131, + "src/renderer/src/components/status-bar/icons.test.tsx": 62, + "src/renderer/src/components/status-bar/inline-usage-bars.test.tsx": 1407, + "src/renderer/src/components/status-bar/mergeSnapshotAndSessions.test.ts": 143, + "src/renderer/src/components/status-bar/ports-status-popover-rows.test.tsx": 631, + "src/renderer/src/components/status-bar/provider-account-sync-key.test.ts": 43, + "src/renderer/src/components/status-bar/provider-segment-balance.test.tsx": 1104, + "src/renderer/src/components/status-bar/provider-segment-monthly-window.test.tsx": 990, + "src/renderer/src/components/status-bar/remote-host-connection-status.test.ts": 70, + "src/renderer/src/components/status-bar/resource-manager-exit-render-budget.test.tsx": 1244, + "src/renderer/src/components/status-bar/resource-manager-terminal-copy.test.ts": 52, + "src/renderer/src/components/status-bar/resource-manager-worktree-target.test.ts": 29, + "src/renderer/src/components/status-bar/resource-memory-metric-copy.test.ts": 48, + "src/renderer/src/components/status-bar/resource-session-bindings.test.ts": 38, + "src/renderer/src/components/status-bar/resource-session-inventory.test.ts": 34, + "src/renderer/src/components/status-bar/resource-session-kill-confirmation.test.ts": 30, + "src/renderer/src/components/status-bar/resource-session-navigation.test.ts": 33, + "src/renderer/src/components/status-bar/resource-session-removal-render-budget.test.tsx": 386, + "src/renderer/src/components/status-bar/resource-usage-folder-merge.test.ts": 73, + "src/renderer/src/components/status-bar/resource-usage-open-slices.test.ts": 1445, + "src/renderer/src/components/status-bar/resource-usage-space-scan-ready.test.ts": 34, + "src/renderer/src/components/status-bar/runtime-host-resources.test.ts": 43, + "src/renderer/src/components/status-bar/ssh-status-segment-copy.test.ts": 34, + "src/renderer/src/components/status-bar/status-bar-agent-gating.test.ts": 35, + "src/renderer/src/components/status-bar/status-bar-antigravity-segment.test.tsx": 1423, + "src/renderer/src/components/status-bar/status-bar-context-menu-policy.test.ts": 38, + "src/renderer/src/components/status-bar/status-bar-copy-localization.test.tsx": 755, + "src/renderer/src/components/status-bar/status-bar-cursor-segment.test.tsx": 1500, + "src/renderer/src/components/status-bar/status-bar-density.test.ts": 35, + "src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx": 1327, + "src/renderer/src/components/status-bar/status-bar-provider-visibility.test.ts": 70, + "src/renderer/src/components/status-bar/status-bar-runtime-groups.test.ts": 365, + "src/renderer/src/components/status-bar/status-bar-usage-collapse.test.ts": 176, + "src/renderer/src/components/status-bar/tooltip.test.ts": 154, + "src/renderer/src/components/status-bar/usage-error-copy.test.ts": 42, + "src/renderer/src/components/status-bar/usage-percentage-label.localization.test.ts": 119, + "src/renderer/src/components/status-bar/usage-percentage-label.test.ts": 22, + "src/renderer/src/components/status-bar/usage-provider-settings-target.test.ts": 30, + "src/renderer/src/components/status-bar/usage-roster-formatting.test.ts": 34, + "src/renderer/src/components/status-bar/usage-roster-row-state.test.ts": 44, + "src/renderer/src/components/status-bar/usage-unit-label-parity.test.tsx": 267, + "src/renderer/src/components/status-bar/usage-unit-label.test.ts": 84, + "src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx": 985, + "src/renderer/src/components/status-bar/use-resource-usage-actions.test.tsx": 532, + "src/renderer/src/components/status-bar/use-resource-usage-derived-model.test.tsx": 553, + "src/renderer/src/components/status-bar/use-workspace-space-decision-projection.test.tsx": 1901, + "src/renderer/src/components/status-bar/workspace-space-breakdown-list.test.tsx": 371, + "src/renderer/src/components/status-bar/workspace-space-delete-host-routing.test.ts": 41, + "src/renderer/src/components/status-bar/workspace-space-format.test.ts": 67, + "src/renderer/src/components/status-bar/workspace-space-layout.test.ts": 37, + "src/renderer/src/components/status-bar/workspace-space-presentation.test.ts": 319, + "src/renderer/src/components/tab-bar/BrowserTab.test.tsx": 1505, + "src/renderer/src/components/tab-bar/ClientHostedBrowserTabRows.test.tsx": 701, + "src/renderer/src/components/tab-bar/CopyTabIdMenuItem.test.tsx": 328, + "src/renderer/src/components/tab-bar/EditorFileTab.test.tsx": 667, + "src/renderer/src/components/tab-bar/EditorFileTabContextMenu.test.tsx": 835, + "src/renderer/src/components/tab-bar/QuickLaunchButton.launch-status.test.tsx": 688, + "src/renderer/src/components/tab-bar/QuickLaunchButton.test.ts": 402, + "src/renderer/src/components/tab-bar/RecentTabSwitcher.test.tsx": 290, + "src/renderer/src/components/tab-bar/SortableTab.rename-shortcut.test.tsx": 473, + "src/renderer/src/components/tab-bar/SortableTab.update-depth-probe.test.tsx": 1688, + "src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx": 656, + "src/renderer/src/components/tab-bar/TabBar.client-hosted-row-active-state.test.ts": 2936, + "src/renderer/src/components/tab-bar/TabBar.context-menu.test.ts": 3906, + "src/renderer/src/components/tab-bar/TabBar.os-file-drop.test.tsx": 875, + "src/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.ts": 2782, + "src/renderer/src/components/tab-bar/TabBar.windows-shell-remote-runtime.test.ts": 2214, + "src/renderer/src/components/tab-bar/TabBar.windows-shell-ssh-host.test.ts": 1702, + "src/renderer/src/components/tab-bar/TabBar.worktree-write-gate.test.tsx": 814, + "src/renderer/src/components/tab-bar/TabBar.worktree-write-gate.windows.test.tsx": 799, + "src/renderer/src/components/tab-bar/TabBarCreateEntry.history.test.tsx": 1474, + "src/renderer/src/components/tab-bar/TabBarCreateEntry.keyboard.test.tsx": 2256, + "src/renderer/src/components/tab-bar/TabBarCreateEntry.search.test.tsx": 2785, + "src/renderer/src/components/tab-bar/TabBarCreateEntry.tab-results.test.tsx": 1973, + "src/renderer/src/components/tab-bar/TabBarCreateEntryRow.test.tsx": 666, + "src/renderer/src/components/tab-bar/TabBarItemRow.test.tsx": 1510, + "src/renderer/src/components/tab-bar/TabBarQuickCommandItem.test.tsx": 360, + "src/renderer/src/components/tab-bar/TabBarQuickCommandsMenu.keyboard.test.ts": 105, + "src/renderer/src/components/tab-bar/TabSessionSurfaceSwitchMenuItems.test.tsx": 1981, + "src/renderer/src/components/tab-bar/TabStripScrollIndicator.test.tsx": 320, + "src/renderer/src/components/tab-bar/TabStripTooltipProvider.test.tsx": 531, + "src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.state-tooltip.test.tsx": 230, + "src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.test.tsx": 220, + "src/renderer/src/components/tab-bar/client-hosted-browser-row-strip-placement.test.ts": 36, + "src/renderer/src/components/tab-bar/drop-indicator.test.ts": 37, + "src/renderer/src/components/tab-bar/group-tab-order.test.ts": 1476, + "src/renderer/src/components/tab-bar/middle-button-default-guard.test.ts": 27, + "src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.test.ts": 60, + "src/renderer/src/components/tab-bar/open-tab-entry-dedupe.test.ts": 273, + "src/renderer/src/components/tab-bar/open-tab-search-retention.test.ts": 590, + "src/renderer/src/components/tab-bar/open-tab-search.test.ts": 405, + "src/renderer/src/components/tab-bar/open-tab-selection-routing.test.ts": 222, + "src/renderer/src/components/tab-bar/query-token-match.test.ts": 40, + "src/renderer/src/components/tab-bar/recent-tab-switching.test.ts": 41, + "src/renderer/src/components/tab-bar/reconcile-order.test.ts": 30, + "src/renderer/src/components/tab-bar/tab-agent-launch-options.test.ts": 216, + "src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts": 62, + "src/renderer/src/components/tab-bar/tab-bar-item-model.git-status.test.ts": 1450, + "src/renderer/src/components/tab-bar/tab-create-entry-action.test.ts": 1394, + "src/renderer/src/components/tab-bar/tab-create-entry-classifier.runtime-listing.test.tsx": 1638, + "src/renderer/src/components/tab-bar/tab-create-entry-classifier.test.ts": 1329, + "src/renderer/src/components/tab-bar/tab-create-entry-file-matches.test.ts": 178, + "src/renderer/src/components/tab-bar/tab-create-entry-forced-search.test.ts": 33, + "src/renderer/src/components/tab-bar/tab-create-entry-history-placement.test.ts": 284, + "src/renderer/src/components/tab-bar/tab-create-entry-local-path.test.ts": 1603, + "src/renderer/src/components/tab-bar/tab-create-entry-search-ranking.test.ts": 383, + "src/renderer/src/components/tab-bar/tab-create-entry-url-classification.test.ts": 308, + "src/renderer/src/components/tab-bar/tab-create-menu-options.test.ts": 81, + "src/renderer/src/components/tab-bar/tab-move-to-pane-column.test.ts": 1265, + "src/renderer/src/components/tab-bar/tab-session-history-switch.parity.test.ts": 1664, + "src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts": 1738, + "src/renderer/src/components/tab-bar/tab-strip-content-resize-observers.test.ts": 173, + "src/renderer/src/components/tab-bar/tab-strip-growth-scroll.test.tsx": 380, + "src/renderer/src/components/tab-bar/tab-strip-overflow-navigation.test.ts": 29, + "src/renderer/src/components/tab-bar/tab-strip-pointer-activation.test.tsx": 1697, + "src/renderer/src/components/tab-bar/tab-strip-scroll-metrics.test.ts": 36, + "src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx": 1711, + "src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts": 154, "src/renderer/src/components/tab-bar/terminal-tab-spinner-launch-agent.test.ts": 103, - "src/renderer/src/components/tab-bar/use-open-tab-search.test.ts": 1793, - "src/renderer/src/components/tab-bar/use-tab-bar-item-projection.test.tsx": 2025, - "src/renderer/src/components/tab-bar/use-tab-bar-quick-command-search-input.test.tsx": 367, - "src/renderer/src/components/tab-bar/windows-shell-launch.test.ts": 51, - "src/renderer/src/components/tab-bar/windows-shell-menu-visibility.test.ts": 50, - "src/renderer/src/components/tab-group/RetainedPaneHost.test.tsx": 360, - "src/renderer/src/components/tab-group/TabGroupPanel.context-menu.test.ts": 43, - "src/renderer/src/components/tab-group/TabGroupPanel.os-file-drop.test.tsx": 1712, + "src/renderer/src/components/tab-bar/use-open-tab-search.test.ts": 1896, + "src/renderer/src/components/tab-bar/use-tab-bar-item-projection.test.tsx": 1972, + "src/renderer/src/components/tab-bar/use-tab-bar-quick-command-search-input.test.tsx": 362, + "src/renderer/src/components/tab-bar/windows-shell-launch.test.ts": 26, + "src/renderer/src/components/tab-bar/windows-shell-menu-visibility.test.ts": 33, + "src/renderer/src/components/tab-group/RetainedPaneHost.test.tsx": 336, + "src/renderer/src/components/tab-group/TabGroupPanel.context-menu.test.ts": 33, + "src/renderer/src/components/tab-group/TabGroupPanel.os-file-drop.test.tsx": 1463, "src/renderer/src/components/tab-group/TabGroupSplitLayout.drag.test.tsx": 485, - "src/renderer/src/components/tab-group/TabGroupSplitLayout.test.ts": 241, - "src/renderer/src/components/tab-group/TabPaneColumnSplitDragOverlay.test.tsx": 99, - "src/renderer/src/components/tab-group/tab-drag-pointer-sensor.test.ts": 145, - "src/renderer/src/components/tab-group/tab-drag-pointer.test.ts": 48, - "src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts": 1235, - "src/renderer/src/components/tab-group/tab-drag-retained-guest-passthrough.test.tsx": 1438, - "src/renderer/src/components/tab-group/tab-drop-zone.test.ts": 50, - "src/renderer/src/components/tab-group/tab-group-body-anchor.test.ts": 48, - "src/renderer/src/components/tab-group/tab-group-body-geometry.test.ts": 168, - "src/renderer/src/components/tab-group/tab-group-panel-split-target.test.ts": 74, - "src/renderer/src/components/tab-group/tab-insertion.test.ts": 67, - "src/renderer/src/components/tab-group/useTabDragSplit.test.ts": 1883, - "src/renderer/src/components/tab-group/useTabGroupCreationCommands.local-shell.test.ts": 998, - "src/renderer/src/components/tab-group/useTabGroupTabCloseCommands.structured-session.test.ts": 543, - "src/renderer/src/components/tab-group/useTabGroupTabCloseCommands.test.tsx": 1900, - "src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts": 1213, - "src/renderer/src/components/tab-group/workspace-tab-close-commands.floating-reclaim.test.ts": 1573, - "src/renderer/src/components/task-page-cache-selectors.test.ts": 71, - "src/renderer/src/components/task-page-checks-pill.test.ts": 46, - "src/renderer/src/components/task-page-default-repo-selection.test.ts": 80, - "src/renderer/src/components/task-page-empty-state.test.ts": 89, - "src/renderer/src/components/task-page-github-dialog-state-authority.test.ts": 191, - "src/renderer/src/components/task-page-github-list-scroll-restore.test.ts": 206, - "src/renderer/src/components/task-page-github-resume-cache.test.ts": 52, - "src/renderer/src/components/task-page-github-reviewer-suggestions.test.ts": 75, - "src/renderer/src/components/task-page-github-status-actions.test.ts": 49, - "src/renderer/src/components/task-page-github-status-state.test.ts": 56, - "src/renderer/src/components/task-page-github-task-kind.test.ts": 70, - "src/renderer/src/components/task-page-github-work-item-filter-membership.test.ts": 47, - "src/renderer/src/components/task-page-github-work-item-mutation-patches.test.ts": 62, - "src/renderer/src/components/task-page-github-work-item-mutation-regressions.test.ts": 98, - "src/renderer/src/components/task-page-github-work-item-mutations.test.ts": 168, - "src/renderer/src/components/task-page-github-work-item-status.test.ts": 58, - "src/renderer/src/components/task-page-gitlab-task-filters.test.ts": 1459, - "src/renderer/src/components/task-page-initial-selection-scaling.test.tsx": 1832, - "src/renderer/src/components/task-page-jira-cache-selectors.test.ts": 49, - "src/renderer/src/components/task-page-jira-create-fields.test.ts": 84, - "src/renderer/src/components/task-page-jira-grouping.test.ts": 577, - "src/renderer/src/components/task-page-jira-item-source-context.test.ts": 60, - "src/renderer/src/components/task-page-jira-load-state.test.ts": 54, - "src/renderer/src/components/task-page-jira-project-selection.test.ts": 49, - "src/renderer/src/components/task-page-jira-search.test.ts": 89, - "src/renderer/src/components/task-page-jira-sort-controls.test.tsx": 702, - "src/renderer/src/components/task-page-jira-sorting.test.ts": 134, - "src/renderer/src/components/task-page-jira-text-fallback-notice.test.tsx": 429, - "src/renderer/src/components/task-page-linear-in-orca-issues.test.ts": 94, - "src/renderer/src/components/task-page-linear-issue-empty-state.test.ts": 47, - "src/renderer/src/components/task-page-linear-issue-grouping.test.ts": 1537, - "src/renderer/src/components/task-page-linear-issue-request.test.ts": 60, - "src/renderer/src/components/task-page-linear-team-selection.test.ts": 48, - "src/renderer/src/components/task-page-list-chrome-visibility.test.ts": 58, - "src/renderer/src/components/task-page-localized-options.test.ts": 256, - "src/renderer/src/components/task-page-mutation-page-allocation.test.ts": 86, - "src/renderer/src/components/task-page-new-issue-draft.test.ts": 60, - "src/renderer/src/components/task-page-pagination-page-numbers.test.ts": 63, - "src/renderer/src/components/task-page-pr-check-summary.test.ts": 45, - "src/renderer/src/components/task-page-repo-source-context.test.ts": 1529, - "src/renderer/src/components/task-page-repo-source-divergence.test.ts": 1378, - "src/renderer/src/components/task-page-task-source-host-availability.test.ts": 1464, - "src/renderer/src/components/task-page-work-item-pagination.test.ts": 84, - "src/renderer/src/components/task-page/jira/IssueAssigneeField.test.tsx": 1049, - "src/renderer/src/components/task-source-context-summary.test.ts": 92, - "src/renderer/src/components/task-source-provider-availability.test.ts": 64, - "src/renderer/src/components/terminal-agent-tab-shortcut.test.ts": 1561, - "src/renderer/src/components/terminal-pane/CloseTerminalDialog.test.tsx": 388, - "src/renderer/src/components/terminal-pane/MobileDriverOverlay.test.tsx": 181, - "src/renderer/src/components/terminal-pane/PinnedTabCloseDialog.test.tsx": 1699, - "src/renderer/src/components/terminal-pane/RunningTerminalCloseDialog.test.tsx": 1847, - "src/renderer/src/components/terminal-pane/SessionRestoredBanner.test.tsx": 233, - "src/renderer/src/components/terminal-pane/TerminalAgentSessionForkDialog.test.tsx": 171, - "src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx": 1769, - "src/renderer/src/components/terminal-pane/TerminalErrorToast.test.ts": 626, - "src/renderer/src/components/terminal-pane/TerminalOverlaySlot.emptied-reaction.test.tsx": 439, - "src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.test.tsx": 1019, - "src/renderer/src/components/terminal-pane/TerminalPaneNativeChatPortal.test.tsx": 327, - "src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.react185.test.tsx": 1492, - "src/renderer/src/components/terminal-pane/TerminalProcessExitOverlay.test.tsx": 370, - "src/renderer/src/components/terminal-pane/TerminalRemoteRuntimeReconnectBanner.test.tsx": 380, - "src/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.test.tsx": 1832, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-attention-dispatch.test.ts": 223, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-completion-replay-guard.test.ts": 206, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-dispose-leak.test.ts": 219, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-done-quiet-window.test.ts": 196, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-title-precedence.test.ts": 204, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-monitoring-turn-end.test.ts": 341, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts": 251, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts": 270, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-exit-turn-boundary.test.ts": 191, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts": 175, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-boundary.test.ts": 227, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-replay.test.ts": 208, - "src/renderer/src/components/terminal-pane/agent-completion-no-evidence-cadence.test.ts": 1486, - "src/renderer/src/components/terminal-pane/agent-completion-poll-interval.test.ts": 111, - "src/renderer/src/components/terminal-pane/agent-completion-stale-evidence-backoff.test.ts": 147, - "src/renderer/src/components/terminal-pane/agent-completion-steady-state-opt-in.test.ts": 188, - "src/renderer/src/components/terminal-pane/agent-hook-terminal-lifecycle.test.ts": 45, - "src/renderer/src/components/terminal-pane/agent-interrupt-inference.test.ts": 155, - "src/renderer/src/components/terminal-pane/agent-launch-pane-notice.test.ts": 43, - "src/renderer/src/components/terminal-pane/agent-process-inspection-queue-rejection-containment.test.ts": 53, - "src/renderer/src/components/terminal-pane/agent-process-inspection-round.test.ts": 61, - "src/renderer/src/components/terminal-pane/agent-question-answered-inference.test.ts": 108, - "src/renderer/src/components/terminal-pane/cache-timer-seeding.test.ts": 100, - "src/renderer/src/components/terminal-pane/claude-old-terminal-banner.test.tsx": 1667, - "src/renderer/src/components/terminal-pane/closed-terminal-leaf-notice.test.ts": 1078, - "src/renderer/src/components/terminal-pane/codex-backfill-error-detector.test.ts": 31, - "src/renderer/src/components/terminal-pane/codex-detached-pane-restart.test.ts": 1928, - "src/renderer/src/components/terminal-pane/codex-shared-server-banner.test.tsx": 2177, - "src/renderer/src/components/terminal-pane/codex-shared-settings-notice.test.ts": 245, - "src/renderer/src/components/terminal-pane/codex-terminal-presence.test.ts": 83, - "src/renderer/src/components/terminal-pane/codex-terminal-server-isolation-notice.test.ts": 232, - "src/renderer/src/components/terminal-pane/command-code-done-settle.test.ts": 53, - "src/renderer/src/components/terminal-pane/command-code-output-ownership.test.ts": 44, - "src/renderer/src/components/terminal-pane/compose-active-terminal-theme.test.ts": 135, - "src/renderer/src/components/terminal-pane/deferred-reattach-live-data-queue.test.ts": 193, - "src/renderer/src/components/terminal-pane/deferred-split-pane-handoff.test.ts": 75, - "src/renderer/src/components/terminal-pane/desktop-fit-fallback.test.ts": 85, - "src/renderer/src/components/terminal-pane/direct-ssh-hidden-output-restore-unavailable-banner.test.ts": 1301, - "src/renderer/src/components/terminal-pane/edge-wrapped-terminal-http-links.test.ts": 2360, - "src/renderer/src/components/terminal-pane/expand-collapse-render-stability.test.ts": 2055, - "src/renderer/src/components/terminal-pane/expand-collapse.test.ts": 101, - "src/renderer/src/components/terminal-pane/floating-file-link-selection.test.ts": 2261, - "src/renderer/src/components/terminal-pane/focus-terminal-pane-event.test.ts": 55, - "src/renderer/src/components/terminal-pane/focused-pane-rim-flash.test.ts": 46, - "src/renderer/src/components/terminal-pane/git-bash-console-capacity.test.ts": 52, - "src/renderer/src/components/terminal-pane/hard-wrapped-terminal-http-links.test.ts": 90, - "src/renderer/src/components/terminal-pane/hidden-output-restore-scheduler.test.ts": 54, - "src/renderer/src/components/terminal-pane/ipc-pty-opencode-model.test.ts": 263, - "src/renderer/src/components/terminal-pane/issue-12112-agent-pane-startup-color-reply-leak.repro.test.ts": 530, - "src/renderer/src/components/terminal-pane/issue-21762-tracking-mode-middle-click-double-paste.repro.test.ts": 349, - "src/renderer/src/components/terminal-pane/issue-4631-terminal-hover-link-provider.repro.test.ts": 919, - "src/renderer/src/components/terminal-pane/keyboard-handlers-ime-composing-chord.test.tsx": 2146, - "src/renderer/src/components/terminal-pane/keyboard-handlers-ime-enter-keyup.test.tsx": 1944, - "src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx": 2229, - "src/renderer/src/components/terminal-pane/keyboard-handlers.test.ts": 1877, - "src/renderer/src/components/terminal-pane/layout-serialization.test.ts": 96, - "src/renderer/src/components/terminal-pane/leaf-scrollback-resolution.test.ts": 263, - "src/renderer/src/components/terminal-pane/manual-terminal-worktree-park-eligibility.test.ts": 1422, - "src/renderer/src/components/terminal-pane/merge-captured-leaf-state.test.ts": 56, - "src/renderer/src/components/terminal-pane/mobile-driver-overlay-collapse.test.ts": 47, - "src/renderer/src/components/terminal-pane/mobile-driver-overlay-focus.test.ts": 45, - "src/renderer/src/components/terminal-pane/mobile-driver-overlay-visibility.test.ts": 38, - "src/renderer/src/components/terminal-pane/mouse-hide-while-typing.test.ts": 48, - "src/renderer/src/components/terminal-pane/native-chat-leaf-title-agent.test.ts": 87, - "src/renderer/src/components/terminal-pane/osc52-clipboard-default-on-notice.test.ts": 278, - "src/renderer/src/components/terminal-pane/osc52-clipboard-toast.test.ts": 1039, - "src/renderer/src/components/terminal-pane/osc52-clipboard.test.ts": 170, - "src/renderer/src/components/terminal-pane/override-affected-panes.test.ts": 48, - "src/renderer/src/components/terminal-pane/paired-parking-unverifiable-host.test.ts": 93, - "src/renderer/src/components/terminal-pane/paired-reconnect-multi-pane-materialization.test.ts": 1915, - "src/renderer/src/components/terminal-pane/pane-agent-session-id.test.ts": 45, - "src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.test.ts": 349, - "src/renderer/src/components/terminal-pane/pane-foreground-inspect-observation-identity.test.ts": 100, - "src/renderer/src/components/terminal-pane/pane-foreground-process-exit-retire.test.ts": 223, - "src/renderer/src/components/terminal-pane/pane-helpers.test.ts": 73, - "src/renderer/src/components/terminal-pane/pane-restart-transport-handoff.test.ts": 556, - "src/renderer/src/components/terminal-pane/pane-title-overlay-rects.test.ts": 58, - "src/renderer/src/components/terminal-pane/pane-title-update-global-scan-budget.test.tsx": 3072, - "src/renderer/src/components/terminal-pane/parked-terminal-buffer-capture.test.ts": 308, - "src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.test.ts": 2465, - "src/renderer/src/components/terminal-pane/parked-terminal-command-status.test.ts": 428, - "src/renderer/src/components/terminal-pane/parked-terminal-tab-capture-episodes.test.ts": 301, - "src/renderer/src/components/terminal-pane/parse-osc7.test.ts": 50, - "src/renderer/src/components/terminal-pane/pending-pane-close-confirmation.test.ts": 1589, - "src/renderer/src/components/terminal-pane/pending-runtime-pane-close.test.ts": 1223, - "src/renderer/src/components/terminal-pane/pending-split-close.test.ts": 1550, - "src/renderer/src/components/terminal-pane/pty-buffer-serializer.test.ts": 50, - "src/renderer/src/components/terminal-pane/pty-connection-agent-session-resume.test.ts": 2046, - "src/renderer/src/components/terminal-pane/pty-connection-cold-restore-agent-resume.test.ts": 1677, - "src/renderer/src/components/terminal-pane/pty-connection-cold-restore-repaint.test.ts": 1145, - "src/renderer/src/components/terminal-pane/pty-connection-cold-restore-resume-command.test.ts": 1608, - "src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts": 7322, - "src/renderer/src/components/terminal-pane/pty-connection-command-finished-late-done.test.ts": 1868, - "src/renderer/src/components/terminal-pane/pty-connection-daemon-snapshot-replay.test.ts": 2005, - "src/renderer/src/components/terminal-pane/pty-connection-deferred-reattach-live-output.test.ts": 1774, - "src/renderer/src/components/terminal-pane/pty-connection-deliberate-sleep-guard.test.ts": 1469, - "src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts": 1745, - "src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts": 2594, - "src/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.test.ts": 879, - "src/renderer/src/components/terminal-pane/pty-connection-foreground-routing-confirmation.test.ts": 1378, - "src/renderer/src/components/terminal-pane/pty-connection-foreground-write-path.test.ts": 2002, - "src/renderer/src/components/terminal-pane/pty-connection-fresh-spawn-guards.test.ts": 2467, - "src/renderer/src/components/terminal-pane/pty-connection-hibernation-wake.test.ts": 2008, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.test.ts": 2262, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.test.ts": 1815, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-snapshot.test.ts": 2838, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.test.ts": 1760, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-delivery-gate.test.ts": 2566, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-output-restore.test.ts": 1595, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-query-snapshot-restore.test.ts": 1664, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-restore-fit-overflow.test.ts": 914, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-live-overlap.test.ts": 1148, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-resize-signals.test.ts": 2595, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-tui-snapshot-replay.test.ts": 1107, - "src/renderer/src/components/terminal-pane/pty-connection-hook-completion-bell-arbitration.test.ts": 1929, - "src/renderer/src/components/terminal-pane/pty-connection-hook-completion-side-effects.test.ts": 1620, - "src/renderer/src/components/terminal-pane/pty-connection-hook-idle-arbitration.test.ts": 1272, - "src/renderer/src/components/terminal-pane/pty-connection-interrupt-inference.test.ts": 2067, - "src/renderer/src/components/terminal-pane/pty-connection-kitty-keyboard-restore.test.ts": 1436, - "src/renderer/src/components/terminal-pane/pty-connection-main-side-effect-authority.test.ts": 2527, - "src/renderer/src/components/terminal-pane/pty-connection-mode-2031-subscriptions.test.ts": 2233, - "src/renderer/src/components/terminal-pane/pty-connection-notification-settings-gating.test.ts": 1856, - "src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts": 922, - "src/renderer/src/components/terminal-pane/pty-connection-post-dispose-restore-termination.test.ts": 1239, - "src/renderer/src/components/terminal-pane/pty-connection-pty-exit-teardown.test.ts": 4853, - "src/renderer/src/components/terminal-pane/pty-connection-queued-startup-consume.test.ts": 1191, - "src/renderer/src/components/terminal-pane/pty-connection-reattach-binding.test.ts": 4133, - "src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.test.ts": 2681, - "src/renderer/src/components/terminal-pane/pty-connection-remote-runtime-attach.test.ts": 2433, - "src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts": 1154, - "src/renderer/src/components/terminal-pane/pty-connection-renderer-risk-repaint.test.ts": 1895, - "src/renderer/src/components/terminal-pane/pty-connection-replay-payload-handling.test.ts": 2078, - "src/renderer/src/components/terminal-pane/pty-connection-restored-baseline-shortfall.test.ts": 1232, - "src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts": 1812, - "src/renderer/src/components/terminal-pane/pty-connection-session-liveness.test.ts": 2447, - "src/renderer/src/components/terminal-pane/pty-connection-setup-split-spawn.test.ts": 1546, + "src/renderer/src/components/tab-group/TabGroupSplitLayout.test.ts": 133, + "src/renderer/src/components/tab-group/TabPaneColumnSplitDragOverlay.test.tsx": 62, + "src/renderer/src/components/tab-group/tab-drag-pointer-sensor.test.ts": 177, + "src/renderer/src/components/tab-group/tab-drag-pointer.test.ts": 38, + "src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts": 1356, + "src/renderer/src/components/tab-group/tab-drag-retained-guest-passthrough.test.tsx": 1738, + "src/renderer/src/components/tab-group/tab-drop-zone.test.ts": 35, + "src/renderer/src/components/tab-group/tab-group-body-anchor.test.ts": 30, + "src/renderer/src/components/tab-group/tab-group-body-geometry.test.ts": 138, + "src/renderer/src/components/tab-group/tab-group-panel-split-target.test.ts": 58, + "src/renderer/src/components/tab-group/tab-insertion.test.ts": 52, + "src/renderer/src/components/tab-group/useTabDragSplit.test.ts": 1673, + "src/renderer/src/components/tab-group/useTabGroupCreationCommands.local-shell.test.ts": 836, + "src/renderer/src/components/tab-group/useTabGroupTabCloseCommands.structured-session.test.ts": 430, + "src/renderer/src/components/tab-group/useTabGroupTabCloseCommands.test.tsx": 1711, + "src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts": 676, + "src/renderer/src/components/tab-group/workspace-tab-close-commands.floating-reclaim.test.ts": 1405, + "src/renderer/src/components/task-page-cache-selectors.test.ts": 69, + "src/renderer/src/components/task-page-checks-pill.test.ts": 39, + "src/renderer/src/components/task-page-default-repo-selection.test.ts": 54, + "src/renderer/src/components/task-page-empty-state.test.ts": 62, + "src/renderer/src/components/task-page-github-dialog-state-authority.test.ts": 67, + "src/renderer/src/components/task-page-github-list-scroll-restore.test.ts": 163, + "src/renderer/src/components/task-page-github-resume-cache.test.ts": 39, + "src/renderer/src/components/task-page-github-reviewer-suggestions.test.ts": 78, + "src/renderer/src/components/task-page-github-status-actions.test.ts": 35, + "src/renderer/src/components/task-page-github-status-state.test.ts": 33, + "src/renderer/src/components/task-page-github-task-kind.test.ts": 61, + "src/renderer/src/components/task-page-github-work-item-filter-membership.test.ts": 31, + "src/renderer/src/components/task-page-github-work-item-mutation-patches.test.ts": 44, + "src/renderer/src/components/task-page-github-work-item-mutation-regressions.test.ts": 96, + "src/renderer/src/components/task-page-github-work-item-mutations.test.ts": 159, + "src/renderer/src/components/task-page-github-work-item-status.test.ts": 37, + "src/renderer/src/components/task-page-gitlab-task-filters.test.ts": 1370, + "src/renderer/src/components/task-page-initial-selection-scaling.test.tsx": 1587, + "src/renderer/src/components/task-page-jira-board-detail-integration.test.tsx": 2017, + "src/renderer/src/components/task-page-jira-board-sections.test.ts": 1498, + "src/renderer/src/components/task-page-jira-board-transitions.test.ts": 49, + "src/renderer/src/components/task-page-jira-board-view.test.tsx": 1803, + "src/renderer/src/components/task-page-jira-board.test.tsx": 1631, + "src/renderer/src/components/task-page-jira-cache-selectors.test.ts": 32, + "src/renderer/src/components/task-page-jira-create-fields.test.ts": 65, + "src/renderer/src/components/task-page-jira-grouping.test.ts": 2252, + "src/renderer/src/components/task-page-jira-item-source-context.test.ts": 33, + "src/renderer/src/components/task-page-jira-linked-issues.test.ts": 57, + "src/renderer/src/components/task-page-jira-load-state.test.ts": 36, + "src/renderer/src/components/task-page-jira-project-selection.test.ts": 31, + "src/renderer/src/components/task-page-jira-quick-copy.test.tsx": 3451, + "src/renderer/src/components/task-page-jira-saved-filter-application.test.tsx": 1859, + "src/renderer/src/components/task-page-jira-search.test.ts": 73, + "src/renderer/src/components/task-page-jira-sort-controls.test.tsx": 715, + "src/renderer/src/components/task-page-jira-sorting.test.ts": 148, + "src/renderer/src/components/task-page-jira-status-catalog.test.ts": 28, + "src/renderer/src/components/task-page-jira-status-order-session.test.tsx": 1881, + "src/renderer/src/components/task-page-jira-text-fallback-notice.test.tsx": 478, + "src/renderer/src/components/task-page-jira-workspace-actions.test.tsx": 2107, + "src/renderer/src/components/task-page-linear-in-orca-issues.test.ts": 81, + "src/renderer/src/components/task-page-linear-issue-empty-state.test.ts": 29, + "src/renderer/src/components/task-page-linear-issue-grouping.test.ts": 1344, + "src/renderer/src/components/task-page-linear-issue-request.test.ts": 52, + "src/renderer/src/components/task-page-linear-team-selection.test.ts": 39, + "src/renderer/src/components/task-page-list-chrome-visibility.test.ts": 36, + "src/renderer/src/components/task-page-localized-options.test.ts": 188, + "src/renderer/src/components/task-page-mutation-page-allocation.test.ts": 95, + "src/renderer/src/components/task-page-new-issue-draft.test.ts": 53, + "src/renderer/src/components/task-page-pagination-page-numbers.test.ts": 39, + "src/renderer/src/components/task-page-pr-check-summary.test.ts": 40, + "src/renderer/src/components/task-page-repo-source-context.test.ts": 1360, + "src/renderer/src/components/task-page-repo-source-divergence.test.ts": 1310, + "src/renderer/src/components/task-page-task-source-host-availability.test.ts": 1553, + "src/renderer/src/components/task-page-work-item-pagination.test.ts": 76, + "src/renderer/src/components/task-page/jira/Content.board-owner.test.tsx": 2237, + "src/renderer/src/components/task-page/jira/Filters.linked-mode.test.tsx": 3095, + "src/renderer/src/components/task-page/jira/IssueAssigneeField.test.tsx": 1121, + "src/renderer/src/components/task-source-context-summary.test.ts": 89, + "src/renderer/src/components/task-source-provider-availability.test.ts": 43, + "src/renderer/src/components/terminal-agent-tab-shortcut.test.ts": 1372, + "src/renderer/src/components/terminal-pane/CloseTerminalDialog.test.tsx": 529, + "src/renderer/src/components/terminal-pane/MobileDriverOverlay.test.tsx": 139, + "src/renderer/src/components/terminal-pane/PinnedTabCloseDialog.test.tsx": 1747, + "src/renderer/src/components/terminal-pane/RunningTerminalCloseDialog.test.tsx": 1887, + "src/renderer/src/components/terminal-pane/SessionRestoredBanner.test.tsx": 184, + "src/renderer/src/components/terminal-pane/TerminalAgentSessionForkDialog.test.tsx": 98, + "src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx": 1496, + "src/renderer/src/components/terminal-pane/TerminalErrorToast.test.ts": 691, + "src/renderer/src/components/terminal-pane/TerminalOverlaySlot.emptied-reaction.test.tsx": 484, + "src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.test.tsx": 1228, + "src/renderer/src/components/terminal-pane/TerminalPaneNativeChatPortal.test.tsx": 323, + "src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.react185.test.tsx": 794, + "src/renderer/src/components/terminal-pane/TerminalPaneResumeInChatButton.test.tsx": 1281, + "src/renderer/src/components/terminal-pane/TerminalProcessExitOverlay.test.tsx": 352, + "src/renderer/src/components/terminal-pane/TerminalRemoteRuntimeReconnectBanner.test.tsx": 1816, + "src/renderer/src/components/terminal-pane/TerminalSleepingPaneOverlay.test.tsx": 387, + "src/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.test.tsx": 1462, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-attention-dispatch.test.ts": 127, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-completion-replay-guard.test.ts": 157, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-dispose-leak.test.ts": 162, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-done-quiet-window.test.ts": 163, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-title-precedence.test.ts": 185, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-monitoring-turn-end.test.ts": 263, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts": 169, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts": 202, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-exit-turn-boundary.test.ts": 175, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts": 179, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-boundary.test.ts": 157, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-replay.test.ts": 182, + "src/renderer/src/components/terminal-pane/agent-completion-no-evidence-cadence.test.ts": 1217, + "src/renderer/src/components/terminal-pane/agent-completion-poll-interval.test.ts": 114, + "src/renderer/src/components/terminal-pane/agent-completion-stale-evidence-backoff.test.ts": 98, + "src/renderer/src/components/terminal-pane/agent-completion-steady-state-opt-in.test.ts": 158, + "src/renderer/src/components/terminal-pane/agent-hook-terminal-lifecycle.test.ts": 28, + "src/renderer/src/components/terminal-pane/agent-interrupt-inference.test.ts": 145, + "src/renderer/src/components/terminal-pane/agent-launch-pane-notice.test.ts": 35, + "src/renderer/src/components/terminal-pane/agent-process-inspection-queue-rejection-containment.test.ts": 46, + "src/renderer/src/components/terminal-pane/agent-process-inspection-round.test.ts": 43, + "src/renderer/src/components/terminal-pane/agent-question-answered-inference.test.ts": 93, + "src/renderer/src/components/terminal-pane/cache-timer-seeding.test.ts": 94, + "src/renderer/src/components/terminal-pane/claude-old-terminal-banner.test.tsx": 1615, + "src/renderer/src/components/terminal-pane/closed-terminal-leaf-notice.test.ts": 781, + "src/renderer/src/components/terminal-pane/codex-backfill-error-detector.test.ts": 24, + "src/renderer/src/components/terminal-pane/codex-detached-pane-restart.test.ts": 1659, + "src/renderer/src/components/terminal-pane/codex-shared-server-banner.test.tsx": 2300, + "src/renderer/src/components/terminal-pane/codex-shared-settings-notice.test.ts": 270, + "src/renderer/src/components/terminal-pane/codex-terminal-presence.test.ts": 78, + "src/renderer/src/components/terminal-pane/codex-terminal-server-isolation-notice.test.ts": 264, + "src/renderer/src/components/terminal-pane/command-code-done-settle.test.ts": 32, + "src/renderer/src/components/terminal-pane/command-code-output-ownership.test.ts": 38, + "src/renderer/src/components/terminal-pane/compose-active-terminal-theme.test.ts": 119, + "src/renderer/src/components/terminal-pane/deferred-reattach-live-data-queue.test.ts": 197, + "src/renderer/src/components/terminal-pane/deferred-split-pane-handoff.test.ts": 53, + "src/renderer/src/components/terminal-pane/desktop-fit-fallback.test.ts": 66, + "src/renderer/src/components/terminal-pane/direct-ssh-hidden-output-restore-unavailable-banner.test.ts": 1395, + "src/renderer/src/components/terminal-pane/edge-wrapped-terminal-http-links.test.ts": 1883, + "src/renderer/src/components/terminal-pane/expand-collapse-render-stability.test.ts": 2208, + "src/renderer/src/components/terminal-pane/expand-collapse.test.ts": 97, + "src/renderer/src/components/terminal-pane/floating-file-link-selection.test.ts": 2004, + "src/renderer/src/components/terminal-pane/focus-terminal-pane-event.test.ts": 43, + "src/renderer/src/components/terminal-pane/focused-pane-rim-flash.test.ts": 33, + "src/renderer/src/components/terminal-pane/git-bash-console-capacity.test.ts": 45, + "src/renderer/src/components/terminal-pane/hard-wrapped-terminal-http-links.test.ts": 45, + "src/renderer/src/components/terminal-pane/hidden-output-restore-scheduler.test.ts": 44, + "src/renderer/src/components/terminal-pane/ipc-pty-opencode-model.test.ts": 279, + "src/renderer/src/components/terminal-pane/issue-12112-agent-pane-startup-color-reply-leak.repro.test.ts": 464, + "src/renderer/src/components/terminal-pane/issue-21762-tracking-mode-middle-click-double-paste.repro.test.ts": 287, + "src/renderer/src/components/terminal-pane/issue-4631-terminal-hover-link-provider.repro.test.ts": 886, + "src/renderer/src/components/terminal-pane/keyboard-handlers-ime-composing-chord.test.tsx": 1676, + "src/renderer/src/components/terminal-pane/keyboard-handlers-ime-enter-keyup.test.tsx": 1589, + "src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx": 2130, + "src/renderer/src/components/terminal-pane/keyboard-handlers.test.ts": 1272, + "src/renderer/src/components/terminal-pane/launch-parity-window-tab.test.ts": 4136, + "src/renderer/src/components/terminal-pane/layout-serialization.test.ts": 89, + "src/renderer/src/components/terminal-pane/leaf-scrollback-resolution.test.ts": 241, + "src/renderer/src/components/terminal-pane/legacy-relay-bridge-transport.test.ts": 271, + "src/renderer/src/components/terminal-pane/manual-terminal-worktree-park-eligibility.test.ts": 1216, + "src/renderer/src/components/terminal-pane/merge-captured-leaf-state.test.ts": 33, + "src/renderer/src/components/terminal-pane/mobile-driver-overlay-collapse.test.ts": 30, + "src/renderer/src/components/terminal-pane/mobile-driver-overlay-focus.test.ts": 36, + "src/renderer/src/components/terminal-pane/mobile-driver-overlay-visibility.test.ts": 28, + "src/renderer/src/components/terminal-pane/mouse-hide-while-typing.test.ts": 34, + "src/renderer/src/components/terminal-pane/native-chat-leaf-title-agent.test.ts": 67, + "src/renderer/src/components/terminal-pane/newer-daemon-hold-banner.test.tsx": 381, + "src/renderer/src/components/terminal-pane/osc52-clipboard-default-on-notice.test.ts": 248, + "src/renderer/src/components/terminal-pane/osc52-clipboard-toast.test.ts": 875, + "src/renderer/src/components/terminal-pane/osc52-clipboard.test.ts": 142, + "src/renderer/src/components/terminal-pane/override-affected-panes.test.ts": 39, + "src/renderer/src/components/terminal-pane/paired-parking-unverifiable-host.test.ts": 217, + "src/renderer/src/components/terminal-pane/paired-reconnect-multi-pane-materialization.test.ts": 1671, + "src/renderer/src/components/terminal-pane/pane-agent-session-id.test.ts": 42, + "src/renderer/src/components/terminal-pane/pane-delayed-agent-startup.test.ts": 138, + "src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.test.ts": 388, + "src/renderer/src/components/terminal-pane/pane-foreground-inspect-observation-identity.test.ts": 95, + "src/renderer/src/components/terminal-pane/pane-foreground-process-exit-retire.test.ts": 180, + "src/renderer/src/components/terminal-pane/pane-helpers.test.ts": 52, + "src/renderer/src/components/terminal-pane/pane-restart-transport-handoff.test.ts": 545, + "src/renderer/src/components/terminal-pane/pane-title-overlay-rects.test.ts": 46, + "src/renderer/src/components/terminal-pane/pane-title-update-global-scan-budget.test.tsx": 3319, + "src/renderer/src/components/terminal-pane/parked-terminal-buffer-capture.test.ts": 257, + "src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.test.ts": 2110, + "src/renderer/src/components/terminal-pane/parked-terminal-command-status.test.ts": 1014, + "src/renderer/src/components/terminal-pane/parked-terminal-tab-capture-episodes.test.ts": 272, + "src/renderer/src/components/terminal-pane/parse-osc7.test.ts": 31, + "src/renderer/src/components/terminal-pane/pending-pane-close-confirmation.test.ts": 2271, + "src/renderer/src/components/terminal-pane/pending-runtime-pane-close.test.ts": 1846, + "src/renderer/src/components/terminal-pane/pending-split-close.test.ts": 1758, + "src/renderer/src/components/terminal-pane/pty-buffer-serializer.test.ts": 41, + "src/renderer/src/components/terminal-pane/pty-connection-agent-session-resume.test.ts": 2230, + "src/renderer/src/components/terminal-pane/pty-connection-cold-restore-agent-resume.test.ts": 1894, + "src/renderer/src/components/terminal-pane/pty-connection-cold-restore-repaint.test.ts": 1199, + "src/renderer/src/components/terminal-pane/pty-connection-cold-restore-resume-command.test.ts": 1548, + "src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts": 5478, + "src/renderer/src/components/terminal-pane/pty-connection-command-finished-late-done.test.ts": 2024, + "src/renderer/src/components/terminal-pane/pty-connection-daemon-snapshot-replay.test.ts": 2341, + "src/renderer/src/components/terminal-pane/pty-connection-deferred-reattach-live-output.test.ts": 1988, + "src/renderer/src/components/terminal-pane/pty-connection-deliberate-sleep-guard.test.ts": 1631, + "src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts": 2049, + "src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts": 2466, + "src/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.test.ts": 834, + "src/renderer/src/components/terminal-pane/pty-connection-foreground-routing-confirmation.test.ts": 1532, + "src/renderer/src/components/terminal-pane/pty-connection-foreground-write-path.test.ts": 2036, + "src/renderer/src/components/terminal-pane/pty-connection-fresh-spawn-guards.test.ts": 2516, + "src/renderer/src/components/terminal-pane/pty-connection-hibernation-wake.test.ts": 2098, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.test.ts": 2328, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.test.ts": 2014, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-snapshot.test.ts": 2810, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.test.ts": 2345, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-delivery-gate.test.ts": 2669, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-output-restore.test.ts": 1500, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-query-snapshot-restore.test.ts": 1710, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-restore-fit-overflow.test.ts": 1000, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-live-overlap.test.ts": 1033, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-resize-signals.test.ts": 2741, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-tui-snapshot-replay.test.ts": 992, + "src/renderer/src/components/terminal-pane/pty-connection-hook-completion-bell-arbitration.test.ts": 1703, + "src/renderer/src/components/terminal-pane/pty-connection-hook-completion-side-effects.test.ts": 1942, + "src/renderer/src/components/terminal-pane/pty-connection-hook-idle-arbitration.test.ts": 1121, + "src/renderer/src/components/terminal-pane/pty-connection-interrupt-inference.test.ts": 2610, + "src/renderer/src/components/terminal-pane/pty-connection-kitty-keyboard-restore.test.ts": 1488, + "src/renderer/src/components/terminal-pane/pty-connection-main-side-effect-authority.test.ts": 2332, + "src/renderer/src/components/terminal-pane/pty-connection-mode-2031-subscriptions.test.ts": 2007, + "src/renderer/src/components/terminal-pane/pty-connection-notification-settings-gating.test.ts": 2023, + "src/renderer/src/components/terminal-pane/pty-connection-park-reveal-model-snapshot.test.ts": 1018, + "src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.test.ts": 1083, + "src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts": 828, + "src/renderer/src/components/terminal-pane/pty-connection-post-dispose-restore-termination.test.ts": 1011, + "src/renderer/src/components/terminal-pane/pty-connection-process-exit-row-retire.test.ts": 3709, + "src/renderer/src/components/terminal-pane/pty-connection-pty-exit-teardown.test.ts": 4219, + "src/renderer/src/components/terminal-pane/pty-connection-queued-startup-consume.test.ts": 1307, + "src/renderer/src/components/terminal-pane/pty-connection-reattach-binding.test.ts": 2228, + "src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.test.ts": 2958, + "src/renderer/src/components/terminal-pane/pty-connection-remote-runtime-attach.test.ts": 2363, + "src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts": 989, + "src/renderer/src/components/terminal-pane/pty-connection-renderer-risk-repaint.test.ts": 1994, + "src/renderer/src/components/terminal-pane/pty-connection-replay-payload-handling.test.ts": 2406, + "src/renderer/src/components/terminal-pane/pty-connection-restored-baseline-shortfall.test.ts": 1107, + "src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts": 1800, + "src/renderer/src/components/terminal-pane/pty-connection-session-liveness.test.ts": 2413, + "src/renderer/src/components/terminal-pane/pty-connection-setup-split-spawn.test.ts": 1980, "src/renderer/src/components/terminal-pane/pty-connection-sleeping-resume-banner.test.ts": 1242, - "src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts": 1408, - "src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts": 950, - "src/renderer/src/components/terminal-pane/pty-connection-stalled-hidden-restore.test.ts": 1743, - "src/renderer/src/components/terminal-pane/pty-connection-startup-command-delivery.test.ts": 1856, - "src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts": 2005, - "src/renderer/src/components/terminal-pane/pty-connection-terminal-input-gating.test.ts": 3182, - "src/renderer/src/components/terminal-pane/pty-connection-typed-agent-identity.test.ts": 2416, - "src/renderer/src/components/terminal-pane/pty-connection-unanswered-command-finish.test.ts": 1067, - "src/renderer/src/components/terminal-pane/pty-connection-visibility-resume-size.test.ts": 2445, - "src/renderer/src/components/terminal-pane/pty-connection-visible-pane-output-pause-latch.test.ts": 1123, - "src/renderer/src/components/terminal-pane/pty-connection-windows-cjk-repaint.test.ts": 1970, - "src/renderer/src/components/terminal-pane/pty-connection-windows-keyboard-reset.test.ts": 2271, - "src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-live-bookmark.test.ts": 132, - "src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-startup-snapshot.test.ts": 65, - "src/renderer/src/components/terminal-pane/pty-connection/disposed-spawn-retention.test.ts": 77, - "src/renderer/src/components/terminal-pane/pty-connection/empty-reattach-retire-evidence.test.ts": 60, - "src/renderer/src/components/terminal-pane/pty-connection/foreground-output-budgets.test.ts": 60, - "src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.test.ts": 99, - "src/renderer/src/components/terminal-pane/pty-connection/foreground-renderer-risk-scan-tail.test.ts": 112, - "src/renderer/src/components/terminal-pane/pty-connection/pane-launch-agent-candidate.test.ts": 51, - "src/renderer/src/components/terminal-pane/pty-connection/pane-pty-layout-binding.test.ts": 39, - "src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-register.test.ts": 139, - "src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.test.ts": 69, - "src/renderer/src/components/terminal-pane/pty-connection/pty-exit-hibernate.test.ts": 1416, - "src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.test.ts": 1490, - "src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler-park-reveal-verdict.test.ts": 189, - "src/renderer/src/components/terminal-pane/pty-connection/reattach-success-settle.test.ts": 226, - "src/renderer/src/components/terminal-pane/pty-connection/ssh-session-gone-verdict.test.ts": 46, - "src/renderer/src/components/terminal-pane/pty-connection/terminal-tab-id.test.ts": 46, - "src/renderer/src/components/terminal-pane/pty-connection/unbound-pane-spawn-recovery.test.ts": 50, - "src/renderer/src/components/terminal-pane/pty-connection/write-pty-output-to-xterm-startup-callback.test.ts": 73, - "src/renderer/src/components/terminal-pane/pty-dispatcher-delivery-interest.test.ts": 258, - "src/renderer/src/components/terminal-pane/pty-dispatcher-delivery-resync.test.ts": 155, - "src/renderer/src/components/terminal-pane/pty-dispatcher-pi-routing.test.ts": 857, - "src/renderer/src/components/terminal-pane/pty-dispatcher-push-reattach.test.ts": 160, - "src/renderer/src/components/terminal-pane/pty-input-write-queue.test.ts": 752, - "src/renderer/src/components/terminal-pane/pty-model-restore-channel.test.ts": 53, - "src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-reentrancy.test.ts": 57, - "src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-retention.test.ts": 130, - "src/renderer/src/components/terminal-pane/pty-pre-handler-buffer-warn-eviction.test.ts": 69, - "src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts": 115, - "src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.test.ts": 85, - "src/renderer/src/components/terminal-pane/pty-renderer-delivery-claims.test.ts": 79, - "src/renderer/src/components/terminal-pane/pty-shutdown-data-suspension.test.ts": 61, - "src/renderer/src/components/terminal-pane/pty-shutdown-exit-deferral.test.ts": 195, - "src/renderer/src/components/terminal-pane/pty-shutdown-output-queue.test.ts": 824, - "src/renderer/src/components/terminal-pane/pty-side-effect-pending-census.test.ts": 551, - "src/renderer/src/components/terminal-pane/pty-size-reassertion.test.ts": 56, - "src/renderer/src/components/terminal-pane/pty-size-reconcile.test.ts": 73, - "src/renderer/src/components/terminal-pane/pty-transport-connect-spawn.test.ts": 1160, - "src/renderer/src/components/terminal-pane/pty-transport-detach-attach-handoff.test.ts": 814, - "src/renderer/src/components/terminal-pane/pty-transport-eager-buffer-replay.test.ts": 1299, - "src/renderer/src/components/terminal-pane/pty-transport-handler-suspension.test.ts": 738, - "src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts": 2267, - "src/renderer/src/components/terminal-pane/pty-transport-output-side-effects.test.ts": 1185, - "src/renderer/src/components/terminal-pane/pty-transport-pi-coalesce.test.ts": 484, - "src/renderer/src/components/terminal-pane/pty-transport-pi-spinner.test.ts": 500, - "src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts": 1113, - "src/renderer/src/components/terminal-pane/pty-transport-recycled-pty-incarnation.test.ts": 591, - "src/renderer/src/components/terminal-pane/pty-transport-spawn-errors.test.ts": 773, - "src/renderer/src/components/terminal-pane/pty-transport-user-input.test.ts": 539, - "src/renderer/src/components/terminal-pane/regular-terminal-focus-ownership.test.ts": 185, - "src/renderer/src/components/terminal-pane/remote-desktop-viewport-claim.test.ts": 46, - "src/renderer/src/components/terminal-pane/remote-execution-host-pty.test.ts": 1319, - "src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts": 1795, - "src/renderer/src/components/terminal-pane/remote-hidden-output-restore-unavailable-banner.repro.test.ts": 1450, - "src/renderer/src/components/terminal-pane/remote-layout-scrollback-retention.test.ts": 64, - "src/renderer/src/components/terminal-pane/remote-pane-layout-push.test.ts": 71, - "src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts": 988, - "src/renderer/src/components/terminal-pane/remote-runtime-connect-failure-recovery.test.ts": 1519, - "src/renderer/src/components/terminal-pane/remote-runtime-disconnected-input-grace-retry.test.ts": 2324, - "src/renderer/src/components/terminal-pane/remote-runtime-disconnected-input-grace.test.ts": 43, - "src/renderer/src/components/terminal-pane/remote-runtime-error-surface-dismissal.test.ts": 1823, - "src/renderer/src/components/terminal-pane/remote-runtime-input-journal.test.ts": 70, - "src/renderer/src/components/terminal-pane/remote-runtime-outage-toast-flood-and-stuck-reconnect.test.ts": 2091, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-batching.test.ts": 105, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-deadline-reattach.test.ts": 3257, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-latched-pane-retention.test.ts": 5440, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-query-reply-immediate.test.ts": 2376, + "src/renderer/src/components/terminal-pane/pty-connection-spawn-deferred-sleep.test.ts": 1332, + "src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts": 1152, + "src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts": 837, + "src/renderer/src/components/terminal-pane/pty-connection-stalled-hidden-restore.test.ts": 2042, + "src/renderer/src/components/terminal-pane/pty-connection-startup-command-delivery.test.ts": 1958, + "src/renderer/src/components/terminal-pane/pty-connection-startup-draft-delivery.test.ts": 1095, + "src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts": 2131, + "src/renderer/src/components/terminal-pane/pty-connection-terminal-input-gating.test.ts": 2601, + "src/renderer/src/components/terminal-pane/pty-connection-typed-agent-identity.test.ts": 2782, + "src/renderer/src/components/terminal-pane/pty-connection-unanswered-command-finish.test.ts": 1232, + "src/renderer/src/components/terminal-pane/pty-connection-unserved-ssh-pane.test.ts": 1465, + "src/renderer/src/components/terminal-pane/pty-connection-visibility-resume-size.test.ts": 2668, + "src/renderer/src/components/terminal-pane/pty-connection-visible-pane-output-pause-latch.test.ts": 1046, + "src/renderer/src/components/terminal-pane/pty-connection-windows-cjk-repaint.test.ts": 2028, + "src/renderer/src/components/terminal-pane/pty-connection-windows-keyboard-reset.test.ts": 2312, + "src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-live-bookmark.test.ts": 298, + "src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-newer-daemon-hold.test.ts": 558, + "src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-startup-snapshot.test.ts": 82, + "src/renderer/src/components/terminal-pane/pty-connection/disposed-spawn-retention.test.ts": 59, + "src/renderer/src/components/terminal-pane/pty-connection/empty-reattach-retire-evidence.test.ts": 36, + "src/renderer/src/components/terminal-pane/pty-connection/foreground-output-budgets.test.ts": 28, + "src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.test.ts": 84, + "src/renderer/src/components/terminal-pane/pty-connection/foreground-renderer-risk-scan-tail.test.ts": 98, + "src/renderer/src/components/terminal-pane/pty-connection/pane-launch-agent-candidate.test.ts": 70, + "src/renderer/src/components/terminal-pane/pty-connection/pane-pty-layout-binding.test.ts": 25, + "src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-register.test.ts": 128, + "src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.test.ts": 53, + "src/renderer/src/components/terminal-pane/pty-connection/pty-exit-hibernate.test.ts": 1307, + "src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.test.ts": 1411, + "src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler-park-reveal-verdict.test.ts": 144, + "src/renderer/src/components/terminal-pane/pty-connection/reattach-success-settle.test.ts": 145, + "src/renderer/src/components/terminal-pane/pty-connection/ssh-session-gone-verdict.test.ts": 42, + "src/renderer/src/components/terminal-pane/pty-connection/terminal-tab-id.test.ts": 25, + "src/renderer/src/components/terminal-pane/pty-connection/unbound-pane-spawn-recovery.test.ts": 48, + "src/renderer/src/components/terminal-pane/pty-connection/write-pty-output-to-xterm-startup-callback.test.ts": 81, + "src/renderer/src/components/terminal-pane/pty-dispatcher-delivery-interest.test.ts": 162, + "src/renderer/src/components/terminal-pane/pty-dispatcher-delivery-resync.test.ts": 125, + "src/renderer/src/components/terminal-pane/pty-dispatcher-pi-routing.test.ts": 901, + "src/renderer/src/components/terminal-pane/pty-dispatcher-push-reattach.test.ts": 125, + "src/renderer/src/components/terminal-pane/pty-input-write-queue.test.ts": 849, + "src/renderer/src/components/terminal-pane/pty-model-restore-channel.test.ts": 35, + "src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-reentrancy.test.ts": 30, + "src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-retention.test.ts": 114, + "src/renderer/src/components/terminal-pane/pty-pre-handler-buffer-warn-eviction.test.ts": 46, + "src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts": 95, + "src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.test.ts": 87, + "src/renderer/src/components/terminal-pane/pty-renderer-delivery-claims.test.ts": 50, + "src/renderer/src/components/terminal-pane/pty-shutdown-data-suspension.test.ts": 45, + "src/renderer/src/components/terminal-pane/pty-shutdown-exit-deferral.test.ts": 168, + "src/renderer/src/components/terminal-pane/pty-shutdown-output-queue.test.ts": 799, + "src/renderer/src/components/terminal-pane/pty-side-effect-pending-census.test.ts": 220, + "src/renderer/src/components/terminal-pane/pty-size-reassertion.test.ts": 57, + "src/renderer/src/components/terminal-pane/pty-size-reconcile.test.ts": 60, + "src/renderer/src/components/terminal-pane/pty-transport-connect-spawn.test.ts": 1450, + "src/renderer/src/components/terminal-pane/pty-transport-detach-attach-handoff.test.ts": 876, + "src/renderer/src/components/terminal-pane/pty-transport-eager-buffer-replay.test.ts": 1129, + "src/renderer/src/components/terminal-pane/pty-transport-handler-suspension.test.ts": 851, + "src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts": 2740, + "src/renderer/src/components/terminal-pane/pty-transport-output-side-effects.test.ts": 909, + "src/renderer/src/components/terminal-pane/pty-transport-pi-coalesce.test.ts": 495, + "src/renderer/src/components/terminal-pane/pty-transport-pi-spinner.test.ts": 513, + "src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts": 983, + "src/renderer/src/components/terminal-pane/pty-transport-recycled-pty-incarnation.test.ts": 585, + "src/renderer/src/components/terminal-pane/pty-transport-spawn-errors.test.ts": 796, + "src/renderer/src/components/terminal-pane/pty-transport-spawn-intent.test.ts": 446, + "src/renderer/src/components/terminal-pane/pty-transport-user-input.test.ts": 627, + "src/renderer/src/components/terminal-pane/regular-terminal-focus-ownership.test.ts": 175, + "src/renderer/src/components/terminal-pane/remote-desktop-viewport-claim.test.ts": 32, + "src/renderer/src/components/terminal-pane/remote-execution-host-pty.test.ts": 1322, + "src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts": 1881, + "src/renderer/src/components/terminal-pane/remote-hidden-output-restore-unavailable-banner.repro.test.ts": 1319, + "src/renderer/src/components/terminal-pane/remote-layout-scrollback-retention.test.ts": 60, + "src/renderer/src/components/terminal-pane/remote-pane-layout-push.test.ts": 52, + "src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts": 896, + "src/renderer/src/components/terminal-pane/remote-runtime-connect-failure-recovery.test.ts": 1728, + "src/renderer/src/components/terminal-pane/remote-runtime-disconnected-input-grace-retry.test.ts": 2507, + "src/renderer/src/components/terminal-pane/remote-runtime-disconnected-input-grace.test.ts": 29, + "src/renderer/src/components/terminal-pane/remote-runtime-error-surface-dismissal.test.ts": 1408, + "src/renderer/src/components/terminal-pane/remote-runtime-input-journal.test.ts": 59, + "src/renderer/src/components/terminal-pane/remote-runtime-outage-toast-flood-and-stuck-reconnect.test.ts": 2228, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-batching.test.ts": 86, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-deadline-reattach.test.ts": 3465, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-latched-pane-retention.test.ts": 6446, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-query-reply-immediate.test.ts": 2001, "src/renderer/src/components/terminal-pane/remote-runtime-pty-recovery-state.test.ts": 70, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-snapshot-escape-tail.test.ts": 1605, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-snapshot-source-grid.test.ts": 1617, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-activation-inventory-fallback.test.ts": 2219, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-attach-subscription.test.ts": 3968, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-create-handoff.test.ts": 4354, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-create-outcome-recovery.test.ts": 3437, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-end-verdict.test.ts": 1769, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-expired-pane-recovery.test.ts": 2693, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-absence-evidence.test.ts": 1553, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-session-launch.test.ts": 3458, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-surface-replacement.test.ts": 9187, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-coalescing.test.ts": 4890, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-fallback.test.ts": 2895, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-replay.test.ts": 8016, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pairing-rotation.test.ts": 3856, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-binding-ownership.test.ts": 2736, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-handle-resolution.test.ts": 4764, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pending-host-surface-attach.test.ts": 3084, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-reattach-type-ahead.test.ts": 1622, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-snapshot-replay.test.ts": 2962, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-split-leaf-activation.test.ts": 3183, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stale-handle-recovery.test.ts": 2943, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-sticky-replacement-policy.test.ts": 2889, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stream-reconnect.test.ts": 5167, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-unpublished-host-graph.test.ts": 3061, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-web-mirror-recovery.test.ts": 3266, - "src/renderer/src/components/terminal-pane/remote-runtime-recovery-input-hold.test.ts": 275, - "src/renderer/src/components/terminal-pane/remote-runtime-resubscribe-failure-recovery-routing.test.ts": 3078, - "src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-host-chain.test.ts": 10249, - "src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-replay.test.ts": 1906, - "src/renderer/src/components/terminal-pane/remote-snapshot-history-seam.test.ts": 3139, - "src/renderer/src/components/terminal-pane/renderer-owned-agent-status-registry.test.ts": 49, - "src/renderer/src/components/terminal-pane/replay-guard.test.ts": 127, - "src/renderer/src/components/terminal-pane/replayed-scrollback-store-release.test.ts": 303, - "src/renderer/src/components/terminal-pane/repro-8299-shift-space-input-source.test.ts": 122, - "src/renderer/src/components/terminal-pane/repro-8832-url-next-line.test.ts": 1708, - "src/renderer/src/components/terminal-pane/request-active-terminal-pane-close.test.ts": 178, - "src/renderer/src/components/terminal-pane/resolve-split-cwd.test.ts": 1372, - "src/renderer/src/components/terminal-pane/restored-snapshot-coverage.test.ts": 50, - "src/renderer/src/components/terminal-pane/scan22-ssh-new-tab-disposed-spawn-kill.repro.test.ts": 412, - "src/renderer/src/components/terminal-pane/session-restored-banner-pane-state.test.tsx": 270, - "src/renderer/src/components/terminal-pane/shutdown-buffer-captures.test.ts": 50, - "src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.test.ts": 60, - "src/renderer/src/components/terminal-pane/split-right-white-screen.test.ts": 58, - "src/renderer/src/components/terminal-pane/stale-document-visibility.test.ts": 90, - "src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.test.ts": 74, - "src/renderer/src/components/terminal-pane/terminal-agent-session-continuation.test.ts": 70, - "src/renderer/src/components/terminal-pane/terminal-agent-session-fork.test.ts": 1153, - "src/renderer/src/components/terminal-pane/terminal-alternate-screen-parse.test.ts": 82, - "src/renderer/src/components/terminal-pane/terminal-appearance.test.ts": 341, - "src/renderer/src/components/terminal-pane/terminal-attention-surface.test.ts": 1370, - "src/renderer/src/components/terminal-pane/terminal-bracketed-paste.test.ts": 68, - "src/renderer/src/components/terminal-pane/terminal-capability-replies.test.ts": 147, - "src/renderer/src/components/terminal-pane/terminal-capped-buffer-retention.test.ts": 646, - "src/renderer/src/components/terminal-pane/terminal-captured-input-dispatch.test.ts": 54, - "src/renderer/src/components/terminal-pane/terminal-chat-ownership-lifecycle.test.tsx": 834, - "src/renderer/src/components/terminal-pane/terminal-cjk-cursor-cell-placement.test.ts": 325, - "src/renderer/src/components/terminal-pane/terminal-clipboard-event-paste.test.ts": 55, - "src/renderer/src/components/terminal-pane/terminal-clipboard-paste.test.ts": 117, - "src/renderer/src/components/terminal-pane/terminal-clipboard-selection-text.test.ts": 67, - "src/renderer/src/components/terminal-pane/terminal-cold-park-exempt-flip.react185.test.tsx": 1752, - "src/renderer/src/components/terminal-pane/terminal-cold-park-pre-gate-loop.react185.test.tsx": 906, - "src/renderer/src/components/terminal-pane/terminal-cold-park-recheck-deadlines.test.ts": 1436, - "src/renderer/src/components/terminal-pane/terminal-cold-park-subscription-narrowing.react185.test.tsx": 556, - "src/renderer/src/components/terminal-pane/terminal-cold-park-tab-model-identity.react185.test.tsx": 890, - "src/renderer/src/components/terminal-pane/terminal-cold-park-timer-rearm.test.tsx": 566, - "src/renderer/src/components/terminal-pane/terminal-cold-park-verdict-loop.test.tsx": 583, - "src/renderer/src/components/terminal-pane/terminal-cold-park-withheld-tabs.test.ts": 69, - "src/renderer/src/components/terminal-pane/terminal-command-lifecycle.test.ts": 42, - "src/renderer/src/components/terminal-pane/terminal-context-menu-dismiss.test.ts": 44, - "src/renderer/src/components/terminal-pane/terminal-copy-rejection-handling.test.ts": 43, - "src/renderer/src/components/terminal-pane/terminal-copy-shortcut.test.ts": 1732, - "src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.test.ts": 1612, - "src/renderer/src/components/terminal-pane/terminal-ctrl-enter.test.ts": 107, - "src/renderer/src/components/terminal-pane/terminal-cursor-appearance-precedence.test.ts": 317, - "src/renderer/src/components/terminal-pane/terminal-cursor-inactive-style.test.ts": 309, - "src/renderer/src/components/terminal-pane/terminal-dead-session-reconcile.test.ts": 61, - "src/renderer/src/components/terminal-pane/terminal-delivery-watchdog.test.ts": 117, - "src/renderer/src/components/terminal-pane/terminal-drop-handler.test.ts": 436, - "src/renderer/src/components/terminal-pane/terminal-drop-image-path.test.ts": 64, - "src/renderer/src/components/terminal-pane/terminal-drop-internal-handler.test.ts": 568, - "src/renderer/src/components/terminal-pane/terminal-drop-local-workspace.test.ts": 574, - "src/renderer/src/components/terminal-pane/terminal-drop-pane-resolution.test.ts": 141, - "src/renderer/src/components/terminal-pane/terminal-drop-path-writer.test.ts": 120, - "src/renderer/src/components/terminal-pane/terminal-drop-runtime-catalog-owner.test.ts": 679, - "src/renderer/src/components/terminal-pane/terminal-drop-shell.test.ts": 60, - "src/renderer/src/components/terminal-pane/terminal-drop-source-owner.test.ts": 509, - "src/renderer/src/components/terminal-pane/terminal-drop-transport-owner.test.ts": 338, - "src/renderer/src/components/terminal-pane/terminal-drop-upload-report.test.ts": 50, - "src/renderer/src/components/terminal-pane/terminal-drop-worktree-path.test.ts": 317, - "src/renderer/src/components/terminal-pane/terminal-drop-write-failure.test.ts": 52, - "src/renderer/src/components/terminal-pane/terminal-error-accumulation.test.ts": 127, - "src/renderer/src/components/terminal-pane/terminal-error-remote-closed-localization.test.ts": 93, - "src/renderer/src/components/terminal-pane/terminal-eviction-exempt-tabs.test.ts": 880, - "src/renderer/src/components/terminal-pane/terminal-file-link-actions.test.ts": 369, - "src/renderer/src/components/terminal-pane/terminal-file-link-target.test.ts": 1292, - "src/renderer/src/components/terminal-pane/terminal-fit-restore.test.ts": 88, - "src/renderer/src/components/terminal-pane/terminal-freeze-report.test.ts": 107, - "src/renderer/src/components/terminal-pane/terminal-handle-copy.test.ts": 63, - "src/renderer/src/components/terminal-pane/terminal-handle-links.test.ts": 412, - "src/renderer/src/components/terminal-pane/terminal-hidden-restore-scrollback.test.ts": 32, - "src/renderer/src/components/terminal-pane/terminal-hidden-view-parking.test.ts": 1508, - "src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.test.ts": 1920, - "src/renderer/src/components/terminal-pane/terminal-http-link-activation.test.ts": 369, - "src/renderer/src/components/terminal-pane/terminal-http-link-source-owner.test.ts": 95, - "src/renderer/src/components/terminal-pane/terminal-http-url-extraction.test.ts": 47, - "src/renderer/src/components/terminal-pane/terminal-ime-candidate-key-release-guard.test.ts": 58, - "src/renderer/src/components/terminal-pane/terminal-ime-composer-placeholder-mask.test.ts": 780, - "src/renderer/src/components/terminal-pane/terminal-ime-composition-route.test.ts": 157, - "src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.test.ts": 147, - "src/renderer/src/components/terminal-pane/terminal-ime-composition-transaction-ownership.test.ts": 165, - "src/renderer/src/components/terminal-pane/terminal-ime-deferred-chord.test.ts": 159, - "src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.test.ts": 230, - "src/renderer/src/components/terminal-pane/terminal-ime-forwarder-space-claim.test.ts": 226, - "src/renderer/src/components/terminal-pane/terminal-ime-hangul-syllable-flush.test.ts": 688, - "src/renderer/src/components/terminal-pane/terminal-ime-hangul-terminating-digit.test.ts": 211, - "src/renderer/src/components/terminal-pane/terminal-ime-input-context-refresh.test.ts": 147, - "src/renderer/src/components/terminal-pane/terminal-ime-linux-candidate-state.test.ts": 77, - "src/renderer/src/components/terminal-pane/terminal-ime-linux-claimed-preedit-release.test.ts": 174, - "src/renderer/src/components/terminal-pane/terminal-ime-linux-claimed-preedit-selector.test.ts": 81, - "src/renderer/src/components/terminal-pane/terminal-ime-macos-keybinding-dict-trace.test.ts": 266, - "src/renderer/src/components/terminal-pane/terminal-ime-native-text-forwarder.test.ts": 233, - "src/renderer/src/components/terminal-pane/terminal-ime-substituted-text-commit.test.ts": 438, - "src/renderer/src/components/terminal-pane/terminal-ime-user-input-provenance.test.ts": 337, - "src/renderer/src/components/terminal-pane/terminal-ime-won-composition-order.test.ts": 454, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-adversarial.test.ts": 2315, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-cancelled-preedit-visibility.test.ts": 385, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-cancel.test.ts": 462, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts": 1414, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-consumed-key-commit.test.ts": 532, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-korean-enter-commit-order.test.ts": 382, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-linux-native-trace-replay.test.ts": 4041, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-midline-preedit-tail.test.ts": 876, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-preedit-cell-grid.test.ts": 1043, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-resumed-preedit-visibility.test.ts": 420, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-trailing-preedit-occlusion.test.ts": 586, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-transaction-events.test.ts": 550, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-windows-resumed-preedit-trace.test.ts": 728, - "src/renderer/src/components/terminal-pane/terminal-inline-image-da1.test.ts": 123, - "src/renderer/src/components/terminal-pane/terminal-input-host-platform.test.ts": 1406, - "src/renderer/src/components/terminal-pane/terminal-input-quarantine.test.ts": 53, - "src/renderer/src/components/terminal-pane/terminal-ios-hangul-preedit-coexistence.test.ts": 786, - "src/renderer/src/components/terminal-pane/terminal-ios-hangul-preedit-device-trace.test.ts": 562, - "src/renderer/src/components/terminal-pane/terminal-ios-hangul-preedit.test.ts": 634, - "src/renderer/src/components/terminal-pane/terminal-jis-yen-input.test.ts": 47, - "src/renderer/src/components/terminal-pane/terminal-keyboard-event-handlers-focus.test.ts": 2037, - "src/renderer/src/components/terminal-pane/terminal-keyboard-native-chat-cover.test.ts": 1797, - "src/renderer/src/components/terminal-pane/terminal-keyboard-pane-resolution.test.ts": 140, - "src/renderer/src/components/terminal-pane/terminal-keyboard-protocol-pane-agent.test.ts": 57, - "src/renderer/src/components/terminal-pane/terminal-keyboard-search-dispatch.test.ts": 1809, - "src/renderer/src/components/terminal-pane/terminal-kitty-csi-u-encoding.test.ts": 69, - "src/renderer/src/components/terminal-pane/terminal-layout-duplicate-pty-replay.test.ts": 73, - "src/renderer/src/components/terminal-pane/terminal-layout-gesture-persist.test.tsx": 356, - "src/renderer/src/components/terminal-pane/terminal-layout-leaf-detach.test.ts": 56, - "src/renderer/src/components/terminal-pane/terminal-layout-leaf-ids.test.ts": 57, - "src/renderer/src/components/terminal-pane/terminal-layout-overlay-focus.test.tsx": 190, - "src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership-depth.test.ts": 98, - "src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership.test.ts": 118, - "src/renderer/src/components/terminal-pane/terminal-link-action-routing.test.ts": 1850, - "src/renderer/src/components/terminal-pane/terminal-link-activation.test.ts": 319, - "src/renderer/src/components/terminal-pane/terminal-link-click-behavior.test.ts": 55, - "src/renderer/src/components/terminal-pane/terminal-link-click-fallback.test.ts": 770, - "src/renderer/src/components/terminal-pane/terminal-link-client-local-file.test.ts": 704, - "src/renderer/src/components/terminal-pane/terminal-link-escaping-file-open.test.ts": 588, - "src/renderer/src/components/terminal-pane/terminal-link-file-open-routing.test.ts": 780, - "src/renderer/src/components/terminal-pane/terminal-link-host-workspace-file.test.ts": 879, - "src/renderer/src/components/terminal-pane/terminal-link-missing-file-open.test.ts": 820, - "src/renderer/src/components/terminal-pane/terminal-link-open-hints.test.ts": 297, - "src/renderer/src/components/terminal-pane/terminal-link-osc-file-targets.test.ts": 767, - "src/renderer/src/components/terminal-pane/terminal-link-osc-url-routing.test.ts": 645, - "src/renderer/src/components/terminal-pane/terminal-link-pointer-gesture.test.ts": 485, - "src/renderer/src/components/terminal-pane/terminal-link-provider-batching.test.ts": 737, - "src/renderer/src/components/terminal-pane/terminal-link-provider-link-detection.test.ts": 929, - "src/renderer/src/components/terminal-pane/terminal-link-pty-mouse-suppression.test.ts": 461, - "src/renderer/src/components/terminal-pane/terminal-link-remote-runtime-ssh-open.test.ts": 509, - "src/renderer/src/components/terminal-pane/terminal-link-same-id-ssh-file-host.test.ts": 735, - "src/renderer/src/components/terminal-pane/terminal-link-worktree-root-activation.test.ts": 763, - "src/renderer/src/components/terminal-pane/terminal-link-wsl-path-mapping.test.ts": 723, - "src/renderer/src/components/terminal-pane/terminal-linkifier-click-priming.test.ts": 317, - "src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.test.ts": 1358, - "src/renderer/src/components/terminal-pane/terminal-native-copy-gutter.test.ts": 159, - "src/renderer/src/components/terminal-pane/terminal-non-latin-control-chord.test.ts": 59, - "src/renderer/src/components/terminal-pane/terminal-notification-state.test.ts": 1566, - "src/renderer/src/components/terminal-pane/terminal-option-kitty-release.test.ts": 66, - "src/renderer/src/components/terminal-pane/terminal-option-punctuation.test.ts": 74, - "src/renderer/src/components/terminal-pane/terminal-output-visibility.test.ts": 56, - "src/renderer/src/components/terminal-pane/terminal-pane-attention-subscriptions.test.ts": 67, - "src/renderer/src/components/terminal-pane/terminal-pane-close-identity.test.ts": 55, - "src/renderer/src/components/terminal-pane/terminal-pane-element-file-drop.test.tsx": 1023, - "src/renderer/src/components/terminal-pane/terminal-pane-host-state-memo.test.ts": 1887, - "src/renderer/src/components/terminal-pane/terminal-pane-host-state.test.ts": 1520, - "src/renderer/src/components/terminal-pane/terminal-pane-manager-options-gesture.test.ts": 2408, - "src/renderer/src/components/terminal-pane/terminal-pane-recovery-unsettled-fallback.test.ts": 568, - "src/renderer/src/components/terminal-pane/terminal-pane-recovery.test.ts": 488, - "src/renderer/src/components/terminal-pane/terminal-pane-spawn-placement.test.ts": 559, - "src/renderer/src/components/terminal-pane/terminal-pane-split-completion.test.ts": 47, - "src/renderer/src/components/terminal-pane/terminal-pane-split-request-routing.test.ts": 151, - "src/renderer/src/components/terminal-pane/terminal-pane-split-with-inherited-cwd.test.ts": 87, - "src/renderer/src/components/terminal-pane/terminal-pane-split-writer-paths.test.ts": 1735, - "src/renderer/src/components/terminal-pane/terminal-pane-store-subscription-budget.test.tsx": 6838, - "src/renderer/src/components/terminal-pane/terminal-pane-surface-ownership.test.ts": 335, - "src/renderer/src/components/terminal-pane/terminal-pane-tab-detach-commit.test.ts": 118, - "src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts": 109, - "src/renderer/src/components/terminal-pane/terminal-park-verdict-flip-telemetry.test.ts": 132, - "src/renderer/src/components/terminal-pane/terminal-park-verdict-worktree-driver-loop.test.tsx": 467, - "src/renderer/src/components/terminal-pane/terminal-parked-pane-candidates.test.ts": 1644, - "src/renderer/src/components/terminal-pane/terminal-parked-tab-eviction-exemption.test.ts": 342, - "src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers-batch-sync.test.ts": 454, - "src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.test.ts": 464, - "src/renderer/src/components/terminal-pane/terminal-parked-watcher-partial-reconciliation.test.ts": 415, - "src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts": 307, - "src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.test.ts": 341, - "src/renderer/src/components/terminal-pane/terminal-parked-watcher-sleep-preserved-exit.test.ts": 404, - "src/renderer/src/components/terminal-pane/terminal-parked-watcher-sole-newborn-exit.test.ts": 368, - "src/renderer/src/components/terminal-pane/terminal-parking-e2e-overrides.test.ts": 2849, - "src/renderer/src/components/terminal-pane/terminal-parser-handler-guard.test.ts": 85, - "src/renderer/src/components/terminal-pane/terminal-paste-coordinator.test.ts": 153, - "src/renderer/src/components/terminal-pane/terminal-paste-executor-default-yield.test.ts": 58, - "src/renderer/src/components/terminal-pane/terminal-paste-multiline-policy.test.ts": 68, - "src/renderer/src/components/terminal-pane/terminal-paste-operation-order.test.ts": 1390, - "src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts": 59, - "src/renderer/src/components/terminal-pane/terminal-paste-target-state.test.ts": 58, - "src/renderer/src/components/terminal-pane/terminal-path-existence-batch.test.ts": 70, - "src/renderer/src/components/terminal-pane/terminal-process-exit-restart.test.ts": 42, - "src/renderer/src/components/terminal-pane/terminal-programmatic-text-paste.test.ts": 1433, - "src/renderer/src/components/terminal-pane/terminal-pty-ack-gate.test.ts": 155, - "src/renderer/src/components/terminal-pane/terminal-pty-paste-writer.test.ts": 57, - "src/renderer/src/components/terminal-pane/terminal-quick-command-dispatch.test.ts": 61, - "src/renderer/src/components/terminal-pane/terminal-remote-runtime-recovery-ui-state.test.ts": 42, - "src/renderer/src/components/terminal-pane/terminal-render-desync-evidence-persistence.test.ts": 49, - "src/renderer/src/components/terminal-pane/terminal-render-desync-frame.test.ts": 68, - "src/renderer/src/components/terminal-pane/terminal-render-desync-sentinel.test.ts": 458, - "src/renderer/src/components/terminal-pane/terminal-render-desync-weight-probe.test.ts": 63, - "src/renderer/src/components/terminal-pane/terminal-renderer-policy.test.ts": 118, - "src/renderer/src/components/terminal-pane/terminal-replay-application-continuity.test.ts": 393, - "src/renderer/src/components/terminal-pane/terminal-replay-cursor-state.test.ts": 130, - "src/renderer/src/components/terminal-pane/terminal-restore-sgr-latch.test.ts": 155, - "src/renderer/src/components/terminal-pane/terminal-restored-viewport.test.ts": 66, - "src/renderer/src/components/terminal-pane/terminal-retention-churn-pins.test.tsx": 643, - "src/renderer/src/components/terminal-pane/terminal-retention-exempt-growth.test.ts": 2874, - "src/renderer/src/components/terminal-pane/terminal-runtime-host-link-routing.test.ts": 2218, - "src/renderer/src/components/terminal-pane/terminal-selection-copy.test.ts": 1451, - "src/renderer/src/components/terminal-pane/terminal-selection-fit-guard.test.ts": 45, - "src/renderer/src/components/terminal-pane/terminal-shortcut-ctrl-arrow.test.ts": 81, - "src/renderer/src/components/terminal-pane/terminal-shortcut-option-compose.test.ts": 154, - "src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts": 132, - "src/renderer/src/components/terminal-pane/terminal-shortcut-select-all.test.ts": 72, - "src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts": 209, - "src/renderer/src/components/terminal-pane/terminal-side-effect-facts-handler.test.ts": 79, - "src/renderer/src/components/terminal-pane/terminal-snapshot-replay-paint.test.ts": 481, - "src/renderer/src/components/terminal-pane/terminal-spawn-error-display.test.ts": 52, - "src/renderer/src/components/terminal-pane/terminal-startup-command-classifier.test.ts": 53, - "src/renderer/src/components/terminal-pane/terminal-startup-grid-settle.test.ts": 64, - "src/renderer/src/components/terminal-pane/terminal-startup-timing-scheduler.test.ts": 116, - "src/renderer/src/components/terminal-pane/terminal-startup-timing.test.ts": 64, - "src/renderer/src/components/terminal-pane/terminal-tab-agent-type-index-retention.test.ts": 2034, - "src/renderer/src/components/terminal-pane/terminal-tab-agent-type-index.test.ts": 71, - "src/renderer/src/components/terminal-pane/terminal-tab-lookup.test.ts": 44, - "src/renderer/src/components/terminal-pane/terminal-title-evidence.test.ts": 102, - "src/renderer/src/components/terminal-pane/terminal-title-tracker-parity.test.ts": 449, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-snapshot-escape-tail.test.ts": 1542, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-snapshot-source-grid.test.ts": 1472, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-activation-inventory-fallback.test.ts": 2816, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-attach-subscription.test.ts": 4180, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-create-handoff.test.ts": 4075, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-create-outcome-recovery.test.ts": 3481, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-end-verdict.test.ts": 1667, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-expired-pane-recovery.test.ts": 2665, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-absence-evidence.test.ts": 1536, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-session-launch.test.ts": 3381, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-surface-replacement.test.ts": 8290, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-coalescing.test.ts": 4827, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-fallback.test.ts": 2959, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-replay.test.ts": 8142, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pairing-rotation.test.ts": 6272, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-binding-ownership.test.ts": 2821, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-handle-resolution.test.ts": 5252, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pending-host-surface-attach.test.ts": 3028, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-reattach-type-ahead.test.ts": 4289, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-snapshot-replay.test.ts": 3189, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-spawn-intent.test.ts": 1814, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-split-leaf-activation.test.ts": 3239, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stale-handle-recovery.test.ts": 3486, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-sticky-replacement-policy.test.ts": 3006, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stream-reconnect.test.ts": 5774, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-unmounted-host-pane.test.ts": 2049, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-unpublished-host-graph.test.ts": 2668, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-web-mirror-recovery.test.ts": 3376, + "src/renderer/src/components/terminal-pane/remote-runtime-recovery-input-hold.test.ts": 346, + "src/renderer/src/components/terminal-pane/remote-runtime-resubscribe-failure-recovery-routing.test.ts": 3138, + "src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-host-chain.test.ts": 9361, + "src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-replay.test.ts": 2220, + "src/renderer/src/components/terminal-pane/remote-snapshot-history-seam.test.ts": 3278, + "src/renderer/src/components/terminal-pane/renderer-owned-agent-status-registry.test.ts": 40, + "src/renderer/src/components/terminal-pane/replay-guard.test.ts": 80, + "src/renderer/src/components/terminal-pane/replayed-scrollback-store-release.test.ts": 278, + "src/renderer/src/components/terminal-pane/repro-8299-shift-space-input-source.test.ts": 95, + "src/renderer/src/components/terminal-pane/repro-8832-url-next-line.test.ts": 1712, + "src/renderer/src/components/terminal-pane/request-active-terminal-pane-close.test.ts": 169, + "src/renderer/src/components/terminal-pane/resolve-split-cwd.test.ts": 1275, + "src/renderer/src/components/terminal-pane/restored-snapshot-coverage.test.ts": 39, + "src/renderer/src/components/terminal-pane/scan22-ssh-new-tab-disposed-spawn-kill.repro.test.ts": 415, + "src/renderer/src/components/terminal-pane/session-restored-banner-pane-state.test.tsx": 229, + "src/renderer/src/components/terminal-pane/shutdown-buffer-captures.test.ts": 28, + "src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.test.ts": 54, + "src/renderer/src/components/terminal-pane/split-right-white-screen.test.ts": 74, + "src/renderer/src/components/terminal-pane/stale-document-visibility.test.ts": 61, + "src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.test.ts": 66, + "src/renderer/src/components/terminal-pane/terminal-agent-session-continuation.test.ts": 66, + "src/renderer/src/components/terminal-pane/terminal-agent-session-fork.test.ts": 1061, + "src/renderer/src/components/terminal-pane/terminal-alternate-screen-parse.test.ts": 65, + "src/renderer/src/components/terminal-pane/terminal-appearance.test.ts": 384, + "src/renderer/src/components/terminal-pane/terminal-attention-surface.test.ts": 1469, + "src/renderer/src/components/terminal-pane/terminal-bracketed-paste.test.ts": 42, + "src/renderer/src/components/terminal-pane/terminal-capability-replies.test.ts": 108, + "src/renderer/src/components/terminal-pane/terminal-capped-buffer-retention.test.ts": 716, + "src/renderer/src/components/terminal-pane/terminal-captured-input-dispatch.test.ts": 41, + "src/renderer/src/components/terminal-pane/terminal-chat-ownership-lifecycle.test.tsx": 850, + "src/renderer/src/components/terminal-pane/terminal-cjk-cursor-cell-placement.test.ts": 298, + "src/renderer/src/components/terminal-pane/terminal-clipboard-event-paste.test.ts": 42, + "src/renderer/src/components/terminal-pane/terminal-clipboard-paste.test.ts": 73, + "src/renderer/src/components/terminal-pane/terminal-clipboard-selection-text.test.ts": 47, + "src/renderer/src/components/terminal-pane/terminal-cold-park-exempt-flip.react185.test.tsx": 1079, + "src/renderer/src/components/terminal-pane/terminal-cold-park-pre-gate-loop.react185.test.tsx": 723, + "src/renderer/src/components/terminal-pane/terminal-cold-park-recheck-deadlines.test.ts": 1409, + "src/renderer/src/components/terminal-pane/terminal-cold-park-subscription-narrowing.react185.test.tsx": 548, + "src/renderer/src/components/terminal-pane/terminal-cold-park-tab-model-identity.react185.test.tsx": 861, + "src/renderer/src/components/terminal-pane/terminal-cold-park-timer-rearm.test.tsx": 465, + "src/renderer/src/components/terminal-pane/terminal-cold-park-verdict-loop.test.tsx": 565, + "src/renderer/src/components/terminal-pane/terminal-cold-park-withheld-tabs.test.ts": 66, + "src/renderer/src/components/terminal-pane/terminal-command-lifecycle.test.ts": 53, + "src/renderer/src/components/terminal-pane/terminal-context-menu-dismiss.test.ts": 33, + "src/renderer/src/components/terminal-pane/terminal-copy-rejection-handling.test.ts": 44, + "src/renderer/src/components/terminal-pane/terminal-copy-shortcut.test.ts": 1457, + "src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.test.ts": 1372, + "src/renderer/src/components/terminal-pane/terminal-ctrl-enter.test.ts": 92, + "src/renderer/src/components/terminal-pane/terminal-cursor-appearance-precedence.test.ts": 296, + "src/renderer/src/components/terminal-pane/terminal-cursor-inactive-style.test.ts": 337, + "src/renderer/src/components/terminal-pane/terminal-dead-session-reconcile.test.ts": 49, + "src/renderer/src/components/terminal-pane/terminal-delivery-watchdog.test.ts": 113, + "src/renderer/src/components/terminal-pane/terminal-drop-handler.test.ts": 542, + "src/renderer/src/components/terminal-pane/terminal-drop-image-path.test.ts": 52, + "src/renderer/src/components/terminal-pane/terminal-drop-internal-handler.test.ts": 715, + "src/renderer/src/components/terminal-pane/terminal-drop-local-workspace.test.ts": 652, + "src/renderer/src/components/terminal-pane/terminal-drop-pane-resolution.test.ts": 163, + "src/renderer/src/components/terminal-pane/terminal-drop-path-writer.test.ts": 87, + "src/renderer/src/components/terminal-pane/terminal-drop-runtime-catalog-owner.test.ts": 681, + "src/renderer/src/components/terminal-pane/terminal-drop-shell.test.ts": 43, + "src/renderer/src/components/terminal-pane/terminal-drop-source-owner.test.ts": 532, + "src/renderer/src/components/terminal-pane/terminal-drop-transport-owner.test.ts": 290, + "src/renderer/src/components/terminal-pane/terminal-drop-upload-report.test.ts": 40, + "src/renderer/src/components/terminal-pane/terminal-drop-worktree-path.test.ts": 344, + "src/renderer/src/components/terminal-pane/terminal-drop-write-failure.test.ts": 39, + "src/renderer/src/components/terminal-pane/terminal-error-accumulation.test.ts": 168, + "src/renderer/src/components/terminal-pane/terminal-error-remote-closed-localization.test.ts": 49, + "src/renderer/src/components/terminal-pane/terminal-eviction-exempt-tabs.test.ts": 750, + "src/renderer/src/components/terminal-pane/terminal-file-link-actions.test.ts": 313, + "src/renderer/src/components/terminal-pane/terminal-file-link-target.test.ts": 1371, + "src/renderer/src/components/terminal-pane/terminal-fit-restore.test.ts": 48, + "src/renderer/src/components/terminal-pane/terminal-freeze-report.test.ts": 83, + "src/renderer/src/components/terminal-pane/terminal-handle-copy.test.ts": 40, + "src/renderer/src/components/terminal-pane/terminal-handle-links.test.ts": 429, + "src/renderer/src/components/terminal-pane/terminal-hidden-restore-scrollback.test.ts": 22, + "src/renderer/src/components/terminal-pane/terminal-hidden-view-parking.test.ts": 1272, + "src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.test.ts": 1822, + "src/renderer/src/components/terminal-pane/terminal-http-link-activation.test.ts": 301, + "src/renderer/src/components/terminal-pane/terminal-http-link-source-owner.test.ts": 102, + "src/renderer/src/components/terminal-pane/terminal-http-url-extraction.test.ts": 36, + "src/renderer/src/components/terminal-pane/terminal-ime-candidate-key-release-guard.test.ts": 47, + "src/renderer/src/components/terminal-pane/terminal-ime-composer-placeholder-mask.test.ts": 769, + "src/renderer/src/components/terminal-pane/terminal-ime-composition-route.test.ts": 179, + "src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.test.ts": 218, + "src/renderer/src/components/terminal-pane/terminal-ime-composition-transaction-ownership.test.ts": 170, + "src/renderer/src/components/terminal-pane/terminal-ime-deferred-chord.test.ts": 168, + "src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.test.ts": 218, + "src/renderer/src/components/terminal-pane/terminal-ime-forwarder-space-claim.test.ts": 240, + "src/renderer/src/components/terminal-pane/terminal-ime-hangul-syllable-flush.test.ts": 841, + "src/renderer/src/components/terminal-pane/terminal-ime-hangul-terminating-digit.test.ts": 518, + "src/renderer/src/components/terminal-pane/terminal-ime-input-context-refresh.test.ts": 185, + "src/renderer/src/components/terminal-pane/terminal-ime-linux-fcitx4-trace-replay.test.ts": 1506, + "src/renderer/src/components/terminal-pane/terminal-ime-linux-kitty-orphan-keyup.test.ts": 761, + "src/renderer/src/components/terminal-pane/terminal-ime-macos-keybinding-dict-trace.test.ts": 375, + "src/renderer/src/components/terminal-pane/terminal-ime-native-text-forwarder.test.ts": 244, + "src/renderer/src/components/terminal-pane/terminal-ime-substituted-text-commit.test.ts": 475, + "src/renderer/src/components/terminal-pane/terminal-ime-user-input-provenance.test.ts": 413, + "src/renderer/src/components/terminal-pane/terminal-ime-won-composition-order.test.ts": 559, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-adversarial.test.ts": 2323, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-cancelled-preedit-visibility.test.ts": 417, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-cancel.test.ts": 423, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts": 1563, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-consumed-key-commit.test.ts": 654, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-korean-enter-commit-order.test.ts": 407, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-linux-native-trace-replay.test.ts": 4785, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-midline-preedit-tail.test.ts": 967, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-preedit-cell-grid.test.ts": 1178, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-resumed-preedit-visibility.test.ts": 432, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-trailing-preedit-occlusion.test.ts": 519, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-transaction-events.test.ts": 562, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-windows-resumed-preedit-trace.test.ts": 720, + "src/renderer/src/components/terminal-pane/terminal-inline-image-da1.test.ts": 85, + "src/renderer/src/components/terminal-pane/terminal-input-host-platform.test.ts": 1425, + "src/renderer/src/components/terminal-pane/terminal-input-quarantine.test.ts": 41, + "src/renderer/src/components/terminal-pane/terminal-ios-hangul-preedit-coexistence.test.ts": 636, + "src/renderer/src/components/terminal-pane/terminal-ios-hangul-preedit-device-trace.test.ts": 653, + "src/renderer/src/components/terminal-pane/terminal-ios-hangul-preedit.test.ts": 818, + "src/renderer/src/components/terminal-pane/terminal-jis-yen-input.test.ts": 36, + "src/renderer/src/components/terminal-pane/terminal-keyboard-event-handlers-focus.test.ts": 1553, + "src/renderer/src/components/terminal-pane/terminal-keyboard-native-chat-cover.test.ts": 1520, + "src/renderer/src/components/terminal-pane/terminal-keyboard-pane-resolution.test.ts": 195, + "src/renderer/src/components/terminal-pane/terminal-keyboard-protocol-pane-agent.test.ts": 36, + "src/renderer/src/components/terminal-pane/terminal-keyboard-search-dispatch.test.ts": 1621, + "src/renderer/src/components/terminal-pane/terminal-kitty-csi-u-encoding.test.ts": 58, + "src/renderer/src/components/terminal-pane/terminal-layout-duplicate-pty-replay.test.ts": 71, + "src/renderer/src/components/terminal-pane/terminal-layout-gesture-persist.test.tsx": 488, + "src/renderer/src/components/terminal-pane/terminal-layout-leaf-detach.test.ts": 55, + "src/renderer/src/components/terminal-pane/terminal-layout-leaf-ids.test.ts": 63, + "src/renderer/src/components/terminal-pane/terminal-layout-overlay-focus.test.tsx": 205, + "src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership-depth.test.ts": 73, + "src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership.test.ts": 103, + "src/renderer/src/components/terminal-pane/terminal-link-action-routing.test.ts": 1819, + "src/renderer/src/components/terminal-pane/terminal-link-activation.test.ts": 300, + "src/renderer/src/components/terminal-pane/terminal-link-click-behavior.test.ts": 33, + "src/renderer/src/components/terminal-pane/terminal-link-click-fallback.test.ts": 781, + "src/renderer/src/components/terminal-pane/terminal-link-client-local-file.test.ts": 713, + "src/renderer/src/components/terminal-pane/terminal-link-escaping-file-open.test.ts": 707, + "src/renderer/src/components/terminal-pane/terminal-link-file-open-routing.test.ts": 725, + "src/renderer/src/components/terminal-pane/terminal-link-host-granted-file-link.test.ts": 685, + "src/renderer/src/components/terminal-pane/terminal-link-host-workspace-file.test.ts": 758, + "src/renderer/src/components/terminal-pane/terminal-link-missing-file-open.test.ts": 762, + "src/renderer/src/components/terminal-pane/terminal-link-open-hints.test.ts": 266, + "src/renderer/src/components/terminal-pane/terminal-link-osc-file-targets.test.ts": 878, + "src/renderer/src/components/terminal-pane/terminal-link-osc-url-routing.test.ts": 779, + "src/renderer/src/components/terminal-pane/terminal-link-pointer-gesture.test.ts": 487, + "src/renderer/src/components/terminal-pane/terminal-link-provider-batching.test.ts": 710, + "src/renderer/src/components/terminal-pane/terminal-link-provider-candidate-failures.test.ts": 767, + "src/renderer/src/components/terminal-pane/terminal-link-provider-chat-cases.test.ts": 1013, + "src/renderer/src/components/terminal-pane/terminal-link-provider-link-detection.test.ts": 739, + "src/renderer/src/components/terminal-pane/terminal-link-pty-mouse-suppression.test.ts": 501, + "src/renderer/src/components/terminal-pane/terminal-link-remote-runtime-ssh-open.test.ts": 510, + "src/renderer/src/components/terminal-pane/terminal-link-same-id-ssh-file-host.test.ts": 790, + "src/renderer/src/components/terminal-pane/terminal-link-worktree-root-activation.test.ts": 753, + "src/renderer/src/components/terminal-pane/terminal-link-wsl-path-mapping.test.ts": 769, + "src/renderer/src/components/terminal-pane/terminal-linkifier-click-priming.test.ts": 258, + "src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.test.ts": 1374, + "src/renderer/src/components/terminal-pane/terminal-native-copy-gutter.test.ts": 177, + "src/renderer/src/components/terminal-pane/terminal-non-latin-control-chord.test.ts": 45, + "src/renderer/src/components/terminal-pane/terminal-notification-state.test.ts": 1578, + "src/renderer/src/components/terminal-pane/terminal-option-kitty-release.test.ts": 48, + "src/renderer/src/components/terminal-pane/terminal-option-punctuation.test.ts": 84, + "src/renderer/src/components/terminal-pane/terminal-output-visibility.test.ts": 39, + "src/renderer/src/components/terminal-pane/terminal-pane-attention-subscriptions.test.ts": 47, + "src/renderer/src/components/terminal-pane/terminal-pane-close-identity.test.ts": 25, + "src/renderer/src/components/terminal-pane/terminal-pane-element-file-drop.test.tsx": 1996, + "src/renderer/src/components/terminal-pane/terminal-pane-host-state-memo.test.ts": 1945, + "src/renderer/src/components/terminal-pane/terminal-pane-host-state.test.ts": 1437, + "src/renderer/src/components/terminal-pane/terminal-pane-manager-options-gesture.test.ts": 2444, + "src/renderer/src/components/terminal-pane/terminal-pane-recovery-unsettled-fallback.test.ts": 691, + "src/renderer/src/components/terminal-pane/terminal-pane-recovery.test.ts": 698, + "src/renderer/src/components/terminal-pane/terminal-pane-spawn-placement.test.ts": 597, + "src/renderer/src/components/terminal-pane/terminal-pane-split-completion.test.ts": 43, + "src/renderer/src/components/terminal-pane/terminal-pane-split-request-routing.test.ts": 180, + "src/renderer/src/components/terminal-pane/terminal-pane-split-with-inherited-cwd.test.ts": 63, + "src/renderer/src/components/terminal-pane/terminal-pane-split-writer-paths.test.ts": 658, + "src/renderer/src/components/terminal-pane/terminal-pane-store-subscription-budget.test.tsx": 7104, + "src/renderer/src/components/terminal-pane/terminal-pane-surface-ownership.test.ts": 362, + "src/renderer/src/components/terminal-pane/terminal-pane-tab-detach-commit.test.ts": 148, + "src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts": 152, + "src/renderer/src/components/terminal-pane/terminal-park-verdict-flip-telemetry.test.ts": 106, + "src/renderer/src/components/terminal-pane/terminal-park-verdict-worktree-driver-loop.test.tsx": 645, + "src/renderer/src/components/terminal-pane/terminal-parked-pane-candidates.test.ts": 1531, + "src/renderer/src/components/terminal-pane/terminal-parked-tab-eviction-exemption.test.ts": 352, + "src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers-batch-sync.test.ts": 449, + "src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.test.ts": 429, + "src/renderer/src/components/terminal-pane/terminal-parked-watcher-partial-reconciliation.test.ts": 355, + "src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts": 306, + "src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.test.ts": 239, + "src/renderer/src/components/terminal-pane/terminal-parked-watcher-sleep-preserved-exit.test.ts": 400, + "src/renderer/src/components/terminal-pane/terminal-parked-watcher-sole-newborn-exit.test.ts": 376, + "src/renderer/src/components/terminal-pane/terminal-parking-e2e-overrides.test.ts": 2938, + "src/renderer/src/components/terminal-pane/terminal-parser-handler-guard.test.ts": 44, + "src/renderer/src/components/terminal-pane/terminal-paste-coordinator.test.ts": 148, + "src/renderer/src/components/terminal-pane/terminal-paste-executor-default-yield.test.ts": 48, + "src/renderer/src/components/terminal-pane/terminal-paste-multiline-policy.test.ts": 40, + "src/renderer/src/components/terminal-pane/terminal-paste-operation-order.test.ts": 1511, + "src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts": 35, + "src/renderer/src/components/terminal-pane/terminal-paste-target-state.test.ts": 38, + "src/renderer/src/components/terminal-pane/terminal-path-existence-batch.test.ts": 44, + "src/renderer/src/components/terminal-pane/terminal-process-exit-restart.test.ts": 36, + "src/renderer/src/components/terminal-pane/terminal-programmatic-text-paste.test.ts": 124, + "src/renderer/src/components/terminal-pane/terminal-pty-ack-gate.test.ts": 180, + "src/renderer/src/components/terminal-pane/terminal-pty-paste-writer.test.ts": 32, + "src/renderer/src/components/terminal-pane/terminal-quick-command-dispatch.test.ts": 39, + "src/renderer/src/components/terminal-pane/terminal-remote-runtime-recovery-ui-state.test.ts": 26, + "src/renderer/src/components/terminal-pane/terminal-render-desync-evidence-persistence.test.ts": 42, + "src/renderer/src/components/terminal-pane/terminal-render-desync-frame.test.ts": 40, + "src/renderer/src/components/terminal-pane/terminal-render-desync-sentinel.test.ts": 463, + "src/renderer/src/components/terminal-pane/terminal-render-desync-weight-probe.test.ts": 38, + "src/renderer/src/components/terminal-pane/terminal-renderer-policy.test.ts": 116, + "src/renderer/src/components/terminal-pane/terminal-replay-application-continuity.test.ts": 365, + "src/renderer/src/components/terminal-pane/terminal-replay-cursor-state.test.ts": 91, + "src/renderer/src/components/terminal-pane/terminal-restore-sgr-latch.test.ts": 147, + "src/renderer/src/components/terminal-pane/terminal-restored-viewport.test.ts": 38, + "src/renderer/src/components/terminal-pane/terminal-retention-churn-pins.test.tsx": 822, + "src/renderer/src/components/terminal-pane/terminal-retention-exempt-growth.test.ts": 1827, + "src/renderer/src/components/terminal-pane/terminal-runtime-host-link-routing.test.ts": 1832, + "src/renderer/src/components/terminal-pane/terminal-selection-copy.test.ts": 1213, + "src/renderer/src/components/terminal-pane/terminal-selection-fit-guard.test.ts": 26, + "src/renderer/src/components/terminal-pane/terminal-shortcut-ctrl-arrow.test.ts": 87, + "src/renderer/src/components/terminal-pane/terminal-shortcut-option-compose.test.ts": 118, + "src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts": 134, + "src/renderer/src/components/terminal-pane/terminal-shortcut-select-all.test.ts": 61, + "src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts": 188, + "src/renderer/src/components/terminal-pane/terminal-side-effect-facts-handler.test.ts": 48, + "src/renderer/src/components/terminal-pane/terminal-snapshot-replay-paint.test.ts": 492, + "src/renderer/src/components/terminal-pane/terminal-spawn-error-display.test.ts": 55, + "src/renderer/src/components/terminal-pane/terminal-startup-command-classifier.test.ts": 100, + "src/renderer/src/components/terminal-pane/terminal-startup-grid-settle.test.ts": 50, + "src/renderer/src/components/terminal-pane/terminal-startup-timing-scheduler.test.ts": 120, + "src/renderer/src/components/terminal-pane/terminal-startup-timing.test.ts": 49, + "src/renderer/src/components/terminal-pane/terminal-tab-agent-type-index-retention.test.ts": 1803, + "src/renderer/src/components/terminal-pane/terminal-tab-agent-type-index.test.ts": 49, + "src/renderer/src/components/terminal-pane/terminal-tab-lookup.test.ts": 38, + "src/renderer/src/components/terminal-pane/terminal-title-evidence.test.ts": 95, + "src/renderer/src/components/terminal-pane/terminal-title-tracker-parity.test.ts": 192, "src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.test.ts": 59, - "src/renderer/src/components/terminal-pane/terminal-url-link-click.test.ts": 1895, - "src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.test.ts": 1787, - "src/renderer/src/components/terminal-pane/terminal-user-input-signal.test.ts": 72, - "src/renderer/src/components/terminal-pane/terminal-view-attributes-publisher.test.ts": 369, - "src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts": 1475, - "src/renderer/src/components/terminal-pane/terminal-webgl-atlas-recovery.test.ts": 67, - "src/renderer/src/components/terminal-pane/terminal-webgl-diagnostics-breadcrumbs.test.ts": 75, - "src/renderer/src/components/terminal-pane/terminal-windows-shift-enter.test.ts": 105, - "src/renderer/src/components/terminal-pane/terminal-worktree-path-link.test.ts": 1473, - "src/renderer/src/components/terminal-pane/title-agent-identity.test.ts": 69, - "src/renderer/src/components/terminal-pane/use-manual-terminal-worktree-parking.test.ts": 1577, - "src/renderer/src/components/terminal-pane/use-mobile-overlay-ticks.perf.test.tsx": 337, - "src/renderer/src/components/terminal-pane/use-notification-dispatch-verdict.test.ts": 438, - "src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts": 477, - "src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx": 495, - "src/renderer/src/components/terminal-pane/use-system-prefers-dark.test.ts": 47, - "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-active-pty-reporting.test.ts": 1361, - "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-paste-events.test.ts": 1425, - "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-sync-fit-registration.test.ts": 1495, - "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-visibility-resume.test.ts": 1703, - "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-window-focus-recovery.test.ts": 1618, - "src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.test.ts": 2584, - "src/renderer/src/components/terminal-pane/use-terminal-park-mount-intent.react185.test.tsx": 441, - "src/renderer/src/components/terminal-pane/use-terminal-scroll-visibility-memory.test.ts": 77, - "src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts": 746, - "src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts": 306, - "src/renderer/src/components/terminal-pane/use-visible-terminal-tab-claim.test.ts": 54, - "src/renderer/src/components/terminal-pane/useSessionRestoredBannerDismiss.test.tsx": 206, - "src/renderer/src/components/terminal-pane/useTerminalFontZoom.test.ts": 180, - "src/renderer/src/components/terminal-pane/wrapped-terminal-link-ranges.test.ts": 65, - "src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts": 87, - "src/renderer/src/components/terminal-pane/xterm-bypass-policy-ios.test.ts": 100, - "src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts": 102, - "src/renderer/src/components/terminal-pane/xterm-bypass-policy.test.ts": 102, - "src/renderer/src/components/terminal-parked-watcher-sync-entries.test.tsx": 1186, - "src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx": 903, - "src/renderer/src/components/terminal-quick-commands/terminal-quick-command-agent-options.test.ts": 128, - "src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.test.ts": 182, - "src/renderer/src/components/terminal-scrollback-decoration-eviction.test.ts": 504, - "src/renderer/src/components/terminal-search-decoration-leak.test.ts": 2052, - "src/renderer/src/components/terminal-search-long-wrapped-line.test.ts": 4844, - "src/renderer/src/components/terminal-search-safe-find.test.ts": 49, - "src/renderer/src/components/terminal-split-workspace-surfaces-startup-hold.test.tsx": 174, - "src/renderer/src/components/terminal-startup-hold-placeholder.test.tsx": 636, - "src/renderer/src/components/terminal-workspace-keydown.test.ts": 591, - "src/renderer/src/components/terminal-workspace-surface-ids.test.tsx": 2255, - "src/renderer/src/components/terminal/activation-deferred-tab-admission.test.ts": 61, - "src/renderer/src/components/terminal/active-terminal-repair-loop.react185.test.tsx": 1667, - "src/renderer/src/components/terminal/active-terminal-repair.test.ts": 57, - "src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts": 1773, - "src/renderer/src/components/terminal/background-terminal-worktree-visibility.test.ts": 51, - "src/renderer/src/components/terminal/cold-activation-deferral-stranding.test.ts": 1948, - "src/renderer/src/components/terminal/initial-terminal-admission-wait.test.tsx": 1079, - "src/renderer/src/components/terminal/initial-terminal-structured-launch.test.tsx": 461, - "src/renderer/src/components/terminal/initial-terminal.test.ts": 60, - "src/renderer/src/components/terminal/pty-running-work-probe-child-evidence.test.ts": 57, - "src/renderer/src/components/terminal/running-terminal-close-guard.test.ts": 108, - "src/renderer/src/components/terminal/split-group-mount.test.ts": 67, - "src/renderer/src/components/terminal/startup-terminal-tab-hold.test.ts": 2061, - "src/renderer/src/components/terminal/tab-type-cycle.test.ts": 49, - "src/renderer/src/components/terminal/terminal-close-confirm-keyboard-vs-mouse.test.ts": 389, - "src/renderer/src/components/terminal/terminal-close-copy-kind.test.ts": 51, - "src/renderer/src/components/terminal/terminal-close-incarnation.test.ts": 122, - "src/renderer/src/components/terminal/terminal-provider-snapshot-bound-pty-ids.test.ts": 1808, - "src/renderer/src/components/terminal/terminal-provider-snapshot-capability-resettlement.test.ts": 1356, + "src/renderer/src/components/terminal-pane/terminal-url-link-click.test.ts": 1659, + "src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.test.ts": 1825, + "src/renderer/src/components/terminal-pane/terminal-user-input-signal.test.ts": 46, + "src/renderer/src/components/terminal-pane/terminal-view-attributes-publisher.test.ts": 331, + "src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts": 1476, + "src/renderer/src/components/terminal-pane/terminal-webgl-atlas-recovery.test.ts": 43, + "src/renderer/src/components/terminal-pane/terminal-webgl-diagnostics-breadcrumbs.test.ts": 52, + "src/renderer/src/components/terminal-pane/terminal-windows-shift-enter.test.ts": 106, + "src/renderer/src/components/terminal-pane/terminal-worktree-path-link.test.ts": 1398, + "src/renderer/src/components/terminal-pane/title-agent-identity.test.ts": 138, + "src/renderer/src/components/terminal-pane/use-manual-terminal-worktree-parking.test.ts": 1497, + "src/renderer/src/components/terminal-pane/use-mobile-overlay-ticks.perf.test.tsx": 311, + "src/renderer/src/components/terminal-pane/use-notification-dispatch-verdict.test.ts": 418, + "src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts": 464, + "src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx": 530, + "src/renderer/src/components/terminal-pane/use-system-prefers-dark.test.ts": 33, + "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-active-pty-reporting.test.ts": 1392, + "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-paste-events.test.ts": 1307, + "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-sync-fit-registration.test.ts": 1400, + "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-visibility-resume.test.ts": 1561, + "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-window-focus-recovery.test.ts": 1516, + "src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.test.ts": 1466, + "src/renderer/src/components/terminal-pane/use-terminal-park-mount-intent.react185.test.tsx": 413, + "src/renderer/src/components/terminal-pane/use-terminal-scroll-visibility-memory.test.ts": 79, + "src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts": 625, + "src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts": 316, + "src/renderer/src/components/terminal-pane/use-visible-terminal-tab-claim.test.ts": 42, + "src/renderer/src/components/terminal-pane/use-wsl-host-unavailable-reason.test.ts": 1236, + "src/renderer/src/components/terminal-pane/useSessionRestoredBannerDismiss.test.tsx": 199, + "src/renderer/src/components/terminal-pane/useTerminalFontZoom.test.ts": 163, + "src/renderer/src/components/terminal-pane/wrapped-terminal-link-ranges.test.ts": 60, + "src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts": 76, + "src/renderer/src/components/terminal-pane/xterm-bypass-policy-ios.test.ts": 69, + "src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts": 111, + "src/renderer/src/components/terminal-pane/xterm-bypass-policy.test.ts": 127, + "src/renderer/src/components/terminal-parked-watcher-sync-entries.test.tsx": 1122, + "src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx": 1203, + "src/renderer/src/components/terminal-quick-commands/terminal-quick-command-agent-options.test.ts": 131, + "src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.test.ts": 153, + "src/renderer/src/components/terminal-scrollback-decoration-eviction.test.ts": 617, + "src/renderer/src/components/terminal-search-decoration-leak.test.ts": 2155, + "src/renderer/src/components/terminal-search-long-wrapped-line.test.ts": 4767, + "src/renderer/src/components/terminal-search-safe-find.test.ts": 37, + "src/renderer/src/components/terminal-split-workspace-surfaces-startup-hold.test.tsx": 198, + "src/renderer/src/components/terminal-startup-hold-placeholder.test.tsx": 742, + "src/renderer/src/components/terminal-workspace-keydown.test.ts": 744, + "src/renderer/src/components/terminal-workspace-surface-ids.test.tsx": 1959, + "src/renderer/src/components/terminal/activation-deferred-tab-admission.test.ts": 41, + "src/renderer/src/components/terminal/active-terminal-repair-loop.react185.test.tsx": 1554, + "src/renderer/src/components/terminal/active-terminal-repair.test.ts": 35, + "src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts": 1397, + "src/renderer/src/components/terminal/background-terminal-worktree-visibility.test.ts": 45, + "src/renderer/src/components/terminal/cold-activation-deferral-stranding.test.ts": 1846, + "src/renderer/src/components/terminal/initial-terminal-admission-wait.test.tsx": 876, + "src/renderer/src/components/terminal/initial-terminal-structured-launch.test.tsx": 408, + "src/renderer/src/components/terminal/initial-terminal.test.ts": 30, + "src/renderer/src/components/terminal/pty-running-work-probe-child-evidence.test.ts": 36, + "src/renderer/src/components/terminal/running-terminal-close-guard.test.ts": 112, + "src/renderer/src/components/terminal/split-group-mount.test.ts": 53, + "src/renderer/src/components/terminal/startup-terminal-tab-hold.test.ts": 1725, + "src/renderer/src/components/terminal/tab-type-cycle.test.ts": 41, + "src/renderer/src/components/terminal/terminal-close-confirm-keyboard-vs-mouse.test.ts": 338, + "src/renderer/src/components/terminal/terminal-close-copy-kind.test.ts": 35, + "src/renderer/src/components/terminal/terminal-close-incarnation.test.ts": 64, + "src/renderer/src/components/terminal/terminal-provider-snapshot-bound-pty-ids.test.ts": 1537, + "src/renderer/src/components/terminal/terminal-provider-snapshot-capability-resettlement.test.ts": 1472, "src/renderer/src/components/terminal/terminal-provider-snapshot-capability.test.ts": 77, - "src/renderer/src/components/terminal/terminal-tab-actions-kill-all.test.ts": 414, - "src/renderer/src/components/terminal/terminal-tab-actions-unified-close.test.ts": 1714, - "src/renderer/src/components/terminal/terminal-tab-actions-unrouted-close.test.ts": 375, - "src/renderer/src/components/terminal/terminal-tab-actions.test.ts": 441, - "src/renderer/src/components/terminal/terminal-tab-close-running-confirm.test.ts": 440, - "src/renderer/src/components/terminal/unsaved-close-queue.test.ts": 54, + "src/renderer/src/components/terminal/terminal-tab-actions-kill-all.test.ts": 482, + "src/renderer/src/components/terminal/terminal-tab-actions-unified-close.test.ts": 1414, + "src/renderer/src/components/terminal/terminal-tab-actions-unrouted-close.test.ts": 391, + "src/renderer/src/components/terminal/terminal-tab-actions.test.ts": 427, + "src/renderer/src/components/terminal/terminal-tab-close-running-confirm.test.ts": 404, + "src/renderer/src/components/terminal/unsaved-close-queue.test.ts": 41, "src/renderer/src/components/terminal/use-terminal-provider-snapshot-capability.test.tsx": 309, - "src/renderer/src/components/terminal/use-worktree-files.test.tsx": 349, - "src/renderer/src/components/terminal/window-close-running-work.test.ts": 118, - "src/renderer/src/components/ui/color-picker.test.tsx": 331, - "src/renderer/src/components/ui/popover-wheel-scroll.test.tsx": 436, - "src/renderer/src/components/ui/repo-multi-combobox.test.ts": 204, - "src/renderer/src/components/ui/scroll-area.test.tsx": 249, - "src/renderer/src/components/ui/slider.test.tsx": 606, - "src/renderer/src/components/ui/switch.test.tsx": 102, - "src/renderer/src/components/unexpected-signout/unexpected-signout-card.test.tsx": 1501, - "src/renderer/src/components/unexpected-signout/unexpected-signout-visibility.test.ts": 61, - "src/renderer/src/components/use-github-task-search-commit.test.ts": 287, - "src/renderer/src/components/use-jira-create-assignee.test.tsx": 250, - "src/renderer/src/components/use-quick-open-interaction.test.tsx": 1646, - "src/renderer/src/components/use-task-creation-draft-retention.test.ts": 562, - "src/renderer/src/components/use-task-page-gitlab-loading.test.tsx": 1645, - "src/renderer/src/components/use-task-page-jira-list-effects.test.ts": 1835, - "src/renderer/src/components/use-terminal-create-actions.test.tsx": 313, - "src/renderer/src/components/use-terminal-editor-close-foundation.window-close.test.tsx": 1581, - "src/renderer/src/components/use-terminal-editor-close-queue.reveal.test.tsx": 646, - "src/renderer/src/components/use-terminal-parking-pass.test.tsx": 1936, - "src/renderer/src/components/use-terminal-window-lifecycle.lazy-ref.test.tsx": 240, - "src/renderer/src/components/use-worktree-jump-palette-browser-ownership.test.ts": 1489, - "src/renderer/src/components/virtualized-list.test.tsx": 391, - "src/renderer/src/components/window-close-request-coordinator.test.ts": 222, - "src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.mount-gating.test.tsx": 2431, - "src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.stale-while-revalidate.test.tsx": 2563, - "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-browse-state.test.tsx": 266, - "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-facet-rows.test.tsx": 389, - "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.test.tsx": 349, - "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-row-order.test.ts": 267, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-active-facets.test.ts": 70, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal-late-settlement.test.ts": 1523, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal-snapshot-batch.test.ts": 1294, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal.test.ts": 189, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-list.test.tsx": 2157, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row-data.test.ts": 124, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row.test.tsx": 2357, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.test.tsx": 532, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-deletion-phases.test.ts": 85, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-notices.test.tsx": 363, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-controls.test.tsx": 601, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-filter.test.ts": 174, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-sort.test.ts": 123, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-bar.test.tsx": 537, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-flat-list.test.tsx": 2613, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-evidence.test.ts": 91, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.test.ts": 130, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-candidates.test.ts": 97, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-settlement.test.ts": 95, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-timeout-recovery.test.ts": 1542, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-scanned-host-confirmation-removal.test.tsx": 3533, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.test.tsx": 439, - "src/renderer/src/components/workspace-emoji/WorkspaceEmojiSuggestionPopover.test.tsx": 390, - "src/renderer/src/components/workspace-emoji/useWorkspaceEmojiShortcodeInput.test.tsx": 376, - "src/renderer/src/components/workspace-surface-projection.test.ts": 97, - "src/renderer/src/components/worktree-creation/WorktreeCreationPanel.test.tsx": 315, - "src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx": 2544, - "src/renderer/src/components/worktree-jump-palette-primitives.test.tsx": 357, - "src/renderer/src/components/worktree-jump-palette-quick-action-availability.test.tsx": 998, - "src/renderer/src/components/worktree-jump-palette-recent-inclusion.test.ts": 124, - "src/renderer/src/components/worktree-jump-palette-sleeping-filter.test.ts": 1424, - "src/renderer/src/components/worktree-jump-palette-status-inputs.test.ts": 65, - "src/renderer/src/hooks/agent-auto-ack-targets.test.ts": 1504, - "src/renderer/src/hooks/agent-hook-completion-background-turn-notifications.test.ts": 351, - "src/renderer/src/hooks/agent-hook-completion-fresh-working.test.ts": 187, - "src/renderer/src/hooks/agent-hook-completion-grok-notifications.test.ts": 454, - "src/renderer/src/hooks/agent-hook-completion-notifications-import.test.ts": 237, - "src/renderer/src/hooks/agent-hook-completion-notifications.test.ts": 504, - "src/renderer/src/hooks/agent-hook-completion-store-sync.test.ts": 75, - "src/renderer/src/hooks/automation-agent-status-entry-change.test.ts": 50, - "src/renderer/src/hooks/automation-dispatch-unverifiable-loss.test.ts": 178, - "src/renderer/src/hooks/automations-changed-event-attribution.test.ts": 1801, - "src/renderer/src/hooks/composer-branch-selection.test.ts": 75, - "src/renderer/src/hooks/composer-drop-failure-toast.test.ts": 92, - "src/renderer/src/hooks/composer-drop-result.test.ts": 47, - "src/renderer/src/hooks/composer-native-file-drop.test.ts": 52, - "src/renderer/src/hooks/composer-state/attachment-drop-failure.test.tsx": 633, - "src/renderer/src/hooks/composer-state/attachment-drop-lifetime.test.tsx": 790, - "src/renderer/src/hooks/composer-state/branch-start-point-actions.test.ts": 323, - "src/renderer/src/hooks/composer-state/composer-attachment-target.test.ts": 70, - "src/renderer/src/hooks/composer-state/composer-name-source-selection.test.ts": 2007, - "src/renderer/src/hooks/composer-state/composer-remote-server-drop.test.tsx": 639, - "src/renderer/src/hooks/composer-state/draft-target-sync.test.ts": 265, - "src/renderer/src/hooks/composer-state/multiple-create-reset.test.ts": 1678, - "src/renderer/src/hooks/composer-state/provider-runtime-sync.test.ts": 1661, - "src/renderer/src/hooks/composer-state/quick-creation-request.test.ts": 110, - "src/renderer/src/hooks/direct-ssh-runtime-wake-isolation.test.tsx": 1605, - "src/renderer/src/hooks/direct-ssh-state-routing.test.ts": 183, - "src/renderer/src/hooks/editor-external-watch-fallback-explorer.test.ts": 355, - "src/renderer/src/hooks/editor-external-watch-selected-host.test.ts": 324, - "src/renderer/src/hooks/editor-runtime-file-watch.test.ts": 55, - "src/renderer/src/hooks/fork-push-warning.test.ts": 65, - "src/renderer/src/hooks/installed-agent-skill-discovery-cache.test.ts": 67, - "src/renderer/src/hooks/installed-agent-skill-discovery.test.ts": 73, - "src/renderer/src/hooks/installed-agent-skill-runtime-eviction.test.ts": 54, - "src/renderer/src/hooks/installed-agent-skill-verdict.test.ts": 1648, - "src/renderer/src/hooks/ipc-events/agent-status-pane-routing-index-retention.test.ts": 2083, - "src/renderer/src/hooks/ipc-events/agent-status-pane-routing-index.test.ts": 1458, - "src/renderer/src/hooks/ipc-events/agent-status-pending-retry-gate.test.ts": 563, - "src/renderer/src/hooks/ipc-events/agent-status-terminal-title-tab-write.test.ts": 1686, - "src/renderer/src/hooks/ipc-events/agent-status-unavailable.test.ts": 82, - "src/renderer/src/hooks/ipc-events/browser-automation-bootstrap-lease.test.ts": 144, - "src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.owner.test.ts": 487, - "src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.profile-switch.test.ts": 535, - "src/renderer/src/hooks/ipc-events/browser-state-capture-paint-hold.test.ts": 293, - "src/renderer/src/hooks/ipc-events/browser-state-open-link-profile.test.ts": 323, - "src/renderer/src/hooks/ipc-events/direct-ssh-connect-reply-routing.test.ts": 1461, - "src/renderer/src/hooks/ipc-events/direct-ssh-hydration-fanout.test.ts": 1432, - "src/renderer/src/hooks/ipc-events/direct-ssh-hydration-target-metadata.test.ts": 1473, - "src/renderer/src/hooks/ipc-events/mobile-terminal-close-ipc-bridge.test.ts": 1564, - "src/renderer/src/hooks/ipc-events/normalize-agent-status-event.test.ts": 60, - "src/renderer/src/hooks/ipc-events/orca-profile-auth-ipc-bridge.test.ts": 1416, - "src/renderer/src/hooks/ipc-events/os-markdown-file-open-bridge.test.ts": 308, - "src/renderer/src/hooks/ipc-events/runtime-client-ipc-bridge-ownership.test.ts": 1295, - "src/renderer/src/hooks/ipc-events/runtime-reconnect-host-status.test.ts": 1629, - "src/renderer/src/hooks/ipc-events/ssh-managed-server-converted-catalogs.test.ts": 2199, - "src/renderer/src/hooks/ipc-events/ssh-managed-server-state-effects.test.ts": 646, - "src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge-placement.test.ts": 1359, - "src/renderer/src/hooks/ipc-events/terminal-ui-routing-ipc-bridge-split.test.ts": 261, - "src/renderer/src/hooks/ipc-events/updater-status-ipc-bridge.test.ts": 51, - "src/renderer/src/hooks/ipc-events/worktree-event-runtime-activation.test.ts": 1461, - "src/renderer/src/hooks/ipc-tab-switch-group-order-hydration.test.ts": 79, - "src/renderer/src/hooks/ipc-tab-switch-structured-tab-cycle.test.ts": 76, - "src/renderer/src/hooks/ipc-tab-switch.test.ts": 81, - "src/renderer/src/hooks/legacy-worker-terminal-recovery-event.test.ts": 67, - "src/renderer/src/hooks/macos-tcc-prompt-notice-subscription.test.ts": 61, - "src/renderer/src/hooks/metadata-request-cache.test.ts": 140, - "src/renderer/src/hooks/mobile-terminal-reveal-tab-adoption.test.ts": 2706, - "src/renderer/src/hooks/modal-return-focus-action.test.ts": 53, - "src/renderer/src/hooks/programmatic-scroll-marks.test.ts": 46, - "src/renderer/src/hooks/runtime-client-events-sync-ownership.test.ts": 70, - "src/renderer/src/hooks/runtime-client-events-sync.test.ts": 114, - "src/renderer/src/hooks/runtime-project-refresh-scheduler.test.ts": 99, - "src/renderer/src/hooks/shortcut-label-cache.test.tsx": 2077, - "src/renderer/src/hooks/structured-session-completion-focus.test.ts": 1950, - "src/renderer/src/hooks/unpaired-device-auth-notification.test.ts": 47, - "src/renderer/src/hooks/use-audio-capture.capture-loss.test.ts": 350, - "src/renderer/src/hooks/use-clipboard-text-copy-feedback.test.ts": 262, - "src/renderer/src/hooks/use-document-dark-theme.test.tsx": 268, - "src/renderer/src/hooks/use-list-multi-selection.test.tsx": 243, - "src/renderer/src/hooks/use-now.test.ts": 250, - "src/renderer/src/hooks/use-os-file-drop-owner.test.tsx": 410, - "src/renderer/src/hooks/use-palette-search-evaluation-context.test.ts": 272, - "src/renderer/src/hooks/use-task-page-github-work-item-mutation-host.test.ts": 1506, - "src/renderer/src/hooks/use-terminal-quick-command-hosts.test.ts": 434, - "src/renderer/src/hooks/use-window-stream-visibility.test.ts": 274, - "src/renderer/src/hooks/useActiveProjectSkillRuntime.test.tsx": 1752, - "src/renderer/src/hooks/useAgentDetectionTarget.test.ts": 1369, - "src/renderer/src/hooks/useAppMenuPaste.test.tsx": 225, - "src/renderer/src/hooks/useAppMenuSelectionActions.test.tsx": 270, - "src/renderer/src/hooks/useReadOnScreenAttention.away.test.ts": 1687, - "src/renderer/src/hooks/useReadOnScreenAttention.clock-skew.test.ts": 1904, - "src/renderer/src/hooks/useReadOnScreenAttention.floating-panel.test.ts": 1665, - "src/renderer/src/hooks/useReadOnScreenAttention.focus-group.test.ts": 1914, - "src/renderer/src/hooks/useReadOnScreenAttention.mixed-workspace.test.ts": 1866, - "src/renderer/src/hooks/useReadOnScreenAttention.test.ts": 1475, - "src/renderer/src/hooks/useAutomationDispatchEvents.test.ts": 1410, - "src/renderer/src/hooks/useComposerState-decisions.test.ts": 59, - "src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts": 118, - "src/renderer/src/hooks/useComposerState-host-retarget.test.ts": 98, - "src/renderer/src/hooks/useComposerState.integration.test.ts": 3120, - "src/renderer/src/hooks/useDetectedAgents.test.tsx": 1507, - "src/renderer/src/hooks/useEditorExternalWatch-complexity.test.ts": 745, - "src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts": 517, - "src/renderer/src/hooks/useEditorExternalWatch-subscriptions.test.tsx": 272, - "src/renderer/src/hooks/useEditorExternalWatch-targets.test.ts": 376, - "src/renderer/src/hooks/useEditorExternalWatch-wsl-repro.test.ts": 579, - "src/renderer/src/hooks/useEditorExternalWatch.test.ts": 590, - "src/renderer/src/hooks/useEphemeralVmRecipeOptions.test.tsx": 219, - "src/renderer/src/hooks/useGitHubSlugMetadata.test.tsx": 224, - "src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx": 1476, - "src/renderer/src/hooks/useInstalledAgentSkills.test.ts": 1366, - "src/renderer/src/hooks/useIpcEvents-agent-dashboard-shortcut.test.ts": 48, - "src/renderer/src/hooks/useIpcEvents-agent-launch-closed-tab.test.ts": 1906, - "src/renderer/src/hooks/useIpcEvents-agent-status-batch-projection.test.ts": 3396, - "src/renderer/src/hooks/useIpcEvents-agent-status-connection-attribution.test.ts": 3402, - "src/renderer/src/hooks/useIpcEvents-agent-status-hook-titles.test.ts": 4162, - "src/renderer/src/hooks/useIpcEvents-agent-status-pane-teardown.test.ts": 2945, - "src/renderer/src/hooks/useIpcEvents-agent-status-queue-ordering.test.ts": 2132, - "src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-hydration.test.ts": 10394, - "src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-replay.test.ts": 2794, - "src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts": 3533, - "src/renderer/src/hooks/useIpcEvents-agent-status-turn-completion.test.ts": 1596, - "src/renderer/src/hooks/useIpcEvents-browser-certificate-failure.test.ts": 2422, - "src/renderer/src/hooks/useIpcEvents-browser-navigation.test.ts": 1521, - "src/renderer/src/hooks/useIpcEvents-browser-tab-create.test.ts": 1398, - "src/renderer/src/hooks/useIpcEvents-cli-worktree-activation.test.ts": 1748, - "src/renderer/src/hooks/useIpcEvents-client-hosted-browser-rows.test.ts": 2627, - "src/renderer/src/hooks/useIpcEvents-close-routing-active-browser-tab.test.ts": 3432, - "src/renderer/src/hooks/useIpcEvents-close-routing-browser-pages.test.ts": 2641, - "src/renderer/src/hooks/useIpcEvents-close-routing-floating-guest.test.ts": 1892, - "src/renderer/src/hooks/useIpcEvents-close-routing-session-tabs.test.ts": 4606, - "src/renderer/src/hooks/useIpcEvents-cmd-j-digit-chord.test.ts": 2911, - "src/renderer/src/hooks/useIpcEvents-lifecycle.test.ts": 1437, - "src/renderer/src/hooks/useIpcEvents-new-workspace-shortcut.test.ts": 72, - "src/renderer/src/hooks/useIpcEvents-rate-limit-hydration.test.ts": 1595, - "src/renderer/src/hooks/useIpcEvents-repos-changed-catalogs.test.ts": 2869, - "src/renderer/src/hooks/useIpcEvents-runtime-environment-selectors.test.ts": 325, - "src/renderer/src/hooks/useIpcEvents-session-tab-close-request.test.ts": 2114, - "src/renderer/src/hooks/useIpcEvents-silent-terminal-adoption.test.ts": 1752, - "src/renderer/src/hooks/useIpcEvents-ssh-disconnect-cleanup.test.ts": 1524, - "src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts": 1746, - "src/renderer/src/hooks/useIpcEvents-updater-status.test.ts": 1988, - "src/renderer/src/hooks/useIssueMetadata.test.tsx": 262, - "src/renderer/src/hooks/useLinearProviderConnected.test.tsx": 1249, - "src/renderer/src/hooks/useMacTccAttributionSeveredNotice.test.tsx": 2312, - "src/renderer/src/hooks/useMacosTccPromptNotice.test.tsx": 1916, - "src/renderer/src/hooks/useMetadataListRequest.test.tsx": 277, - "src/renderer/src/hooks/useModalReturnFocus.test.tsx": 1441, - "src/renderer/src/hooks/usePrimarySelectionPaste.terminal-native-paste.test.tsx": 199, - "src/renderer/src/hooks/usePrimarySelectionPaste.test.tsx": 282, - "src/renderer/src/hooks/useResetCountdownClock.test.ts": 303, - "src/renderer/src/hooks/useRetiredWorktreeNames.test.ts": 600, - "src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx": 966, - "src/renderer/src/hooks/useSettingsNavigationMetadata.language-switch.test.tsx": 2576, - "src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts": 2176, - "src/renderer/src/hooks/useSidebarResize.test.ts": 59, - "src/renderer/src/hooks/useSkillFreshness.test.tsx": 273, - "src/renderer/src/hooks/useUnreadDockBadge.test.ts": 1844, - "src/renderer/src/hooks/useVirtualizedScrollAnchor.listener-deps.test.ts": 88, - "src/renderer/src/hooks/useVirtualizedScrollAnchor.marks-restore-signal.test.ts": 144, - "src/renderer/src/hooks/useVirtualizedScrollAnchor.test.ts": 53, - "src/renderer/src/hooks/useVirtualizedScrollAnchor.test.tsx": 260, - "src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.test.tsx": 226, - "src/renderer/src/hooks/viewport-size-change-listener.test.ts": 56, - "src/renderer/src/hooks/worktree-change-refresh-queue.test.ts": 61, - "src/renderer/src/hooks/worktree-head-identity-apply.test.ts": 52, - "src/renderer/src/hooks/zoom-routing.test.ts": 686, - "src/renderer/src/i18n/I18nProvider.test.tsx": 1693, - "src/renderer/src/i18n/intl-locale.test.ts": 148, - "src/renderer/src/i18n/ja-technical-literal-mistranslations.test.ts": 105, - "src/renderer/src/i18n/ko-ui-semantic-mistranslations.test.ts": 101, - "src/renderer/src/i18n/lazy-locale.test.ts": 194, - "src/renderer/src/i18n/locale-english-regression.test.ts": 825, - "src/renderer/src/i18n/native-chat-locales.test.ts": 952, - "src/renderer/src/i18n/plugin-chrome-allowlist.test.ts": 96, - "src/renderer/src/i18n/provider-credit-balance-locales.test.ts": 364, - "src/renderer/src/i18n/pseudo-localization.test.ts": 80, - "src/renderer/src/i18n/relative-duration-copy-localization.test.ts": 693, - "src/renderer/src/i18n/relative-time-format.test.ts": 61, - "src/renderer/src/i18n/runtime-required-catalog.test.ts": 664, - "src/renderer/src/i18n/settings-status-label-localization.test.ts": 288, - "src/renderer/src/i18n/technical-literal-catalog-values.test.ts": 250, - "src/renderer/src/i18n/weekday-names.test.ts": 86, - "src/renderer/src/i18n/zh-technical-literal-mistranslations.test.ts": 135, - "src/renderer/src/lazy-modal-mount-state.test.ts": 57, - "src/renderer/src/lib/activate-ai-vault-structured-session-host.test.ts": 1143, - "src/renderer/src/lib/activate-ai-vault-structured-session-reveal.test.ts": 1773, - "src/renderer/src/lib/activate-ai-vault-structured-session.test.ts": 1867, - "src/renderer/src/lib/activate-tab-and-focus-pane.test.ts": 59, - "src/renderer/src/lib/active-agent-note-send-explicit-target.test.ts": 966, - "src/renderer/src/lib/active-agent-note-send-focused-session.test.ts": 638, - "src/renderer/src/lib/active-agent-note-send-runtime-error-codes.test.ts": 80, - "src/renderer/src/lib/active-agent-note-send-target-detection.test.ts": 525, - "src/renderer/src/lib/active-view-persist.test.ts": 46, - "src/renderer/src/lib/activity-thread-display.test.ts": 68, - "src/renderer/src/lib/agent-background-session-launch-host.test.ts": 1472, - "src/renderer/src/lib/agent-detection-target-inventory.test.ts": 1465, - "src/renderer/src/lib/agent-draft-readiness.test.ts": 142, - "src/renderer/src/lib/agent-followup-delivery.test.ts": 464, - "src/renderer/src/lib/agent-hibernation-confirmation.test.ts": 48, - "src/renderer/src/lib/agent-hibernation-coordinator.test.ts": 1779, - "src/renderer/src/lib/agent-hibernation-output-activity.test.ts": 48, - "src/renderer/src/lib/agent-hibernation-pane-age.test.ts": 56, - "src/renderer/src/lib/agent-hibernation-planner.test.ts": 207, - "src/renderer/src/lib/agent-hibernation-runtime-liveness-race.test.ts": 1423, - "src/renderer/src/lib/agent-hibernation-visibility.test.ts": 1582, - "src/renderer/src/lib/agent-launch-caller-permissions.test.ts": 1006, - "src/renderer/src/lib/agent-launch-caller-placement.test.ts": 1164, - "src/renderer/src/lib/agent-launch-caller-prompt-transport.test.ts": 1248, - "src/renderer/src/lib/agent-launch-caller-routing.test.ts": 1132, - "src/renderer/src/lib/agent-launch-pane-spawn-hold.test.ts": 47, - "src/renderer/src/lib/agent-launch-pane-verdict-application.test.ts": 1221, - "src/renderer/src/lib/agent-launch-prompt-delivery.test.ts": 82, - "src/renderer/src/lib/agent-launch-route-connection-fallback.test.ts": 1819, - "src/renderer/src/lib/agent-launch-route-input.test.ts": 1635, - "src/renderer/src/lib/agent-launch-route-registered-agents.test.ts": 1571, - "src/renderer/src/lib/agent-launch-routing.test.ts": 83, - "src/renderer/src/lib/agent-launch-tab-publication.test.ts": 1452, - "src/renderer/src/lib/agent-launch-through-host.test.ts": 1481, - "src/renderer/src/lib/agent-message-send.test.ts": 251, - "src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts": 175, - "src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts": 194, - "src/renderer/src/lib/agent-paste-draft.test.ts": 324, - "src/renderer/src/lib/agent-picker-search.test.ts": 167, - "src/renderer/src/lib/agent-ready-wait.test.ts": 125, - "src/renderer/src/lib/agent-resume-launch-target.test.ts": 3321, - "src/renderer/src/lib/agent-row-primary-text.test.ts": 61, - "src/renderer/src/lib/agent-row-tool-preview.test.ts": 59, - "src/renderer/src/lib/agent-send-title-status.test.ts": 98, - "src/renderer/src/lib/agent-session-fork-context.test.ts": 68, - "src/renderer/src/lib/agent-session-launch-plan.test.ts": 1740, - "src/renderer/src/lib/agent-skill-cli-prerequisite.test.ts": 143, - "src/renderer/src/lib/agent-skill-nav-install-status.test.ts": 1387, - "src/renderer/src/lib/agent-startup-delayed-delivery-perf.test.ts": 1373, - "src/renderer/src/lib/agent-status-connection-ownership.test.ts": 127, - "src/renderer/src/lib/agent-status-count.test.ts": 101, - "src/renderer/src/lib/agent-status-epoch-clock.test.ts": 52, - "src/renderer/src/lib/agent-status-evidence-clock.test.ts": 89, - "src/renderer/src/lib/agent-status-terminal-title.test.ts": 117, - "src/renderer/src/lib/agent-status-worktree-attribution.test.ts": 57, - "src/renderer/src/lib/agent-status.test.ts": 489, - "src/renderer/src/lib/agent-tab-shortcuts.test.ts": 78, - "src/renderer/src/lib/ai-vault-antigravity-reference-host.test.ts": 450, - "src/renderer/src/lib/ai-vault-omp-cold-resume.test.ts": 1762, - "src/renderer/src/lib/ai-vault-resume-command.drop-repin.test.ts": 447, - "src/renderer/src/lib/ai-vault-resume-command.resumable-agent.test.ts": 459, - "src/renderer/src/lib/ai-vault-resume-command.test.ts": 412, - "src/renderer/src/lib/ai-vault-resume-shell.test.ts": 466, - "src/renderer/src/lib/ai-vault-resume-target.test.ts": 110, - "src/renderer/src/lib/ai-vault-session-drag.test.ts": 67, - "src/renderer/src/lib/ai-vault-session-fork-startup.test.ts": 413, - "src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts": 64, - "src/renderer/src/lib/ai-vault-tab-title-sync-inputs.test.ts": 423, - "src/renderer/src/lib/ai-vault-tab-title-sync.test.ts": 957, - "src/renderer/src/lib/app-command-dispatch.test.ts": 79, - "src/renderer/src/lib/app-font-family.test.ts": 270, - "src/renderer/src/lib/app-menu-paste.test.ts": 209, - "src/renderer/src/lib/app-menu-selection-actions.test.ts": 122, - "src/renderer/src/lib/automation-session-observer.test.ts": 135, - "src/renderer/src/lib/automation-session-reuse.test.ts": 91, - "src/renderer/src/lib/automation-terminal-ownership.test.ts": 1337, - "src/renderer/src/lib/bounded-generation-map.test.ts": 46, - "src/renderer/src/lib/browser-cookie-import-toast.test.ts": 102, - "src/renderer/src/lib/browser-history-match.performance.test.ts": 344, - "src/renderer/src/lib/browser-history-match.test.ts": 334, - "src/renderer/src/lib/browser-page-conversion-history.test.ts": 59, - "src/renderer/src/lib/browser-page-palette-activation.test.ts": 1521, - "src/renderer/src/lib/browser-palette-page-entries.test.ts": 417, - "src/renderer/src/lib/browser-palette-search.test.ts": 322, - "src/renderer/src/lib/browser-uuid.test.ts": 54, - "src/renderer/src/lib/browser-workspace-source-resolution.test.ts": 48, - "src/renderer/src/lib/browser-workspace-tab-activation.test.ts": 1497, - "src/renderer/src/lib/client-creation-action-policy.test.ts": 298, - "src/renderer/src/lib/cmd-j-github-url-lookup.test.ts": 54, - "src/renderer/src/lib/cmd-j-host-qualified-candidate-ownership.test.ts": 422, - "src/renderer/src/lib/cmd-j-linear-issue-intent.test.ts": 55, + "src/renderer/src/components/terminal/use-worktree-files.test.tsx": 301, + "src/renderer/src/components/terminal/window-close-running-work.test.ts": 85, + "src/renderer/src/components/ui/color-picker.test.tsx": 353, + "src/renderer/src/components/ui/popover-wheel-scroll.test.tsx": 507, + "src/renderer/src/components/ui/repo-multi-combobox.test.ts": 161, + "src/renderer/src/components/ui/scroll-area.test.tsx": 205, + "src/renderer/src/components/ui/slider.test.tsx": 396, + "src/renderer/src/components/ui/switch.test.tsx": 80, + "src/renderer/src/components/unexpected-signout/unexpected-signout-card.test.tsx": 1940, + "src/renderer/src/components/unexpected-signout/unexpected-signout-visibility.test.ts": 49, + "src/renderer/src/components/use-github-task-search-commit.test.ts": 260, + "src/renderer/src/components/use-jira-create-assignee.test.tsx": 236, + "src/renderer/src/components/use-jira-project-inventory.test.tsx": 601, + "src/renderer/src/components/use-quick-open-interaction.test.tsx": 1634, + "src/renderer/src/components/use-task-creation-draft-retention.test.ts": 383, + "src/renderer/src/components/use-task-page-gitlab-loading.test.tsx": 1771, + "src/renderer/src/components/use-task-page-jira-list-effects.test.ts": 2405, + "src/renderer/src/components/use-task-page-jira-list-state.owner.test.tsx": 562, + "src/renderer/src/components/use-task-page-jira-project-scope.test.tsx": 1702, + "src/renderer/src/components/use-task-page-jira-quick-filters.test.tsx": 1272, + "src/renderer/src/components/use-task-page-jira-saved-filters.test.tsx": 1357, + "src/renderer/src/components/use-task-page-jira-summary-generation.test.tsx": 362, + "src/renderer/src/components/use-terminal-create-actions.test.tsx": 282, + "src/renderer/src/components/use-terminal-editor-close-foundation.window-close.test.tsx": 1550, + "src/renderer/src/components/use-terminal-editor-close-queue.reveal.test.tsx": 606, + "src/renderer/src/components/use-terminal-parking-pass.test.tsx": 1779, + "src/renderer/src/components/use-terminal-window-lifecycle.lazy-ref.test.tsx": 233, + "src/renderer/src/components/use-worktree-jump-palette-browser-ownership.test.ts": 1613, + "src/renderer/src/components/use-worktree-jump-palette-task-search.test.tsx": 1788, + "src/renderer/src/components/virtualized-list.test.tsx": 543, + "src/renderer/src/components/window-close-request-coordinator.test.ts": 246, + "src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.mount-gating.test.tsx": 2698, + "src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.stale-while-revalidate.test.tsx": 2451, + "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-browse-state.test.tsx": 233, + "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-facet-rows.test.tsx": 550, + "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.test.tsx": 316, + "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-row-order.test.ts": 236, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-active-facets.test.ts": 52, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal-late-settlement.test.ts": 1481, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal-snapshot-batch.test.ts": 1386, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal.test.ts": 131, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-list.test.tsx": 1878, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row-data.test.ts": 114, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row.test.tsx": 1930, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.test.tsx": 407, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-deletion-phases.test.ts": 67, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-notices.test.tsx": 432, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-controls.test.tsx": 702, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-filter.test.ts": 137, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-sort.test.ts": 91, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-bar.test.tsx": 672, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-flat-list.test.tsx": 2272, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-evidence.test.ts": 85, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.test.ts": 121, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-candidates.test.ts": 65, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-settlement.test.ts": 73, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-timeout-recovery.test.ts": 1337, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.test.tsx": 375, + "src/renderer/src/components/workspace-emoji/WorkspaceEmojiSuggestionPopover.test.tsx": 456, + "src/renderer/src/components/workspace-emoji/useWorkspaceEmojiShortcodeInput.test.tsx": 290, + "src/renderer/src/components/workspace-surface-projection.test.ts": 66, + "src/renderer/src/components/worktree-creation/WorktreeCreationPanel.test.tsx": 314, + "src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx": 3913, + "src/renderer/src/components/worktree-jump-palette-primitives.test.tsx": 355, + "src/renderer/src/components/worktree-jump-palette-quick-action-availability.test.tsx": 752, + "src/renderer/src/components/worktree-jump-palette-recent-inclusion.test.ts": 133, + "src/renderer/src/components/worktree-jump-palette-sleeping-filter.test.ts": 1266, + "src/renderer/src/components/worktree-jump-palette-status-inputs.test.ts": 43, + "src/renderer/src/hooks/agent-auto-ack-targets.test.ts": 1391, + "src/renderer/src/hooks/agent-hook-completion-background-turn-notifications.test.ts": 282, + "src/renderer/src/hooks/agent-hook-completion-fresh-working.test.ts": 179, + "src/renderer/src/hooks/agent-hook-completion-grok-notifications.test.ts": 417, + "src/renderer/src/hooks/agent-hook-completion-notifications-import.test.ts": 164, + "src/renderer/src/hooks/agent-hook-completion-notifications.test.ts": 471, + "src/renderer/src/hooks/agent-hook-completion-store-sync.test.ts": 38, + "src/renderer/src/hooks/automation-agent-status-entry-change.test.ts": 39, + "src/renderer/src/hooks/automation-dispatch-unverifiable-loss.test.ts": 154, + "src/renderer/src/hooks/automations-changed-event-attribution.test.ts": 1857, + "src/renderer/src/hooks/composer-branch-selection.test.ts": 60, + "src/renderer/src/hooks/composer-drop-failure-toast.test.ts": 87, + "src/renderer/src/hooks/composer-drop-result.test.ts": 29, + "src/renderer/src/hooks/composer-native-file-drop.test.ts": 44, + "src/renderer/src/hooks/composer-state/attachment-drop-failure.test.tsx": 579, + "src/renderer/src/hooks/composer-state/attachment-drop-lifetime.test.tsx": 726, + "src/renderer/src/hooks/composer-state/branch-start-point-actions.test.ts": 221, + "src/renderer/src/hooks/composer-state/composer-attachment-target.test.ts": 58, + "src/renderer/src/hooks/composer-state/composer-deferred-source-name.test.ts": 1570, + "src/renderer/src/hooks/composer-state/composer-name-ownership.test.ts": 1396, + "src/renderer/src/hooks/composer-state/composer-name-source-selection.test.ts": 1817, + "src/renderer/src/hooks/composer-state/composer-remote-server-drop.test.tsx": 666, + "src/renderer/src/hooks/composer-state/composer-review-base-fencing.test.ts": 2084, + "src/renderer/src/hooks/composer-state/composer-review-base-submit-cache.test.ts": 1951, + "src/renderer/src/hooks/composer-state/composer-submit-source-ownership.test.ts": 1917, + "src/renderer/src/hooks/composer-state/composer-target-state.jira-mapping.test.tsx": 1690, + "src/renderer/src/hooks/composer-state/draft-target-sync.test.ts": 234, + "src/renderer/src/hooks/composer-state/initial-target-state.jira-mapping.test.tsx": 265, + "src/renderer/src/hooks/composer-state/jira-mapped-owner-flow.integration.test.tsx": 3172, + "src/renderer/src/hooks/composer-state/multiple-create-reset.test.ts": 1589, + "src/renderer/src/hooks/composer-state/provider-runtime-sync.test.ts": 1548, + "src/renderer/src/hooks/composer-state/quick-creation-request.test.ts": 116, + "src/renderer/src/hooks/composer-state/server-ssh-repo-connect.test.ts": 708, + "src/renderer/src/hooks/direct-ssh-runtime-wake-isolation.test.tsx": 1591, + "src/renderer/src/hooks/direct-ssh-state-routing.test.ts": 164, + "src/renderer/src/hooks/editor-external-watch-fallback-explorer.test.ts": 333, + "src/renderer/src/hooks/editor-external-watch-selected-host.test.ts": 309, + "src/renderer/src/hooks/editor-runtime-file-watch.test.ts": 61, + "src/renderer/src/hooks/fork-push-warning.test.ts": 44, + "src/renderer/src/hooks/installed-agent-skill-discovery-cache.test.ts": 43, + "src/renderer/src/hooks/installed-agent-skill-discovery.test.ts": 63, + "src/renderer/src/hooks/installed-agent-skill-runtime-eviction.test.ts": 39, + "src/renderer/src/hooks/installed-agent-skill-verdict.test.ts": 1343, + "src/renderer/src/hooks/ipc-events/agent-status-pane-routing-index-retention.test.ts": 2123, + "src/renderer/src/hooks/ipc-events/agent-status-pane-routing-index.test.ts": 1512, + "src/renderer/src/hooks/ipc-events/agent-status-pending-retry-gate.test.ts": 371, + "src/renderer/src/hooks/ipc-events/agent-status-terminal-title-tab-write.test.ts": 1744, + "src/renderer/src/hooks/ipc-events/agent-status-unavailable.test.ts": 83, + "src/renderer/src/hooks/ipc-events/browser-automation-bootstrap-lease.test.ts": 169, + "src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.owner.test.ts": 464, + "src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.profile-switch.test.ts": 517, + "src/renderer/src/hooks/ipc-events/browser-state-capture-paint-hold.test.ts": 252, + "src/renderer/src/hooks/ipc-events/browser-state-open-link-profile.test.ts": 253, + "src/renderer/src/hooks/ipc-events/direct-ssh-connect-reply-routing.test.ts": 1418, + "src/renderer/src/hooks/ipc-events/direct-ssh-hydration-fanout.test.ts": 1356, + "src/renderer/src/hooks/ipc-events/direct-ssh-hydration-target-metadata.test.ts": 1280, + "src/renderer/src/hooks/ipc-events/floating-workspace-reveal-routing.test.ts": 1589, + "src/renderer/src/hooks/ipc-events/mobile-terminal-close-ipc-bridge.test.ts": 1566, + "src/renderer/src/hooks/ipc-events/normalize-agent-status-event.test.ts": 33, + "src/renderer/src/hooks/ipc-events/orca-profile-auth-ipc-bridge.test.ts": 1487, + "src/renderer/src/hooks/ipc-events/os-markdown-file-open-bridge.test.ts": 291, + "src/renderer/src/hooks/ipc-events/runtime-client-ipc-bridge-ownership.test.ts": 1203, + "src/renderer/src/hooks/ipc-events/runtime-file-position.test.ts": 1522, + "src/renderer/src/hooks/ipc-events/runtime-reconnect-host-status.test.ts": 1584, + "src/renderer/src/hooks/ipc-events/ssh-managed-server-converted-catalogs.test.ts": 2158, + "src/renderer/src/hooks/ipc-events/ssh-managed-server-state-effects.test.ts": 464, + "src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.test.ts": 79, + "src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge-placement.test.ts": 1354, + "src/renderer/src/hooks/ipc-events/terminal-ui-routing-ipc-bridge-split.test.ts": 488, + "src/renderer/src/hooks/ipc-events/updater-status-ipc-bridge.test.ts": 31, + "src/renderer/src/hooks/ipc-events/worktree-event-lineage-refresh.test.ts": 1383, + "src/renderer/src/hooks/ipc-events/worktree-event-runtime-activation.test.ts": 1322, + "src/renderer/src/hooks/ipc-events/wsl-account-notice-ipc-bridge.test.ts": 53, + "src/renderer/src/hooks/ipc-events/wsl-host-ipc-bridge.test.ts": 1388, + "src/renderer/src/hooks/ipc-tab-switch-group-order-hydration.test.ts": 54, + "src/renderer/src/hooks/ipc-tab-switch-structured-tab-cycle.test.ts": 54, + "src/renderer/src/hooks/ipc-tab-switch.test.ts": 58, + "src/renderer/src/hooks/legacy-worker-terminal-recovery-event.test.ts": 50, + "src/renderer/src/hooks/macos-tcc-prompt-notice-subscription.test.ts": 53, + "src/renderer/src/hooks/metadata-request-cache.test.ts": 157, + "src/renderer/src/hooks/mobile-terminal-reveal-tab-adoption.test.ts": 2077, + "src/renderer/src/hooks/modal-return-focus-action.test.ts": 42, + "src/renderer/src/hooks/programmatic-scroll-marks.test.ts": 34, + "src/renderer/src/hooks/runtime-client-events-sync-ownership.test.ts": 58, + "src/renderer/src/hooks/runtime-client-events-sync.test.ts": 118, + "src/renderer/src/hooks/runtime-project-refresh-scheduler.test.ts": 76, + "src/renderer/src/hooks/shortcut-label-cache.test.tsx": 1760, + "src/renderer/src/hooks/structured-session-completion-focus.test.ts": 1490, + "src/renderer/src/hooks/unpaired-device-auth-notification.test.ts": 46, + "src/renderer/src/hooks/use-audio-capture.capture-loss.test.ts": 249, + "src/renderer/src/hooks/use-clipboard-text-copy-feedback.test.ts": 253, + "src/renderer/src/hooks/use-document-dark-theme.test.tsx": 234, + "src/renderer/src/hooks/use-list-multi-selection.test.tsx": 231, + "src/renderer/src/hooks/use-now.test.ts": 235, + "src/renderer/src/hooks/use-os-file-drop-owner.test.tsx": 366, + "src/renderer/src/hooks/use-palette-search-evaluation-context.test.ts": 231, + "src/renderer/src/hooks/use-task-page-github-work-item-mutation-host.test.ts": 1575, + "src/renderer/src/hooks/use-terminal-quick-command-hosts.test.ts": 505, + "src/renderer/src/hooks/use-window-stream-visibility.test.ts": 278, + "src/renderer/src/hooks/useActiveProjectSkillRuntime.test.tsx": 1638, + "src/renderer/src/hooks/useAgentDetectionTarget.test.ts": 1335, + "src/renderer/src/hooks/useAppMenuPaste.test.tsx": 205, + "src/renderer/src/hooks/useAppMenuSelectionActions.test.tsx": 235, + "src/renderer/src/hooks/useAutomationDispatchEvents.test.ts": 1529, + "src/renderer/src/hooks/useComposerState-decisions.test.ts": 73, + "src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts": 74, + "src/renderer/src/hooks/useComposerState-host-retarget.test.ts": 51, + "src/renderer/src/hooks/useComposerState.integration.test.ts": 2293, + "src/renderer/src/hooks/useDetectedAgents.re-pair.test.tsx": 1433, + "src/renderer/src/hooks/useDetectedAgents.test.tsx": 1598, + "src/renderer/src/hooks/useEditorExternalWatch-complexity.test.ts": 606, + "src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts": 513, + "src/renderer/src/hooks/useEditorExternalWatch-subscriptions.test.tsx": 242, + "src/renderer/src/hooks/useEditorExternalWatch-targets.test.ts": 314, + "src/renderer/src/hooks/useEditorExternalWatch-wsl-repro.test.ts": 547, + "src/renderer/src/hooks/useEditorExternalWatch.test.ts": 658, + "src/renderer/src/hooks/useEphemeralVmRecipeOptions.test.tsx": 184, + "src/renderer/src/hooks/useGitHubSlugMetadata.test.tsx": 196, + "src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx": 1698, + "src/renderer/src/hooks/useInstalledAgentSkills.test.ts": 1414, + "src/renderer/src/hooks/useIpcEvents-agent-dashboard-shortcut.test.ts": 38, + "src/renderer/src/hooks/useIpcEvents-agent-launch-closed-tab.test.ts": 1176, + "src/renderer/src/hooks/useIpcEvents-agent-status-batch-projection.test.ts": 3518, + "src/renderer/src/hooks/useIpcEvents-agent-status-connection-attribution.test.ts": 2736, + "src/renderer/src/hooks/useIpcEvents-agent-status-hook-titles.test.ts": 3370, + "src/renderer/src/hooks/useIpcEvents-agent-status-pane-teardown.test.ts": 2298, + "src/renderer/src/hooks/useIpcEvents-agent-status-queue-ordering.test.ts": 1785, + "src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-hydration.test.ts": 5155, + "src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-replay.test.ts": 2163, + "src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts": 2461, + "src/renderer/src/hooks/useIpcEvents-agent-status-turn-completion.test.ts": 1170, + "src/renderer/src/hooks/useIpcEvents-browser-certificate-failure.test.ts": 1445, + "src/renderer/src/hooks/useIpcEvents-browser-navigation.test.ts": 1091, + "src/renderer/src/hooks/useIpcEvents-browser-tab-create.test.ts": 1229, + "src/renderer/src/hooks/useIpcEvents-cli-worktree-activation.test.ts": 1415, + "src/renderer/src/hooks/useIpcEvents-client-hosted-browser-rows.test.ts": 1782, + "src/renderer/src/hooks/useIpcEvents-close-routing-active-browser-tab.test.ts": 2166, + "src/renderer/src/hooks/useIpcEvents-close-routing-browser-pages.test.ts": 1525, + "src/renderer/src/hooks/useIpcEvents-close-routing-floating-guest.test.ts": 1638, + "src/renderer/src/hooks/useIpcEvents-close-routing-session-tabs.test.ts": 3732, + "src/renderer/src/hooks/useIpcEvents-cmd-j-digit-chord.test.ts": 1530, + "src/renderer/src/hooks/useIpcEvents-lifecycle.test.ts": 1363, + "src/renderer/src/hooks/useIpcEvents-new-workspace-shortcut.test.ts": 54, + "src/renderer/src/hooks/useIpcEvents-rate-limit-hydration.test.ts": 1194, + "src/renderer/src/hooks/useIpcEvents-repos-changed-catalogs.test.ts": 2739, + "src/renderer/src/hooks/useIpcEvents-runtime-environment-selectors.test.ts": 456, + "src/renderer/src/hooks/useIpcEvents-session-tab-close-request.test.ts": 1256, + "src/renderer/src/hooks/useIpcEvents-silent-terminal-adoption.test.ts": 1382, + "src/renderer/src/hooks/useIpcEvents-ssh-disconnect-cleanup.test.ts": 1238, + "src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts": 995, + "src/renderer/src/hooks/useIpcEvents-updater-status.test.ts": 1443, + "src/renderer/src/hooks/useIssueMetadata.test.tsx": 387, + "src/renderer/src/hooks/useLinearProviderConnected.test.tsx": 1342, + "src/renderer/src/hooks/useMacTccAttributionSeveredNotice.test.tsx": 2244, + "src/renderer/src/hooks/useMacosTccPromptNotice.test.tsx": 1945, + "src/renderer/src/hooks/useMetadataListRequest.test.tsx": 222, + "src/renderer/src/hooks/useModalReturnFocus.test.tsx": 1582, + "src/renderer/src/hooks/usePrefersReducedMotion.test.ts": 34, + "src/renderer/src/hooks/usePrimarySelectionPaste.terminal-native-paste.test.tsx": 227, + "src/renderer/src/hooks/usePrimarySelectionPaste.test.tsx": 238, + "src/renderer/src/hooks/useReadOnScreenAttention.away.test.ts": 1680, + "src/renderer/src/hooks/useReadOnScreenAttention.clock-skew.test.ts": 1402, + "src/renderer/src/hooks/useReadOnScreenAttention.floating-panel.test.ts": 1634, + "src/renderer/src/hooks/useReadOnScreenAttention.focus-group.test.ts": 1709, + "src/renderer/src/hooks/useReadOnScreenAttention.mixed-workspace.test.ts": 1490, + "src/renderer/src/hooks/useReadOnScreenAttention.test.ts": 1280, + "src/renderer/src/hooks/useResetCountdownClock.test.ts": 246, + "src/renderer/src/hooks/useRetiredWorktreeNames.test.ts": 586, + "src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx": 709, + "src/renderer/src/hooks/useSettingsNavigationMetadata.language-switch.test.tsx": 3222, + "src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts": 2270, + "src/renderer/src/hooks/useSidebarResize.test.ts": 36, + "src/renderer/src/hooks/useSkillFreshness.test.tsx": 259, + "src/renderer/src/hooks/useUnreadDockBadge.test.ts": 1321, + "src/renderer/src/hooks/useVirtualizedScrollAnchor.listener-deps.test.ts": 56, + "src/renderer/src/hooks/useVirtualizedScrollAnchor.marks-restore-signal.test.ts": 106, + "src/renderer/src/hooks/useVirtualizedScrollAnchor.test.ts": 30, + "src/renderer/src/hooks/useVirtualizedScrollAnchor.test.tsx": 285, + "src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.test.tsx": 446, + "src/renderer/src/hooks/viewport-size-change-listener.test.ts": 33, + "src/renderer/src/hooks/worktree-change-refresh-queue.test.ts": 51, + "src/renderer/src/hooks/worktree-head-identity-apply.test.ts": 35, + "src/renderer/src/hooks/zoom-routing.test.ts": 636, + "src/renderer/src/i18n/I18nProvider.test.tsx": 1676, + "src/renderer/src/i18n/catalog-nested-settings.test.ts": 336, + "src/renderer/src/i18n/intl-locale.test.ts": 119, + "src/renderer/src/i18n/lazy-locale.test.ts": 200, + "src/renderer/src/i18n/plugin-chrome-allowlist.test.ts": 111, + "src/renderer/src/i18n/project-runtime-repair-message.test.ts": 91, + "src/renderer/src/i18n/provider-credit-balance-locales.test.ts": 696, + "src/renderer/src/i18n/pseudo-localization.test.ts": 56, + "src/renderer/src/i18n/relative-time-format.test.ts": 41, + "src/renderer/src/i18n/runtime-required-catalog.test.ts": 409, + "src/renderer/src/i18n/weekday-names.test.ts": 56, + "src/renderer/src/lazy-modal-mount-state.test.ts": 31, + "src/renderer/src/lib/activate-ai-vault-structured-session-host.test.ts": 1179, + "src/renderer/src/lib/activate-ai-vault-structured-session-reveal.test.ts": 1657, + "src/renderer/src/lib/activate-ai-vault-structured-session.test.ts": 1581, + "src/renderer/src/lib/activate-tab-and-focus-pane.test.ts": 48, + "src/renderer/src/lib/active-agent-note-send-explicit-target.test.ts": 956, + "src/renderer/src/lib/active-agent-note-send-focused-session.test.ts": 541, + "src/renderer/src/lib/active-agent-note-send-runtime-error-codes.test.ts": 262, + "src/renderer/src/lib/active-agent-note-send-target-detection.test.ts": 529, + "src/renderer/src/lib/active-view-persist.test.ts": 33, + "src/renderer/src/lib/activity-thread-display.test.ts": 85, + "src/renderer/src/lib/agent-background-session-launch-host.test.ts": 1363, + "src/renderer/src/lib/agent-detection-target-inventory.test.ts": 1428, + "src/renderer/src/lib/agent-draft-readiness.test.ts": 93, + "src/renderer/src/lib/agent-followup-delivery.test.ts": 91, + "src/renderer/src/lib/agent-hibernation-confirmation.test.ts": 38, + "src/renderer/src/lib/agent-hibernation-coordinator.test.ts": 1458, + "src/renderer/src/lib/agent-hibernation-output-activity.test.ts": 34, + "src/renderer/src/lib/agent-hibernation-pane-age.test.ts": 35, + "src/renderer/src/lib/agent-hibernation-planner.test.ts": 171, + "src/renderer/src/lib/agent-hibernation-runtime-liveness-race.test.ts": 1324, + "src/renderer/src/lib/agent-hibernation-visibility.test.ts": 1519, + "src/renderer/src/lib/agent-launch-caller-permissions.test.ts": 1399, + "src/renderer/src/lib/agent-launch-caller-placement.test.ts": 1025, + "src/renderer/src/lib/agent-launch-caller-prompt-transport.test.ts": 1100, + "src/renderer/src/lib/agent-launch-caller-routing.test.ts": 1302, + "src/renderer/src/lib/agent-launch-pane-start.test.ts": 33, + "src/renderer/src/lib/agent-launch-pane-verdict-application.test.ts": 1491, + "src/renderer/src/lib/agent-launch-prompt-delivery.test.ts": 55, + "src/renderer/src/lib/agent-launch-route-connection-fallback.test.ts": 1604, + "src/renderer/src/lib/agent-launch-route-input.test.ts": 1556, + "src/renderer/src/lib/agent-launch-route-registered-agents.test.ts": 1398, + "src/renderer/src/lib/agent-launch-routing.test.ts": 250, + "src/renderer/src/lib/agent-launch-tab-publication.test.ts": 1473, + "src/renderer/src/lib/agent-launch-through-host.test.ts": 1552, + "src/renderer/src/lib/agent-message-send.test.ts": 264, + "src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts": 176, + "src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts": 172, + "src/renderer/src/lib/agent-paste-draft.test.ts": 352, + "src/renderer/src/lib/agent-picker-search.test.ts": 123, + "src/renderer/src/lib/agent-ready-wait.test.ts": 124, + "src/renderer/src/lib/agent-resume-launch-target.test.ts": 1607, + "src/renderer/src/lib/agent-row-primary-text.test.ts": 44, + "src/renderer/src/lib/agent-row-tool-preview.test.ts": 42, + "src/renderer/src/lib/agent-send-title-status.test.ts": 64, + "src/renderer/src/lib/agent-session-fork-context.test.ts": 74, + "src/renderer/src/lib/agent-session-launch-plan.test.ts": 1485, + "src/renderer/src/lib/agent-skill-cli-prerequisite.test.ts": 136, + "src/renderer/src/lib/agent-skill-nav-install-status.test.ts": 1402, + "src/renderer/src/lib/agent-startup-delayed-delivery-perf.test.ts": 1506, + "src/renderer/src/lib/agent-status-connection-ownership.test.ts": 88, + "src/renderer/src/lib/agent-status-count.test.ts": 91, + "src/renderer/src/lib/agent-status-epoch-clock.test.ts": 36, + "src/renderer/src/lib/agent-status-evidence-clock.test.ts": 80, + "src/renderer/src/lib/agent-status-terminal-title.test.ts": 81, + "src/renderer/src/lib/agent-status-worktree-attribution.test.ts": 33, + "src/renderer/src/lib/agent-status.test.ts": 180, + "src/renderer/src/lib/agent-tab-shortcuts.test.ts": 67, + "src/renderer/src/lib/ai-vault-antigravity-reference-host.test.ts": 537, + "src/renderer/src/lib/ai-vault-omp-cold-resume.test.ts": 1410, + "src/renderer/src/lib/ai-vault-resume-command.drop-repin.test.ts": 441, + "src/renderer/src/lib/ai-vault-resume-command.resumable-agent.test.ts": 404, + "src/renderer/src/lib/ai-vault-resume-command.test.ts": 483, + "src/renderer/src/lib/ai-vault-resume-shell.test.ts": 436, + "src/renderer/src/lib/ai-vault-resume-target.test.ts": 76, + "src/renderer/src/lib/ai-vault-session-drag.test.ts": 61, + "src/renderer/src/lib/ai-vault-session-fork-startup.test.ts": 397, + "src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts": 58, + "src/renderer/src/lib/ai-vault-tab-title-sync-inputs.test.ts": 384, + "src/renderer/src/lib/ai-vault-tab-title-sync.test.ts": 837, + "src/renderer/src/lib/app-command-dispatch.test.ts": 68, + "src/renderer/src/lib/app-font-family.test.ts": 274, + "src/renderer/src/lib/app-menu-paste.test.ts": 211, + "src/renderer/src/lib/app-menu-selection-actions.test.ts": 134, + "src/renderer/src/lib/automation-session-observer.test.ts": 114, + "src/renderer/src/lib/automation-session-reuse.test.ts": 34, + "src/renderer/src/lib/automation-terminal-ownership.test.ts": 1336, + "src/renderer/src/lib/bounded-generation-map.test.ts": 33, + "src/renderer/src/lib/browser-cookie-import-toast.test.ts": 169, + "src/renderer/src/lib/browser-history-match.performance.test.ts": 300, + "src/renderer/src/lib/browser-history-match.test.ts": 293, + "src/renderer/src/lib/browser-page-conversion-history.test.ts": 47, + "src/renderer/src/lib/browser-page-palette-activation.test.ts": 1597, + "src/renderer/src/lib/browser-palette-page-entries.test.ts": 382, + "src/renderer/src/lib/browser-palette-search.test.ts": 318, + "src/renderer/src/lib/browser-uuid.test.ts": 38, + "src/renderer/src/lib/browser-workspace-source-resolution.test.ts": 33, + "src/renderer/src/lib/browser-workspace-tab-activation.test.ts": 1604, + "src/renderer/src/lib/client-creation-action-policy.test.ts": 327, + "src/renderer/src/lib/cmd-j-github-url-lookup.test.ts": 42, + "src/renderer/src/lib/cmd-j-host-qualified-candidate-ownership.test.ts": 395, + "src/renderer/src/lib/cmd-j-linear-issue-intent.test.ts": 30, "src/renderer/src/lib/cmd-j-section-leadership.test.ts": 77, - "src/renderer/src/lib/codex-account-display-label.test.ts": 55, - "src/renderer/src/lib/codex-account-restart-startup.test.ts": 1537, - "src/renderer/src/lib/codex-pane-restart-eligibility.test.ts": 81, - "src/renderer/src/lib/codex-pane-selection-lane.test.ts": 379, - "src/renderer/src/lib/codex-session-restart-account-recheck.test.ts": 1182, - "src/renderer/src/lib/codex-session-restart-shell-flap.test.ts": 1412, - "src/renderer/src/lib/codex-session-restart.test.ts": 3754, - "src/renderer/src/lib/codex-stale-pane-account-identity.test.ts": 1583, - "src/renderer/src/lib/codex-stale-pane-sweep.test.ts": 1535, - "src/renderer/src/lib/comment-body-line-count.test.ts": 115, - "src/renderer/src/lib/comment-body-submit-state.test.ts": 85, - "src/renderer/src/lib/composer-issue-command.test.ts": 1472, - "src/renderer/src/lib/composer-submit-cancellation.test.ts": 54, - "src/renderer/src/lib/connection-context.test.ts": 1611, - "src/renderer/src/lib/crash-diagnostics.test.ts": 136, - "src/renderer/src/lib/create-untitled-markdown.test.ts": 444, - "src/renderer/src/lib/default-creation-host.test.ts": 48, - "src/renderer/src/lib/default-terminal-tabs-applied-persist.test.ts": 1617, - "src/renderer/src/lib/desktop-window-chrome.test.ts": 56, - "src/renderer/src/lib/diff-comment-compat.test.ts": 50, - "src/renderer/src/lib/diff-comments-format.test.ts": 47, - "src/renderer/src/lib/direct-ssh-target-scope.test.ts": 70, - "src/renderer/src/lib/doc-preview-grants.test.ts": 370, - "src/renderer/src/lib/document-theme.test.ts": 55, - "src/renderer/src/lib/editable-target.test.ts": 175, - "src/renderer/src/lib/editor-file-operation-owner.test.ts": 1442, - "src/renderer/src/lib/editor-font-zoom.test.ts": 77, - "src/renderer/src/lib/empty-workspace-default-agent-chat.test.ts": 1422, - "src/renderer/src/lib/ensure-hooks-confirmed.test.ts": 589, - "src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts": 652, - "src/renderer/src/lib/ensure-simulator-tab.test.ts": 47, - "src/renderer/src/lib/ephemeral-vm-failed-create-cleanup.test.ts": 49, - "src/renderer/src/lib/ephemeral-vm-runtime-cleanup.test.ts": 82, - "src/renderer/src/lib/ephemeral-vm-workspace-target.integration.test.ts": 297, - "src/renderer/src/lib/ephemeral-vm-workspace-target.test.ts": 317, - "src/renderer/src/lib/ephemeral-vm-worktree-creation.test.ts": 102, - "src/renderer/src/lib/execute-open-editor-path-move.test.ts": 1738, - "src/renderer/src/lib/external-editor-open-capability.test.ts": 53, - "src/renderer/src/lib/feature-education-telemetry.test.ts": 63, - "src/renderer/src/lib/feedback-image-attachments.test.ts": 314, - "src/renderer/src/lib/feedback-image-shrink.test.ts": 53, - "src/renderer/src/lib/file-owner-runtime-target.test.ts": 284, - "src/renderer/src/lib/file-preview-action-visibility.test.tsx": 826, - "src/renderer/src/lib/file-preview.test.ts": 588, - "src/renderer/src/lib/file-search-result-owner.test.ts": 56, - "src/renderer/src/lib/file-search-selection.test.ts": 52, - "src/renderer/src/lib/file-search-shortcut-policy.test.ts": 181, - "src/renderer/src/lib/file-type-icons.test.ts": 108, - "src/renderer/src/lib/find-query-bounds.test.ts": 49, + "src/renderer/src/lib/codex-account-display-label.test.ts": 36, + "src/renderer/src/lib/codex-account-restart-startup.test.ts": 1480, + "src/renderer/src/lib/codex-pane-restart-eligibility.test.ts": 77, + "src/renderer/src/lib/codex-pane-selection-lane.test.ts": 576, + "src/renderer/src/lib/codex-session-restart-account-recheck.test.ts": 1365, + "src/renderer/src/lib/codex-session-restart-shell-flap.test.ts": 1503, + "src/renderer/src/lib/codex-session-restart.test.ts": 1561, + "src/renderer/src/lib/codex-stale-pane-account-identity.test.ts": 1418, + "src/renderer/src/lib/codex-stale-pane-sweep.test.ts": 1243, + "src/renderer/src/lib/comment-body-line-count.test.ts": 68, + "src/renderer/src/lib/comment-body-submit-state.test.ts": 33, + "src/renderer/src/lib/composer-issue-command.test.ts": 1235, + "src/renderer/src/lib/composer-submit-cancellation.test.ts": 40, + "src/renderer/src/lib/connection-context.test.ts": 1403, + "src/renderer/src/lib/crash-diagnostics.test.ts": 140, + "src/renderer/src/lib/create-untitled-markdown.test.ts": 373, + "src/renderer/src/lib/default-creation-host.test.ts": 44, + "src/renderer/src/lib/default-terminal-tabs-applied-persist.test.ts": 1437, + "src/renderer/src/lib/desktop-window-chrome.test.ts": 41, + "src/renderer/src/lib/diff-comment-compat.test.ts": 41, + "src/renderer/src/lib/diff-comments-format.test.ts": 36, + "src/renderer/src/lib/direct-ssh-target-scope.test.ts": 52, + "src/renderer/src/lib/doc-preview-grants.test.ts": 386, + "src/renderer/src/lib/document-theme.test.ts": 37, + "src/renderer/src/lib/editable-target.test.ts": 174, + "src/renderer/src/lib/editor-file-operation-owner.test.ts": 1375, + "src/renderer/src/lib/editor-font-zoom.test.ts": 40, + "src/renderer/src/lib/empty-workspace-default-agent-chat.test.ts": 1363, + "src/renderer/src/lib/ensure-hooks-confirmed.test.ts": 756, + "src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts": 933, + "src/renderer/src/lib/ensure-simulator-tab.test.ts": 35, + "src/renderer/src/lib/ephemeral-vm-failed-create-cleanup.test.ts": 24, + "src/renderer/src/lib/ephemeral-vm-runtime-cleanup.test.ts": 55, + "src/renderer/src/lib/ephemeral-vm-workspace-target.integration.test.ts": 323, + "src/renderer/src/lib/ephemeral-vm-workspace-target.test.ts": 303, + "src/renderer/src/lib/ephemeral-vm-worktree-creation.test.ts": 75, + "src/renderer/src/lib/execute-open-editor-path-move.test.ts": 1893, + "src/renderer/src/lib/external-editor-open-capability.test.ts": 40, + "src/renderer/src/lib/feature-education-telemetry.test.ts": 69, + "src/renderer/src/lib/feedback-image-attachments.test.ts": 312, + "src/renderer/src/lib/feedback-image-shrink.test.ts": 40, + "src/renderer/src/lib/file-artifact-source-key.test.ts": 412, + "src/renderer/src/lib/file-owner-runtime-target.test.ts": 340, + "src/renderer/src/lib/file-preview-action-visibility.test.tsx": 835, + "src/renderer/src/lib/file-preview.test.ts": 589, + "src/renderer/src/lib/file-search-result-owner.test.ts": 53, + "src/renderer/src/lib/file-search-selection.test.ts": 43, + "src/renderer/src/lib/file-search-shortcut-policy.test.ts": 212, + "src/renderer/src/lib/file-type-icons.test.ts": 62, + "src/renderer/src/lib/find-query-bounds.test.ts": 43, "src/renderer/src/lib/finished-agent-resume-resurrection.test.ts": 1532, - "src/renderer/src/lib/fix-checks-agent-launch.test.ts": 500, - "src/renderer/src/lib/flatten-retained-slice.test.ts": 177, - "src/renderer/src/lib/floating-terminal.test.ts": 41, - "src/renderer/src/lib/floating-workspace-panel-reveal.test.ts": 291, - "src/renderer/src/lib/floating-workspace-terminal-actions.test.ts": 449, - "src/renderer/src/lib/floating-workspace-tour-interaction-snapshot.test.ts": 68, - "src/renderer/src/lib/focus-terminal-tab-surface.split-focus.test.ts": 131, - "src/renderer/src/lib/focus-terminal-tab-surface.test.ts": 71, - "src/renderer/src/lib/folder-workspace-path-status.test.ts": 108, - "src/renderer/src/lib/foreground-terminal-tabs.test.ts": 60, - "src/renderer/src/lib/github-links.test.ts": 69, - "src/renderer/src/lib/github-pr-start-point.test.ts": 91, - "src/renderer/src/lib/github-source-runtime-context.test.ts": 55, - "src/renderer/src/lib/github-work-item-source-lookup.test.ts": 93, - "src/renderer/src/lib/github-work-item-workspace-attachment.test.ts": 98, - "src/renderer/src/lib/gitlab-links.test.ts": 64, - "src/renderer/src/lib/gitlab-work-item-source-lookup.test.ts": 139, - "src/renderer/src/lib/hook-command-delayed-delivery-perf.test.ts": 1322, - "src/renderer/src/lib/hook-command-delayed-delivery.test.ts": 1644, - "src/renderer/src/lib/host-mirror-handle-gap-drain.test.ts": 1339, - "src/renderer/src/lib/host-mirror-handle-gap-landed-handle.test.ts": 1317, - "src/renderer/src/lib/host-mirror-handle-gap-resume.test.ts": 1612, - "src/renderer/src/lib/host-mirror-handle-gap-snapshot-lifetime.test.ts": 2086, - "src/renderer/src/lib/host-mirror-handle-gap-subscription-lifetime.test.ts": 1885, - "src/renderer/src/lib/host-mirror-handle-gap-teardown.test.ts": 1675, - "src/renderer/src/lib/host-mirror-handle-gap-verdict-union.test.ts": 1490, - "src/renderer/src/lib/host-mirrored-pane-resume-replay.test.ts": 1451, - "src/renderer/src/lib/http-link-destinations.test.ts": 308, - "src/renderer/src/lib/http-link-modifier-routing.test.ts": 307, - "src/renderer/src/lib/http-link-routing.test.ts": 326, - "src/renderer/src/lib/image-blob-png.test.ts": 234, - "src/renderer/src/lib/ime-composition-keyboard-event.test.ts": 299, - "src/renderer/src/lib/ime-overlay-escape.test.tsx": 647, - "src/renderer/src/lib/ime-text-field.test.tsx": 599, - "src/renderer/src/lib/ios-web-platform.test.ts": 53, - "src/renderer/src/lib/ipc-error.test.ts": 53, - "src/renderer/src/lib/jira-source-host.test.ts": 52, - "src/renderer/src/lib/keyboard-layout/detect-option-as-alt.test.ts": 60, - "src/renderer/src/lib/keyboard-layout/input-source-id.test.ts": 52, - "src/renderer/src/lib/keyboard-layout/layout-base-character.test.ts": 52, - "src/renderer/src/lib/keyboard-layout/option-as-alt-probe.test.ts": 98, - "src/renderer/src/lib/keyboard-layout/option-key-location-state.test.ts": 44, - "src/renderer/src/lib/language-detect.test.ts": 97, - "src/renderer/src/lib/large-text-control-paste.test.ts": 178, - "src/renderer/src/lib/launch-agent-background-session-extra-args.test.ts": 465, - "src/renderer/src/lib/launch-agent-background-session-remote.test.ts": 684, - "src/renderer/src/lib/launch-agent-background-session.test.ts": 556, - "src/renderer/src/lib/launch-agent-execution-context.test.ts": 1461, - "src/renderer/src/lib/launch-agent-fresh-new-tab-host-route.test.ts": 154, - "src/renderer/src/lib/launch-agent-in-new-tab-cwd.test.ts": 1001, - "src/renderer/src/lib/launch-agent-in-new-tab-fresh-host.test.ts": 1688, - "src/renderer/src/lib/launch-agent-in-new-tab-host-agents.test.ts": 591, - "src/renderer/src/lib/launch-agent-in-new-tab-host-resolution.test.ts": 965, - "src/renderer/src/lib/launch-agent-in-new-tab-main-surface.test.ts": 1838, - "src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts": 993, - "src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts": 1042, - "src/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.ts": 1018, - "src/renderer/src/lib/launch-agent-in-new-tab.test.ts": 1018, - "src/renderer/src/lib/launch-agent-new-tab-host-route-readiness.test.ts": 337, - "src/renderer/src/lib/launch-agent-new-tab-host-route.test.ts": 1582, - "src/renderer/src/lib/launch-agent-session-continuation.test.ts": 71, - "src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts": 1111, - "src/renderer/src/lib/launch-ai-vault-session.test.ts": 61, - "src/renderer/src/lib/launch-structured-agent-session.test.ts": 2251, - "src/renderer/src/lib/launch-work-item-direct-agent-routing.test.ts": 1691, - "src/renderer/src/lib/launch-work-item-direct-agent.test.ts": 163, - "src/renderer/src/lib/launch-work-item-direct-messages.test.ts": 48, - "src/renderer/src/lib/launch-work-item-direct.test.ts": 1033, - "src/renderer/src/lib/lazy-chunk-recovery-reload.test.ts": 200, - "src/renderer/src/lib/lazy-with-retry.never-landed-reload.test.ts": 172, - "src/renderer/src/lib/lazy-with-retry.test.ts": 232, - "src/renderer/src/lib/left-sidebar-appearance.test.ts": 314, - "src/renderer/src/lib/linear-agent-skill-update-command.test.ts": 1484, - "src/renderer/src/lib/linear-board-drag-payload.test.ts": 63, - "src/renderer/src/lib/linear-issue-context-snapshot.test.ts": 95, - "src/renderer/src/lib/linear-issue-url-lookup.test.ts": 100, - "src/renderer/src/lib/linear-issue-workspace-attachment.test.ts": 88, - "src/renderer/src/lib/linear-issue-workspace-open.test.ts": 144, - "src/renderer/src/lib/linear-linked-work-item.test.ts": 81, - "src/renderer/src/lib/link-source-local-open-owner.test.ts": 295, - "src/renderer/src/lib/linked-work-item-context.test.ts": 67, - "src/renderer/src/lib/linked-work-item-provider.test.ts": 1457, - "src/renderer/src/lib/list-multi-selection.test.ts": 49, - "src/renderer/src/lib/list-row-interaction.test.ts": 141, - "src/renderer/src/lib/local-file-access.test.ts": 1545, - "src/renderer/src/lib/local-path-open-guard.test.ts": 87, - "src/renderer/src/lib/local-preflight-context-cache.test.ts": 397, - "src/renderer/src/lib/local-preflight-context.test.ts": 327, - "src/renderer/src/lib/locale-text-collators.test.ts": 48, - "src/renderer/src/lib/manual-terminal-worktree-parking.test.ts": 182, - "src/renderer/src/lib/markdown-comment-blocks.test.ts": 50, - "src/renderer/src/lib/markdown-document-templates.test.ts": 438, - "src/renderer/src/lib/markdown-review-note-copy.test.ts": 60, - "src/renderer/src/lib/markdown-review-notes-setting.test.ts": 47, - "src/renderer/src/lib/markdown-review-notes.test.ts": 81, - "src/renderer/src/lib/migration-unsupported-agent-entry.test.ts": 45, - "src/renderer/src/lib/mobile-terminal-tab-mount.test.ts": 55, - "src/renderer/src/lib/monaco-delayer-cancellation-guard.test.ts": 116, - "src/renderer/src/lib/monaco-diff-editor-disposal.test.ts": 49, - "src/renderer/src/lib/monaco-filename-language.test.ts": 68, - "src/renderer/src/lib/monaco-languages/monarch-embed-entry-recursion.test.ts": 530, - "src/renderer/src/lib/monaco-languages/monarch-upstream-mdx-recursion.test.ts": 2807, - "src/renderer/src/lib/monaco-languages/register-astro.test.ts": 152, - "src/renderer/src/lib/monaco-languages/register-jsonl.test.ts": 345, - "src/renderer/src/lib/monaco-languages/register-nim.test.ts": 69, - "src/renderer/src/lib/monaco-languages/register-quarto-adversarial.test.ts": 171, - "src/renderer/src/lib/monaco-languages/register-quarto.test.ts": 153, - "src/renderer/src/lib/monaco-languages/register-shell-markdown-aliases.test.ts": 50, - "src/renderer/src/lib/monaco-languages/register-svelte.test.ts": 169, - "src/renderer/src/lib/monaco-languages/register-typst.test.ts": 64, - "src/renderer/src/lib/monaco-languages/register-vue.test.ts": 325, - "src/renderer/src/lib/monaco-languages/textmate-language-registration.test.ts": 60, - "src/renderer/src/lib/monaco-languages/textmate-token-provider.test.ts": 287, - "src/renderer/src/lib/monaco-peek-preview-options.test.ts": 911, - "src/renderer/src/lib/monaco-setup-steps.test.ts": 58, - "src/renderer/src/lib/monospace-font-family.test.ts": 54, - "src/renderer/src/lib/native-chat-launch-draft-mirrorability.test.ts": 298, - "src/renderer/src/lib/native-chat-transcript-readability.test.ts": 50, - "src/renderer/src/lib/native-file-drop-rejection-message.test.ts": 89, - "src/renderer/src/lib/nested-repo-selected-paths.test.ts": 50, - "src/renderer/src/lib/new-agent-prompt-outcome.test.ts": 641, - "src/renderer/src/lib/new-workspace-composer-repo.test.ts": 64, - "src/renderer/src/lib/new-workspace-create-gates.test.ts": 54, - "src/renderer/src/lib/new-workspace-enter-guard.test.ts": 53, - "src/renderer/src/lib/new-workspace-project-options.test.ts": 74, - "src/renderer/src/lib/new-workspace-ssh-gate.test.ts": 52, - "src/renderer/src/lib/new-workspace.test.ts": 4504, - "src/renderer/src/lib/non-secure-context-crypto.repro.test.ts": 1464, - "src/renderer/src/lib/notes-send-agent-targets.test.ts": 369, - "src/renderer/src/lib/onboarding-project-checklist.test.ts": 54, - "src/renderer/src/lib/open-agent-message-sender.test.ts": 437, - "src/renderer/src/lib/open-document-in-floating-workspace.test.ts": 272, - "src/renderer/src/lib/open-mobile-emulator-tab.test.ts": 371, - "src/renderer/src/lib/open-tab-occupant-agent.test.ts": 123, - "src/renderer/src/lib/orca-hook-trust.test.ts": 53, - "src/renderer/src/lib/orca-yaml-trust-prompt-slot-eviction.test.ts": 796, - "src/renderer/src/lib/orchestration-setup-state.test.ts": 51, - "src/renderer/src/lib/orchestration-skill-coverage.test.ts": 142, + "src/renderer/src/lib/fix-checks-agent-launch.test.ts": 474, + "src/renderer/src/lib/flatten-retained-slice.test.ts": 220, + "src/renderer/src/lib/floating-terminal.test.ts": 39, + "src/renderer/src/lib/floating-workspace-panel-reveal.test.ts": 271, + "src/renderer/src/lib/floating-workspace-terminal-actions.test.ts": 437, + "src/renderer/src/lib/floating-workspace-tour-interaction-snapshot.test.ts": 35, + "src/renderer/src/lib/focus-terminal-tab-surface.split-focus.test.ts": 188, + "src/renderer/src/lib/focus-terminal-tab-surface.test.ts": 49, + "src/renderer/src/lib/folder-workspace-path-status.test.ts": 104, + "src/renderer/src/lib/foreground-terminal-tabs.test.ts": 33, + "src/renderer/src/lib/github-links.test.ts": 48, + "src/renderer/src/lib/github-pr-start-point.test.ts": 212, + "src/renderer/src/lib/github-source-runtime-context.test.ts": 33, + "src/renderer/src/lib/github-work-item-source-lookup.test.ts": 274, + "src/renderer/src/lib/github-work-item-workspace-attachment.test.ts": 95, + "src/renderer/src/lib/gitlab-links.test.ts": 56, + "src/renderer/src/lib/gitlab-work-item-source-lookup.test.ts": 291, + "src/renderer/src/lib/hook-command-delayed-delivery-perf.test.ts": 1364, + "src/renderer/src/lib/hook-command-delayed-delivery.test.ts": 1217, + "src/renderer/src/lib/host-mirror-handle-gap-drain.test.ts": 1366, + "src/renderer/src/lib/host-mirror-handle-gap-landed-handle.test.ts": 1357, + "src/renderer/src/lib/host-mirror-handle-gap-resume.test.ts": 1621, + "src/renderer/src/lib/host-mirror-handle-gap-snapshot-lifetime.test.ts": 2211, + "src/renderer/src/lib/host-mirror-handle-gap-subscription-lifetime.test.ts": 1726, + "src/renderer/src/lib/host-mirror-handle-gap-teardown.test.ts": 1356, + "src/renderer/src/lib/host-mirror-handle-gap-verdict-union.test.ts": 1443, + "src/renderer/src/lib/host-mirrored-pane-resume-replay.test.ts": 1464, + "src/renderer/src/lib/http-link-destinations.test.ts": 317, + "src/renderer/src/lib/http-link-modifier-routing.test.ts": 299, + "src/renderer/src/lib/http-link-routing.test.ts": 366, + "src/renderer/src/lib/image-blob-png.test.ts": 319, + "src/renderer/src/lib/ime-composition-keyboard-event.test.ts": 251, + "src/renderer/src/lib/ime-overlay-escape.test.tsx": 714, + "src/renderer/src/lib/ime-text-field.test.tsx": 512, + "src/renderer/src/lib/ios-web-platform.test.ts": 36, + "src/renderer/src/lib/ipc-error.test.ts": 49, + "src/renderer/src/lib/jira-board-drag-payload.test.ts": 46, + "src/renderer/src/lib/jira-issue-workspace-attachment.test.ts": 107, + "src/renderer/src/lib/jira-source-host.test.ts": 34, + "src/renderer/src/lib/keyboard-layout/detect-option-as-alt.test.ts": 51, + "src/renderer/src/lib/keyboard-layout/input-source-id.test.ts": 40, + "src/renderer/src/lib/keyboard-layout/layout-base-character.test.ts": 37, + "src/renderer/src/lib/keyboard-layout/option-as-alt-probe.test.ts": 91, + "src/renderer/src/lib/keyboard-layout/option-key-location-state.test.ts": 31, + "src/renderer/src/lib/language-detect.test.ts": 58, + "src/renderer/src/lib/large-text-control-paste.test.ts": 192, + "src/renderer/src/lib/launch-agent-background-session-extra-args.test.ts": 614, + "src/renderer/src/lib/launch-agent-background-session-launch-parity.test.ts": 1960, + "src/renderer/src/lib/launch-agent-background-session-remote.test.ts": 743, + "src/renderer/src/lib/launch-agent-background-session.test.ts": 1041, + "src/renderer/src/lib/launch-agent-execution-context.test.ts": 1405, + "src/renderer/src/lib/launch-agent-in-new-tab-cwd.test.ts": 940, + "src/renderer/src/lib/launch-agent-in-new-tab-host-agents.test.ts": 576, + "src/renderer/src/lib/launch-agent-in-new-tab-host-resolution.test.ts": 1014, + "src/renderer/src/lib/launch-agent-in-new-tab-main-surface.test.ts": 1768, + "src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts": 1188, + "src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts": 838, + "src/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.ts": 1006, + "src/renderer/src/lib/launch-agent-in-new-tab.test.ts": 1109, + "src/renderer/src/lib/launch-agent-new-tab-host-route-readiness.test.ts": 381, + "src/renderer/src/lib/launch-agent-new-tab-host-route.test.ts": 51, + "src/renderer/src/lib/launch-agent-session-continuation.test.ts": 87, + "src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts": 1177, + "src/renderer/src/lib/launch-ai-vault-session.test.ts": 55, + "src/renderer/src/lib/launch-parity-plus-planner.test.ts": 1409, + "src/renderer/src/lib/launch-structured-agent-session.test.ts": 2191, + "src/renderer/src/lib/launch-work-item-direct-agent-routing.test.ts": 1508, + "src/renderer/src/lib/launch-work-item-direct-agent.test.ts": 130, + "src/renderer/src/lib/launch-work-item-direct-messages.test.ts": 35, + "src/renderer/src/lib/launch-work-item-direct-owner-row.test.ts": 989, + "src/renderer/src/lib/launch-work-item-direct.test.ts": 1020, + "src/renderer/src/lib/lazy-chunk-recovery-reload.test.ts": 215, + "src/renderer/src/lib/lazy-with-retry.never-landed-reload.test.ts": 157, + "src/renderer/src/lib/lazy-with-retry.test.ts": 259, + "src/renderer/src/lib/left-sidebar-appearance.test.ts": 272, + "src/renderer/src/lib/linear-agent-skill-update-command.test.ts": 1375, + "src/renderer/src/lib/linear-board-drag-payload.test.ts": 50, + "src/renderer/src/lib/linear-issue-context-snapshot.test.ts": 56, + "src/renderer/src/lib/linear-issue-url-lookup.test.ts": 248, + "src/renderer/src/lib/linear-issue-workspace-attachment.test.ts": 106, + "src/renderer/src/lib/linear-issue-workspace-open.test.ts": 137, + "src/renderer/src/lib/linear-linked-work-item.test.ts": 54, + "src/renderer/src/lib/link-source-local-open-owner.test.ts": 294, + "src/renderer/src/lib/linked-work-item-context.test.ts": 48, + "src/renderer/src/lib/linked-work-item-provider.test.ts": 1379, + "src/renderer/src/lib/list-multi-selection.test.ts": 46, + "src/renderer/src/lib/list-row-interaction.test.ts": 165, + "src/renderer/src/lib/local-file-access.test.ts": 1221, + "src/renderer/src/lib/local-path-open-guard.test.ts": 70, + "src/renderer/src/lib/local-preflight-context-cache.test.ts": 302, + "src/renderer/src/lib/local-preflight-context.test.ts": 425, + "src/renderer/src/lib/locale-text-collators.test.ts": 56, + "src/renderer/src/lib/manual-terminal-worktree-parking.test.ts": 169, + "src/renderer/src/lib/markdown-comment-blocks.test.ts": 35, + "src/renderer/src/lib/markdown-document-templates.test.ts": 388, + "src/renderer/src/lib/markdown-review-note-copy.test.ts": 53, + "src/renderer/src/lib/markdown-review-notes-setting.test.ts": 33, + "src/renderer/src/lib/markdown-review-notes.test.ts": 65, + "src/renderer/src/lib/migration-unsupported-agent-entry.test.ts": 28, + "src/renderer/src/lib/mobile-terminal-tab-mount.test.ts": 49, + "src/renderer/src/lib/monaco-delayer-cancellation-guard.test.ts": 184, + "src/renderer/src/lib/monaco-diff-editor-disposal.test.ts": 37, + "src/renderer/src/lib/monaco-editor-gutter-disposal.test.ts": 301, + "src/renderer/src/lib/monaco-filename-language.test.ts": 73, + "src/renderer/src/lib/monaco-languages/monarch-embed-entry-recursion.test.ts": 649, + "src/renderer/src/lib/monaco-languages/monarch-upstream-mdx-recursion.test.ts": 3810, + "src/renderer/src/lib/monaco-languages/register-astro.test.ts": 120, + "src/renderer/src/lib/monaco-languages/register-jsonl.test.ts": 104, + "src/renderer/src/lib/monaco-languages/register-nim.test.ts": 49, + "src/renderer/src/lib/monaco-languages/register-quarto-adversarial.test.ts": 366, + "src/renderer/src/lib/monaco-languages/register-quarto.test.ts": 343, + "src/renderer/src/lib/monaco-languages/register-shell-markdown-aliases.test.ts": 28, + "src/renderer/src/lib/monaco-languages/register-svelte.test.ts": 415, + "src/renderer/src/lib/monaco-languages/register-typst.test.ts": 56, + "src/renderer/src/lib/monaco-languages/register-vue.test.ts": 119, + "src/renderer/src/lib/monaco-languages/textmate-language-registration.test.ts": 43, + "src/renderer/src/lib/monaco-languages/textmate-token-provider.test.ts": 206, + "src/renderer/src/lib/monaco-peek-preview-options.test.ts": 975, + "src/renderer/src/lib/monaco-setup-steps.test.ts": 32, + "src/renderer/src/lib/monospace-font-family.test.ts": 41, + "src/renderer/src/lib/native-chat-launch-draft-mirrorability.test.ts": 406, + "src/renderer/src/lib/native-chat-transcript-readability.test.ts": 31, + "src/renderer/src/lib/native-file-drop-rejection-message.test.ts": 71, + "src/renderer/src/lib/nested-repo-selected-paths.test.ts": 43, + "src/renderer/src/lib/new-agent-prompt-outcome.test.ts": 849, + "src/renderer/src/lib/new-workspace-composer-repo.test.ts": 42, + "src/renderer/src/lib/new-workspace-create-gates.test.ts": 35, + "src/renderer/src/lib/new-workspace-enter-guard.test.ts": 41, + "src/renderer/src/lib/new-workspace-project-options.test.ts": 84, + "src/renderer/src/lib/new-workspace-ssh-gate.test.ts": 42, + "src/renderer/src/lib/new-workspace.test.ts": 4502, + "src/renderer/src/lib/non-secure-context-crypto.repro.test.ts": 1468, + "src/renderer/src/lib/notes-send-agent-targets.test.ts": 341, + "src/renderer/src/lib/onboarding-project-checklist.test.ts": 40, + "src/renderer/src/lib/open-agent-message-sender.test.ts": 457, + "src/renderer/src/lib/open-document-in-floating-workspace.test.ts": 237, + "src/renderer/src/lib/open-mobile-emulator-tab.test.ts": 389, + "src/renderer/src/lib/open-tab-occupant-agent.test.ts": 106, + "src/renderer/src/lib/orca-hook-trust.test.ts": 27, + "src/renderer/src/lib/orca-yaml-trust-prompt-slot-eviction.test.ts": 787, + "src/renderer/src/lib/orchestration-setup-state.test.ts": 26, + "src/renderer/src/lib/orchestration-skill-coverage.test.ts": 144, "src/renderer/src/lib/order-empty-query-worktrees.test.ts": 62, - "src/renderer/src/lib/os-file-drop-cancellation-guard.test.ts": 173, - "src/renderer/src/lib/os-file-drop-preload-integration.test.tsx": 489, - "src/renderer/src/lib/outside-dismiss-guard.test.ts": 50, - "src/renderer/src/lib/paired-web-client-host.test.ts": 405, - "src/renderer/src/lib/palette-match/cmd-j-ranking-contract.test.ts": 83, - "src/renderer/src/lib/palette-match/match-field-allocation.test.ts": 154, - "src/renderer/src/lib/palette-match/palette-match-core.test.ts": 121, - "src/renderer/src/lib/palette-match/palette-match-performance.test.ts": 6685, - "src/renderer/src/lib/palette-match/palette-phrase-placement.test.ts": 80, - "src/renderer/src/lib/palette-match/palette-ranking.test.ts": 63, - "src/renderer/src/lib/palette-repo-resolution.test.ts": 59, - "src/renderer/src/lib/palette-type-alias-match.test.ts": 51, - "src/renderer/src/lib/pane-agent-evidence.test.ts": 92, - "src/renderer/src/lib/pane-manager/browser-mobile-driver-state.test.ts": 61, - "src/renderer/src/lib/pane-manager/browser-remote-viewer-state.test.ts": 43, - "src/renderer/src/lib/pane-manager/client-hosted-browser-row-ephemerality.test.ts": 1413, - "src/renderer/src/lib/pane-manager/client-hosted-browser-row-state.test.ts": 63, - "src/renderer/src/lib/pane-manager/focus-follows-mouse.test.ts": 49, - "src/renderer/src/lib/pane-manager/mobile-driver-state.test.ts": 43, - "src/renderer/src/lib/pane-manager/mobile-fit-overrides-hydration.test.ts": 54, - "src/renderer/src/lib/pane-manager/mobile-fit-overrides.test.ts": 66, - "src/renderer/src/lib/pane-manager/pane-container-listener-lifecycle.test.ts": 607, - "src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.test.ts": 478, - "src/renderer/src/lib/pane-manager/pane-divider-capture-loss.test.ts": 59, - "src/renderer/src/lib/pane-manager/pane-divider-stray-touch.test.ts": 58, - "src/renderer/src/lib/pane-manager/pane-divider.test.ts": 78, - "src/renderer/src/lib/pane-manager/pane-dom-creation.test.ts": 274, - "src/renderer/src/lib/pane-manager/pane-dom-focus-class-sync-lifecycle.test.ts": 482, - "src/renderer/src/lib/pane-manager/pane-drag-reorder.test.ts": 118, - "src/renderer/src/lib/pane-manager/pane-drop-no-op.test.ts": 235, - "src/renderer/src/lib/pane-manager/pane-fit-resize-observer.test.ts": 105, - "src/renderer/src/lib/pane-manager/pane-fit.test.ts": 139, - "src/renderer/src/lib/pane-manager/pane-initial-fit-lifecycle.test.ts": 197, - "src/renderer/src/lib/pane-manager/pane-inline-image-recovery.test.ts": 76, - "src/renderer/src/lib/pane-manager/pane-inline-images.test.ts": 97, - "src/renderer/src/lib/pane-manager/pane-key-resolution.test.ts": 50, - "src/renderer/src/lib/pane-manager/pane-layout-geometry-apply.test.ts": 384, - "src/renderer/src/lib/pane-manager/pane-layout-gesture.test.ts": 228, - "src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts": 243, - "src/renderer/src/lib/pane-manager/pane-manager-registry.test.ts": 70, - "src/renderer/src/lib/pane-manager/pane-manager-spawn-placement.test.ts": 458, - "src/renderer/src/lib/pane-manager/pane-metric-options-deferral.test.ts": 63, - "src/renderer/src/lib/pane-manager/pane-overlay-focus.test.ts": 387, - "src/renderer/src/lib/pane-manager/pane-pointer-focus.test.ts": 52, - "src/renderer/src/lib/pane-manager/pane-pty-resize-hold.test.ts": 47, - "src/renderer/src/lib/pane-manager/pane-reveal-fit.test.ts": 62, - "src/renderer/src/lib/pane-manager/pane-reveal-lifetime.test.ts": 1002, - "src/renderer/src/lib/pane-manager/pane-reveal-repaint.test.ts": 350, - "src/renderer/src/lib/pane-manager/pane-scroll.test.ts": 179, - "src/renderer/src/lib/pane-manager/pane-split-close.test.ts": 67, - "src/renderer/src/lib/pane-manager/pane-split-scroll.test.ts": 85, - "src/renderer/src/lib/pane-manager/pane-terminal-foreground-render-settle.test.ts": 54, - "src/renderer/src/lib/pane-manager/pane-terminal-gpu-acceleration.test.ts": 108, + "src/renderer/src/lib/os-file-drop-cancellation-guard.test.ts": 175, + "src/renderer/src/lib/os-file-drop-preload-integration.test.tsx": 497, + "src/renderer/src/lib/outside-dismiss-guard.test.ts": 34, + "src/renderer/src/lib/paired-web-client-host.test.ts": 327, + "src/renderer/src/lib/palette-match/cmd-j-ranking-contract.test.ts": 80, + "src/renderer/src/lib/palette-match/match-field-allocation.test.ts": 146, + "src/renderer/src/lib/palette-match/palette-match-core.test.ts": 105, + "src/renderer/src/lib/palette-match/palette-match-performance.test.ts": 6044, + "src/renderer/src/lib/palette-match/palette-phrase-placement.test.ts": 73, + "src/renderer/src/lib/palette-match/palette-ranking.test.ts": 46, + "src/renderer/src/lib/palette-repo-resolution.test.ts": 47, + "src/renderer/src/lib/pane-agent-evidence.test.ts": 71, + "src/renderer/src/lib/pane-manager/browser-mobile-driver-state.test.ts": 37, + "src/renderer/src/lib/pane-manager/browser-remote-viewer-state.test.ts": 34, + "src/renderer/src/lib/pane-manager/client-hosted-browser-row-ephemerality.test.ts": 1254, + "src/renderer/src/lib/pane-manager/client-hosted-browser-row-state.test.ts": 42, + "src/renderer/src/lib/pane-manager/focus-follows-mouse.test.ts": 33, + "src/renderer/src/lib/pane-manager/mobile-driver-state.test.ts": 42, + "src/renderer/src/lib/pane-manager/mobile-fit-overrides-hydration.test.ts": 39, + "src/renderer/src/lib/pane-manager/mobile-fit-overrides.test.ts": 58, + "src/renderer/src/lib/pane-manager/pane-container-listener-lifecycle.test.ts": 311, + "src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.test.ts": 553, + "src/renderer/src/lib/pane-manager/pane-divider-capture-loss.test.ts": 40, + "src/renderer/src/lib/pane-manager/pane-divider-stray-touch.test.ts": 47, + "src/renderer/src/lib/pane-manager/pane-divider.test.ts": 46, + "src/renderer/src/lib/pane-manager/pane-dom-creation.test.ts": 309, + "src/renderer/src/lib/pane-manager/pane-dom-focus-class-sync-lifecycle.test.ts": 472, + "src/renderer/src/lib/pane-manager/pane-drag-reorder.test.ts": 95, + "src/renderer/src/lib/pane-manager/pane-drop-no-op.test.ts": 233, + "src/renderer/src/lib/pane-manager/pane-fit-resize-observer.test.ts": 95, + "src/renderer/src/lib/pane-manager/pane-fit.test.ts": 146, + "src/renderer/src/lib/pane-manager/pane-initial-fit-lifecycle.test.ts": 146, + "src/renderer/src/lib/pane-manager/pane-inline-image-recovery.test.ts": 39, + "src/renderer/src/lib/pane-manager/pane-inline-images.test.ts": 78, + "src/renderer/src/lib/pane-manager/pane-key-resolution.test.ts": 34, + "src/renderer/src/lib/pane-manager/pane-layout-geometry-apply.test.ts": 433, + "src/renderer/src/lib/pane-manager/pane-layout-gesture.test.ts": 195, + "src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts": 359, + "src/renderer/src/lib/pane-manager/pane-manager-registry.test.ts": 60, + "src/renderer/src/lib/pane-manager/pane-manager-spawn-placement.test.ts": 709, + "src/renderer/src/lib/pane-manager/pane-metric-options-deferral.test.ts": 41, + "src/renderer/src/lib/pane-manager/pane-overlay-focus.test.ts": 345, + "src/renderer/src/lib/pane-manager/pane-pointer-focus.test.ts": 30, + "src/renderer/src/lib/pane-manager/pane-pty-resize-hold.test.ts": 32, + "src/renderer/src/lib/pane-manager/pane-reveal-fit.test.ts": 52, + "src/renderer/src/lib/pane-manager/pane-reveal-lifetime.test.ts": 1263, + "src/renderer/src/lib/pane-manager/pane-reveal-repaint.test.ts": 232, + "src/renderer/src/lib/pane-manager/pane-scroll.test.ts": 166, + "src/renderer/src/lib/pane-manager/pane-split-close.test.ts": 54, + "src/renderer/src/lib/pane-manager/pane-split-scroll.test.ts": 49, + "src/renderer/src/lib/pane-manager/pane-terminal-foreground-render-settle.test.ts": 34, + "src/renderer/src/lib/pane-manager/pane-terminal-gpu-acceleration.test.ts": 62, "src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.test.ts": 82, - "src/renderer/src/lib/pane-manager/pane-terminal-output-queue-chunks.test.ts": 52, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-ack-credit.test.ts": 167, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-backlog-cap.test.ts": 160, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-disposed-writes.test.ts": 102, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-foreground-refresh.test.ts": 158, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-queue-retention.test.ts": 279, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-synchronized-frames.test.ts": 190, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-throughput.bench.test.ts": 324, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.test.ts": 208, - "src/renderer/src/lib/pane-manager/pane-tree-equalization-parity.test.ts": 676, + "src/renderer/src/lib/pane-manager/pane-terminal-output-queue-chunks.test.ts": 39, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-ack-credit.test.ts": 178, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-backlog-cap.test.ts": 171, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-disposed-writes.test.ts": 87, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-foreground-refresh.test.ts": 187, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-queue-retention.test.ts": 335, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-synchronized-frames.test.ts": 207, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-throughput.bench.test.ts": 318, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.test.ts": 251, + "src/renderer/src/lib/pane-manager/pane-tree-equalization-parity.test.ts": 803, "src/renderer/src/lib/pane-manager/pane-tree-equalization-scaling.test.ts": 200, - "src/renderer/src/lib/pane-manager/pane-tree-ops.test.ts": 148, - "src/renderer/src/lib/pane-manager/pane-tree-reparent-frame.test.ts": 80, - "src/renderer/src/lib/pane-manager/pane-viewport-present-lifetime.test.ts": 779, - "src/renderer/src/lib/pane-manager/pane-webgl-context-recovery.test.ts": 124, - "src/renderer/src/lib/pane-manager/pane-webgl-refresh-lifecycle.test.ts": 159, - "src/renderer/src/lib/pane-manager/pane-webgl-renderer.test.ts": 356, - "src/renderer/src/lib/pane-manager/terminal-arabic-shaping-joiner.test.ts": 73, - "src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.test.ts": 53, - "src/renderer/src/lib/pane-manager/terminal-complex-script.test.ts": 75, - "src/renderer/src/lib/pane-manager/terminal-delivery-credit.test.ts": 43, - "src/renderer/src/lib/pane-manager/terminal-dom-block-fill.test.ts": 283, - "src/renderer/src/lib/pane-manager/terminal-foreground-repair-convergence.test.ts": 217, - "src/renderer/src/lib/pane-manager/terminal-ime-anchor.test.ts": 47, - "src/renderer/src/lib/pane-manager/terminal-ime-candidate-anchor.test.ts": 206, - "src/renderer/src/lib/pane-manager/terminal-keyboard-protocol.test.ts": 63, - "src/renderer/src/lib/pane-manager/terminal-ligatures-addon.test.ts": 83, - "src/renderer/src/lib/pane-manager/terminal-link-provider-guard.test.ts": 63, - "src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset-on-mouseleave.test.ts": 56, - "src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset-on-write.test.ts": 56, - "src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset.test.ts": 43, - "src/renderer/src/lib/pane-manager/terminal-mouse-encoding-tracker.test.ts": 126, - "src/renderer/src/lib/pane-manager/terminal-render-pause-release-parked-resize.test.ts": 60, - "src/renderer/src/lib/pane-manager/terminal-render-pause-release.test.ts": 59, - "src/renderer/src/lib/pane-manager/terminal-scroll-intent-input-resync.test.ts": 71, - "src/renderer/src/lib/pane-manager/terminal-scroll-intent-key-retention.test.ts": 106, - "src/renderer/src/lib/pane-manager/terminal-scroll-intent-structural-transitions.test.ts": 56, - "src/renderer/src/lib/pane-manager/terminal-scroll-intent.test.ts": 93, - "src/renderer/src/lib/pane-manager/terminal-structural-replay-coordinator.test.ts": 76, - "src/renderer/src/lib/pane-manager/terminal-webgl-auto-policy.test.ts": 55, - "src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.test.ts": 365, - "src/renderer/src/lib/pane-manager/terminal-windows-ctrl-alt-chord-classification.test.ts": 196, - "src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.test.ts": 73, - "src/renderer/src/lib/pane-manager/windows-pty-compatibility.test.ts": 76, - "src/renderer/src/lib/pane-manager/xterm-instance-disposed.test.ts": 86, - "src/renderer/src/lib/pane-manager/xterm-user-scrolling-contract.test.ts": 2444, - "src/renderer/src/lib/pane-manager/xterm-write-callback-guard.test.ts": 72, - "src/renderer/src/lib/parked-terminal-host-hydration.test.ts": 323, - "src/renderer/src/lib/passive-macos-app-data-access.test.ts": 49, - "src/renderer/src/lib/paste-payload-metadata.test.ts": 60, - "src/renderer/src/lib/path-head-elision.test.ts": 56, - "src/renderer/src/lib/path.test.ts": 59, - "src/renderer/src/lib/pending-worktree-creation.test.ts": 40, - "src/renderer/src/lib/pi-live-session-no-duplicate-tab.test.ts": 1634, - "src/renderer/src/lib/pi-session-resume-wake.test.ts": 1408, - "src/renderer/src/lib/plugin-command-execution.test.ts": 165, - "src/renderer/src/lib/plugin-command-keybindings.test.ts": 121, - "src/renderer/src/lib/pr-bot-author-overrides.test.ts": 302, - "src/renderer/src/lib/pr-comment-action-state.test.ts": 54, - "src/renderer/src/lib/pr-comment-reactions.test.ts": 44, - "src/renderer/src/lib/primary-selection-paste.test.ts": 247, - "src/renderer/src/lib/primary-selection.test.ts": 70, - "src/renderer/src/lib/project-clone-url-prefill.test.ts": 59, - "src/renderer/src/lib/project-host-clone-url.test.ts": 49, - "src/renderer/src/lib/project-host-setup-options.test.ts": 325, - "src/renderer/src/lib/project-host-workspace-target-managed-host.test.ts": 340, - "src/renderer/src/lib/project-host-workspace-target.test.ts": 67, - "src/renderer/src/lib/project-skill-runtime.test.ts": 82, - "src/renderer/src/lib/provisioned-root-create-options.test.ts": 52, - "src/renderer/src/lib/quick-workspace-agent-selection.test.ts": 141, - "src/renderer/src/lib/react-commit-cascade-observer.react185.test.tsx": 271, - "src/renderer/src/lib/react-commit-cascade-observer.test.ts": 180, - "src/renderer/src/lib/react-commit-cascade-store-write-samples.test.ts": 66, - "src/renderer/src/lib/react-commit-cascade-telemetry.test.ts": 164, - "src/renderer/src/lib/react-error-boundary-reporting.test.ts": 73, - "src/renderer/src/lib/react-grab-dev-gate.test.ts": 54, - "src/renderer/src/lib/react-renderer-root.test.ts": 54, - "src/renderer/src/lib/recent-workspace-tab-rows.test.ts": 121, - "src/renderer/src/lib/remap-open-editor-tabs-for-path-change.test.ts": 1646, - "src/renderer/src/lib/rename-user-named-tab-access.test.ts": 1706, - "src/renderer/src/lib/renderer-agent-status-observation-ingress.test.ts": 115, - "src/renderer/src/lib/renderer-app-platform.test.ts": 156, - "src/renderer/src/lib/renderer-memory-profile.test.ts": 75, - "src/renderer/src/lib/repo-display-labels.test.ts": 60, - "src/renderer/src/lib/repo-runtime-owner.test.ts": 90, - "src/renderer/src/lib/repo-search.test.ts": 67, - "src/renderer/src/lib/repo-slug-cache.test.ts": 56, - "src/renderer/src/lib/repo-slug-index.test.ts": 2468, - "src/renderer/src/lib/resolve-owner.test.ts": 320, - "src/renderer/src/lib/resolved-worktree-execution-host.test.ts": 282, - "src/renderer/src/lib/resume-sleeping-agent-session-direct-ssh-hydration-gap.test.ts": 1548, - "src/renderer/src/lib/resume-sleeping-agent-session-execution-host-scope.test.ts": 1708, - "src/renderer/src/lib/resume-sleeping-agent-session-provider-claim.test.ts": 1682, - "src/renderer/src/lib/resume-sleeping-agent-session-replay.test.ts": 1680, - "src/renderer/src/lib/resume-sleeping-agent-session-slept-pane-recovery.test.ts": 1876, - "src/renderer/src/lib/resume-sleeping-agent-session-suppress-navigation.test.ts": 1521, - "src/renderer/src/lib/resume-sleeping-agent-session.test.ts": 1401, - "src/renderer/src/lib/resume-stale-structured-agent-session.test.ts": 1623, - "src/renderer/src/lib/reveal-in-file-manager.test.ts": 220, - "src/renderer/src/lib/right-sidebar-visibility.test.ts": 61, - "src/renderer/src/lib/run-quick-command-in-new-tab.test.ts": 72, - "src/renderer/src/lib/running-agent-targets.test.ts": 341, - "src/renderer/src/lib/runtime-host-unverifiable-probe-readers.test.ts": 396, - "src/renderer/src/lib/runtime-pane-title-leaf-id.test.ts": 47, - "src/renderer/src/lib/runtime-session-mirror-targets.test.ts": 63, - "src/renderer/src/lib/runtime-session-mirror-unverifiable-host.test.ts": 53, - "src/renderer/src/lib/runtime-workspace-file-route.test.ts": 319, - "src/renderer/src/lib/screen-submit-shortcut.test.ts": 62, - "src/renderer/src/lib/script-textarea-rows.test.ts": 106, - "src/renderer/src/lib/scroll-cache.test.ts": 56, - "src/renderer/src/lib/serve-desktop-promotion-session-continuity.test.ts": 1625, - "src/renderer/src/lib/session-write-subscriber-acknowledgment.test.ts": 1407, - "src/renderer/src/lib/session-write-subscriber-allocation.test.ts": 427, - "src/renderer/src/lib/session-write-subscriber-deferred-persist.test.ts": 1534, - "src/renderer/src/lib/session-write-subscriber.test.ts": 1444, - "src/renderer/src/lib/settled-worker-wake-policy.test.ts": 1691, - "src/renderer/src/lib/setup-runner.test.ts": 57, - "src/renderer/src/lib/setup-script-prompt.test.ts": 289, - "src/renderer/src/lib/setup-split-activation-single-owner.test.ts": 1879, - "src/renderer/src/lib/sha256.test.ts": 55, - "src/renderer/src/lib/shutdown-checkpoint-guard.test.ts": 201, - "src/renderer/src/lib/sidebar-worktree-activation.test.ts": 87, - "src/renderer/src/lib/simulator-launch-coordination.test.ts": 114, - "src/renderer/src/lib/simulator-palette-search.test.ts": 335, - "src/renderer/src/lib/simulator-pane-shutdown-scheduler.test.ts": 1417, - "src/renderer/src/lib/simulator-tab-palette-activation.test.ts": 1343, - "src/renderer/src/lib/skill-freshness-display-status.test.ts": 60, - "src/renderer/src/lib/sleeping-agent-pane-ownership.test.ts": 53, - "src/renderer/src/lib/sleeping-agent-session-launch-windows-quoting.test.ts": 524, - "src/renderer/src/lib/smart-github-submit.test.ts": 104, - "src/renderer/src/lib/solidity-language-id-registration.test.ts": 48, - "src/renderer/src/lib/source-control-agent-action-launch-check.test.ts": 1358, - "src/renderer/src/lib/source-control-generation-plan.test.ts": 114, - "src/renderer/src/lib/source-control-launch-platform.test.ts": 58, - "src/renderer/src/lib/source-control-remote-error.test.ts": 62, - "src/renderer/src/lib/sparse-preset-draft.test.ts": 168, - "src/renderer/src/lib/ssh-mutation-expectation.test.ts": 1556, - "src/renderer/src/lib/ssh-workspace-browser-route-eligibility.test.ts": 47, - "src/renderer/src/lib/startup-draft-input-kind.test.ts": 464, - "src/renderer/src/lib/startup-ui-hydration.test.ts": 270, - "src/renderer/src/lib/state-collection-byte-estimate.test.ts": 113, - "src/renderer/src/lib/structured-agent-launch-settlement.test.ts": 64, - "src/renderer/src/lib/structured-agent-session-launch-admission.test.ts": 3534, - "src/renderer/src/lib/structured-agent-session-launch-after-failure.test.ts": 585, - "src/renderer/src/lib/structured-agent-session-launch-cancellation.test.ts": 1639, - "src/renderer/src/lib/structured-agent-session-launch-close-race.test.ts": 663, - "src/renderer/src/lib/structured-agent-session-launch-create-message.test.ts": 1863, - "src/renderer/src/lib/structured-agent-session-launch-different-request.test.ts": 607, - "src/renderer/src/lib/structured-agent-session-launch-exited-owner.test.ts": 944, - "src/renderer/src/lib/structured-agent-session-launch-join-delivery.test.ts": 508, - "src/renderer/src/lib/structured-agent-session-launch-options.test.ts": 602, - "src/renderer/src/lib/structured-agent-session-launch-persistence.test.ts": 374, - "src/renderer/src/lib/structured-agent-session-launch-probe-outage.test.ts": 1677, - "src/renderer/src/lib/structured-agent-session-launch-prompt-stop.test.ts": 511, - "src/renderer/src/lib/structured-agent-session-launch-prompt.test.ts": 481, - "src/renderer/src/lib/structured-agent-session-launch-record.test.ts": 203, - "src/renderer/src/lib/structured-agent-session-launch-registered-agent.test.ts": 610, - "src/renderer/src/lib/structured-agent-session-launch-reload.test.ts": 573, - "src/renderer/src/lib/structured-agent-session-launch-resume-identity.test.ts": 602, - "src/renderer/src/lib/structured-agent-session-launch-unconfirmed-or-retried.test.ts": 539, - "src/renderer/src/lib/structured-agent-session-launch.test.ts": 698, - "src/renderer/src/lib/structured-agent-session-new-chat-own-launch.test.ts": 656, - "src/renderer/src/lib/structured-agent-session-provisional-tab.test.ts": 1115, - "src/renderer/src/lib/structured-agent-session-tab-activation.test.ts": 59, - "src/renderer/src/lib/structured-chat-tab-index.test.ts": 42, - "src/renderer/src/lib/syntax-highlighting/incremental-syntax-tokenizer.test.ts": 553, - "src/renderer/src/lib/syntax-highlighting/syntax-highlight-scheduler.test.ts": 151, - "src/renderer/src/lib/syntax-highlighting/syntax-highlighter.test.ts": 476, - "src/renderer/src/lib/tab-agent-identity-decision-table.test.ts": 542, - "src/renderer/src/lib/tab-agent-status-index.test.ts": 224, - "src/renderer/src/lib/tab-agent.test.ts": 127, - "src/renderer/src/lib/tab-has-live-pty.test.ts": 62, - "src/renderer/src/lib/tab-number-shortcuts.test.ts": 1566, - "src/renderer/src/lib/task-source-runtime-target.test.ts": 47, - "src/renderer/src/lib/terminal-contrast-correction.test.ts": 64, - "src/renderer/src/lib/terminal-file-uri-link.test.ts": 74, - "src/renderer/src/lib/terminal-input-activity-coalescing.test.ts": 58, + "src/renderer/src/lib/pane-manager/pane-tree-ops.test.ts": 124, + "src/renderer/src/lib/pane-manager/pane-tree-reparent-frame.test.ts": 76, + "src/renderer/src/lib/pane-manager/pane-viewport-present-lifetime.test.ts": 971, + "src/renderer/src/lib/pane-manager/pane-webgl-context-recovery.test.ts": 160, + "src/renderer/src/lib/pane-manager/pane-webgl-refresh-lifecycle.test.ts": 297, + "src/renderer/src/lib/pane-manager/pane-webgl-renderer.test.ts": 322, + "src/renderer/src/lib/pane-manager/terminal-arabic-shaping-joiner.test.ts": 65, + "src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.test.ts": 43, + "src/renderer/src/lib/pane-manager/terminal-complex-script.test.ts": 69, + "src/renderer/src/lib/pane-manager/terminal-delivery-credit.test.ts": 30, + "src/renderer/src/lib/pane-manager/terminal-dom-block-fill.test.ts": 321, + "src/renderer/src/lib/pane-manager/terminal-foreground-repair-convergence.test.ts": 275, + "src/renderer/src/lib/pane-manager/terminal-grid-size-for-box.test.ts": 170, + "src/renderer/src/lib/pane-manager/terminal-ime-anchor.test.ts": 39, + "src/renderer/src/lib/pane-manager/terminal-ime-candidate-anchor.test.ts": 283, + "src/renderer/src/lib/pane-manager/terminal-keyboard-protocol.test.ts": 50, + "src/renderer/src/lib/pane-manager/terminal-ligatures-addon.test.ts": 44, + "src/renderer/src/lib/pane-manager/terminal-link-provider-guard.test.ts": 42, + "src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset-on-mouseleave.test.ts": 38, + "src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset-on-write.test.ts": 54, + "src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset.test.ts": 34, + "src/renderer/src/lib/pane-manager/terminal-mouse-encoding-tracker.test.ts": 84, + "src/renderer/src/lib/pane-manager/terminal-render-pause-release-parked-resize.test.ts": 38, + "src/renderer/src/lib/pane-manager/terminal-render-pause-release.test.ts": 45, + "src/renderer/src/lib/pane-manager/terminal-scroll-intent-input-resync.test.ts": 61, + "src/renderer/src/lib/pane-manager/terminal-scroll-intent-key-retention.test.ts": 130, + "src/renderer/src/lib/pane-manager/terminal-scroll-intent-structural-transitions.test.ts": 38, + "src/renderer/src/lib/pane-manager/terminal-scroll-intent.test.ts": 89, + "src/renderer/src/lib/pane-manager/terminal-structural-replay-coordinator.test.ts": 54, + "src/renderer/src/lib/pane-manager/terminal-webgl-auto-policy.test.ts": 46, + "src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.test.ts": 541, + "src/renderer/src/lib/pane-manager/terminal-windows-ctrl-alt-chord-classification.test.ts": 203, + "src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.test.ts": 46, + "src/renderer/src/lib/pane-manager/windows-pty-compatibility.test.ts": 51, + "src/renderer/src/lib/pane-manager/xterm-instance-disposed.test.ts": 41, + "src/renderer/src/lib/pane-manager/xterm-user-scrolling-contract.test.ts": 2372, + "src/renderer/src/lib/pane-manager/xterm-write-callback-guard.test.ts": 45, + "src/renderer/src/lib/parked-terminal-host-hydration.test.ts": 294, + "src/renderer/src/lib/passive-macos-app-data-access.test.ts": 37, + "src/renderer/src/lib/paste-payload-metadata.test.ts": 45, + "src/renderer/src/lib/path-head-elision.test.ts": 37, + "src/renderer/src/lib/path.test.ts": 42, + "src/renderer/src/lib/pending-worktree-creation.test.ts": 52, + "src/renderer/src/lib/pi-live-session-no-duplicate-tab.test.ts": 1341, + "src/renderer/src/lib/pi-session-resume-wake.test.ts": 1359, + "src/renderer/src/lib/plugin-command-execution.test.ts": 174, + "src/renderer/src/lib/plugin-command-keybindings.test.ts": 100, + "src/renderer/src/lib/pr-bot-author-overrides.test.ts": 284, + "src/renderer/src/lib/pr-comment-action-state.test.ts": 38, + "src/renderer/src/lib/pr-comment-reactions.test.ts": 27, + "src/renderer/src/lib/primary-selection-paste.test.ts": 252, + "src/renderer/src/lib/primary-selection.test.ts": 53, + "src/renderer/src/lib/project-clone-url-prefill.test.ts": 55, + "src/renderer/src/lib/project-host-clone-url.test.ts": 43, + "src/renderer/src/lib/project-host-setup-options.test.ts": 372, + "src/renderer/src/lib/project-host-workspace-target-managed-host.test.ts": 345, + "src/renderer/src/lib/project-host-workspace-target.jira-mapping.test.ts": 55, + "src/renderer/src/lib/project-host-workspace-target.test.ts": 74, + "src/renderer/src/lib/project-skill-runtime.test.ts": 67, + "src/renderer/src/lib/provisioned-root-create-options.test.ts": 27, + "src/renderer/src/lib/quick-workspace-agent-selection.test.ts": 149, + "src/renderer/src/lib/react-commit-cascade-observer.react185.test.tsx": 280, + "src/renderer/src/lib/react-commit-cascade-observer.test.ts": 188, + "src/renderer/src/lib/react-commit-cascade-store-write-samples.test.ts": 47, + "src/renderer/src/lib/react-commit-cascade-telemetry.test.ts": 182, + "src/renderer/src/lib/react-error-boundary-reporting.test.ts": 40, + "src/renderer/src/lib/react-grab-dev-gate.test.ts": 29, + "src/renderer/src/lib/react-renderer-root.test.ts": 44, + "src/renderer/src/lib/recent-workspace-tab-rows.test.ts": 120, + "src/renderer/src/lib/recipe-vm-managed-host.test.ts": 31, + "src/renderer/src/lib/remap-open-editor-tabs-for-path-change.test.ts": 1238, + "src/renderer/src/lib/rename-user-named-tab-access.test.ts": 1628, + "src/renderer/src/lib/renderer-agent-status-observation-ingress.test.ts": 137, + "src/renderer/src/lib/renderer-app-platform.test.ts": 177, + "src/renderer/src/lib/renderer-memory-profile.test.ts": 64, + "src/renderer/src/lib/repo-display-labels.test.ts": 50, + "src/renderer/src/lib/repo-runtime-owner.test.ts": 61, + "src/renderer/src/lib/repo-search.test.ts": 43, + "src/renderer/src/lib/repo-slug-cache.test.ts": 37, + "src/renderer/src/lib/repo-slug-index.test.ts": 1967, + "src/renderer/src/lib/repo-ssh-connection.test.ts": 102, + "src/renderer/src/lib/resolve-owner.test.ts": 347, + "src/renderer/src/lib/resolved-worktree-execution-host.test.ts": 268, + "src/renderer/src/lib/resume-sleeping-agent-session-deferred-pane.test.ts": 1635, + "src/renderer/src/lib/resume-sleeping-agent-session-direct-ssh-hydration-gap.test.ts": 1451, + "src/renderer/src/lib/resume-sleeping-agent-session-execution-host-scope.test.ts": 1527, + "src/renderer/src/lib/resume-sleeping-agent-session-provider-claim.test.ts": 1301, + "src/renderer/src/lib/resume-sleeping-agent-session-replay.test.ts": 1480, + "src/renderer/src/lib/resume-sleeping-agent-session-slept-pane-recovery.test.ts": 1297, + "src/renderer/src/lib/resume-sleeping-agent-session-suppress-navigation.test.ts": 1294, + "src/renderer/src/lib/resume-sleeping-agent-session.test.ts": 1785, + "src/renderer/src/lib/resume-stale-structured-agent-session.test.ts": 1351, + "src/renderer/src/lib/reveal-in-file-manager.test.ts": 274, + "src/renderer/src/lib/right-sidebar-visibility.test.ts": 49, + "src/renderer/src/lib/run-quick-command-in-new-tab.test.ts": 73, + "src/renderer/src/lib/running-agent-targets.test.ts": 301, + "src/renderer/src/lib/runtime-host-unverifiable-probe-readers.test.ts": 370, + "src/renderer/src/lib/runtime-pane-title-leaf-id.test.ts": 45, + "src/renderer/src/lib/runtime-session-mirror-targets.test.ts": 39, + "src/renderer/src/lib/runtime-session-mirror-unverifiable-host.test.ts": 46, + "src/renderer/src/lib/runtime-workspace-file-route.test.ts": 312, + "src/renderer/src/lib/screen-submit-shortcut.test.ts": 34, + "src/renderer/src/lib/script-textarea-rows.test.ts": 80, + "src/renderer/src/lib/scroll-cache.test.ts": 43, + "src/renderer/src/lib/serve-desktop-promotion-session-continuity.test.ts": 1474, + "src/renderer/src/lib/session-write-subscriber-acknowledgment.test.ts": 1414, + "src/renderer/src/lib/session-write-subscriber-allocation.test.ts": 404, + "src/renderer/src/lib/session-write-subscriber.test.ts": 1363, + "src/renderer/src/lib/settled-worker-wake-policy.test.ts": 1297, + "src/renderer/src/lib/setup-runner.test.ts": 48, + "src/renderer/src/lib/setup-script-prompt.test.ts": 293, + "src/renderer/src/lib/setup-split-activation-single-owner.test.ts": 1817, + "src/renderer/src/lib/sha256.test.ts": 38, + "src/renderer/src/lib/shutdown-checkpoint-guard.test.ts": 189, + "src/renderer/src/lib/sidebar-worktree-activation.test.ts": 58, + "src/renderer/src/lib/simulator-launch-coordination.test.ts": 95, + "src/renderer/src/lib/simulator-palette-search.test.ts": 293, + "src/renderer/src/lib/simulator-pane-shutdown-scheduler.test.ts": 1355, + "src/renderer/src/lib/simulator-tab-palette-activation.test.ts": 1479, + "src/renderer/src/lib/skill-freshness-display-status.test.ts": 54, + "src/renderer/src/lib/sleeping-agent-pane-ownership.test.ts": 43, + "src/renderer/src/lib/sleeping-agent-session-launch-windows-quoting.test.ts": 459, + "src/renderer/src/lib/smart-github-submit.test.ts": 81, + "src/renderer/src/lib/solidity-language-id-registration.test.ts": 39, + "src/renderer/src/lib/source-control-agent-action-launch-check.test.ts": 1403, + "src/renderer/src/lib/source-control-generation-plan.test.ts": 98, + "src/renderer/src/lib/source-control-launch-platform.test.ts": 38, + "src/renderer/src/lib/source-control-remote-error.test.ts": 51, + "src/renderer/src/lib/sparse-preset-draft.test.ts": 158, + "src/renderer/src/lib/ssh-mutation-expectation.test.ts": 1261, + "src/renderer/src/lib/ssh-workspace-browser-route-eligibility.test.ts": 224, + "src/renderer/src/lib/startup-draft-input-kind.test.ts": 612, + "src/renderer/src/lib/startup-ui-hydration.test.ts": 261, + "src/renderer/src/lib/state-collection-byte-estimate.test.ts": 114, + "src/renderer/src/lib/structured-agent-launch-settlement.test.ts": 43, + "src/renderer/src/lib/structured-agent-session-launch-admission.test.ts": 3161, + "src/renderer/src/lib/structured-agent-session-launch-after-failure.test.ts": 606, + "src/renderer/src/lib/structured-agent-session-launch-cancellation.test.ts": 1447, + "src/renderer/src/lib/structured-agent-session-launch-close-race.test.ts": 747, + "src/renderer/src/lib/structured-agent-session-launch-create-message.test.ts": 1817, + "src/renderer/src/lib/structured-agent-session-launch-different-request.test.ts": 730, + "src/renderer/src/lib/structured-agent-session-launch-exited-owner.test.ts": 1103, + "src/renderer/src/lib/structured-agent-session-launch-join-delivery.test.ts": 611, + "src/renderer/src/lib/structured-agent-session-launch-options.test.ts": 611, + "src/renderer/src/lib/structured-agent-session-launch-persistence.test.ts": 421, + "src/renderer/src/lib/structured-agent-session-launch-probe-outage.test.ts": 1687, + "src/renderer/src/lib/structured-agent-session-launch-prompt-stop.test.ts": 433, + "src/renderer/src/lib/structured-agent-session-launch-prompt.test.ts": 513, + "src/renderer/src/lib/structured-agent-session-launch-record.test.ts": 279, + "src/renderer/src/lib/structured-agent-session-launch-registered-agent.test.ts": 643, + "src/renderer/src/lib/structured-agent-session-launch-reload.test.ts": 637, + "src/renderer/src/lib/structured-agent-session-launch-resume-identity.test.ts": 679, + "src/renderer/src/lib/structured-agent-session-launch-unconfirmed-or-retried.test.ts": 656, + "src/renderer/src/lib/structured-agent-session-launch.test.ts": 822, + "src/renderer/src/lib/structured-agent-session-new-chat-own-launch.test.ts": 701, + "src/renderer/src/lib/structured-agent-session-provisional-tab.test.ts": 829, + "src/renderer/src/lib/structured-agent-session-tab-activation.test.ts": 50, + "src/renderer/src/lib/structured-chat-tab-index.test.ts": 27, + "src/renderer/src/lib/syntax-highlighting/incremental-syntax-tokenizer.test.ts": 711, + "src/renderer/src/lib/syntax-highlighting/syntax-highlight-scheduler.test.ts": 133, + "src/renderer/src/lib/syntax-highlighting/syntax-highlighter.test.ts": 546, + "src/renderer/src/lib/syntax-highlighting/textmate-core.test.ts": 41, + "src/renderer/src/lib/tab-agent-identity-decision-table.test.ts": 560, + "src/renderer/src/lib/tab-agent-status-index.test.ts": 215, + "src/renderer/src/lib/tab-agent.test.ts": 98, + "src/renderer/src/lib/tab-has-live-pty.test.ts": 24, + "src/renderer/src/lib/tab-number-shortcuts.test.ts": 1334, + "src/renderer/src/lib/task-source-runtime-target.test.ts": 34, + "src/renderer/src/lib/terminal-contrast-correction.test.ts": 44, + "src/renderer/src/lib/terminal-file-uri-link.test.ts": 43, + "src/renderer/src/lib/terminal-input-activity-coalescing.test.ts": 38, "src/renderer/src/lib/terminal-links-cjk.test.ts": 72, - "src/renderer/src/lib/terminal-links-redos.test.ts": 74, - "src/renderer/src/lib/terminal-links.test.ts": 282, - "src/renderer/src/lib/terminal-pane-title-sanitization.test.ts": 49, - "src/renderer/src/lib/terminal-path-normalization.test.ts": 50, - "src/renderer/src/lib/terminal-quick-command-project-scope.test.ts": 58, - "src/renderer/src/lib/terminal-quick-command-score-budget.test.ts": 57, - "src/renderer/src/lib/terminal-quick-command-search.test.ts": 70, - "src/renderer/src/lib/terminal-reveal-identity.test.ts": 42, - "src/renderer/src/lib/terminal-shortcut-capture-notification.test.tsx": 1656, - "src/renderer/src/lib/terminal-tab-for-pty-id.test.ts": 63, - "src/renderer/src/lib/terminal-tab-owner-activation.test.ts": 50, - "src/renderer/src/lib/terminal-theme.test.ts": 84, - "src/renderer/src/lib/terminal-worktree-route.test.ts": 312, - "src/renderer/src/lib/text-control-paste-ownership.test.ts": 190, - "src/renderer/src/lib/text-control-paste.test.ts": 166, - "src/renderer/src/lib/titlebar-left-chrome.test.ts": 51, - "src/renderer/src/lib/titlebar-worktree-history-controls.test.ts": 54, - "src/renderer/src/lib/tui-agent-startup.test.ts": 155, - "src/renderer/src/lib/typing-latency/diagnostic-lifecycle.test.ts": 183, - "src/renderer/src/lib/typing-latency/diagnostic-summary.test.ts": 61, - "src/renderer/src/lib/typing-latency/echo-instrumentation.test.ts": 145, - "src/renderer/src/lib/typing-latency/input-events.test.ts": 205, - "src/renderer/src/lib/typing-latency/input-source.test.ts": 77, - "src/renderer/src/lib/typing-latency/sample-window.test.ts": 53, - "src/renderer/src/lib/ui-zoom.test.ts": 73, - "src/renderer/src/lib/unified-tab-anchor-insertion.test.ts": 48, - "src/renderer/src/lib/unread-badge-count-migration.test.ts": 306, - "src/renderer/src/lib/unread-badge-count-selector.test.ts": 1368, - "src/renderer/src/lib/unread-badge-count.test.ts": 190, - "src/renderer/src/lib/update-check-click-options.test.ts": 49, - "src/renderer/src/lib/updater-beforeunload.test.ts": 58, - "src/renderer/src/lib/use-tab-agent-observed-signal-dispatch.test.tsx": 1424, - "src/renderer/src/lib/use-tab-agent-opencode-native-title.test.ts": 1734, - "src/renderer/src/lib/use-tab-agent-pi-identity.test.ts": 125, - "src/renderer/src/lib/use-tab-agent-process-signals.test.ts": 1363, - "src/renderer/src/lib/use-tab-agent-remote-pty-selector.test.ts": 159, - "src/renderer/src/lib/use-tab-agent-retained-identity.test.ts": 1903, - "src/renderer/src/lib/use-tab-agent-sleeping-session.test.ts": 1563, - "src/renderer/src/lib/use-tab-agent.test.ts": 1826, - "src/renderer/src/lib/user-opened-local-path.test.ts": 1464, - "src/renderer/src/lib/visible-overlay.test.ts": 165, - "src/renderer/src/lib/wake-sleeping-agents-in-background.test.ts": 203, - "src/renderer/src/lib/wake-sleeping-agents-live-done.test.ts": 161, - "src/renderer/src/lib/web-client-location.test.ts": 51, - "src/renderer/src/lib/window-label-formatter.test.ts": 44, - "src/renderer/src/lib/window-visibility-interval.test.ts": 66, - "src/renderer/src/lib/window-visibility-timeout-poller.test.ts": 173, - "src/renderer/src/lib/windows-terminal-capabilities-race.test.ts": 220, - "src/renderer/src/lib/windows-terminal-capabilities.test.ts": 282, - "src/renderer/src/lib/windows-terminal-capability-reprobe.test.ts": 190, - "src/renderer/src/lib/work-item-link-query-bounds.test.ts": 55, - "src/renderer/src/lib/work-item-lookup-text.test.ts": 57, - "src/renderer/src/lib/worker-terminal-takeover-report.test.ts": 299, - "src/renderer/src/lib/workspace-activation-path-gate.test.ts": 1672, - "src/renderer/src/lib/workspace-activation-terminal-focus.test.ts": 340, - "src/renderer/src/lib/workspace-attachment-terminal-origin.test.ts": 88, - "src/renderer/src/lib/workspace-browser-tab-open.test.ts": 398, - "src/renderer/src/lib/workspace-composer-initial-focus.test.ts": 148, - "src/renderer/src/lib/workspace-create-error-format.test.ts": 82, - "src/renderer/src/lib/workspace-directory.test.ts": 359, - "src/renderer/src/lib/workspace-doc-address-input.test.ts": 283, - "src/renderer/src/lib/workspace-emoji-shortcodes.lazy.test.ts": 95, - "src/renderer/src/lib/workspace-emoji-shortcodes.test.ts": 95, - "src/renderer/src/lib/workspace-file-drag-source-runtime.test.ts": 62, - "src/renderer/src/lib/workspace-file-drag.test.ts": 80, - "src/renderer/src/lib/workspace-port-groups.test.ts": 44, - "src/renderer/src/lib/workspace-port-host-availability.test.ts": 63, - "src/renderer/src/lib/workspace-port-host-scoped-scan.test.ts": 337, - "src/renderer/src/lib/workspace-port-scan-debounce.test.ts": 51, - "src/renderer/src/lib/workspace-port-scan-publish.test.ts": 445, - "src/renderer/src/lib/workspace-session-browser-history.test.ts": 331, - "src/renderer/src/lib/workspace-session-editor-drafts.test.ts": 312, - "src/renderer/src/lib/workspace-session-host-contention.test.ts": 357, - "src/renderer/src/lib/workspace-session-host-persistence.test.ts": 333, - "src/renderer/src/lib/workspace-session-host-split.test.ts": 316, - "src/renderer/src/lib/workspace-session-hydration-keys.test.ts": 58, - "src/renderer/src/lib/workspace-session-liveness.test.ts": 284, - "src/renderer/src/lib/workspace-session-patch.test.ts": 292, - "src/renderer/src/lib/workspace-session-persistence-gate.test.ts": 287, - "src/renderer/src/lib/workspace-session-relevant-fields.test.ts": 325, - "src/renderer/src/lib/workspace-session-ssh-partition-ownership.test.ts": 338, - "src/renderer/src/lib/workspace-session-ssh-partition-round-trip.test.ts": 347, - "src/renderer/src/lib/workspace-session-staged-browser-tabs.test.ts": 48, - "src/renderer/src/lib/workspace-session.test.ts": 318, - "src/renderer/src/lib/workspace-tab-agent-metadata.test.ts": 56, - "src/renderer/src/lib/workspace-tab-palette-activation.store.test.ts": 1688, - "src/renderer/src/lib/workspace-tab-palette-activation.test.ts": 294, - "src/renderer/src/lib/workspace-tab-palette-results.test.ts": 350, - "src/renderer/src/lib/workspace-tab-palette-search.test.ts": 423, - "src/renderer/src/lib/workspace-terminal-host-authority.test.ts": 1753, - "src/renderer/src/lib/worktree-activation-agent-startup.test.ts": 2012, - "src/renderer/src/lib/worktree-activation-automation-filter.test.ts": 1819, - "src/renderer/src/lib/worktree-activation-created-agent.test.ts": 2057, - "src/renderer/src/lib/worktree-activation-default-agent-chat.test.ts": 1659, - "src/renderer/src/lib/worktree-activation-default-tabs.test.ts": 1571, - "src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts": 1934, - "src/renderer/src/lib/worktree-activation-empty-remote.test.ts": 1634, - "src/renderer/src/lib/worktree-activation-issue-command.test.ts": 1693, - "src/renderer/src/lib/worktree-activation-launch-awaiting-host.test.ts": 1657, - "src/renderer/src/lib/worktree-activation-pty-inventory.test.ts": 321, - "src/renderer/src/lib/worktree-activation-reveal.test.ts": 1759, - "src/renderer/src/lib/worktree-activation-setup-script.test.ts": 1905, - "src/renderer/src/lib/worktree-activation-structured-chat-surface.test.ts": 1865, - "src/renderer/src/lib/worktree-activation-web-runtime.test.ts": 2050, - "src/renderer/src/lib/worktree-activity-state.test.ts": 66, - "src/renderer/src/lib/worktree-agent-activation-gate.test.ts": 1647, - "src/renderer/src/lib/worktree-agent-activation-seam.test.ts": 1849, - "src/renderer/src/lib/worktree-agent-structured-inventory.test.ts": 282, - "src/renderer/src/lib/worktree-attachment-label.test.ts": 57, - "src/renderer/src/lib/worktree-creation-agent-seeding.test.ts": 1201, - "src/renderer/src/lib/worktree-creation-agent-seeds.test.ts": 1452, - "src/renderer/src/lib/worktree-creation-backend-startup-focus.test.ts": 1881, - "src/renderer/src/lib/worktree-creation-background-mount.test.ts": 2059, - "src/renderer/src/lib/worktree-creation-chat-setup.test.ts": 2093, - "src/renderer/src/lib/worktree-creation-flow-dedupe.test.ts": 1253, - "src/renderer/src/lib/worktree-creation-flow-ready-toast.test.ts": 1699, - "src/renderer/src/lib/worktree-creation-flow-stranded-surface.test.ts": 1183, - "src/renderer/src/lib/worktree-creation-flow.test.ts": 1987, - "src/renderer/src/lib/worktree-creation-hook-flow.test.ts": 1338, - "src/renderer/src/lib/worktree-creation-hook-preparation.test.ts": 185, - "src/renderer/src/lib/worktree-creation-ready-toast.test.tsx": 173, - "src/renderer/src/lib/worktree-creation-structured-session-host-decline.test.ts": 1994, - "src/renderer/src/lib/worktree-creation-structured-session.test.ts": 1041, - "src/renderer/src/lib/worktree-creation-surface.test.ts": 54, - "src/renderer/src/lib/worktree-default-display-name.test.ts": 52, - "src/renderer/src/lib/worktree-display-name-order.test.ts": 47, - "src/renderer/src/lib/worktree-draft-startup-view-mode.test.ts": 1335, - "src/renderer/src/lib/worktree-git-identity-display.test.ts": 46, - "src/renderer/src/lib/worktree-host-connection-phase.test.ts": 1513, - "src/renderer/src/lib/worktree-initial-terminal-close-records.test.ts": 1617, - "src/renderer/src/lib/worktree-jump-navigation.test.ts": 354, - "src/renderer/src/lib/worktree-live-terminal-surface-owners.test.ts": 314, - "src/renderer/src/lib/worktree-operation-generation.test.ts": 379, - "src/renderer/src/lib/worktree-operation-route.test.ts": 356, - "src/renderer/src/lib/worktree-palette-attachment-urls.test.ts": 109, - "src/renderer/src/lib/worktree-palette-comment-snippet.test.ts": 50, - "src/renderer/src/lib/worktree-palette-create-action.test.ts": 63, - "src/renderer/src/lib/worktree-palette-multi-keyword.test.ts": 235, - "src/renderer/src/lib/worktree-palette-review-match.test.ts": 194, - "src/renderer/src/lib/worktree-palette-runtime-owner-identity.test.ts": 402, - "src/renderer/src/lib/worktree-palette-search.test.ts": 236, - "src/renderer/src/lib/worktree-palette-task-url-match.test.ts": 133, - "src/renderer/src/lib/worktree-reactivation-preserved-pane-replacement.test.ts": 1546, - "src/renderer/src/lib/worktree-reactivation-runtime-owned-resume-deferral.test.ts": 1410, - "src/renderer/src/lib/worktree-reactivation-tab-forkbomb.test.ts": 1937, - "src/renderer/src/lib/worktree-row-open-in-owner.test.ts": 364, - "src/renderer/src/lib/worktree-runtime-owner-index.detected-perf.test.ts": 298, - "src/renderer/src/lib/worktree-runtime-owner-index.detected.test.ts": 102, - "src/renderer/src/lib/worktree-runtime-owner-index.test.ts": 49, - "src/renderer/src/lib/worktree-runtime-owner.test.ts": 356, - "src/renderer/src/lib/worktree-sort-order-host-split.test.ts": 320, - "src/renderer/src/lib/worktree-sort-order-persistence.test.ts": 342, - "src/renderer/src/lib/worktree-status-spinner-launch-agent.test.ts": 127, - "src/renderer/src/lib/worktree-status-terminal-layout-roots.test.ts": 115, - "src/renderer/src/lib/worktree-status.interrupted.test.ts": 106, - "src/renderer/src/lib/worktree-status.test.ts": 121, - "src/renderer/src/lib/worktree-visit-recency.test.ts": 45, - "src/renderer/src/runtime/abortable-runtime-environment-call.test.ts": 181, - "src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts": 70, - "src/renderer/src/runtime/agent-session-keyboard-capability.test.ts": 96, - "src/renderer/src/runtime/agent-session-operation-id.test.ts": 87, - "src/renderer/src/runtime/antigravity-generation-compatibility.test.ts": 349, - "src/renderer/src/runtime/browser-client-host-identity.test.ts": 52, - "src/renderer/src/runtime/browser-workspace-tab-close-plan.test.ts": 55, - "src/renderer/src/runtime/browser-workspace-tab-close.test.ts": 238, - "src/renderer/src/runtime/client-hosted-browser-close-intent-replay.test.ts": 374, - "src/renderer/src/runtime/client-hosted-browser-close-intents.test.ts": 234, - "src/renderer/src/runtime/client-hosted-browser-row-close.test.ts": 264, - "src/renderer/src/runtime/close-mirrored-editor-tab.test.ts": 224, - "src/renderer/src/runtime/file-explorer-delete-owner-provenance.test.ts": 1862, - "src/renderer/src/runtime/focus-runtime-terminal-surface-chat-view.test.ts": 570, - "src/renderer/src/runtime/focus-runtime-terminal-surface-readiness.test.ts": 437, - "src/renderer/src/runtime/github-check-details-timeout.test.ts": 64, - "src/renderer/src/runtime/gitlab-job-trace-client.test.ts": 137, - "src/renderer/src/runtime/gitlab-owner-api.test.ts": 111, - "src/renderer/src/runtime/host-model-catalog-snapshots.test.ts": 1805, - "src/renderer/src/runtime/host-route-call.test.ts": 338, - "src/renderer/src/runtime/host-session-mirror-empty-inventory-settle.test.ts": 944, - "src/renderer/src/runtime/host-session-mirror-flap-hydration.test.ts": 384, - "src/renderer/src/runtime/host-session-mirror-hydration-drain.test.ts": 343, - "src/renderer/src/runtime/host-session-mirror-hydration-frame-ordering.test.tsx": 2135, - "src/renderer/src/runtime/host-session-mirror-settle-receipt-frames.test.tsx": 2168, - "src/renderer/src/runtime/host-session-snapshot-authority-client-hosted.test.ts": 54, - "src/renderer/src/runtime/host-structured-agents.test.ts": 537, - "src/renderer/src/runtime/local-runtime-capabilities.test.ts": 139, - "src/renderer/src/runtime/local-session-tab-close-owner.test.ts": 44, - "src/renderer/src/runtime/local-structured-chats.test.ts": 69, - "src/renderer/src/runtime/local-structured-session-empty-worktree-visibility.test.ts": 1662, - "src/renderer/src/runtime/local-structured-session-retired-epoch-repair.test.ts": 1933, - "src/renderer/src/runtime/local-structured-session-reveal-visibility.test.ts": 2162, - "src/renderer/src/runtime/local-structured-session-tabs-host-isolation.test.ts": 1048, - "src/renderer/src/runtime/local-structured-session-tabs-sync-hook.test.ts": 1933, - "src/renderer/src/runtime/local-structured-session-tabs-sync.test.ts": 2619, - "src/renderer/src/runtime/local-structured-session-tabs-sync/retired-epoch-repair.test.ts": 63, - "src/renderer/src/runtime/local-structured-session-unverifiable-inventory.test.ts": 1704, - "src/renderer/src/runtime/mirrored-agent-status-clock-skew.test.ts": 1534, - "src/renderer/src/runtime/mobile-markdown-bridge-save-guards.test.ts": 1255, - "src/renderer/src/runtime/mobile-markdown-bridge.test.ts": 2156, - "src/renderer/src/runtime/native-chat-launch-draft-runtime-resolution.test.ts": 53, - "src/renderer/src/runtime/paired-agent-row-turn-outcome-arms.test.ts": 1736, - "src/renderer/src/runtime/paired-host-client-capabilities.test.ts": 251, - "src/renderer/src/runtime/paired-host-structured-launch-bookkeeping.test.ts": 487, - "src/renderer/src/runtime/paired-reconnect-sidebar-agent-count.test.ts": 1745, - "src/renderer/src/runtime/remote-agent-row-last-assistant-message.test.ts": 1747, - "src/renderer/src/runtime/remote-agent-session-launch.test.ts": 101, - "src/renderer/src/runtime/remote-host-file-delete-repro.test.ts": 1622, - "src/renderer/src/runtime/remote-host-file-open-repro.test.ts": 1387, - "src/renderer/src/runtime/remote-runtime-session-tabs-inflight.test.ts": 64, - "src/renderer/src/runtime/remote-runtime-snapshot-outcome.test.ts": 150, - "src/renderer/src/runtime/remote-runtime-terminal-color-push.test.ts": 89, - "src/renderer/src/runtime/remote-runtime-terminal-end-verdict.test.ts": 156, - "src/renderer/src/runtime/remote-runtime-terminal-frame-drop-resync.test.ts": 152, - "src/renderer/src/runtime/remote-runtime-terminal-parse-backpressure.test.ts": 442, - "src/renderer/src/runtime/remote-runtime-terminal-snapshot-kitty-flags.test.ts": 141, - "src/renderer/src/runtime/remote-runtime-terminal-stale-stream-frames.test.ts": 115, - "src/renderer/src/runtime/remote-runtime-terminal-stall-recovery.test.ts": 273, - "src/renderer/src/runtime/remote-server-install-failure-probe.test.ts": 49, - "src/renderer/src/runtime/remote-server-parity.test.ts": 1540, - "src/renderer/src/runtime/remote-server-restart-wait.test.ts": 64, - "src/renderer/src/runtime/remote-server-update-coordinator.test.ts": 112, - "src/renderer/src/runtime/remote-terminal-layout-resolution.test.ts": 55, - "src/renderer/src/runtime/remote-terminal-stream-watchdog.test.ts": 54, - "src/renderer/src/runtime/restored-client-hosted-browser-host-attach.test.ts": 59, - "src/renderer/src/runtime/restored-client-hosted-browser-host-restart-attach.test.ts": 301, - "src/renderer/src/runtime/runtime-antigravity-accounts-client.test.ts": 69, - "src/renderer/src/runtime/runtime-client-events.test.ts": 61, - "src/renderer/src/runtime/runtime-client-target.test.ts": 46, - "src/renderer/src/runtime/runtime-environment-pairing-refresh.test.ts": 98, - "src/renderer/src/runtime/runtime-environment-ssh-state.test.ts": 1589, - "src/renderer/src/runtime/runtime-file-client-download.test.ts": 397, - "src/renderer/src/runtime/runtime-file-client-external-import.test.ts": 419, - "src/renderer/src/runtime/runtime-file-client-mutation-ownership.test.ts": 406, - "src/renderer/src/runtime/runtime-file-client-search-listing.test.ts": 606, - "src/renderer/src/runtime/runtime-file-client-watch.test.ts": 508, - "src/renderer/src/runtime/runtime-file-client.test.ts": 397, - "src/renderer/src/runtime/runtime-file-import-pairing-revision.test.ts": 394, - "src/renderer/src/runtime/runtime-file-range-client.test.ts": 338, - "src/renderer/src/runtime/runtime-file-search-bounds.test.ts": 49, - "src/renderer/src/runtime/runtime-git-client-branch-line-total.test.ts": 308, - "src/renderer/src/runtime/runtime-git-client-merge.test.ts": 338, - "src/renderer/src/runtime/runtime-git-client.test.ts": 421, - "src/renderer/src/runtime/runtime-git-worktree-scope-client.test.ts": 439, - "src/renderer/src/runtime/runtime-hooks-client.test.ts": 101, - "src/renderer/src/runtime/runtime-host-connection-state.test.ts": 57, - "src/renderer/src/runtime/runtime-host-contact-parity.test.ts": 58, - "src/renderer/src/runtime/runtime-host-contact-regained.test.ts": 342, - "src/renderer/src/runtime/runtime-jira-client.test.ts": 230, - "src/renderer/src/runtime/runtime-jira-connection-status.test.ts": 50, - "src/renderer/src/runtime/runtime-jira-payload-stream.test.ts": 198, + "src/renderer/src/lib/terminal-links-redos.test.ts": 49, + "src/renderer/src/lib/terminal-links.test.ts": 237, + "src/renderer/src/lib/terminal-pane-title-sanitization.test.ts": 40, + "src/renderer/src/lib/terminal-path-normalization.test.ts": 39, + "src/renderer/src/lib/terminal-quick-command-project-scope.test.ts": 54, + "src/renderer/src/lib/terminal-quick-command-score-budget.test.ts": 44, + "src/renderer/src/lib/terminal-quick-command-search.test.ts": 57, + "src/renderer/src/lib/terminal-reveal-identity.test.ts": 31, + "src/renderer/src/lib/terminal-shortcut-capture-notification.test.tsx": 1693, + "src/renderer/src/lib/terminal-tab-for-pty-id.test.ts": 38, + "src/renderer/src/lib/terminal-tab-owner-activation.test.ts": 33, + "src/renderer/src/lib/terminal-theme.test.ts": 76, + "src/renderer/src/lib/terminal-worktree-route.test.ts": 356, + "src/renderer/src/lib/text-control-paste-ownership.test.ts": 173, + "src/renderer/src/lib/text-control-paste.test.ts": 189, + "src/renderer/src/lib/titlebar-left-chrome.test.ts": 37, + "src/renderer/src/lib/titlebar-worktree-history-controls.test.ts": 34, + "src/renderer/src/lib/tui-agent-startup.test.ts": 115, + "src/renderer/src/lib/typing-latency/diagnostic-lifecycle.test.ts": 202, + "src/renderer/src/lib/typing-latency/diagnostic-summary.test.ts": 55, + "src/renderer/src/lib/typing-latency/echo-instrumentation.test.ts": 118, + "src/renderer/src/lib/typing-latency/input-events.test.ts": 161, + "src/renderer/src/lib/typing-latency/input-source.test.ts": 49, + "src/renderer/src/lib/typing-latency/sample-window.test.ts": 29, + "src/renderer/src/lib/ui-zoom.test.ts": 50, + "src/renderer/src/lib/unified-tab-anchor-insertion.test.ts": 35, + "src/renderer/src/lib/unread-badge-count-migration.test.ts": 510, + "src/renderer/src/lib/unread-badge-count-selector.test.ts": 1599, + "src/renderer/src/lib/unread-badge-count.test.ts": 393, + "src/renderer/src/lib/update-check-click-options.test.ts": 40, + "src/renderer/src/lib/updater-beforeunload.test.ts": 34, + "src/renderer/src/lib/use-tab-agent-observed-signal-dispatch.test.tsx": 1456, + "src/renderer/src/lib/use-tab-agent-opencode-native-title.test.ts": 1667, + "src/renderer/src/lib/use-tab-agent-pi-identity.test.ts": 128, + "src/renderer/src/lib/use-tab-agent-process-signals.test.ts": 1283, + "src/renderer/src/lib/use-tab-agent-remote-pty-selector.test.ts": 95, + "src/renderer/src/lib/use-tab-agent-retained-identity.test.ts": 1503, + "src/renderer/src/lib/use-tab-agent-sleeping-session.test.ts": 1597, + "src/renderer/src/lib/use-tab-agent.test.ts": 1239, + "src/renderer/src/lib/user-opened-local-path.test.ts": 1426, + "src/renderer/src/lib/visible-overlay.test.ts": 181, + "src/renderer/src/lib/wake-sleeping-agents-in-background.test.ts": 210, + "src/renderer/src/lib/wake-sleeping-agents-live-done.test.ts": 192, + "src/renderer/src/lib/web-client-location.test.ts": 32, + "src/renderer/src/lib/window-label-formatter.test.ts": 38, + "src/renderer/src/lib/window-visibility-interval.test.ts": 50, + "src/renderer/src/lib/window-visibility-timeout-poller.test.ts": 203, + "src/renderer/src/lib/windows-path-access-guards.test.ts": 271, + "src/renderer/src/lib/windows-terminal-capabilities-race.test.ts": 326, + "src/renderer/src/lib/windows-terminal-capabilities.test.ts": 448, + "src/renderer/src/lib/windows-terminal-capability-reprobe.test.ts": 197, + "src/renderer/src/lib/work-item-link-query-bounds.test.ts": 44, + "src/renderer/src/lib/work-item-lookup-text.test.ts": 34, + "src/renderer/src/lib/worker-terminal-takeover-report.test.ts": 295, + "src/renderer/src/lib/workspace-activation-path-gate.test.ts": 1725, + "src/renderer/src/lib/workspace-activation-terminal-focus.test.ts": 341, + "src/renderer/src/lib/workspace-attachment-terminal-origin.test.ts": 84, + "src/renderer/src/lib/workspace-browser-tab-open.test.ts": 583, + "src/renderer/src/lib/workspace-composer-initial-focus.test.ts": 177, + "src/renderer/src/lib/workspace-create-error-format.test.ts": 59, + "src/renderer/src/lib/workspace-directory.test.ts": 281, + "src/renderer/src/lib/workspace-doc-address-input.test.ts": 324, + "src/renderer/src/lib/workspace-emoji-shortcodes.lazy.test.ts": 58, + "src/renderer/src/lib/workspace-emoji-shortcodes.test.ts": 76, + "src/renderer/src/lib/workspace-file-drag-source-runtime.test.ts": 57, + "src/renderer/src/lib/workspace-file-drag.test.ts": 45, + "src/renderer/src/lib/workspace-port-groups.test.ts": 35, + "src/renderer/src/lib/workspace-port-host-availability.test.ts": 42, + "src/renderer/src/lib/workspace-port-host-scoped-scan.test.ts": 338, + "src/renderer/src/lib/workspace-port-scan-debounce.test.ts": 49, + "src/renderer/src/lib/workspace-port-scan-publish.test.ts": 507, + "src/renderer/src/lib/workspace-session-browser-history.test.ts": 281, + "src/renderer/src/lib/workspace-session-editor-drafts.test.ts": 292, + "src/renderer/src/lib/workspace-session-host-contention.test.ts": 334, + "src/renderer/src/lib/workspace-session-host-persistence.test.ts": 322, + "src/renderer/src/lib/workspace-session-host-split.test.ts": 314, + "src/renderer/src/lib/workspace-session-hydration-keys.test.ts": 62, + "src/renderer/src/lib/workspace-session-liveness.test.ts": 275, + "src/renderer/src/lib/workspace-session-patch.test.ts": 327, + "src/renderer/src/lib/workspace-session-persistence-gate.test.ts": 275, + "src/renderer/src/lib/workspace-session-relevant-fields.test.ts": 305, + "src/renderer/src/lib/workspace-session-ssh-partition-ownership.test.ts": 324, + "src/renderer/src/lib/workspace-session-ssh-partition-round-trip.test.ts": 322, + "src/renderer/src/lib/workspace-session-staged-browser-tabs.test.ts": 33, + "src/renderer/src/lib/workspace-session.test.ts": 335, + "src/renderer/src/lib/workspace-surface-reveal.test.ts": 217, + "src/renderer/src/lib/workspace-tab-agent-metadata.test.ts": 43, + "src/renderer/src/lib/workspace-tab-palette-activation.store.test.ts": 1570, + "src/renderer/src/lib/workspace-tab-palette-activation.test.ts": 288, + "src/renderer/src/lib/workspace-tab-palette-results.test.ts": 328, + "src/renderer/src/lib/workspace-tab-palette-search.test.ts": 412, + "src/renderer/src/lib/workspace-terminal-host-authority.test.ts": 1514, + "src/renderer/src/lib/worktree-activation-agent-startup.test.ts": 1464, + "src/renderer/src/lib/worktree-activation-automation-filter.test.ts": 1761, + "src/renderer/src/lib/worktree-activation-created-agent.test.ts": 1604, + "src/renderer/src/lib/worktree-activation-default-agent-chat.test.ts": 1652, + "src/renderer/src/lib/worktree-activation-default-tabs.test.ts": 1507, + "src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts": 1785, + "src/renderer/src/lib/worktree-activation-empty-remote.test.ts": 1541, + "src/renderer/src/lib/worktree-activation-issue-command.test.ts": 1532, + "src/renderer/src/lib/worktree-activation-launch-awaiting-host.test.ts": 1667, + "src/renderer/src/lib/worktree-activation-pty-inventory.test.ts": 336, + "src/renderer/src/lib/worktree-activation-reveal.test.ts": 1823, + "src/renderer/src/lib/worktree-activation-setup-script.test.ts": 1773, + "src/renderer/src/lib/worktree-activation-structured-chat-surface.test.ts": 1704, + "src/renderer/src/lib/worktree-activation-web-runtime.test.ts": 1702, + "src/renderer/src/lib/worktree-activity-state.test.ts": 67, + "src/renderer/src/lib/worktree-agent-activation-gate.test.ts": 1507, + "src/renderer/src/lib/worktree-agent-activation-seam.test.ts": 2047, + "src/renderer/src/lib/worktree-agent-structured-inventory.test.ts": 278, + "src/renderer/src/lib/worktree-attachment-label.test.ts": 37, + "src/renderer/src/lib/worktree-creation-agent-seeding.test.ts": 1047, + "src/renderer/src/lib/worktree-creation-agent-seeds.test.ts": 1313, + "src/renderer/src/lib/worktree-creation-backend-startup-focus.test.ts": 1458, + "src/renderer/src/lib/worktree-creation-background-mount.test.ts": 1910, + "src/renderer/src/lib/worktree-creation-chat-setup.test.ts": 1851, + "src/renderer/src/lib/worktree-creation-flow-dedupe.test.ts": 1029, + "src/renderer/src/lib/worktree-creation-flow-ready-toast.test.ts": 1742, + "src/renderer/src/lib/worktree-creation-flow-stranded-surface.test.ts": 1068, + "src/renderer/src/lib/worktree-creation-flow.test.ts": 2048, + "src/renderer/src/lib/worktree-creation-hook-flow.test.ts": 1457, + "src/renderer/src/lib/worktree-creation-hook-preparation.test.ts": 155, + "src/renderer/src/lib/worktree-creation-ready-toast.test.tsx": 415, + "src/renderer/src/lib/worktree-creation-structured-session-host-decline.test.ts": 2069, + "src/renderer/src/lib/worktree-creation-structured-session.test.ts": 1144, + "src/renderer/src/lib/worktree-creation-surface.test.ts": 37, + "src/renderer/src/lib/worktree-default-display-name.test.ts": 39, + "src/renderer/src/lib/worktree-display-name-order.test.ts": 33, + "src/renderer/src/lib/worktree-draft-startup-view-mode.test.ts": 1417, + "src/renderer/src/lib/worktree-git-identity-display.test.ts": 25, + "src/renderer/src/lib/worktree-host-connection-phase.test.ts": 1341, + "src/renderer/src/lib/worktree-initial-terminal-close-records.test.ts": 1795, + "src/renderer/src/lib/worktree-jump-navigation.test.ts": 328, + "src/renderer/src/lib/worktree-live-terminal-surface-owners.test.ts": 316, + "src/renderer/src/lib/worktree-operation-generation.test.ts": 443, + "src/renderer/src/lib/worktree-operation-route.test.ts": 358, + "src/renderer/src/lib/worktree-palette-attachment-urls.test.ts": 159, + "src/renderer/src/lib/worktree-palette-comment-snippet.test.ts": 44, + "src/renderer/src/lib/worktree-palette-create-action.test.ts": 57, + "src/renderer/src/lib/worktree-palette-multi-keyword.test.ts": 274, + "src/renderer/src/lib/worktree-palette-review-match.test.ts": 121, + "src/renderer/src/lib/worktree-palette-runtime-owner-identity.test.ts": 373, + "src/renderer/src/lib/worktree-palette-search.test.ts": 187, + "src/renderer/src/lib/worktree-palette-task-url-match.test.ts": 153, + "src/renderer/src/lib/worktree-reactivation-preserved-pane-replacement.test.ts": 1824, + "src/renderer/src/lib/worktree-reactivation-runtime-owned-resume-deferral.test.ts": 1567, + "src/renderer/src/lib/worktree-reactivation-tab-forkbomb.test.ts": 1873, + "src/renderer/src/lib/worktree-row-open-in-owner.test.ts": 340, + "src/renderer/src/lib/worktree-runtime-owner-index.detected-perf.test.ts": 284, + "src/renderer/src/lib/worktree-runtime-owner-index.detected.test.ts": 113, + "src/renderer/src/lib/worktree-runtime-owner-index.test.ts": 32, + "src/renderer/src/lib/worktree-runtime-owner.test.ts": 370, + "src/renderer/src/lib/worktree-sort-order-host-split.test.ts": 322, + "src/renderer/src/lib/worktree-sort-order-persistence.test.ts": 346, + "src/renderer/src/lib/worktree-status-spinner-launch-agent.test.ts": 91, + "src/renderer/src/lib/worktree-status-terminal-layout-roots.test.ts": 95, + "src/renderer/src/lib/worktree-status.interrupted.test.ts": 77, + "src/renderer/src/lib/worktree-status.test.ts": 117, + "src/renderer/src/lib/worktree-visit-recency.test.ts": 35, + "src/renderer/src/lib/wsl-host-account-owner.test.ts": 53, + "src/renderer/src/runtime/abortable-runtime-environment-call.test.ts": 183, + "src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts": 255, + "src/renderer/src/runtime/agent-session-keyboard-capability.test.ts": 236, + "src/renderer/src/runtime/agent-session-operation-id.test.ts": 33, + "src/renderer/src/runtime/antigravity-generation-compatibility.test.ts": 292, + "src/renderer/src/runtime/browser-client-host-identity.test.ts": 36, + "src/renderer/src/runtime/browser-workspace-tab-close-plan.test.ts": 40, + "src/renderer/src/runtime/browser-workspace-tab-close.test.ts": 193, + "src/renderer/src/runtime/client-hosted-browser-close-intent-replay.test.ts": 326, + "src/renderer/src/runtime/client-hosted-browser-close-intents.test.ts": 226, + "src/renderer/src/runtime/client-hosted-browser-row-close.test.ts": 293, + "src/renderer/src/runtime/close-mirrored-editor-tab.test.ts": 199, + "src/renderer/src/runtime/file-explorer-delete-owner-provenance.test.ts": 2152, + "src/renderer/src/runtime/focus-runtime-terminal-surface-chat-view.test.ts": 168, + "src/renderer/src/runtime/focus-runtime-terminal-surface-readiness.test.ts": 173, + "src/renderer/src/runtime/github-check-details-timeout.test.ts": 42, + "src/renderer/src/runtime/gitlab-job-trace-client.test.ts": 287, + "src/renderer/src/runtime/gitlab-owner-api.test.ts": 284, + "src/renderer/src/runtime/host-model-catalog-snapshots.test.ts": 1780, + "src/renderer/src/runtime/host-route-call.test.ts": 349, + "src/renderer/src/runtime/host-session-mirror-empty-inventory-settle.test.ts": 600, + "src/renderer/src/runtime/host-session-mirror-flap-hydration.test.ts": 373, + "src/renderer/src/runtime/host-session-mirror-hydration-drain.test.ts": 354, + "src/renderer/src/runtime/host-session-mirror-hydration-frame-ordering.test.tsx": 1709, + "src/renderer/src/runtime/host-session-mirror-settle-receipt-frames.test.tsx": 1883, + "src/renderer/src/runtime/host-session-snapshot-authority-client-hosted.test.ts": 35, + "src/renderer/src/runtime/host-structured-agents.test.ts": 596, + "src/renderer/src/runtime/local-runtime-capabilities.test.ts": 169, + "src/renderer/src/runtime/local-session-tab-close-owner.test.ts": 30, + "src/renderer/src/runtime/local-structured-chats.test.ts": 280, + "src/renderer/src/runtime/local-structured-session-empty-worktree-visibility.test.ts": 1348, + "src/renderer/src/runtime/local-structured-session-retired-epoch-repair.test.ts": 1490, + "src/renderer/src/runtime/local-structured-session-reveal-visibility.test.ts": 1498, + "src/renderer/src/runtime/local-structured-session-tabs-host-isolation.test.ts": 808, + "src/renderer/src/runtime/local-structured-session-tabs-sync-hook.test.ts": 1689, + "src/renderer/src/runtime/local-structured-session-tabs-sync.test.ts": 2060, + "src/renderer/src/runtime/local-structured-session-tabs-sync/retired-epoch-repair.test.ts": 45, + "src/renderer/src/runtime/local-structured-session-unverifiable-inventory.test.ts": 1581, + "src/renderer/src/runtime/mirrored-agent-status-clock-skew.test.ts": 1538, + "src/renderer/src/runtime/mobile-markdown-bridge-save-guards.test.ts": 1399, + "src/renderer/src/runtime/mobile-markdown-bridge.test.ts": 2091, + "src/renderer/src/runtime/mounted-terminal-tab-registry.test.ts": 523, + "src/renderer/src/runtime/native-chat-launch-draft-runtime-resolution.test.ts": 29, + "src/renderer/src/runtime/paired-agent-row-turn-outcome-arms.test.ts": 1326, + "src/renderer/src/runtime/paired-host-client-capabilities.test.ts": 328, + "src/renderer/src/runtime/paired-host-structured-launch-bookkeeping.test.ts": 587, + "src/renderer/src/runtime/paired-reconnect-sidebar-agent-count.test.ts": 1639, + "src/renderer/src/runtime/remote-agent-row-last-assistant-message.test.ts": 1363, + "src/renderer/src/runtime/remote-agent-session-launch.test.ts": 255, + "src/renderer/src/runtime/remote-host-file-delete-repro.test.ts": 1568, + "src/renderer/src/runtime/remote-host-file-open-repro.test.ts": 1332, + "src/renderer/src/runtime/remote-restore-spawn-intent.test.ts": 205, + "src/renderer/src/runtime/remote-runtime-session-tabs-inflight.test.ts": 39, + "src/renderer/src/runtime/remote-runtime-snapshot-outcome.test.ts": 106, + "src/renderer/src/runtime/remote-runtime-terminal-color-push.test.ts": 191, + "src/renderer/src/runtime/remote-runtime-terminal-end-verdict.test.ts": 101, + "src/renderer/src/runtime/remote-runtime-terminal-frame-drop-resync.test.ts": 116, + "src/renderer/src/runtime/remote-runtime-terminal-parse-backpressure.test.ts": 385, + "src/renderer/src/runtime/remote-runtime-terminal-snapshot-kitty-flags.test.ts": 92, + "src/renderer/src/runtime/remote-runtime-terminal-stale-stream-frames.test.ts": 82, + "src/renderer/src/runtime/remote-runtime-terminal-stall-recovery.test.ts": 197, + "src/renderer/src/runtime/remote-server-install-failure-probe.test.ts": 31, + "src/renderer/src/runtime/remote-server-parity.test.ts": 1348, + "src/renderer/src/runtime/remote-server-restart-wait.test.ts": 40, + "src/renderer/src/runtime/remote-server-update-coordinator.test.ts": 102, + "src/renderer/src/runtime/remote-terminal-layout-resolution.test.ts": 45, + "src/renderer/src/runtime/remote-terminal-stream-watchdog.test.ts": 37, + "src/renderer/src/runtime/restored-client-hosted-browser-host-attach.test.ts": 46, + "src/renderer/src/runtime/restored-client-hosted-browser-host-restart-attach.test.ts": 418, + "src/renderer/src/runtime/resume-host-automatic-restores.test.ts": 205, + "src/renderer/src/runtime/runtime-antigravity-accounts-client.test.ts": 49, + "src/renderer/src/runtime/runtime-client-events.test.ts": 48, + "src/renderer/src/runtime/runtime-client-target.test.ts": 40, + "src/renderer/src/runtime/runtime-environment-pairing-refresh.test.ts": 94, + "src/renderer/src/runtime/runtime-environment-ssh-state.test.ts": 1370, + "src/renderer/src/runtime/runtime-file-client-download.test.ts": 324, + "src/renderer/src/runtime/runtime-file-client-external-import.test.ts": 444, + "src/renderer/src/runtime/runtime-file-client-mutation-ownership.test.ts": 419, + "src/renderer/src/runtime/runtime-file-client-search-listing.test.ts": 464, + "src/renderer/src/runtime/runtime-file-client-watch.test.ts": 322, + "src/renderer/src/runtime/runtime-file-client.test.ts": 420, + "src/renderer/src/runtime/runtime-file-import-pairing-revision.test.ts": 398, + "src/renderer/src/runtime/runtime-file-range-client.test.ts": 362, + "src/renderer/src/runtime/runtime-file-search-bounds.test.ts": 35, + "src/renderer/src/runtime/runtime-git-client-branch-line-total.test.ts": 346, + "src/renderer/src/runtime/runtime-git-client-merge.test.ts": 293, + "src/renderer/src/runtime/runtime-git-client.test.ts": 375, + "src/renderer/src/runtime/runtime-git-worktree-scope-client.test.ts": 462, + "src/renderer/src/runtime/runtime-hooks-client.test.ts": 204, + "src/renderer/src/runtime/runtime-host-connection-state.test.ts": 47, + "src/renderer/src/runtime/runtime-host-contact-parity.test.ts": 51, + "src/renderer/src/runtime/runtime-host-contact-regained.test.ts": 376, + "src/renderer/src/runtime/runtime-jira-board-filter-compatibility.test.ts": 372, + "src/renderer/src/runtime/runtime-jira-client.test.ts": 443, + "src/renderer/src/runtime/runtime-jira-connection-status.test.ts": 36, + "src/renderer/src/runtime/runtime-jira-detail-client.test.ts": 445, + "src/renderer/src/runtime/runtime-jira-detail-images.test.ts": 383, + "src/renderer/src/runtime/runtime-jira-image-cache.test.ts": 1057, + "src/renderer/src/runtime/runtime-jira-image-client.test.ts": 170, + "src/renderer/src/runtime/runtime-jira-payload-stream.test.ts": 174, + "src/renderer/src/runtime/runtime-jira-priority-client.test.ts": 410, + "src/renderer/src/runtime/runtime-jira-project-client.test.ts": 175, + "src/renderer/src/runtime/runtime-jira-saved-filter-state.test.ts": 171, + "src/renderer/src/runtime/runtime-jira-status-filters.test.ts": 325, + "src/renderer/src/runtime/runtime-jira-summary-generation-client.test.ts": 330, "src/renderer/src/runtime/runtime-layout-client.test.ts": 115, - "src/renderer/src/runtime/runtime-legacy-inventory-budget.test.ts": 58, - "src/renderer/src/runtime/runtime-legacy-quick-open-inventory-expiry.test.ts": 733, - "src/renderer/src/runtime/runtime-legacy-recent-freshness.test.ts": 343, - "src/renderer/src/runtime/runtime-linear-client.test.ts": 118, - "src/renderer/src/runtime/runtime-path-existence-batch.test.ts": 327, - "src/renderer/src/runtime/runtime-path-existence-error-parity.test.ts": 339, - "src/renderer/src/runtime/runtime-path-existence-pairing.test.ts": 307, - "src/renderer/src/runtime/runtime-path-existence-queue-pairing.test.ts": 320, - "src/renderer/src/runtime/runtime-provider-accounts-client.test.ts": 132, - "src/renderer/src/runtime/runtime-provider-search-bounds.test.ts": 50, - "src/renderer/src/runtime/runtime-quick-open-version-compatibility.test.ts": 484, - "src/renderer/src/runtime/runtime-repo-client.test.ts": 183, - "src/renderer/src/runtime/runtime-repo-search-bounds.test.ts": 53, - "src/renderer/src/runtime/runtime-rpc-client-pairing-revision.test.ts": 89, - "src/renderer/src/runtime/runtime-rpc-client.test.ts": 128, - "src/renderer/src/runtime/runtime-rpc-result.test.ts": 46, - "src/renderer/src/runtime/runtime-server-directory-browser.test.ts": 86, - "src/renderer/src/runtime/runtime-skills-client.test.ts": 90, - "src/renderer/src/runtime/runtime-skills-delete-client.test.ts": 74, - "src/renderer/src/runtime/runtime-terminal-inspection.test.ts": 1879, - "src/renderer/src/runtime/runtime-terminal-stream.test.ts": 408, - "src/renderer/src/runtime/runtime-terminal-verified-input.test.ts": 1396, - "src/renderer/src/runtime/runtime-worktree-selector.test.ts": 310, - "src/renderer/src/runtime/structured-agent-session-client.test.ts": 184, - "src/renderer/src/runtime/structured-agent-session-close.test.ts": 92, - "src/renderer/src/runtime/structured-agent-session-host-capability.test.tsx": 371, - "src/renderer/src/runtime/structured-agent-session-status-feed-lifecycle.test.ts": 95, - "src/renderer/src/runtime/structured-agent-session-status-feed.test.ts": 104, - "src/renderer/src/runtime/structured-agent-session-tab-retirement.test.ts": 1480, - "src/renderer/src/runtime/structured-agent-session-turn-completion-feed.test.ts": 102, - "src/renderer/src/runtime/structured-conversation-tab-replacement.test.ts": 1794, - "src/renderer/src/runtime/structured-session-orca-session-id.test.ts": 271, - "src/renderer/src/runtime/structured-session-tab-inventory.test.ts": 57, - "src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts": 448, - "src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts": 488, - "src/renderer/src/runtime/sync-runtime-graph-automation-leaf.test.ts": 471, - "src/renderer/src/runtime/sync-runtime-graph-browser.test.ts": 411, - "src/renderer/src/runtime/sync-runtime-graph-conversion-publish.test.ts": 1416, - "src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts": 425, - "src/renderer/src/runtime/sync-runtime-graph-key-reuse.test.ts": 504, - "src/renderer/src/runtime/sync-runtime-graph-parked-leaf.test.ts": 445, - "src/renderer/src/runtime/sync-runtime-graph-payload-partition.test.ts": 443, - "src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts": 513, - "src/renderer/src/runtime/sync-runtime-graph-publication-cost.test.ts": 538, - "src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts": 396, - "src/renderer/src/runtime/sync-runtime-graph-terminal-layout.test.ts": 877, - "src/renderer/src/runtime/sync-runtime-graph-terminal-registration-ownership.test.ts": 429, - "src/renderer/src/runtime/sync-runtime-graph-terminal-surface-projection.test.ts": 513, - "src/renderer/src/runtime/sync-runtime-graph-workspace-publication.test.ts": 423, - "src/renderer/src/runtime/sync-runtime-graph.test.ts": 483, - "src/renderer/src/runtime/sync-runtime-graph/mobile-terminal-theme.test.ts": 74, - "src/renderer/src/runtime/use-remote-runtime-recovery-triggers.test.ts": 277, - "src/renderer/src/runtime/use-runtime-session-mirror-environment-key.test.ts": 1954, - "src/renderer/src/runtime/use-worktree-runtime-target.test.ts": 1773, - "src/renderer/src/runtime/web-runtime-browser-capability-cleanup.test.ts": 603, - "src/renderer/src/runtime/web-runtime-browser-creation-placement.test.ts": 1772, - "src/renderer/src/runtime/web-runtime-browser-materialization.test.ts": 44, - "src/renderer/src/runtime/web-runtime-browser-tab-staging-hosting-intent.test.ts": 57, - "src/renderer/src/runtime/web-runtime-pane-layout-intent.test.ts": 561, - "src/renderer/src/runtime/web-runtime-session-browser-client-placement.test.ts": 681, - "src/renderer/src/runtime/web-runtime-session-browser-create-failure.test.ts": 610, - "src/renderer/src/runtime/web-runtime-session-browser-create-focus.test.ts": 871, - "src/renderer/src/runtime/web-runtime-session-browser-create-split-placement.test.ts": 969, - "src/renderer/src/runtime/web-runtime-session-browser-create-staged-focus.test.ts": 643, - "src/renderer/src/runtime/web-runtime-session-browser-create-staged-hosting-intent.test.ts": 480, - "src/renderer/src/runtime/web-runtime-session-browser-create-staging.test.ts": 1143, - "src/renderer/src/runtime/web-runtime-session-browser-placement-staleness.test.ts": 606, - "src/renderer/src/runtime/web-runtime-session-tab-activate-close.test.ts": 647, - "src/renderer/src/runtime/web-runtime-session-tab-move.test.ts": 580, - "src/renderer/src/runtime/web-runtime-session-tab-props.test.ts": 667, - "src/renderer/src/runtime/web-runtime-session-terminal-host-authority.test.ts": 636, - "src/renderer/src/runtime/web-runtime-session-terminal-legacy-create.test.ts": 597, - "src/renderer/src/runtime/web-runtime-session-terminal-pane-delegation.test.ts": 1145, - "src/renderer/src/runtime/web-runtime-session-terminal-workspace-routing.test.ts": 612, - "src/renderer/src/runtime/web-runtime-session.test.ts": 512, - "src/renderer/src/runtime/web-runtime-terminal-identity.test.ts": 59, - "src/renderer/src/runtime/web-runtime-wake-terminal-respawn.test.ts": 53, - "src/renderer/src/runtime/web-session-browser-placement.test.ts": 87, - "src/renderer/src/runtime/web-session-close-intent.test.ts": 51, - "src/renderer/src/runtime/web-session-existing-tab-index.test.ts": 41, - "src/renderer/src/runtime/web-session-focus-intent-visible-tab.test.ts": 1527, - "src/renderer/src/runtime/web-session-intent-owner.test.ts": 64, - "src/renderer/src/runtime/web-session-open-files-batch-equivalence.test.ts": 1429, - "src/renderer/src/runtime/web-session-restored-placeholder-identity.test.ts": 743, - "src/renderer/src/runtime/web-session-split-pane-retirement-fences.test.ts": 851, - "src/renderer/src/runtime/web-session-structured-tab-focus.test.ts": 1830, - "src/renderer/src/runtime/web-session-tabs-agent-completion-notifications.test.ts": 1651, - "src/renderer/src/runtime/web-session-tabs-late-merged-predecessor-lockout.test.ts": 620, - "src/renderer/src/runtime/web-session-tabs-publisher-identity-lineage.test.ts": 566, - "src/renderer/src/runtime/web-session-tabs-removed-frame-retires-live-publisher.test.ts": 524, - "src/renderer/src/runtime/web-session-tabs-retired-epoch-census-repair.test.tsx": 1870, - "src/renderer/src/runtime/web-session-tabs-stream-end-resubscribe.test.tsx": 1954, - "src/renderer/src/runtime/web-session-tabs-sync-agent-handoff.test.ts": 858, - "src/renderer/src/runtime/web-session-tabs-sync-agent-status.test.ts": 936, - "src/renderer/src/runtime/web-session-tabs-sync-browser-tabs.test.ts": 924, - "src/renderer/src/runtime/web-session-tabs-sync-client-owned-page-content.test.ts": 1153, - "src/renderer/src/runtime/web-session-tabs-sync-client-owned-placement.test.ts": 926, - "src/renderer/src/runtime/web-session-tabs-sync-diff-focus-steal.test.ts": 880, - "src/renderer/src/runtime/web-session-tabs-sync-editor-ownership.test.ts": 1610, - "src/renderer/src/runtime/web-session-tabs-sync-editor-tabs.test.ts": 920, - "src/renderer/src/runtime/web-session-tabs-sync-emptied-workspace-tombstone.test.ts": 907, - "src/renderer/src/runtime/web-session-tabs-sync-focus-intent.test.ts": 904, - "src/renderer/src/runtime/web-session-tabs-sync-host-restart-browser-rows.test.ts": 889, - "src/renderer/src/runtime/web-session-tabs-sync-host-tab-retraction-ghost-rows.test.ts": 1787, - "src/renderer/src/runtime/web-session-tabs-sync-html-preview-focus.test.ts": 907, - "src/renderer/src/runtime/web-session-tabs-sync-initial-terminal-relaunch.test.tsx": 1999, - "src/renderer/src/runtime/web-session-tabs-sync-layout-duplicate-groups.test.ts": 856, - "src/renderer/src/runtime/web-session-tabs-sync-layout-groups.test.ts": 849, - "src/renderer/src/runtime/web-session-tabs-sync-mirror-identity.test.ts": 1630, - "src/renderer/src/runtime/web-session-tabs-sync-mirrored-draft-close.test.ts": 1875, - "src/renderer/src/runtime/web-session-tabs-sync-reconnect-resubscribe.test.tsx": 2282, - "src/renderer/src/runtime/web-session-tabs-sync-remote-main-agent-verdict.test.ts": 1574, - "src/renderer/src/runtime/web-session-tabs-sync-remote-status-title-flap.test.ts": 1543, - "src/renderer/src/runtime/web-session-tabs-sync-restored-browser-rows.test.ts": 891, - "src/renderer/src/runtime/web-session-tabs-sync-snapshot-batch.test.ts": 1094, - "src/renderer/src/runtime/web-session-tabs-sync-staged-browser-adoption.test.ts": 847, - "src/renderer/src/runtime/web-session-tabs-sync-staged-browser-authority.test.ts": 752, - "src/renderer/src/runtime/web-session-tabs-sync-terminal-bootstrap.test.ts": 594, - "src/renderer/src/runtime/web-session-tabs-sync-terminal-mirroring.test.ts": 835, - "src/renderer/src/runtime/web-session-tabs-sync-tracking-teardown.test.ts": 764, - "src/renderer/src/runtime/web-session-tabs-sync-unverifiable-host.test.tsx": 1909, - "src/renderer/src/runtime/web-session-tabs-sync-visibility-collision.test.tsx": 1961, - "src/renderer/src/runtime/web-session-tabs-sync-window-visibility.test.tsx": 8419, - "src/renderer/src/runtime/web-session-tabs-sync.test.ts": 936, - "src/renderer/src/runtime/web-session-tabs-sync/mirrored-agent-tab-label.test.ts": 1550, - "src/renderer/src/runtime/web-session-tabs-sync/mirrored-status-tab-retractions.test.ts": 1499, - "src/renderer/src/runtime/web-session-tabs-sync/mirrored-terminal-recovery-ledger.test.ts": 1593, - "src/renderer/src/runtime/web-session-tabs-sync/retraction-pane-ownership.test.ts": 1249, - "src/renderer/src/runtime/web-session-tabs-sync/state-equality-core.test.ts": 50, - "src/renderer/src/runtime/web-session-tabs-sync/tracking-lifecycle.test.ts": 1895, - "src/renderer/src/runtime/web-session-terminal-handle-events.test.ts": 57, - "src/renderer/src/runtime/web-session-terminal-orphan-absence-across-republication.test.ts": 646, - "src/renderer/src/runtime/web-session-terminal-orphan-inventory-retry.test.ts": 1437, - "src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts": 911, - "src/renderer/src/runtime/web-session-terminal-orphan-recovery-adoption-regressions.test.ts": 983, - "src/renderer/src/runtime/web-session-terminal-orphan-recovery-host-scope-gate.test.ts": 568, - "src/renderer/src/runtime/web-session-terminal-orphan-recovery-prior-removal.test.ts": 563, - "src/renderer/src/runtime/web-session-terminal-orphan-recovery-regressions.test.ts": 1006, - "src/renderer/src/runtime/web-session-terminal-orphan-recovery-topology-fence.test.ts": 760, - "src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts": 719, - "src/renderer/src/runtime/web-session-terminal-pending-handle-recovery.test.ts": 679, - "src/renderer/src/runtime/web-session-terminal-recovery-snapshot-validation.test.ts": 750, - "src/renderer/src/runtime/web-session-terminal-retirement-proof-ledger.test.ts": 528, - "src/renderer/src/runtime/window-visibility-subscription-parking-stream-end.test.ts": 189, - "src/renderer/src/runtime/window-visibility-subscription-parking.test.ts": 218, - "src/renderer/src/runtime/worktree-create-base.test.ts": 45, - "src/renderer/src/ssh/ssh-connect-in-flight.test.ts": 77, - "src/renderer/src/ssh/ssh-connect-ui-timeout.test.ts": 58, - "src/renderer/src/ssh/ssh-connect-verb.test.ts": 57, - "src/renderer/src/ssh/ssh-connection-recoverability.test.ts": 52, - "src/renderer/src/startup/active-workspace-ssh-targets.test.ts": 70, - "src/renderer/src/startup/ssh-startup-reconnect.test.ts": 65, - "src/renderer/src/startup/startup-degraded-recovery.test.ts": 408, - "src/renderer/src/startup/startup-ssh-connection-restore.test.ts": 77, - "src/renderer/src/store/active-terminal-chrome-selector.test.ts": 52, - "src/renderer/src/store/adopt-from-endpoint.test.ts": 344, - "src/renderer/src/store/always-mounted-selector-scan-cost.test.ts": 1434, + "src/renderer/src/runtime/runtime-legacy-inventory-budget.test.ts": 52, + "src/renderer/src/runtime/runtime-legacy-quick-open-inventory-expiry.test.ts": 761, + "src/renderer/src/runtime/runtime-legacy-recent-freshness.test.ts": 327, + "src/renderer/src/runtime/runtime-linear-client.test.ts": 285, + "src/renderer/src/runtime/runtime-path-existence-batch.test.ts": 315, + "src/renderer/src/runtime/runtime-path-existence-error-parity.test.ts": 377, + "src/renderer/src/runtime/runtime-path-existence-pairing.test.ts": 287, + "src/renderer/src/runtime/runtime-path-existence-queue-pairing.test.ts": 344, + "src/renderer/src/runtime/runtime-provider-accounts-client.test.ts": 270, + "src/renderer/src/runtime/runtime-provider-search-bounds.test.ts": 31, + "src/renderer/src/runtime/runtime-quick-open-version-compatibility.test.ts": 372, + "src/renderer/src/runtime/runtime-repo-client.test.ts": 368, + "src/renderer/src/runtime/runtime-repo-search-bounds.test.ts": 38, + "src/renderer/src/runtime/runtime-rpc-client-pairing-revision.test.ts": 257, + "src/renderer/src/runtime/runtime-rpc-client.test.ts": 283, + "src/renderer/src/runtime/runtime-rpc-result.test.ts": 33, + "src/renderer/src/runtime/runtime-server-directory-browser.test.ts": 252, + "src/renderer/src/runtime/runtime-skills-client.test.ts": 242, + "src/renderer/src/runtime/runtime-skills-delete-client.test.ts": 47, + "src/renderer/src/runtime/runtime-ssh-target-management.test.ts": 61, + "src/renderer/src/runtime/runtime-terminal-artifact-read.test.ts": 311, + "src/renderer/src/runtime/runtime-terminal-inspection.test.ts": 1692, + "src/renderer/src/runtime/runtime-terminal-stream.test.ts": 436, + "src/renderer/src/runtime/runtime-terminal-verified-input.test.ts": 1343, + "src/renderer/src/runtime/runtime-worktree-selector.test.ts": 275, + "src/renderer/src/runtime/structured-agent-session-client.test.ts": 423, + "src/renderer/src/runtime/structured-agent-session-close.test.ts": 225, + "src/renderer/src/runtime/structured-agent-session-host-capability.test.tsx": 575, + "src/renderer/src/runtime/structured-agent-session-status-feed-lifecycle.test.ts": 236, + "src/renderer/src/runtime/structured-agent-session-status-feed.test.ts": 282, + "src/renderer/src/runtime/structured-agent-session-tab-retirement.test.ts": 1575, + "src/renderer/src/runtime/structured-agent-session-turn-completion-feed.test.ts": 257, + "src/renderer/src/runtime/structured-conversation-tab-replacement.test.ts": 1610, + "src/renderer/src/runtime/structured-session-orca-session-id.test.ts": 282, + "src/renderer/src/runtime/structured-session-tab-inventory.test.ts": 43, + "src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts": 466, + "src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts": 464, + "src/renderer/src/runtime/sync-runtime-graph-automation-leaf.test.ts": 367, + "src/renderer/src/runtime/sync-runtime-graph-browser.test.ts": 399, + "src/renderer/src/runtime/sync-runtime-graph-conversion-publish.test.ts": 1455, + "src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts": 386, + "src/renderer/src/runtime/sync-runtime-graph-key-reuse.test.ts": 459, + "src/renderer/src/runtime/sync-runtime-graph-parked-leaf.test.ts": 461, + "src/renderer/src/runtime/sync-runtime-graph-payload-partition.test.ts": 478, + "src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts": 544, + "src/renderer/src/runtime/sync-runtime-graph-publication-cost.test.ts": 489, + "src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts": 384, + "src/renderer/src/runtime/sync-runtime-graph-terminal-group-grid-sizes.test.ts": 377, + "src/renderer/src/runtime/sync-runtime-graph-terminal-layout.test.ts": 1004, + "src/renderer/src/runtime/sync-runtime-graph-terminal-surface-projection.test.ts": 331, + "src/renderer/src/runtime/sync-runtime-graph-workspace-publication.test.ts": 392, + "src/renderer/src/runtime/sync-runtime-graph.test.ts": 422, + "src/renderer/src/runtime/sync-runtime-graph/mobile-terminal-theme.test.ts": 73, + "src/renderer/src/runtime/sync-runtime-graph/terminal-group-grid-sizes.test.ts": 144, + "src/renderer/src/runtime/use-remote-runtime-recovery-triggers.test.ts": 257, + "src/renderer/src/runtime/use-runtime-session-mirror-environment-key.test.ts": 1869, + "src/renderer/src/runtime/use-worktree-runtime-target.test.ts": 1567, + "src/renderer/src/runtime/web-runtime-browser-capability-cleanup.test.ts": 608, + "src/renderer/src/runtime/web-runtime-browser-creation-placement.test.ts": 1620, + "src/renderer/src/runtime/web-runtime-browser-materialization.test.ts": 26, + "src/renderer/src/runtime/web-runtime-browser-tab-staging-hosting-intent.test.ts": 44, + "src/renderer/src/runtime/web-runtime-pane-layout-intent.test.ts": 545, + "src/renderer/src/runtime/web-runtime-session-browser-client-placement.test.ts": 638, + "src/renderer/src/runtime/web-runtime-session-browser-create-failure.test.ts": 750, + "src/renderer/src/runtime/web-runtime-session-browser-create-focus.test.ts": 896, + "src/renderer/src/runtime/web-runtime-session-browser-create-split-placement.test.ts": 925, + "src/renderer/src/runtime/web-runtime-session-browser-create-staged-focus.test.ts": 626, + "src/renderer/src/runtime/web-runtime-session-browser-create-staged-hosting-intent.test.ts": 563, + "src/renderer/src/runtime/web-runtime-session-browser-create-staging.test.ts": 922, + "src/renderer/src/runtime/web-runtime-session-browser-placement-staleness.test.ts": 552, + "src/renderer/src/runtime/web-runtime-session-legacy-relay-bridged-activation.test.ts": 523, + "src/renderer/src/runtime/web-runtime-session-tab-activate-close.test.ts": 671, + "src/renderer/src/runtime/web-runtime-session-tab-move.test.ts": 321, + "src/renderer/src/runtime/web-runtime-session-tab-props.test.ts": 644, + "src/renderer/src/runtime/web-runtime-session-terminal-host-authority.test.ts": 865, + "src/renderer/src/runtime/web-runtime-session-terminal-legacy-create.test.ts": 536, + "src/renderer/src/runtime/web-runtime-session-terminal-pane-delegation.test.ts": 1173, + "src/renderer/src/runtime/web-runtime-session-terminal-workspace-routing.test.ts": 594, + "src/renderer/src/runtime/web-runtime-session.test.ts": 329, + "src/renderer/src/runtime/web-runtime-terminal-identity.test.ts": 32, + "src/renderer/src/runtime/web-runtime-wake-terminal-respawn.test.ts": 31, + "src/renderer/src/runtime/web-session-browser-placement.test.ts": 47, + "src/renderer/src/runtime/web-session-close-intent.test.ts": 40, + "src/renderer/src/runtime/web-session-existing-tab-index.test.ts": 33, + "src/renderer/src/runtime/web-session-focus-intent-visible-tab.test.ts": 1391, + "src/renderer/src/runtime/web-session-intent-owner.test.ts": 45, + "src/renderer/src/runtime/web-session-open-files-batch-equivalence.test.ts": 1115, + "src/renderer/src/runtime/web-session-restored-placeholder-identity.test.ts": 753, + "src/renderer/src/runtime/web-session-split-pane-retirement-fences.test.ts": 766, + "src/renderer/src/runtime/web-session-structured-tab-focus.test.ts": 1399, + "src/renderer/src/runtime/web-session-tabs-agent-completion-notifications.test.ts": 1491, + "src/renderer/src/runtime/web-session-tabs-late-merged-predecessor-lockout.test.ts": 585, + "src/renderer/src/runtime/web-session-tabs-publisher-identity-lineage.test.ts": 565, + "src/renderer/src/runtime/web-session-tabs-removed-frame-retires-live-publisher.test.ts": 690, + "src/renderer/src/runtime/web-session-tabs-retired-epoch-census-repair.test.tsx": 1768, + "src/renderer/src/runtime/web-session-tabs-stream-end-resubscribe.test.tsx": 2022, + "src/renderer/src/runtime/web-session-tabs-sync-agent-handoff.test.ts": 773, + "src/renderer/src/runtime/web-session-tabs-sync-agent-status.test.ts": 852, + "src/renderer/src/runtime/web-session-tabs-sync-browser-tabs.test.ts": 838, + "src/renderer/src/runtime/web-session-tabs-sync-client-owned-page-content.test.ts": 814, + "src/renderer/src/runtime/web-session-tabs-sync-client-owned-placement.test.ts": 818, + "src/renderer/src/runtime/web-session-tabs-sync-diff-focus-steal.test.ts": 820, + "src/renderer/src/runtime/web-session-tabs-sync-editor-ownership.test.ts": 1521, + "src/renderer/src/runtime/web-session-tabs-sync-editor-tabs.test.ts": 791, + "src/renderer/src/runtime/web-session-tabs-sync-emptied-workspace-tombstone.test.ts": 877, + "src/renderer/src/runtime/web-session-tabs-sync-focus-intent.test.ts": 853, + "src/renderer/src/runtime/web-session-tabs-sync-host-group-mapping.test.ts": 807, + "src/renderer/src/runtime/web-session-tabs-sync-host-restart-browser-rows.test.ts": 790, + "src/renderer/src/runtime/web-session-tabs-sync-host-tab-retraction-ghost-rows.test.ts": 1417, + "src/renderer/src/runtime/web-session-tabs-sync-html-preview-focus.test.ts": 903, + "src/renderer/src/runtime/web-session-tabs-sync-initial-terminal-relaunch.test.tsx": 1884, + "src/renderer/src/runtime/web-session-tabs-sync-layout-duplicate-groups.test.ts": 789, + "src/renderer/src/runtime/web-session-tabs-sync-layout-groups.test.ts": 743, + "src/renderer/src/runtime/web-session-tabs-sync-mirror-identity.test.ts": 1508, + "src/renderer/src/runtime/web-session-tabs-sync-mirrored-draft-close.test.ts": 1628, + "src/renderer/src/runtime/web-session-tabs-sync-reconnect-resubscribe.test.tsx": 1932, + "src/renderer/src/runtime/web-session-tabs-sync-remote-main-agent-verdict.test.ts": 1409, + "src/renderer/src/runtime/web-session-tabs-sync-remote-status-title-flap.test.ts": 1471, + "src/renderer/src/runtime/web-session-tabs-sync-restored-browser-rows.test.ts": 1077, + "src/renderer/src/runtime/web-session-tabs-sync-snapshot-batch.test.ts": 955, + "src/renderer/src/runtime/web-session-tabs-sync-staged-browser-adoption.test.ts": 747, + "src/renderer/src/runtime/web-session-tabs-sync-staged-browser-authority.test.ts": 744, + "src/renderer/src/runtime/web-session-tabs-sync-terminal-bootstrap.test.ts": 627, + "src/renderer/src/runtime/web-session-tabs-sync-terminal-mirroring.test.ts": 738, + "src/renderer/src/runtime/web-session-tabs-sync-tracking-teardown.test.ts": 706, + "src/renderer/src/runtime/web-session-tabs-sync-unverifiable-host.test.tsx": 1762, + "src/renderer/src/runtime/web-session-tabs-sync-visibility-collision.test.tsx": 1988, + "src/renderer/src/runtime/web-session-tabs-sync-window-visibility.test.tsx": 1860, + "src/renderer/src/runtime/web-session-tabs-sync.test.ts": 647, + "src/renderer/src/runtime/web-session-tabs-sync/mirrored-agent-tab-label.test.ts": 369, + "src/renderer/src/runtime/web-session-tabs-sync/mirrored-status-tab-retractions.test.ts": 1373, + "src/renderer/src/runtime/web-session-tabs-sync/mirrored-terminal-recovery-ledger.test.ts": 360, + "src/renderer/src/runtime/web-session-tabs-sync/retraction-pane-ownership.test.ts": 1290, + "src/renderer/src/runtime/web-session-tabs-sync/state-equality-core.test.ts": 39, + "src/renderer/src/runtime/web-session-tabs-sync/tracking-lifecycle.test.ts": 1416, + "src/renderer/src/runtime/web-session-terminal-handle-events.test.ts": 37, + "src/renderer/src/runtime/web-session-terminal-orphan-absence-across-republication.test.ts": 596, + "src/renderer/src/runtime/web-session-terminal-orphan-inventory-retry.test.ts": 1589, + "src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts": 718, + "src/renderer/src/runtime/web-session-terminal-orphan-recovery-adoption-regressions.test.ts": 1017, + "src/renderer/src/runtime/web-session-terminal-orphan-recovery-host-scope-gate.test.ts": 612, + "src/renderer/src/runtime/web-session-terminal-orphan-recovery-prior-removal.test.ts": 560, + "src/renderer/src/runtime/web-session-terminal-orphan-recovery-regressions.test.ts": 859, + "src/renderer/src/runtime/web-session-terminal-orphan-recovery-topology-fence.test.ts": 682, + "src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts": 840, + "src/renderer/src/runtime/web-session-terminal-pending-handle-recovery.test.ts": 644, + "src/renderer/src/runtime/web-session-terminal-recovery-snapshot-validation.test.ts": 584, + "src/renderer/src/runtime/web-session-terminal-retirement-proof-ledger.test.ts": 591, + "src/renderer/src/runtime/window-visibility-subscription-parking-stream-end.test.ts": 204, + "src/renderer/src/runtime/window-visibility-subscription-parking.test.ts": 219, + "src/renderer/src/runtime/worktree-create-base.test.ts": 35, + "src/renderer/src/ssh/ssh-connect-in-flight.test.ts": 41, + "src/renderer/src/ssh/ssh-connect-ui-timeout.test.ts": 41, + "src/renderer/src/ssh/ssh-connect-verb.test.ts": 34, + "src/renderer/src/ssh/ssh-connection-recoverability.test.ts": 41, + "src/renderer/src/ssh/ssh-host-server-status-copy.test.ts": 1309, + "src/renderer/src/startup/active-workspace-ssh-targets.test.ts": 48, + "src/renderer/src/startup/ssh-startup-reconnect.test.ts": 32, + "src/renderer/src/startup/startup-degraded-recovery.test.ts": 474, + "src/renderer/src/startup/startup-ssh-connection-restore.test.ts": 71, + "src/renderer/src/store/active-terminal-chrome-selector.test.ts": 37, + "src/renderer/src/store/adopt-from-endpoint.test.ts": 333, + "src/renderer/src/store/always-mounted-selector-scan-cost.test.ts": 1506, "src/renderer/src/store/copy-on-write-record.test.ts": 46, - "src/renderer/src/store/folder-workspaces/folder-workspace-catalog.test.ts": 378, - "src/renderer/src/store/github/cache-persistence.test.ts": 1784, - "src/renderer/src/store/github/check-actions-errors.test.ts": 192, - "src/renderer/src/store/github/project-field-mutations.test.ts": 421, - "src/renderer/src/store/github/provider-request-coalescing.test.ts": 542, - "src/renderer/src/store/github/refresh-event-noop-notification.test.ts": 1347, + "src/renderer/src/store/folder-workspaces/folder-path-status-ownership.test.ts": 1443, + "src/renderer/src/store/folder-workspaces/folder-workspace-catalog.test.ts": 359, + "src/renderer/src/store/github/cache-persistence.test.ts": 1916, + "src/renderer/src/store/github/check-actions-errors.test.ts": 370, + "src/renderer/src/store/github/project-field-mutations.test.ts": 517, + "src/renderer/src/store/github/provider-request-coalescing.test.ts": 714, + "src/renderer/src/store/github/refresh-event-noop-notification.test.ts": 1250, + "src/renderer/src/store/github/visible-detached-review-refresh.test.ts": 434, "src/renderer/src/store/github/visible-hosted-review-refresh-scheduler.test.ts": 91, - "src/renderer/src/store/github/visible-hosted-review-refresh-targets.test.ts": 490, - "src/renderer/src/store/github/work-item-routing.test.ts": 281, - "src/renderer/src/store/host-catalog-fencing.test.ts": 75, + "src/renderer/src/store/github/visible-hosted-review-refresh-targets.test.ts": 588, + "src/renderer/src/store/github/work-item-routing.test.ts": 350, + "src/renderer/src/store/host-catalog-fencing.test.ts": 48, "src/renderer/src/store/pinned-tab-close-guard.test.ts": 53, - "src/renderer/src/store/plugin-language-packs-startup.test.ts": 375, - "src/renderer/src/store/plugin-language-packs.test.ts": 206, - "src/renderer/src/store/plugin-panel-retry-timer-cleanup.test.ts": 103, - "src/renderer/src/store/plugin-panels.test.ts": 145, - "src/renderer/src/store/project-host-setup-selector.test.ts": 76, - "src/renderer/src/store/projects/project-catalog-null-field-ingest.test.ts": 101, - "src/renderer/src/store/projects/project-wsl-filesystem-boundary-advisory.test.ts": 63, - "src/renderer/src/store/react-commit-cascade-write-probe.test.ts": 80, - "src/renderer/src/store/repos/converted-ssh-browser-pages.test.ts": 349, - "src/renderer/src/store/repos/converted-ssh-host-rows.test.ts": 60, - "src/renderer/src/store/repos/repo-removal.test.ts": 324, - "src/renderer/src/store/repos/safe-auto-fork-sync.test.ts": 320, - "src/renderer/src/store/right-sidebar-route.test.ts": 55, - "src/renderer/src/store/running-terminal-close-confirm.test.ts": 73, - "src/renderer/src/store/selectors.test.ts": 1496, - "src/renderer/src/store/settled-sort-epoch.test.ts": 1678, - "src/renderer/src/store/slices/active-selection-owner-scoping.test.ts": 1368, - "src/renderer/src/store/slices/active-tab-owner-worktree.test.ts": 70, - "src/renderer/src/store/slices/activity-cleared-at.test.ts": 1529, - "src/renderer/src/store/slices/agent-generated-tab-title.test.ts": 1429, - "src/renderer/src/store/slices/agent-hibernation-live-anchor-e2e.test.ts": 1457, - "src/renderer/src/store/slices/agent-pane-authority.test.ts": 1460, - "src/renderer/src/store/slices/agent-status-ack-cleanup.test.ts": 1469, - "src/renderer/src/store/slices/agent-status-batch.test.ts": 1496, - "src/renderer/src/store/slices/agent-status-cursor-checkpoint.test.ts": 1418, - "src/renderer/src/store/slices/agent-status-drop-ipc.test.ts": 1802, - "src/renderer/src/store/slices/agent-status-freshness-cache.test.ts": 68, - "src/renderer/src/store/slices/agent-status-freshness-scheduler.test.ts": 63, - "src/renderer/src/store/slices/agent-status-live-freshness-request.test.ts": 143, - "src/renderer/src/store/slices/agent-status-live-map-leak.test.ts": 7374, - "src/renderer/src/store/slices/agent-status-main-agent-fact.test.ts": 1387, - "src/renderer/src/store/slices/agent-status-manual-sleep-capture.test.ts": 1473, - "src/renderer/src/store/slices/agent-status-pane-keyed-records.test.ts": 84, - "src/renderer/src/store/slices/agent-status-pr-refresh-handoff.test.ts": 1504, - "src/renderer/src/store/slices/agent-status-provider-session.test.ts": 1961, - "src/renderer/src/store/slices/agent-status-quit-capture-resumable-agents.test.ts": 1365, - "src/renderer/src/store/slices/agent-status-quit-capture.test.ts": 1559, - "src/renderer/src/store/slices/agent-status-reminted-pane-key.test.ts": 1580, - "src/renderer/src/store/slices/agent-status-retention-prefix-sweep.test.ts": 1850, - "src/renderer/src/store/slices/agent-status-runtime-orchestration.test.ts": 1464, - "src/renderer/src/store/slices/agent-status-session-boundary-done.test.ts": 1563, - "src/renderer/src/store/slices/agent-status-ssh-connection-clear.test.ts": 1659, - "src/renderer/src/store/slices/agent-status-tool-assistant-fields.test.ts": 1413, - "src/renderer/src/store/slices/agent-status-turn-started-at.test.ts": 1312, - "src/renderer/src/store/slices/agent-status-verdict-copies.test.ts": 431, - "src/renderer/src/store/slices/agent-status-worktree-purge-leak.test.ts": 1377, - "src/renderer/src/store/slices/agent-status.test.ts": 1842, - "src/renderer/src/store/slices/ambiguous-owner-warning-worktree-removal-leak.test.ts": 1533, - "src/renderer/src/store/slices/browser-annotation-retention.test.ts": 603, - "src/renderer/src/store/slices/browser-cleanup-close.test.ts": 1587, - "src/renderer/src/store/slices/browser-page-close-intent-recording.test.ts": 1828, - "src/renderer/src/store/slices/browser-page-conversion.test.ts": 1558, - "src/renderer/src/store/slices/browser-remote-page-lifecycle.test.ts": 671, - "src/renderer/src/store/slices/browser-remote-tab-creation.test.ts": 384, - "src/renderer/src/store/slices/browser-session-host-selection.test.ts": 672, + "src/renderer/src/store/plugin-language-packs-startup.test.ts": 386, + "src/renderer/src/store/plugin-language-packs.test.ts": 227, + "src/renderer/src/store/plugin-panel-retry-timer-cleanup.test.ts": 69, + "src/renderer/src/store/plugin-panels.test.ts": 183, + "src/renderer/src/store/project-host-setup-selector.test.ts": 52, + "src/renderer/src/store/projects/project-catalog-null-field-ingest.test.ts": 267, + "src/renderer/src/store/projects/project-wsl-filesystem-boundary-advisory.test.ts": 38, + "src/renderer/src/store/react-commit-cascade-write-probe.test.ts": 36, + "src/renderer/src/store/repos/converted-ssh-browser-pages.test.ts": 340, + "src/renderer/src/store/repos/converted-ssh-host-rows.test.ts": 53, + "src/renderer/src/store/repos/repo-removal.test.ts": 350, + "src/renderer/src/store/repos/safe-auto-fork-sync.test.ts": 302, + "src/renderer/src/store/right-sidebar-route.test.ts": 35, + "src/renderer/src/store/running-terminal-close-confirm.test.ts": 54, + "src/renderer/src/store/selectors.test.ts": 1437, + "src/renderer/src/store/settled-sort-epoch.test.ts": 1447, + "src/renderer/src/store/slices/active-selection-owner-scoping.test.ts": 1270, + "src/renderer/src/store/slices/active-tab-owner-worktree.test.ts": 54, + "src/renderer/src/store/slices/activity-cleared-at.test.ts": 1386, + "src/renderer/src/store/slices/agent-generated-tab-title.test.ts": 1238, + "src/renderer/src/store/slices/agent-hibernation-live-anchor-e2e.test.ts": 1265, + "src/renderer/src/store/slices/agent-pane-authority.test.ts": 1446, + "src/renderer/src/store/slices/agent-status-ack-cleanup.test.ts": 1309, + "src/renderer/src/store/slices/agent-status-batch.test.ts": 1403, + "src/renderer/src/store/slices/agent-status-cursor-checkpoint.test.ts": 1220, + "src/renderer/src/store/slices/agent-status-drop-ipc.test.ts": 1690, + "src/renderer/src/store/slices/agent-status-freshness-cache.test.ts": 82, + "src/renderer/src/store/slices/agent-status-freshness-scheduler.test.ts": 51, + "src/renderer/src/store/slices/agent-status-live-freshness-request.test.ts": 146, + "src/renderer/src/store/slices/agent-status-live-map-leak.test.ts": 2819, + "src/renderer/src/store/slices/agent-status-main-agent-fact.test.ts": 1531, + "src/renderer/src/store/slices/agent-status-manual-sleep-capture.test.ts": 1409, + "src/renderer/src/store/slices/agent-status-pane-keyed-records.test.ts": 56, + "src/renderer/src/store/slices/agent-status-pr-refresh-handoff.test.ts": 1240, + "src/renderer/src/store/slices/agent-status-provider-session.test.ts": 1400, + "src/renderer/src/store/slices/agent-status-quit-capture-resumable-agents.test.ts": 1480, + "src/renderer/src/store/slices/agent-status-quit-capture.test.ts": 1480, + "src/renderer/src/store/slices/agent-status-remember-sleeping-session.test.ts": 1112, + "src/renderer/src/store/slices/agent-status-reminted-pane-key.test.ts": 1368, + "src/renderer/src/store/slices/agent-status-retention-prefix-sweep.test.ts": 1328, + "src/renderer/src/store/slices/agent-status-runtime-orchestration.test.ts": 1514, + "src/renderer/src/store/slices/agent-status-session-boundary-done.test.ts": 1186, + "src/renderer/src/store/slices/agent-status-ssh-connection-clear.test.ts": 1400, + "src/renderer/src/store/slices/agent-status-tool-assistant-fields.test.ts": 1278, + "src/renderer/src/store/slices/agent-status-turn-started-at.test.ts": 1283, + "src/renderer/src/store/slices/agent-status-verdict-copies.test.ts": 473, + "src/renderer/src/store/slices/agent-status-worktree-purge-leak.test.ts": 1148, + "src/renderer/src/store/slices/agent-status.test.ts": 1659, + "src/renderer/src/store/slices/all-host-catalog-settlement.test.ts": 1126, + "src/renderer/src/store/slices/ambiguous-owner-warning-worktree-removal-leak.test.ts": 1596, + "src/renderer/src/store/slices/browser-annotation-retention.test.ts": 551, + "src/renderer/src/store/slices/browser-cleanup-close.test.ts": 1410, + "src/renderer/src/store/slices/browser-mount-admission-removal.test.ts": 1413, + "src/renderer/src/store/slices/browser-page-close-intent-recording.test.ts": 1433, + "src/renderer/src/store/slices/browser-page-conversion.test.ts": 1342, + "src/renderer/src/store/slices/browser-remote-page-lifecycle.test.ts": 651, + "src/renderer/src/store/slices/browser-remote-tab-creation.test.ts": 392, + "src/renderer/src/store/slices/browser-session-host-selection.test.ts": 579, "src/renderer/src/store/slices/browser-session-profiles.test.ts": 434, - "src/renderer/src/store/slices/browser-tab-creation-placement.test.ts": 1348, - "src/renderer/src/store/slices/browser-webview-cleanup.test.ts": 76, - "src/renderer/src/store/slices/browser-workspace-doc-location.test.ts": 1525, - "src/renderer/src/store/slices/browser-workspace-host-ownership.test.ts": 441, - "src/renderer/src/store/slices/browser.test.ts": 560, - "src/renderer/src/store/slices/bulk-worktree-purge-terminal-maps-leak.test.ts": 1419, - "src/renderer/src/store/slices/cmd-j-create-actions.test.ts": 1303, - "src/renderer/src/store/slices/codex-restart-notice-lifecycle.test.ts": 1446, - "src/renderer/src/store/slices/degraded-repo-hydration.test.ts": 1498, - "src/renderer/src/store/slices/detected-agents-environment-prune-leak.test.ts": 1505, - "src/renderer/src/store/slices/detected-agents.test.ts": 482, - "src/renderer/src/store/slices/detected-worktree-refresh-leases.test.ts": 61, - "src/renderer/src/store/slices/dictation-model-state-stabilisation.test.ts": 179, - "src/renderer/src/store/slices/diffComments.test.ts": 1533, - "src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts": 1416, - "src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts": 86, - "src/renderer/src/store/slices/editor-branch-diff-snapshots.test.ts": 765, - "src/renderer/src/store/slices/editor-branch-line-total.test.ts": 637, - "src/renderer/src/store/slices/editor-chat-visual-tabs.test.ts": 700, - "src/renderer/src/store/slices/editor-check-details-tabs.test.ts": 745, - "src/renderer/src/store/slices/editor-close-document-siblings.test.ts": 1085, - "src/renderer/src/store/slices/editor-close-file-cleanup.test.ts": 1090, - "src/renderer/src/store/slices/editor-close-file-stale-active.test.ts": 1353, - "src/renderer/src/store/slices/editor-close-workspace-selection.test.ts": 784, - "src/renderer/src/store/slices/editor-git-status-reconciliation.test.ts": 712, - "src/renderer/src/store/slices/editor-hydration-scaling.test.ts": 1448, - "src/renderer/src/store/slices/editor-markdown-link-activation.test.ts": 803, - "src/renderer/src/store/slices/editor-markdown-view-state.test.ts": 756, - "src/renderer/src/store/slices/editor-open-diff.test.ts": 831, - "src/renderer/src/store/slices/editor-preview-tab-setting.test.ts": 735, - "src/renderer/src/store/slices/editor-read-only-tabs.test.ts": 625, - "src/renderer/src/store/slices/editor-recently-closed-tabs.test.ts": 882, - "src/renderer/src/store/slices/editor-rekey-open-files.test.ts": 1416, - "src/renderer/src/store/slices/editor-remote-branch-actions.test.ts": 766, - "src/renderer/src/store/slices/editor-rich-markdown-size-override.test.ts": 649, - "src/renderer/src/store/slices/editor-right-sidebar-state.test.ts": 860, - "src/renderer/src/store/slices/editor-session-duplicate-restore.test.ts": 1413, - "src/renderer/src/store/slices/editor-state-worktree-purge-leak.test.ts": 1441, - "src/renderer/src/store/slices/editor-tab-host-ownership.test.ts": 651, - "src/renderer/src/store/slices/editor-tab-placement.test.ts": 729, - "src/renderer/src/store/slices/editor/actions/editor-cursor-line.test.ts": 667, - "src/renderer/src/store/slices/editor/actions/editor-draft-state.test.ts": 645, - "src/renderer/src/store/slices/editor/actions/markdown-preview-actions.test.ts": 294, - "src/renderer/src/store/slices/editor/file-ids/hydrated-editor-file-index.test.ts": 484, - "src/renderer/src/store/slices/editor/file-ids/hydrated-editor-projections.test.ts": 451, - "src/renderer/src/store/slices/editor/git/git-status-reconciliation.perf.test.ts": 84, - "src/renderer/src/store/slices/empty-update-notifications.test.ts": 1428, - "src/renderer/src/store/slices/floating-workspace-row.test.ts": 1455, - "src/renderer/src/store/slices/folder-workspace-activation-and-activity.test.ts": 1470, - "src/renderer/src/store/slices/folder-workspace-diff-comments.test.ts": 809, - "src/renderer/src/store/slices/folder-workspace-owner-routed-mutations.test.ts": 1733, - "src/renderer/src/store/slices/generation-records-worktree-removal-leak.test.ts": 1460, - "src/renderer/src/store/slices/github-branch-mismatched-linked-pr.test.ts": 229, - "src/renderer/src/store/slices/github-cache-eviction-and-bounds.test.ts": 330, - "src/renderer/src/store/slices/github-checks-cache.test.ts": 235, - "src/renderer/src/store/slices/github-checks.test.ts": 56, - "src/renderer/src/store/slices/github-issue-source-indicator-suppression.test.ts": 252, - "src/renderer/src/store/slices/github-issue-state-machine.test.ts": 224, - "src/renderer/src/store/slices/github-pr-branch-coordinator-events.test.ts": 247, - "src/renderer/src/store/slices/github-pr-branch-direct-refresh-scope.test.ts": 228, - "src/renderer/src/store/slices/github-pr-branch-fallback-results.test.ts": 235, - "src/renderer/src/store/slices/github-pr-branch-hosted-review-cache.test.ts": 234, - "src/renderer/src/store/slices/github-pr-branch-linked-pr-divergence.test.ts": 248, - "src/renderer/src/store/slices/github-pr-checks-fetch.test.ts": 292, - "src/renderer/src/store/slices/github-pr-comments.test.ts": 233, - "src/renderer/src/store/slices/github-pr-refresh-host-guard.test.ts": 202, - "src/renderer/src/store/slices/github-pr-refresh-hosted-review-cache-leak.test.ts": 362, - "src/renderer/src/store/slices/github-pr-refresh-owner-routing.test.ts": 364, - "src/renderer/src/store/slices/github-pr-refresh-sequences-leak.test.ts": 230, - "src/renderer/src/store/slices/github-pr-refresh-states-leak.test.ts": 903, - "src/renderer/src/store/slices/github-pr-request-lifetime.test.ts": 713, - "src/renderer/src/store/slices/github-project-request-coordination.test.ts": 233, - "src/renderer/src/store/slices/github-project-row-owner.test.ts": 53, - "src/renderer/src/store/slices/github-project-view-tables.test.ts": 237, - "src/renderer/src/store/slices/github-provider-request-concurrency.test.ts": 206, - "src/renderer/src/store/slices/github-refresh-sweep.test.ts": 348, - "src/renderer/src/store/slices/github-repo-lookup-index.test.ts": 47, - "src/renderer/src/store/slices/github-review-thread-actions.test.ts": 217, - "src/renderer/src/store/slices/github-work-item-cache-identity.test.ts": 204, - "src/renderer/src/store/slices/github-work-items-error-envelope.test.ts": 1787, - "src/renderer/src/store/slices/github-work-items-pagination.test.ts": 1619, - "src/renderer/src/store/slices/github-work-items-runtime-routing.test.ts": 1676, - "src/renderer/src/store/slices/github-worktree-refresh-if-stale.test.ts": 272, - "src/renderer/src/store/slices/hidden-ssh-host-session-round-trip.test.ts": 1423, - "src/renderer/src/store/slices/hosted-review-cache-race.test.ts": 76, - "src/renderer/src/store/slices/hosted-review-cache.test.ts": 130, + "src/renderer/src/store/slices/browser-tab-creation-placement.test.ts": 1269, + "src/renderer/src/store/slices/browser-webview-cleanup.test.ts": 44, + "src/renderer/src/store/slices/browser-workspace-doc-location.test.ts": 1517, + "src/renderer/src/store/slices/browser-workspace-host-ownership.test.ts": 361, + "src/renderer/src/store/slices/browser.test.ts": 471, + "src/renderer/src/store/slices/bulk-worktree-purge-terminal-maps-leak.test.ts": 1409, + "src/renderer/src/store/slices/cmd-j-create-actions.test.ts": 1251, + "src/renderer/src/store/slices/codex-restart-notice-lifecycle.test.ts": 1404, + "src/renderer/src/store/slices/degraded-repo-hydration.test.ts": 1346, + "src/renderer/src/store/slices/detected-agents-environment-prune-leak.test.ts": 1362, + "src/renderer/src/store/slices/detected-agents.test.ts": 499, + "src/renderer/src/store/slices/detected-worktree-refresh-leases.test.ts": 44, + "src/renderer/src/store/slices/dictation-model-state-stabilisation.test.ts": 170, + "src/renderer/src/store/slices/diffComments.test.ts": 1391, + "src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts": 1476, + "src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts": 62, + "src/renderer/src/store/slices/editor-branch-diff-snapshots.test.ts": 649, + "src/renderer/src/store/slices/editor-branch-line-total.test.ts": 517, + "src/renderer/src/store/slices/editor-check-details-tabs.test.ts": 633, + "src/renderer/src/store/slices/editor-close-document-siblings.test.ts": 1036, + "src/renderer/src/store/slices/editor-close-file-cleanup.test.ts": 929, + "src/renderer/src/store/slices/editor-close-file-stale-active.test.ts": 1310, + "src/renderer/src/store/slices/editor-close-pinned-sibling.test.ts": 761, + "src/renderer/src/store/slices/editor-close-workspace-selection.test.ts": 665, + "src/renderer/src/store/slices/editor-git-status-reconciliation.test.ts": 611, + "src/renderer/src/store/slices/editor-hydration-scaling.test.ts": 1329, + "src/renderer/src/store/slices/editor-markdown-link-activation.test.ts": 787, + "src/renderer/src/store/slices/editor-markdown-view-state.test.ts": 672, + "src/renderer/src/store/slices/editor-open-diff.test.ts": 724, + "src/renderer/src/store/slices/editor-preview-tab-setting.test.ts": 808, + "src/renderer/src/store/slices/editor-read-only-tabs.test.ts": 798, + "src/renderer/src/store/slices/editor-recently-closed-tabs.test.ts": 774, + "src/renderer/src/store/slices/editor-rekey-open-files.test.ts": 1250, + "src/renderer/src/store/slices/editor-remote-branch-actions.test.ts": 741, + "src/renderer/src/store/slices/editor-rich-markdown-size-override.test.ts": 727, + "src/renderer/src/store/slices/editor-right-sidebar-state.test.ts": 811, + "src/renderer/src/store/slices/editor-session-duplicate-restore.test.ts": 1351, + "src/renderer/src/store/slices/editor-set-active-file-guard.test.ts": 748, + "src/renderer/src/store/slices/editor-state-worktree-purge-leak.test.ts": 1337, + "src/renderer/src/store/slices/editor-tab-host-ownership.test.ts": 806, + "src/renderer/src/store/slices/editor-tab-placement.test.ts": 775, + "src/renderer/src/store/slices/editor/actions/editor-cursor-line.test.ts": 692, + "src/renderer/src/store/slices/editor/actions/editor-draft-state.test.ts": 885, + "src/renderer/src/store/slices/editor/actions/markdown-preview-actions.test.ts": 273, + "src/renderer/src/store/slices/editor/file-ids/hydrated-editor-file-index.test.ts": 470, + "src/renderer/src/store/slices/editor/file-ids/hydrated-editor-projections.test.ts": 454, + "src/renderer/src/store/slices/editor/git/git-status-reconciliation.perf.test.ts": 65, + "src/renderer/src/store/slices/empty-update-notifications.test.ts": 1242, + "src/renderer/src/store/slices/floating-workspace-row.test.ts": 1445, + "src/renderer/src/store/slices/folder-workspace-activation-and-activity.test.ts": 1473, + "src/renderer/src/store/slices/folder-workspace-diff-comments.test.ts": 795, + "src/renderer/src/store/slices/folder-workspace-owner-routed-mutations.test.ts": 1467, + "src/renderer/src/store/slices/generation-records-worktree-removal-leak.test.ts": 1363, + "src/renderer/src/store/slices/github-branch-mismatched-linked-pr.test.ts": 346, + "src/renderer/src/store/slices/github-cache-eviction-and-bounds.test.ts": 568, + "src/renderer/src/store/slices/github-checks-cache.test.ts": 380, + "src/renderer/src/store/slices/github-checks.test.ts": 52, + "src/renderer/src/store/slices/github-issue-source-indicator-suppression.test.ts": 325, + "src/renderer/src/store/slices/github-issue-state-machine.test.ts": 399, + "src/renderer/src/store/slices/github-pr-branch-coordinator-events.test.ts": 345, + "src/renderer/src/store/slices/github-pr-branch-direct-refresh-scope.test.ts": 353, + "src/renderer/src/store/slices/github-pr-branch-fallback-results.test.ts": 398, + "src/renderer/src/store/slices/github-pr-branch-hosted-review-cache.test.ts": 385, + "src/renderer/src/store/slices/github-pr-branch-linked-pr-divergence.test.ts": 363, + "src/renderer/src/store/slices/github-pr-checks-fetch.test.ts": 453, + "src/renderer/src/store/slices/github-pr-comments.test.ts": 427, + "src/renderer/src/store/slices/github-pr-refresh-host-guard.test.ts": 285, + "src/renderer/src/store/slices/github-pr-refresh-hosted-review-cache-leak.test.ts": 519, + "src/renderer/src/store/slices/github-pr-refresh-owner-routing.test.ts": 539, + "src/renderer/src/store/slices/github-pr-refresh-sequences-leak.test.ts": 378, + "src/renderer/src/store/slices/github-pr-refresh-states-leak.test.ts": 985, + "src/renderer/src/store/slices/github-pr-rejected-cache.test.ts": 258, + "src/renderer/src/store/slices/github-pr-request-lifetime.test.ts": 904, + "src/renderer/src/store/slices/github-project-request-coordination.test.ts": 320, + "src/renderer/src/store/slices/github-project-row-owner.test.ts": 31, + "src/renderer/src/store/slices/github-project-view-tables.test.ts": 372, + "src/renderer/src/store/slices/github-provider-request-concurrency.test.ts": 334, + "src/renderer/src/store/slices/github-refresh-sweep.test.ts": 507, + "src/renderer/src/store/slices/github-repo-lookup-index.test.ts": 37, + "src/renderer/src/store/slices/github-review-thread-actions.test.ts": 407, + "src/renderer/src/store/slices/github-work-item-cache-identity.test.ts": 407, + "src/renderer/src/store/slices/github-work-items-error-envelope.test.ts": 1388, + "src/renderer/src/store/slices/github-work-items-pagination.test.ts": 1357, + "src/renderer/src/store/slices/github-work-items-runtime-routing.test.ts": 1485, + "src/renderer/src/store/slices/github-worktree-refresh-if-stale.test.ts": 431, + "src/renderer/src/store/slices/hidden-ssh-host-session-round-trip.test.ts": 1388, + "src/renderer/src/store/slices/hosted-review-cache-race.test.ts": 62, + "src/renderer/src/store/slices/hosted-review-cache.test.ts": 109, "src/renderer/src/store/slices/hosted-review-request-lifetime.test.ts": 89, - "src/renderer/src/store/slices/hosted-review.test.ts": 130, - "src/renderer/src/store/slices/jira-connection-identity.test.ts": 90, - "src/renderer/src/store/slices/jira-web-client-status.test.ts": 126, - "src/renderer/src/store/slices/jira.test.ts": 166, - "src/renderer/src/store/slices/linear-credential-error-recovery.test.ts": 189, + "src/renderer/src/store/slices/hosted-review.test.ts": 77, + "src/renderer/src/store/slices/jira-connection-identity.test.ts": 93, + "src/renderer/src/store/slices/jira-issue-patch-action.test.ts": 1116, + "src/renderer/src/store/slices/jira-sprint-filter-cache.test.ts": 100, + "src/renderer/src/store/slices/jira-status-filter-cache.test.ts": 89, + "src/renderer/src/store/slices/jira-web-client-status.test.ts": 275, + "src/renderer/src/store/slices/jira.test.ts": 178, + "src/renderer/src/store/slices/linear-credential-error-recovery.test.ts": 326, "src/renderer/src/store/slices/linear-invalidation.test.ts": 142, - "src/renderer/src/store/slices/linear-issue-cache-refresh.test.ts": 177, - "src/renderer/src/store/slices/linear-scoped-collection-cache.test.ts": 184, - "src/renderer/src/store/slices/linear-source-context-cache-scope.test.ts": 200, - "src/renderer/src/store/slices/linear.test.ts": 168, - "src/renderer/src/store/slices/local-detected-agent-state.test.ts": 357, - "src/renderer/src/store/slices/memory.test.ts": 58, - "src/renderer/src/store/slices/native-chat-launch-draft-teardown.test.ts": 1473, - "src/renderer/src/store/slices/new-issue-draft.test.ts": 50, - "src/renderer/src/store/slices/new-markdown.test.ts": 654, - "src/renderer/src/store/slices/orca-profiles-auth-actions-connect-overlap.test.ts": 1439, - "src/renderer/src/store/slices/orca-profiles-auth-actions.test.ts": 1418, - "src/renderer/src/store/slices/orca-profiles.test.ts": 1424, - "src/renderer/src/store/slices/pane-column-split-drop-no-op.test.ts": 46, - "src/renderer/src/store/slices/persisted-ui-write-baseline.test.ts": 57, - "src/renderer/src/store/slices/pinned-tab-close-confirm.test.ts": 69, - "src/renderer/src/store/slices/preflight.test.ts": 347, - "src/renderer/src/store/slices/project-group-removal-targets.test.ts": 69, - "src/renderer/src/store/slices/project-removal-owner-host.test.ts": 1320, - "src/renderer/src/store/slices/purge-stale-runtime-host-groups.test.ts": 1389, - "src/renderer/src/store/slices/purge-stale-runtime-host-ownership.test.ts": 1523, - "src/renderer/src/store/slices/purge-stale-runtime-host-state.test.ts": 1413, - "src/renderer/src/store/slices/readopted-ssh-worktree-rows.test.ts": 49, - "src/renderer/src/store/slices/recently-closed-tabs.test.ts": 1467, - "src/renderer/src/store/slices/remote-server-updates.integration.test.ts": 305, - "src/renderer/src/store/slices/repo-identity-reconcile.test.ts": 72, - "src/renderer/src/store/slices/repo-owner-cache-identity.test.ts": 53, - "src/renderer/src/store/slices/repo-reorder-host-split.test.ts": 49, - "src/renderer/src/store/slices/repos-add-races.test.ts": 1465, - "src/renderer/src/store/slices/repos-all-hosts-folder-workspaces.test.ts": 1523, - "src/renderer/src/store/slices/repos-all-hosts-generation.test.ts": 1480, - "src/renderer/src/store/slices/repos-all-hosts.test.ts": 1476, - "src/renderer/src/store/slices/repos-catalog-connection-fence.test.ts": 1552, - "src/renderer/src/store/slices/repos-catalog-merge-identity.test.ts": 1544, - "src/renderer/src/store/slices/repos-catalog-notifications.test.ts": 1367, - "src/renderer/src/store/slices/repos-cross-host-project-collisions.test.ts": 1355, - "src/renderer/src/store/slices/repos-cross-host-refresh-identity.test.ts": 1448, - "src/renderer/src/store/slices/repos-ephemeral-vm-cleanup-retention.test.ts": 2007, - "src/renderer/src/store/slices/repos-host-identity-routing.test.ts": 1747, - "src/renderer/src/store/slices/repos-manual-order-hydration.test.ts": 1398, - "src/renderer/src/store/slices/repos-module-lifetime-coordinators.test.ts": 1925, - "src/renderer/src/store/slices/repos-nested-import-refresh-failures.test.ts": 1395, - "src/renderer/src/store/slices/repos-nested-ssh-projection.test.ts": 1383, - "src/renderer/src/store/slices/repos-onboarding-folder-startup.test.ts": 1708, - "src/renderer/src/store/slices/repos-paired-runtime-add.test.ts": 1444, - "src/renderer/src/store/slices/repos-project-group-create-race.test.ts": 1513, - "src/renderer/src/store/slices/repos-project-groups-delete.test.ts": 1217, - "src/renderer/src/store/slices/repos-project-groups-owner-routing.test.ts": 1410, - "src/renderer/src/store/slices/repos-project-groups.test.ts": 1564, - "src/renderer/src/store/slices/repos-project-host-capability.test.ts": 1159, - "src/renderer/src/store/slices/repos-project-host-lifecycle.test.ts": 2194, - "src/renderer/src/store/slices/repos-project-runtime.test.ts": 1478, - "src/renderer/src/store/slices/repos-refresh-identity.test.ts": 1791, - "src/renderer/src/store/slices/repos-remove-missing-remote-project.test.ts": 1545, - "src/renderer/src/store/slices/repos-remove-project-purge-leak.test.ts": 1355, - "src/renderer/src/store/slices/repos-runtime-project-groups.test.ts": 1437, - "src/renderer/src/store/slices/repos-runtime-visibility-defaults.test.ts": 1554, - "src/renderer/src/store/slices/repos-selected-owner-routing.test.ts": 1507, - "src/renderer/src/store/slices/repos-setup-script-dismissals.test.ts": 1336, - "src/renderer/src/store/slices/repos-shared-project-badge-color.test.ts": 1400, - "src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts": 1557, - "src/renderer/src/store/slices/repos-stale-fetch.test.ts": 1405, - "src/renderer/src/store/slices/repos-update-serialization.test.ts": 1554, - "src/renderer/src/store/slices/repos.runtime-fallback.test.ts": 1456, - "src/renderer/src/store/slices/repos.test.ts": 1736, - "src/renderer/src/store/slices/restored-editor-owner-reparent.test.ts": 1695, - "src/renderer/src/store/slices/runtime-catalog-merge-removed-env-guard.test.ts": 1585, - "src/renderer/src/store/slices/runtime-detected-agents.test.ts": 1466, - "src/renderer/src/store/slices/runtime-environment-peer-replacement.test.ts": 47, - "src/renderer/src/store/slices/runtime-environment-ssh.test.ts": 1392, - "src/renderer/src/store/slices/runtime-host-purge-session-partition-split.test.ts": 1569, - "src/renderer/src/store/slices/runtime-managed-repair-keeps-workspaces.test.ts": 1477, - "src/renderer/src/store/slices/runtime-pane-title-sort-epoch.test.ts": 1397, - "src/renderer/src/store/slices/runtime-status-catalog-identity.test.ts": 1377, - "src/renderer/src/store/slices/runtime-status-first-publication.test.ts": 340, - "src/renderer/src/store/slices/runtime-status-reconnect-connection-generation.test.ts": 1417, - "src/renderer/src/store/slices/runtime-status-refresh-diagnostics.test.ts": 83, - "src/renderer/src/store/slices/runtime-status-restored-browser-host-attach.test.ts": 158, - "src/renderer/src/store/slices/runtime-status-skill-cache-eviction.test.ts": 1723, - "src/renderer/src/store/slices/runtime-status-snapshot.test.ts": 194, - "src/renderer/src/store/slices/runtime-status.test.ts": 497, - "src/renderer/src/store/slices/runtime-switch-settings-persistence.test.ts": 1350, - "src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts": 1402, - "src/renderer/src/store/slices/set-runtime-environments-purge-wiring.test.ts": 1800, - "src/renderer/src/store/slices/settings-owner-hydration-write-fence.test.ts": 1615, - "src/renderer/src/store/slices/settings-search-state.test.ts": 57, - "src/renderer/src/store/slices/settings.test.ts": 1698, - "src/renderer/src/store/slices/sparse-presets-repo-removal-purge-leak.test.ts": 1627, - "src/renderer/src/store/slices/sparse-presets.test.ts": 93, - "src/renderer/src/store/slices/ssh-target-cleanup.test.ts": 1693, - "src/renderer/src/store/slices/ssh.test.ts": 1441, - "src/renderer/src/store/slices/stale-runtime-host-rows.test.ts": 59, - "src/renderer/src/store/slices/store-active-worktree-selection.test.ts": 1522, - "src/renderer/src/store/slices/store-active-worktree-split-groups.test.ts": 1521, - "src/renderer/src/store/slices/store-active-worktree-tab-close.test.ts": 1491, - "src/renderer/src/store/slices/store-active-worktree-terminal-creation.test.ts": 1552, - "src/renderer/src/store/slices/store-create-tab-id-hint.test.ts": 1663, - "src/renderer/src/store/slices/store-session-browser-hydration.test.ts": 1483, - "src/renderer/src/store/slices/store-session-cascades.test.ts": 1360, - "src/renderer/src/store/slices/store-session-editor-hydration.test.ts": 1343, - "src/renderer/src/store/slices/store-session-terminal-activity.test.ts": 1448, - "src/renderer/src/store/slices/store-session-terminal-reconnect.test.ts": 1555, - "src/renderer/src/store/slices/store-session-workspace-hydration.test.ts": 1701, - "src/renderer/src/store/slices/store-sleep-agent-status-retention.test.ts": 2125, - "src/renderer/src/store/slices/store-sleep-exact-runtime-stop.test.ts": 1412, - "src/renderer/src/store/slices/store-sleep-pane-hibernation.test.ts": 1442, - "src/renderer/src/store/slices/store-sleep-runtime-convergence.test.ts": 1758, - "src/renderer/src/store/slices/store-sleep-shutdown-order.test.ts": 1456, - "src/renderer/src/store/slices/store-worktree-removal-cascade.test.ts": 1459, - "src/renderer/src/store/slices/superseded-ssh-repo-rows.test.ts": 54, - "src/renderer/src/store/slices/tab-group-reference-repair.test.ts": 74, - "src/renderer/src/store/slices/tab-group-state.test.ts": 69, - "src/renderer/src/store/slices/tab-view-mode.test.ts": 1235, - "src/renderer/src/store/slices/tab-worktree-orphan-map-purge-leak.test.ts": 1589, - "src/renderer/src/store/slices/tabs-close-dangling-group.test.ts": 1520, - "src/renderer/src/store/slices/tabs-hydration-generated-title.test.ts": 67, - "src/renderer/src/store/slices/tabs-hydration-group-validation.test.ts": 59, - "src/renderer/src/store/slices/tabs-hydration.test.ts": 68, - "src/renderer/src/store/slices/tabs-label-and-pin-state.test.ts": 1510, - "src/renderer/src/store/slices/tabs-model-reconciliation.test.ts": 1681, - "src/renderer/src/store/slices/tabs-open-close-lifecycle.test.ts": 1560, - "src/renderer/src/store/slices/tabs-pane-layout-operations.test.ts": 1393, - "src/renderer/src/store/slices/tabs-session-hydration.test.ts": 1813, - "src/renderer/src/store/slices/tabs-unread-and-focus.test.ts": 1566, - "src/renderer/src/store/slices/tabs/hydrated-workspace-reconciliation-batch.test.ts": 1722, - "src/renderer/src/store/slices/tabs/structured-chat-close-owner.test.ts": 1367, - "src/renderer/src/store/slices/tabs/tab-selection-contract.test.ts": 67, - "src/renderer/src/store/slices/tabs/tabs-reconciliation-batch-identity.test.ts": 1401, - "src/renderer/src/store/slices/task-creation-drafts.test.ts": 45, - "src/renderer/src/store/slices/terminal-helpers.test.ts": 91, - "src/renderer/src/store/slices/terminal-input-activity-store-write.test.ts": 1964, - "src/renderer/src/store/slices/terminal-layout-pty-ownership.test.ts": 1509, - "src/renderer/src/store/slices/terminal-orphan-helpers.test.ts": 393, - "src/renderer/src/store/slices/terminal-pane-detach-agent-identity.test.ts": 1381, - "src/renderer/src/store/slices/terminal-pane-detach-agent-retention.test.ts": 1758, - "src/renderer/src/store/slices/terminal-pane-retirement-ownership.test.ts": 420, - "src/renderer/src/store/slices/terminal-pty-identity-replacement.test.ts": 1511, - "src/renderer/src/store/slices/terminal-quick-command-hosts.test.ts": 1285, - "src/renderer/src/store/slices/terminal-startup-command-retention.test.ts": 2880, - "src/renderer/src/store/slices/terminal-tab-id-hydration.test.ts": 1368, - "src/renderer/src/store/slices/terminal-tab-owner-index.test.ts": 57, - "src/renderer/src/store/slices/terminal-tab-recovery-remount.test.ts": 1395, - "src/renderer/src/store/slices/terminal-tab-retirement-store.test.ts": 1489, - "src/renderer/src/store/slices/terminal-tab-retirement.test.ts": 446, - "src/renderer/src/store/slices/terminal-tab-title-batch.test.ts": 1406, - "src/renderer/src/store/slices/terminals-explicit-empty-hydration.test.ts": 1342, - "src/renderer/src/store/slices/terminals-hydration-canonical-pty-overlap.test.ts": 1470, - "src/renderer/src/store/slices/terminals-hydration-canonical-rows.test.ts": 1346, - "src/renderer/src/store/slices/terminals-hydration.test.ts": 1435, - "src/renderer/src/store/slices/ui-acknowledge-agents-clock-skew.test.ts": 411, - "src/renderer/src/store/slices/ui-agent-send-target.test.ts": 589, - "src/renderer/src/store/slices/ui-contextual-tours.test.ts": 556, - "src/renderer/src/store/slices/ui-feature-interactions.test.ts": 660, - "src/renderer/src/store/slices/ui-host-filter-sync.test.ts": 485, - "src/renderer/src/store/slices/ui-hydration-view-layout.test.ts": 618, - "src/renderer/src/store/slices/ui-hydration-workspace-cleanup-browse.test.ts": 426, - "src/renderer/src/store/slices/ui-hydration-workspace-preferences.test.ts": 520, - "src/renderer/src/store/slices/ui-modal-slot-dismissal.test.ts": 507, - "src/renderer/src/store/slices/ui-notice-dismissals.test.ts": 536, - "src/renderer/src/store/slices/ui-page-navigation.test.ts": 563, - "src/renderer/src/store/slices/ui/ui-slice-surface-actions.test.ts": 1598, - "src/renderer/src/store/slices/usage-snapshot-refresh.benchmark.test.ts": 56, - "src/renderer/src/store/slices/usage-web-client-fallback.test.ts": 44, - "src/renderer/src/store/slices/workspace-chat-draft-removal.test.ts": 1530, - "src/renderer/src/store/slices/workspace-cleanup-browse.test.ts": 69, - "src/renderer/src/store/slices/workspace-cleanup-cache-hydration.test.ts": 551, - "src/renderer/src/store/slices/workspace-cleanup-enrichment-performance.test.ts": 671, - "src/renderer/src/store/slices/workspace-cleanup-host-qualified-list-state.test.ts": 1355, - "src/renderer/src/store/slices/workspace-cleanup-local-evidence-invariants.test.ts": 423, - "src/renderer/src/store/slices/workspace-cleanup-removal-preflight.test.ts": 423, - "src/renderer/src/store/slices/workspace-cleanup-scan-progress-boundary.test.ts": 96, - "src/renderer/src/store/slices/workspace-cleanup-scan-progress.test.ts": 1070, - "src/renderer/src/store/slices/workspace-cleanup-unverified-removal-consent.test.ts": 432, - "src/renderer/src/store/slices/workspace-cleanup-wrong-host-removal-guard.test.ts": 1619, - "src/renderer/src/store/slices/workspace-cleanup-wrong-host-removal.test.ts": 1785, - "src/renderer/src/store/slices/workspace-document-title-refresh.test.ts": 1408, - "src/renderer/src/store/slices/workspace-space.test.ts": 55, - "src/renderer/src/store/slices/worktree-by-id-index.test.ts": 42, - "src/renderer/src/store/slices/worktree-catalog-reconciliation.test.ts": 49, - "src/renderer/src/store/slices/worktree-helpers.test.ts": 88, - "src/renderer/src/store/slices/worktree-listing-branch-switch.test.ts": 50, - "src/renderer/src/store/slices/worktree-meta-update-application.test.ts": 104, - "src/renderer/src/store/slices/worktree-nav-history-view-entries.test.ts": 111, - "src/renderer/src/store/slices/worktree-nav-history.test.ts": 100, - "src/renderer/src/store/slices/worktree-removal-maps-leak.test.ts": 1546, - "src/renderer/src/store/slices/worktree-sleep-intent-lifecycle.test.ts": 1394, - "src/renderer/src/store/slices/worktree-terminal-removal-teardown.test.ts": 1404, - "src/renderer/src/store/slices/worktree-visibility-owner-settings.test.ts": 158, - "src/renderer/src/store/slices/worktree-visibility-settings-write.test.ts": 1445, - "src/renderer/src/store/slices/worktrees-activity-persistence.test.ts": 824, - "src/renderer/src/store/slices/worktrees-always-force-delete.test.ts": 786, - "src/renderer/src/store/slices/worktrees-background-removal.test.ts": 1562, - "src/renderer/src/store/slices/worktrees-create-base-status.test.ts": 887, - "src/renderer/src/store/slices/worktrees-create-captured-host.test.ts": 787, - "src/renderer/src/store/slices/worktrees-create-parent-pick.test.ts": 962, - "src/renderer/src/store/slices/worktrees-fetch-listing-merge.test.ts": 814, - "src/renderer/src/store/slices/worktrees-fetch-owner-routing.test.ts": 1908, - "src/renderer/src/store/slices/worktrees-fetch-persisted-metadata-fallback.test.ts": 886, - "src/renderer/src/store/slices/worktrees-fetch-refresh-coalescing.test.ts": 715, - "src/renderer/src/store/slices/worktrees-fetch-remote-lineage.test.ts": 883, - "src/renderer/src/store/slices/worktrees-fetch-removal-purge.test.ts": 800, - "src/renderer/src/store/slices/worktrees-fetch-stale-catalog-version.test.ts": 866, - "src/renderer/src/store/slices/worktrees-git-identity-branch-title.test.ts": 885, - "src/renderer/src/store/slices/worktrees-git-identity-review-clear-persistence.test.ts": 1395, - "src/renderer/src/store/slices/worktrees-hydration-purge.test.ts": 965, - "src/renderer/src/store/slices/worktrees-identity-migration.test.ts": 1031, - "src/renderer/src/store/slices/worktrees-lineage-state.test.ts": 863, - "src/renderer/src/store/slices/worktrees-linked-review-push-target.test.ts": 829, - "src/renderer/src/store/slices/worktrees-metadata-persistence.test.ts": 993, - "src/renderer/src/store/slices/worktrees-multiple-attachments.test.ts": 808, - "src/renderer/src/store/slices/worktrees-pending-creation-state.test.ts": 794, - "src/renderer/src/store/slices/worktrees-purge-terminal-state.test.ts": 832, - "src/renderer/src/store/slices/worktrees-remote-runtime-create.test.ts": 867, - "src/renderer/src/store/slices/worktrees-remote-runtime-removal.test.ts": 879, - "src/renderer/src/store/slices/worktrees-removal-state-cleanup.test.ts": 888, - "src/renderer/src/store/slices/worktrees-runtime-connection-generation-fence.test.ts": 1005, - "src/renderer/src/store/slices/worktrees-runtime-host-metadata-retirement.test.ts": 779, - "src/renderer/src/store/slices/worktrees-terminal-pr-exact-observation.test.ts": 805, - "src/renderer/src/store/slices/worktrees-terminal-pr-url-linking.test.ts": 1028, - "src/renderer/src/store/slices/worktrees-terminal-reference-admission.test.ts": 1524, - "src/renderer/src/store/slices/worktrees-terminal-reference-origins.test.ts": 914, - "src/renderer/src/store/slices/worktrees-terminal-reference-source-race.test.ts": 720, - "src/renderer/src/store/slices/worktrees-unread-state.test.ts": 932, - "src/renderer/src/store/slices/worktrees-workspace-selection-state.test.ts": 740, - "src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.test.ts": 338, - "src/renderer/src/store/slices/worktrees/listing/detected-worktree-unavailable-reason.test.ts": 325, - "src/renderer/src/store/slices/worktrees/listing/stale-listing-purges-created-worktree.test.ts": 1688, - "src/renderer/src/store/slices/worktrees/listing/worktree-host-ownership-unstamped.test.ts": 98, - "src/renderer/src/store/slices/worktrees/metadata/worktree-meta-persist.test.ts": 359, - "src/renderer/src/store/slices/worktrees/session/worktree-identity-rename-row-worktree-ids.test.ts": 797, - "src/renderer/src/store/slices/worktrees/session/worktree-no-op-notifications.test.ts": 883, - "src/renderer/src/store/slices/worktrees/teardown/host-qualified-removal-refusal.test.ts": 388, - "src/renderer/src/store/slices/worktrees/teardown/record-key-omission.test.ts": 50, - "src/renderer/src/store/slices/worktrees/teardown/remove-worktree-map-identity.test.ts": 299, - "src/renderer/src/store/slices/worktrees/teardown/worktree-teardown-array-identity.test.ts": 443, - "src/renderer/src/store/store-identity-churn-probe.test.ts": 71, - "src/renderer/src/store/store-listener-census.test.ts": 59, - "src/renderer/src/store/terminals/restored-relay-session-identity.test.ts": 1782, - "src/renderer/src/store/terminals/structured-chat-tab-rename.test.ts": 1376, - "src/renderer/src/store/terminals/terminal-exited-pane-liveness.test.ts": 1393, - "src/renderer/src/store/terminals/terminal-no-op-subscriber.test.ts": 1428, - "src/renderer/src/store/terminals/terminal-pane-expansion-write-bailout.test.tsx": 1787, - "src/renderer/src/store/terminals/terminal-shutdown-guards-identity.test.ts": 57, - "src/renderer/src/store/terminals/terminal-shutdown-map-identity.test.ts": 245, - "src/renderer/src/store/terminals/terminal-tab-close-intent.test.ts": 1531, - "src/renderer/src/store/terminals/terminal-workspace-routing.scale.test.ts": 347, - "src/renderer/src/store/terminals/workspace-terminal-placeholders.test.ts": 273, - "src/renderer/src/store/workspace-layout-cache.test.ts": 66, - "src/renderer/src/store/worktree-diff-comments-selector.test.ts": 101, - "src/renderer/src/store/worktree-host-collision-index.test.ts": 51, - "src/renderer/src/store/worktree-visibility-defaults-by-host.test.ts": 45, - "src/renderer/src/web/preload-api/web-agent-session-attachment-upload.test.ts": 52, - "src/renderer/src/web/preload-api/web-ai-vault-search-compat.test.ts": 267, - "src/renderer/src/web/preload-api/web-fallback-api.test.ts": 59, - "src/renderer/src/web/preload-api/web-git-stage-scope.test.ts": 528, - "src/renderer/src/web/preload-api/web-gitlab-api.test.ts": 56, - "src/renderer/src/web/preload-api/web-ui-compact-notice.test.ts": 818, - "src/renderer/src/web/preload-api/web-ui-explorer-root.test.ts": 695, - "src/renderer/src/web/preload-api/web-worktree-attachment-mutations.test.ts": 314, - "src/renderer/src/web/web-clipboard-copy-fallback.test.ts": 46, - "src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts": 140, - "src/renderer/src/web/web-clipboard-image-presence.test.ts": 367, - "src/renderer/src/web/web-file-mutation-methods.test.ts": 281, - "src/renderer/src/web/web-native-chat-appearance-settings.test.ts": 259, - "src/renderer/src/web/web-pairing.test.ts": 250, - "src/renderer/src/web/web-preload-api-agent-providers.test.ts": 795, - "src/renderer/src/web/web-preload-api-clipboard.test.ts": 1407, - "src/renderer/src/web/web-preload-api-composition.test.ts": 768, - "src/renderer/src/web/web-preload-api-filesystem.test.ts": 694, - "src/renderer/src/web/web-preload-api-git.test.ts": 605, - "src/renderer/src/web/web-preload-api-github.test.ts": 710, - "src/renderer/src/web/web-preload-api-gitlab.test.ts": 833, - "src/renderer/src/web/web-preload-api-jira.test.ts": 1051, - "src/renderer/src/web/web-preload-api-keybindings.test.ts": 597, - "src/renderer/src/web/web-preload-api-rate-limits.test.ts": 1529, - "src/renderer/src/web/web-preload-api-runtime-calls.test.ts": 1289, - "src/renderer/src/web/web-preload-api-runtime-environment.test.ts": 1527, - "src/renderer/src/web/web-preload-api-settings.test.ts": 2698, - "src/renderer/src/web/web-preload-api-ssh.test.ts": 451, - "src/renderer/src/web/web-preload-api-ui-pairing-local-fields.test.ts": 2567, - "src/renderer/src/web/web-preload-api-ui.test.ts": 2101, - "src/renderer/src/web/web-preload-api-workspace-catalog.test.ts": 2033, - "src/renderer/src/web/web-preload-updater-package-recovery.test.ts": 554, - "src/renderer/src/web/web-runtime-client-file-watch-replay.test.ts": 213, - "src/renderer/src/web/web-runtime-client-heartbeat.test.ts": 234, - "src/renderer/src/web/web-runtime-client-timeout-budget.test.ts": 264, - "src/renderer/src/web/web-runtime-client.test.ts": 603, - "src/renderer/src/web/web-runtime-connection-frame-router.test.ts": 223, - "src/renderer/src/web/web-runtime-connection-heartbeat-unsendable-probe.test.ts": 46, - "src/renderer/src/web/web-runtime-direct-proxy-live.integration.test.ts": 251, - "src/renderer/src/web/web-runtime-environment-repair.test.ts": 827, - "src/renderer/src/web/web-runtime-status-owner.test.ts": 981, - "src/renderer/src/web/web-shell-paths-exist.test.ts": 40, - "src/renderer/src/web/web-workspace-session.test.ts": 272, - "src/renderer/src/web/web-zcode-plan-settings.test.ts": 726, - "src/shared/__tests__/process-host/windows-cmd-shim-resolution.test.ts": 98, - "src/shared/__tests__/process-host/windows-cmd-shim-resolution.win32.test.ts": 71, - "src/shared/__tests__/process-host/windows-command-line.win32.test.ts": 53, - "src/shared/abort-signal-reason.test.ts": 46, - "src/shared/absolute-dir-override.test.ts": 62, - "src/shared/add-repo-existing-workspaces-telemetry.test.ts": 281, - "src/shared/agent-child-row-model.test.ts": 85, - "src/shared/agent-cli-flag-detection.test.ts": 56, - "src/shared/agent-cli-install-dir-fallback.test.ts": 73, - "src/shared/agent-decorative-title-signature.test.ts": 67, - "src/shared/agent-detection.test.ts": 146, - "src/shared/agent-feature-install-commands.test.ts": 65, - "src/shared/agent-hook-endpoint-file.test.ts": 49, - "src/shared/agent-hook-endpoint-temp-cleanup.test.ts": 58, - "src/shared/agent-hook-listener-antigravity.test.ts": 202, - "src/shared/agent-hook-listener-claude-compatible-vendors.test.ts": 187, - "src/shared/agent-hook-listener-claude-reordered-task-notification.test.ts": 192, - "src/shared/agent-hook-listener-claude-subagents.test.ts": 204, - "src/shared/agent-hook-listener-claude-task-notification.test.ts": 353, - "src/shared/agent-hook-listener-claude-turn-state.test.ts": 158, - "src/shared/agent-hook-listener-codex-approval-ownership.test.ts": 211, - "src/shared/agent-hook-listener-codex-interrupt.test.ts": 164, - "src/shared/agent-hook-listener-codex-main-agent.test.ts": 190, - "src/shared/agent-hook-listener-command-code-transcript.test.ts": 255, - "src/shared/agent-hook-listener-extraction-characterization.test.ts": 207, - "src/shared/agent-hook-listener-grok-completion.test.ts": 232, - "src/shared/agent-hook-listener-grok.test.ts": 232, - "src/shared/agent-hook-listener-hermes-codex-droid.test.ts": 203, - "src/shared/agent-hook-listener-interactive-prompts.test.ts": 179, - "src/shared/agent-hook-listener-jcode.test.ts": 214, - "src/shared/agent-hook-listener-omp-approval-ownership.test.ts": 200, - "src/shared/agent-hook-listener-opencode-reattribution.test.ts": 192, - "src/shared/agent-hook-listener-opencode-session-correlation.test.ts": 71, - "src/shared/agent-hook-listener-opencode-session-registry.test.ts": 71, - "src/shared/agent-hook-listener-opencode-tui-identity.test.ts": 180, - "src/shared/agent-hook-listener-pi-compatible.test.ts": 182, - "src/shared/agent-hook-listener-roster-retention.test.ts": 266, - "src/shared/agent-hook-listener-session-replacement.test.ts": 186, - "src/shared/agent-hook-listener-startless-child-lifecycle.test.ts": 169, - "src/shared/agent-hook-listener-transport.test.ts": 239, - "src/shared/agent-hook-listener/providers/dsh-events.test.ts": 161, - "src/shared/agent-hook-listener/providers/hermes-events.test.ts": 112, - "src/shared/agent-hook-listener/providers/jcode-turn-prompt.test.ts": 115, - "src/shared/agent-hook-listener/providers/kiro-events.test.ts": 196, - "src/shared/agent-hook-listener/providers/muse-events.test.ts": 207, - "src/shared/agent-hook-listener/providers/zcode-events.test.ts": 197, - "src/shared/agent-hook-listener/reverse-file-region-scan.test.ts": 57, - "src/shared/agent-hook-listener/transcript-reader.test.ts": 94, - "src/shared/agent-hook-relay.test.ts": 54, - "src/shared/agent-hook-request-body-memory.test.ts": 92, - "src/shared/agent-hook-scrub-safe-env.test.ts": 58, - "src/shared/agent-hook-spool-read.test.ts": 62, - "src/shared/agent-hook-status-cache.test.ts": 80, - "src/shared/agent-hook-transport-interference.test.ts": 47, - "src/shared/agent-image-paste.test.ts": 57, - "src/shared/agent-kind.test.ts": 280, - "src/shared/agent-launch-operation.test.ts": 255, - "src/shared/agent-launch-preferences.test.ts": 45, - "src/shared/agent-launch-remote.test.ts": 43, - "src/shared/agent-lead-status-fold.test.ts": 55, - "src/shared/agent-main-agent-verdict.test.ts": 102, - "src/shared/agent-model-probe-spec.test.ts": 76, - "src/shared/agent-notification-id.test.ts": 48, - "src/shared/agent-process-presence-platform.test.ts": 55, - "src/shared/agent-process-presence-probe.test.ts": 69, - "src/shared/agent-process-recognition.test.ts": 204, - "src/shared/agent-prompt-injection.test.ts": 91, - "src/shared/agent-resume-argv-drop.test.ts": 134, - "src/shared/agent-resume-launch-command.test.ts": 195, - "src/shared/agent-row-conversation-name.test.ts": 70, - "src/shared/agent-scratch-worktrees.test.ts": 53, - "src/shared/agent-session-account-home.test.ts": 270, - "src/shared/agent-session-approval-blocked-path.test.ts": 50, - "src/shared/agent-session-approval-subject.test.ts": 228, - "src/shared/agent-session-attachments.test.ts": 46, - "src/shared/agent-session-availability.test.ts": 271, - "src/shared/agent-session-context-clear.test.ts": 362, - "src/shared/agent-session-conversation-name.test.ts": 64, - "src/shared/agent-session-definitive-refusal.test.ts": 277, - "src/shared/agent-session-failure-words.test.ts": 369, - "src/shared/agent-session-failure.test.ts": 269, - "src/shared/agent-session-journal-body-admission.test.ts": 303, - "src/shared/agent-session-journal-producer.test.ts": 47, - "src/shared/agent-session-journal-schemas.test.ts": 296, - "src/shared/agent-session-journal-submission-schema.test.ts": 239, - "src/shared/agent-session-lease-adjudication.test.ts": 62, - "src/shared/agent-session-legacy-handoff-lease.test.ts": 259, - "src/shared/agent-session-message-source.test.ts": 286, - "src/shared/agent-session-mutation-envelope.test.ts": 264, - "src/shared/agent-session-operation-ledger.test.ts": 284, - "src/shared/agent-session-option-catalog-grok.test.ts": 96, - "src/shared/agent-session-option-catalog-omp.test.ts": 86, - "src/shared/agent-session-option-catalog-opencode.test.ts": 79, - "src/shared/agent-session-option-catalog-removers.test.ts": 87, - "src/shared/agent-session-option-catalog.test.ts": 73, - "src/shared/agent-session-provider-context.test.ts": 267, - "src/shared/agent-session-provider-handle.test.ts": 100, - "src/shared/agent-session-question-answer.test.ts": 51, - "src/shared/agent-session-question-free-text.test.ts": 53, - "src/shared/agent-session-record-stored-form.test.ts": 264, - "src/shared/agent-session-refusal-notice.test.ts": 433, - "src/shared/agent-session-refusal-retry.test.ts": 53, - "src/shared/agent-session-registered-agents.test.ts": 233, - "src/shared/agent-session-restart-continuation.test.ts": 51, - "src/shared/agent-session-resume-cursor.test.ts": 144, - "src/shared/agent-session-resume.test.ts": 83, - "src/shared/agent-session-sign-in.test.ts": 269, - "src/shared/agent-session-status-snapshot-fold.test.ts": 62, - "src/shared/agent-session-stored-agent.test.ts": 250, - "src/shared/agent-session-thread-goal.test.ts": 56, - "src/shared/agent-session-turn-record.test.ts": 244, - "src/shared/agent-session-wire-refusals.test.ts": 250, - "src/shared/agent-session-write-failure.test.ts": 269, - "src/shared/agent-skill-sharing-contract.test.ts": 218, - "src/shared/agent-skill-sharing-gate.test.ts": 253, - "src/shared/agent-startup-plan-inputs-extra-agent-args.test.ts": 167, - "src/shared/agent-startup-plan-inputs.test.ts": 149, - "src/shared/agent-status-child-work-admission-activity.test.ts": 400, - "src/shared/agent-status-child-work-admission-collision.test.ts": 313, - "src/shared/agent-status-child-work-admission-parse.test.ts": 483, - "src/shared/agent-status-child-work-admission-sparse.test.ts": 334, - "src/shared/agent-status-child-work-admission.test.ts": 383, - "src/shared/agent-status-child-work-legacy-golden.test.ts": 77, - "src/shared/agent-status-child-work-legality.test.ts": 395, - "src/shared/agent-status-child-work-lifetime.test.ts": 464, - "src/shared/agent-status-child-work-liveness.test.ts": 47, - "src/shared/agent-status-child-work-projection.test.ts": 66, - "src/shared/agent-status-child-work-reconciliation.test.ts": 596, - "src/shared/agent-status-child-work-view-wire.test.ts": 49, - "src/shared/agent-status-child-work-view.test.ts": 339, - "src/shared/agent-status-child-work.test.ts": 311, - "src/shared/agent-status-legacy-adapter.test.ts": 65, - "src/shared/agent-status-observation.test.ts": 51, - "src/shared/agent-status-osc-pending-retention.test.ts": 126, - "src/shared/agent-status-osc-scan-budget.test.ts": 267, - "src/shared/agent-status-osc-split-frame-scan-budget.test.ts": 128, + "src/renderer/src/store/slices/linear-issue-cache-refresh.test.ts": 340, + "src/renderer/src/store/slices/linear-scoped-collection-cache.test.ts": 366, + "src/renderer/src/store/slices/linear-source-context-cache-scope.test.ts": 344, + "src/renderer/src/store/slices/linear.test.ts": 339, + "src/renderer/src/store/slices/local-detected-agent-state.test.ts": 404, + "src/renderer/src/store/slices/memory.test.ts": 48, + "src/renderer/src/store/slices/native-chat-launch-draft-teardown.test.ts": 1475, + "src/renderer/src/store/slices/new-issue-draft.test.ts": 31, + "src/renderer/src/store/slices/new-markdown.test.ts": 699, + "src/renderer/src/store/slices/orca-profiles-auth-actions-connect-overlap.test.ts": 1207, + "src/renderer/src/store/slices/orca-profiles-auth-actions.test.ts": 1291, + "src/renderer/src/store/slices/orca-profiles.test.ts": 1385, + "src/renderer/src/store/slices/pane-column-split-drop-no-op.test.ts": 34, + "src/renderer/src/store/slices/persisted-ui-write-baseline.test.ts": 50, + "src/renderer/src/store/slices/pinned-tab-close-confirm.test.ts": 85, + "src/renderer/src/store/slices/preflight.test.ts": 341, + "src/renderer/src/store/slices/project-group-removal-targets.test.ts": 38, + "src/renderer/src/store/slices/project-removal-owner-host.test.ts": 1305, + "src/renderer/src/store/slices/project-removal-runtime-terminal-order.test.ts": 1505, + "src/renderer/src/store/slices/pull-request-generation-jira-title.test.ts": 45, + "src/renderer/src/store/slices/purge-stale-runtime-host-groups.test.ts": 1231, + "src/renderer/src/store/slices/purge-stale-runtime-host-ownership.test.ts": 1471, + "src/renderer/src/store/slices/purge-stale-runtime-host-state.test.ts": 1296, + "src/renderer/src/store/slices/readopted-ssh-worktree-rows.test.ts": 40, + "src/renderer/src/store/slices/recently-closed-tabs.test.ts": 1416, + "src/renderer/src/store/slices/remote-server-updates.integration.test.ts": 284, + "src/renderer/src/store/slices/repo-identity-reconcile.test.ts": 47, + "src/renderer/src/store/slices/repo-owner-cache-identity.test.ts": 32, + "src/renderer/src/store/slices/repo-reorder-host-split.test.ts": 31, + "src/renderer/src/store/slices/repos-add-races.test.ts": 1400, + "src/renderer/src/store/slices/repos-all-hosts-folder-workspaces.test.ts": 1250, + "src/renderer/src/store/slices/repos-all-hosts-generation.test.ts": 1336, + "src/renderer/src/store/slices/repos-all-hosts.test.ts": 1295, + "src/renderer/src/store/slices/repos-catalog-connection-fence.test.ts": 1349, + "src/renderer/src/store/slices/repos-catalog-merge-identity.test.ts": 1449, + "src/renderer/src/store/slices/repos-catalog-notifications.test.ts": 1416, + "src/renderer/src/store/slices/repos-cross-host-project-collisions.test.ts": 1409, + "src/renderer/src/store/slices/repos-cross-host-refresh-identity.test.ts": 1286, + "src/renderer/src/store/slices/repos-ephemeral-vm-cleanup-retention.test.ts": 1564, + "src/renderer/src/store/slices/repos-host-identity-routing.test.ts": 1502, + "src/renderer/src/store/slices/repos-manual-order-hydration.test.ts": 1376, + "src/renderer/src/store/slices/repos-module-lifetime-coordinators.test.ts": 1733, + "src/renderer/src/store/slices/repos-nested-import-refresh-failures.test.ts": 1314, + "src/renderer/src/store/slices/repos-nested-ssh-projection.test.ts": 1387, + "src/renderer/src/store/slices/repos-onboarding-folder-startup.test.ts": 1560, + "src/renderer/src/store/slices/repos-paired-runtime-add.test.ts": 1298, + "src/renderer/src/store/slices/repos-project-group-create-race.test.ts": 1409, + "src/renderer/src/store/slices/repos-project-groups-delete.test.ts": 1163, + "src/renderer/src/store/slices/repos-project-groups-owner-routing.test.ts": 1308, + "src/renderer/src/store/slices/repos-project-groups.test.ts": 1404, + "src/renderer/src/store/slices/repos-project-host-capability.test.ts": 1473, + "src/renderer/src/store/slices/repos-project-host-lifecycle.test.ts": 1304, + "src/renderer/src/store/slices/repos-project-runtime.test.ts": 1492, + "src/renderer/src/store/slices/repos-refresh-identity.test.ts": 1464, + "src/renderer/src/store/slices/repos-remove-missing-remote-project.test.ts": 1274, + "src/renderer/src/store/slices/repos-remove-project-purge-leak.test.ts": 1266, + "src/renderer/src/store/slices/repos-runtime-project-groups.test.ts": 1224, + "src/renderer/src/store/slices/repos-runtime-visibility-defaults.test.ts": 1337, + "src/renderer/src/store/slices/repos-selected-owner-routing.test.ts": 1447, + "src/renderer/src/store/slices/repos-setup-script-dismissals.test.ts": 1304, + "src/renderer/src/store/slices/repos-shared-project-badge-color.test.ts": 1371, + "src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts": 1383, + "src/renderer/src/store/slices/repos-stale-fetch.test.ts": 1393, + "src/renderer/src/store/slices/repos-update-serialization.test.ts": 1388, + "src/renderer/src/store/slices/repos.runtime-fallback.test.ts": 1452, + "src/renderer/src/store/slices/repos.test.ts": 1355, + "src/renderer/src/store/slices/restored-editor-owner-reparent.test.ts": 1664, + "src/renderer/src/store/slices/runtime-catalog-merge-removed-env-guard.test.ts": 1344, + "src/renderer/src/store/slices/runtime-detected-agents-pairing.test.ts": 1101, + "src/renderer/src/store/slices/runtime-detected-agents.test.ts": 1396, + "src/renderer/src/store/slices/runtime-environment-peer-replacement.test.ts": 232, + "src/renderer/src/store/slices/runtime-environment-ssh.test.ts": 1366, + "src/renderer/src/store/slices/runtime-host-purge-session-partition-split.test.ts": 1420, + "src/renderer/src/store/slices/runtime-managed-repair-keeps-workspaces.test.ts": 1285, + "src/renderer/src/store/slices/runtime-pane-title-sort-epoch.test.ts": 1322, + "src/renderer/src/store/slices/runtime-status-catalog-identity.test.ts": 1425, + "src/renderer/src/store/slices/runtime-status-first-publication.test.ts": 315, + "src/renderer/src/store/slices/runtime-status-reconnect-connection-generation.test.ts": 1411, + "src/renderer/src/store/slices/runtime-status-refresh-diagnostics.test.ts": 43, + "src/renderer/src/store/slices/runtime-status-restored-browser-host-attach.test.ts": 323, + "src/renderer/src/store/slices/runtime-status-skill-cache-eviction.test.ts": 1667, + "src/renderer/src/store/slices/runtime-status-snapshot.test.ts": 397, + "src/renderer/src/store/slices/runtime-status.test.ts": 630, + "src/renderer/src/store/slices/runtime-switch-settings-persistence.test.ts": 1392, + "src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts": 1327, + "src/renderer/src/store/slices/set-runtime-environments-purge-wiring.test.ts": 1305, + "src/renderer/src/store/slices/settings-owner-hydration-write-fence.test.ts": 1502, + "src/renderer/src/store/slices/settings-search-state.test.ts": 32, + "src/renderer/src/store/slices/settings.test.ts": 1580, + "src/renderer/src/store/slices/sparse-presets-repo-removal-purge-leak.test.ts": 1343, + "src/renderer/src/store/slices/sparse-presets.test.ts": 69, + "src/renderer/src/store/slices/ssh-target-cleanup.test.ts": 1483, + "src/renderer/src/store/slices/ssh.test.ts": 1294, + "src/renderer/src/store/slices/stale-runtime-host-rows.test.ts": 45, + "src/renderer/src/store/slices/store-active-worktree-selection.test.ts": 1470, + "src/renderer/src/store/slices/store-active-worktree-split-groups.test.ts": 1463, + "src/renderer/src/store/slices/store-active-worktree-tab-close.test.ts": 1610, + "src/renderer/src/store/slices/store-active-worktree-terminal-creation.test.ts": 1369, + "src/renderer/src/store/slices/store-create-tab-first-pane-layout.test.ts": 1422, + "src/renderer/src/store/slices/store-create-tab-id-hint.test.ts": 1351, + "src/renderer/src/store/slices/store-session-browser-hydration.test.ts": 1413, + "src/renderer/src/store/slices/store-session-cascades.test.ts": 1570, + "src/renderer/src/store/slices/store-session-editor-hydration.test.ts": 1767, + "src/renderer/src/store/slices/store-session-terminal-activity.test.ts": 1414, + "src/renderer/src/store/slices/store-session-terminal-reconnect.test.ts": 1423, + "src/renderer/src/store/slices/store-session-workspace-hydration.test.ts": 1325, + "src/renderer/src/store/slices/store-sleep-agent-status-retention.test.ts": 1830, + "src/renderer/src/store/slices/store-sleep-exact-runtime-stop.test.ts": 1381, + "src/renderer/src/store/slices/store-sleep-pane-hibernation.test.ts": 1252, + "src/renderer/src/store/slices/store-sleep-runtime-convergence.test.ts": 1696, + "src/renderer/src/store/slices/store-sleep-shutdown-order.test.ts": 1299, + "src/renderer/src/store/slices/store-worktree-removal-cascade.test.ts": 1464, + "src/renderer/src/store/slices/superseded-ssh-repo-rows.test.ts": 45, + "src/renderer/src/store/slices/tab-group-reference-repair.test.ts": 64, + "src/renderer/src/store/slices/tab-group-state.test.ts": 47, + "src/renderer/src/store/slices/tab-view-mode.test.ts": 1309, + "src/renderer/src/store/slices/tab-worktree-orphan-map-purge-leak.test.ts": 1514, + "src/renderer/src/store/slices/tabs-close-dangling-group.test.ts": 1500, + "src/renderer/src/store/slices/tabs-hydration-generated-title.test.ts": 42, + "src/renderer/src/store/slices/tabs-hydration-group-validation.test.ts": 46, + "src/renderer/src/store/slices/tabs-hydration.test.ts": 67, + "src/renderer/src/store/slices/tabs-label-and-pin-state.test.ts": 1407, + "src/renderer/src/store/slices/tabs-model-reconciliation.test.ts": 1244, + "src/renderer/src/store/slices/tabs-open-close-lifecycle.test.ts": 1492, + "src/renderer/src/store/slices/tabs-pane-layout-operations.test.ts": 1539, + "src/renderer/src/store/slices/tabs-session-hydration.test.ts": 1269, + "src/renderer/src/store/slices/tabs-unread-and-focus.test.ts": 1375, + "src/renderer/src/store/slices/tabs/hydrated-workspace-reconciliation-batch.test.ts": 1301, + "src/renderer/src/store/slices/tabs/structured-chat-close-owner.test.ts": 1337, + "src/renderer/src/store/slices/tabs/tab-selection-contract.test.ts": 48, + "src/renderer/src/store/slices/tabs/tabs-reconciliation-batch-identity.test.ts": 1451, + "src/renderer/src/store/slices/task-creation-drafts.test.ts": 38, + "src/renderer/src/store/slices/terminal-helpers.test.ts": 79, + "src/renderer/src/store/slices/terminal-input-activity-store-write.test.ts": 1172, + "src/renderer/src/store/slices/terminal-layout-pty-ownership.test.ts": 1444, + "src/renderer/src/store/slices/terminal-orphan-helpers.test.ts": 308, + "src/renderer/src/store/slices/terminal-pane-detach-agent-identity.test.ts": 1265, + "src/renderer/src/store/slices/terminal-pane-detach-agent-retention.test.ts": 1699, + "src/renderer/src/store/slices/terminal-pane-retirement-ownership.test.ts": 279, + "src/renderer/src/store/slices/terminal-pty-identity-replacement.test.ts": 1347, + "src/renderer/src/store/slices/terminal-quick-command-hosts.test.ts": 1420, + "src/renderer/src/store/slices/terminal-startup-command-retention.test.ts": 1871, + "src/renderer/src/store/slices/terminal-tab-id-hydration.test.ts": 1387, + "src/renderer/src/store/slices/terminal-tab-owner-index.test.ts": 53, + "src/renderer/src/store/slices/terminal-tab-recovery-remount.test.ts": 1449, + "src/renderer/src/store/slices/terminal-tab-retirement-store.test.ts": 1384, + "src/renderer/src/store/slices/terminal-tab-retirement.test.ts": 349, + "src/renderer/src/store/slices/terminal-tab-title-batch.test.ts": 1592, + "src/renderer/src/store/slices/terminals-explicit-empty-hydration.test.ts": 1294, + "src/renderer/src/store/slices/terminals-hydration-canonical-pty-overlap.test.ts": 1413, + "src/renderer/src/store/slices/terminals-hydration-canonical-rows.test.ts": 1304, + "src/renderer/src/store/slices/terminals-hydration.test.ts": 1408, + "src/renderer/src/store/slices/ui-acknowledge-agents-clock-skew.test.ts": 474, + "src/renderer/src/store/slices/ui-agent-send-target.test.ts": 567, + "src/renderer/src/store/slices/ui-contextual-tours.test.ts": 482, + "src/renderer/src/store/slices/ui-feature-interactions.test.ts": 509, + "src/renderer/src/store/slices/ui-host-filter-sync.test.ts": 516, + "src/renderer/src/store/slices/ui-hydration-view-layout.test.ts": 569, + "src/renderer/src/store/slices/ui-hydration-workspace-cleanup-browse.test.ts": 477, + "src/renderer/src/store/slices/ui-hydration-workspace-preferences.test.ts": 610, + "src/renderer/src/store/slices/ui-modal-slot-dismissal.test.ts": 547, + "src/renderer/src/store/slices/ui-notice-dismissals.test.ts": 611, + "src/renderer/src/store/slices/ui-page-navigation.test.ts": 668, + "src/renderer/src/store/slices/ui/ui-slice-surface-actions.test.ts": 1266, + "src/renderer/src/store/slices/usage-snapshot-refresh.benchmark.test.ts": 41, + "src/renderer/src/store/slices/usage-web-client-fallback.test.ts": 35, + "src/renderer/src/store/slices/workspace-activation-preparation.test.ts": 1480, + "src/renderer/src/store/slices/workspace-attention/workspace-live-markers.test.ts": 274, + "src/renderer/src/store/slices/workspace-attention/workspace-unread-characterization.test.ts": 1477, + "src/renderer/src/store/slices/workspace-attention/workspace-unread-on-screen-characterization.test.ts": 1264, + "src/renderer/src/store/slices/workspace-attention/workspace-unread-removal-characterization.test.ts": 1613, + "src/renderer/src/store/slices/workspace-attention/workspace-unread-screen-moves-characterization.test.ts": 1578, + "src/renderer/src/store/slices/workspace-chat-draft-removal.test.ts": 1488, + "src/renderer/src/store/slices/workspace-cleanup-browse.test.ts": 47, + "src/renderer/src/store/slices/workspace-cleanup-cache-hydration.test.ts": 548, + "src/renderer/src/store/slices/workspace-cleanup-enrichment-performance.test.ts": 658, + "src/renderer/src/store/slices/workspace-cleanup-host-qualified-list-state.test.ts": 1309, + "src/renderer/src/store/slices/workspace-cleanup-local-evidence-invariants.test.ts": 363, + "src/renderer/src/store/slices/workspace-cleanup-removal-preflight.test.ts": 466, + "src/renderer/src/store/slices/workspace-cleanup-scan-progress-boundary.test.ts": 81, + "src/renderer/src/store/slices/workspace-cleanup-scan-progress.test.ts": 1015, + "src/renderer/src/store/slices/workspace-cleanup-unverified-removal-consent.test.ts": 404, + "src/renderer/src/store/slices/workspace-cleanup-wrong-host-removal-guard.test.ts": 1185, + "src/renderer/src/store/slices/workspace-cleanup-wrong-host-removal.test.ts": 1336, + "src/renderer/src/store/slices/workspace-document-title-refresh.test.ts": 1379, + "src/renderer/src/store/slices/workspace-space.test.ts": 41, + "src/renderer/src/store/slices/worktree-by-id-index.test.ts": 37, + "src/renderer/src/store/slices/worktree-catalog-reconciliation.test.ts": 31, + "src/renderer/src/store/slices/worktree-helpers.test.ts": 79, + "src/renderer/src/store/slices/worktree-listing-branch-switch.test.ts": 44, + "src/renderer/src/store/slices/worktree-meta-update-application.test.ts": 70, + "src/renderer/src/store/slices/worktree-nav-history-view-entries.test.ts": 110, + "src/renderer/src/store/slices/worktree-nav-history.test.ts": 74, + "src/renderer/src/store/slices/worktree-removal-maps-leak.test.ts": 1410, + "src/renderer/src/store/slices/worktree-sleep-intent-lifecycle.test.ts": 1374, + "src/renderer/src/store/slices/worktree-terminal-removal-teardown.test.ts": 1408, + "src/renderer/src/store/slices/worktree-visibility-owner-settings.test.ts": 275, + "src/renderer/src/store/slices/worktree-visibility-settings-write.test.ts": 1290, + "src/renderer/src/store/slices/worktrees-activity-persistence.test.ts": 890, + "src/renderer/src/store/slices/worktrees-always-force-delete.test.ts": 831, + "src/renderer/src/store/slices/worktrees-background-removal.test.ts": 1421, + "src/renderer/src/store/slices/worktrees-create-base-status.test.ts": 802, + "src/renderer/src/store/slices/worktrees-create-captured-host.test.ts": 702, + "src/renderer/src/store/slices/worktrees-create-parent-pick.test.ts": 782, + "src/renderer/src/store/slices/worktrees-fetch-listing-merge.test.ts": 909, + "src/renderer/src/store/slices/worktrees-fetch-owner-routing.test.ts": 1433, + "src/renderer/src/store/slices/worktrees-fetch-persisted-metadata-fallback.test.ts": 800, + "src/renderer/src/store/slices/worktrees-fetch-refresh-coalescing.test.ts": 736, + "src/renderer/src/store/slices/worktrees-fetch-remote-lineage.test.ts": 821, + "src/renderer/src/store/slices/worktrees-fetch-removal-purge.test.ts": 894, + "src/renderer/src/store/slices/worktrees-fetch-stale-catalog-version.test.ts": 914, + "src/renderer/src/store/slices/worktrees-git-identity-branch-title.test.ts": 806, + "src/renderer/src/store/slices/worktrees-git-identity-review-clear-persistence.test.ts": 1290, + "src/renderer/src/store/slices/worktrees-hydration-purge.test.ts": 820, + "src/renderer/src/store/slices/worktrees-identity-migration.test.ts": 1116, + "src/renderer/src/store/slices/worktrees-lineage-state.test.ts": 859, + "src/renderer/src/store/slices/worktrees-linked-review-push-target.test.ts": 831, + "src/renderer/src/store/slices/worktrees-metadata-persistence.test.ts": 873, + "src/renderer/src/store/slices/worktrees-multiple-attachments.test.ts": 884, + "src/renderer/src/store/slices/worktrees-pending-creation-state.test.ts": 959, + "src/renderer/src/store/slices/worktrees-purge-terminal-state.test.ts": 775, + "src/renderer/src/store/slices/worktrees-remote-runtime-create.test.ts": 940, + "src/renderer/src/store/slices/worktrees-remote-runtime-removal.test.ts": 839, + "src/renderer/src/store/slices/worktrees-removal-state-cleanup.test.ts": 903, + "src/renderer/src/store/slices/worktrees-runtime-connection-generation-fence.test.ts": 871, + "src/renderer/src/store/slices/worktrees-runtime-host-metadata-retirement.test.ts": 917, + "src/renderer/src/store/slices/worktrees-terminal-pr-exact-observation.test.ts": 768, + "src/renderer/src/store/slices/worktrees-terminal-pr-url-linking.test.ts": 849, + "src/renderer/src/store/slices/worktrees-terminal-reference-admission.test.ts": 1666, + "src/renderer/src/store/slices/worktrees-terminal-reference-origins.test.ts": 708, + "src/renderer/src/store/slices/worktrees-terminal-reference-source-race.test.ts": 858, + "src/renderer/src/store/slices/worktrees-unread-state.test.ts": 1581, + "src/renderer/src/store/slices/worktrees-workspace-selection-state.test.ts": 911, + "src/renderer/src/store/slices/worktrees/create/runtime-provisioned-root-adoption.test.ts": 33, + "src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.test.ts": 276, + "src/renderer/src/store/slices/worktrees/listing/detected-worktree-unavailable-reason.test.ts": 276, + "src/renderer/src/store/slices/worktrees/listing/stale-listing-purges-created-worktree.test.ts": 1412, + "src/renderer/src/store/slices/worktrees/listing/worktree-host-ownership-unstamped.test.ts": 78, + "src/renderer/src/store/slices/worktrees/metadata/worktree-meta-persist.test.ts": 379, + "src/renderer/src/store/slices/worktrees/session/worktree-identity-rename-row-worktree-ids.test.ts": 769, + "src/renderer/src/store/slices/worktrees/session/worktree-no-op-notifications.test.ts": 703, + "src/renderer/src/store/slices/worktrees/teardown/host-qualified-removal-refusal.test.ts": 455, + "src/renderer/src/store/slices/worktrees/teardown/record-key-omission.test.ts": 40, + "src/renderer/src/store/slices/worktrees/teardown/remove-worktree-map-identity.test.ts": 394, + "src/renderer/src/store/slices/worktrees/teardown/worktree-teardown-array-identity.test.ts": 447, + "src/renderer/src/store/store-identity-churn-probe.test.ts": 44, + "src/renderer/src/store/store-listener-census.test.ts": 36, + "src/renderer/src/store/terminals/restored-relay-session-identity.test.ts": 1401, + "src/renderer/src/store/terminals/structured-chat-tab-rename.test.ts": 1235, + "src/renderer/src/store/terminals/terminal-exited-pane-liveness.test.ts": 1180, + "src/renderer/src/store/terminals/terminal-no-op-subscriber.test.ts": 1216, + "src/renderer/src/store/terminals/terminal-pane-expansion-write-bailout.test.tsx": 1564, + "src/renderer/src/store/terminals/terminal-shutdown-guards-identity.test.ts": 34, + "src/renderer/src/store/terminals/terminal-shutdown-map-identity.test.ts": 572, + "src/renderer/src/store/terminals/terminal-startup-queue-restart.test.ts": 1284, + "src/renderer/src/store/terminals/terminal-tab-close-intent.test.ts": 1411, + "src/renderer/src/store/terminals/terminal-workspace-routing.scale.test.ts": 319, + "src/renderer/src/store/terminals/workspace-terminal-placeholders.test.ts": 250, + "src/renderer/src/store/workspace-layout-cache.test.ts": 35, + "src/renderer/src/store/worktree-diff-comments-selector.test.ts": 103, + "src/renderer/src/store/worktree-host-collision-index.test.ts": 37, + "src/renderer/src/store/worktree-visibility-defaults-by-host.test.ts": 28, + "src/renderer/src/web/preload-api/web-agent-session-attachment-upload.test.ts": 37, + "src/renderer/src/web/preload-api/web-ai-vault-search-compat.test.ts": 265, + "src/renderer/src/web/preload-api/web-fallback-api.test.ts": 34, + "src/renderer/src/web/preload-api/web-git-stage-scope.test.ts": 730, + "src/renderer/src/web/preload-api/web-gitlab-api.test.ts": 229, + "src/renderer/src/web/preload-api/web-ui-compact-notice.test.ts": 916, + "src/renderer/src/web/preload-api/web-ui-explorer-root.test.ts": 629, + "src/renderer/src/web/preload-api/web-worktree-attachment-mutations.test.ts": 234, + "src/renderer/src/web/web-clipboard-copy-fallback.test.ts": 35, + "src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts": 162, + "src/renderer/src/web/web-clipboard-image-presence.test.ts": 620, + "src/renderer/src/web/web-device-key.test.ts": 92, + "src/renderer/src/web/web-file-mutation-methods.test.ts": 282, + "src/renderer/src/web/web-native-chat-appearance-settings.test.ts": 282, + "src/renderer/src/web/web-pairing.test.ts": 231, + "src/renderer/src/web/web-preload-api-agent-providers.test.ts": 949, + "src/renderer/src/web/web-preload-api-clipboard.test.ts": 2183, + "src/renderer/src/web/web-preload-api-composition.test.ts": 547, + "src/renderer/src/web/web-preload-api-filesystem.test.ts": 722, + "src/renderer/src/web/web-preload-api-git.test.ts": 535, + "src/renderer/src/web/web-preload-api-github.test.ts": 806, + "src/renderer/src/web/web-preload-api-gitlab.test.ts": 901, + "src/renderer/src/web/web-preload-api-jira.test.ts": 662, + "src/renderer/src/web/web-preload-api-keybindings.test.ts": 774, + "src/renderer/src/web/web-preload-api-rate-limits.test.ts": 2180, + "src/renderer/src/web/web-preload-api-runtime-calls.test.ts": 994, + "src/renderer/src/web/web-preload-api-runtime-environment.test.ts": 1776, + "src/renderer/src/web/web-preload-api-settings.test.ts": 2494, + "src/renderer/src/web/web-preload-api-ssh.test.ts": 454, + "src/renderer/src/web/web-preload-api-ui-pairing-local-fields.test.ts": 2126, + "src/renderer/src/web/web-preload-api-ui.test.ts": 2812, + "src/renderer/src/web/web-preload-api-workspace-catalog.test.ts": 1835, + "src/renderer/src/web/web-preload-updater-package-recovery.test.ts": 566, + "src/renderer/src/web/web-runtime-auth-verdict.test.ts": 33, + "src/renderer/src/web/web-runtime-client-file-watch-replay.test.ts": 348, + "src/renderer/src/web/web-runtime-client-heartbeat.test.ts": 375, + "src/renderer/src/web/web-runtime-client-timeout-budget.test.ts": 312, + "src/renderer/src/web/web-runtime-client.test.ts": 763, + "src/renderer/src/web/web-runtime-connection-frame-router.test.ts": 268, + "src/renderer/src/web/web-runtime-connection-heartbeat-unsendable-probe.test.ts": 33, + "src/renderer/src/web/web-runtime-credential-source.test.ts": 149, + "src/renderer/src/web/web-runtime-direct-proxy-live.integration.test.ts": 307, + "src/renderer/src/web/web-runtime-environment-repair.test.ts": 845, + "src/renderer/src/web/web-runtime-environment-token.test.ts": 191, + "src/renderer/src/web/web-runtime-status-owner.test.ts": 1031, + "src/renderer/src/web/web-shell-paths-exist.test.ts": 27, + "src/renderer/src/web/web-workspace-session.test.ts": 242, + "src/renderer/src/web/web-zcode-plan-settings.test.ts": 898, + "src/shared/__tests__/process-host/windows-cmd-shim-resolution.test.ts": 77, + "src/shared/__tests__/process-host/windows-cmd-shim-resolution.win32.test.ts": 48, + "src/shared/__tests__/process-host/windows-command-line.win32.test.ts": 60, + "src/shared/abort-signal-reason.test.ts": 30, + "src/shared/absolute-dir-override.test.ts": 38, + "src/shared/add-repo-existing-workspaces-telemetry.test.ts": 266, + "src/shared/agent-chat-permission-mode.test.ts": 233, + "src/shared/agent-child-row-model.test.ts": 80, + "src/shared/agent-cli-flag-detection.test.ts": 39, + "src/shared/agent-cli-install-dir-fallback.test.ts": 69, + "src/shared/agent-command-line-intent.test.ts": 75, + "src/shared/agent-decorative-title-signature.test.ts": 46, + "src/shared/agent-detection.test.ts": 124, + "src/shared/agent-feature-install-commands.test.ts": 35, + "src/shared/agent-hook-endpoint-file.test.ts": 33, + "src/shared/agent-hook-endpoint-temp-cleanup.test.ts": 61, + "src/shared/agent-hook-listener-antigravity.test.ts": 245, + "src/shared/agent-hook-listener-claude-compatible-vendors.test.ts": 195, + "src/shared/agent-hook-listener-claude-reordered-task-notification.test.ts": 187, + "src/shared/agent-hook-listener-claude-subagents.test.ts": 260, + "src/shared/agent-hook-listener-claude-task-notification.test.ts": 344, + "src/shared/agent-hook-listener-claude-turn-state.test.ts": 218, + "src/shared/agent-hook-listener-codex-approval-ownership.test.ts": 200, + "src/shared/agent-hook-listener-codex-interrupt.test.ts": 165, + "src/shared/agent-hook-listener-codex-main-agent.test.ts": 177, + "src/shared/agent-hook-listener-command-code-transcript.test.ts": 282, + "src/shared/agent-hook-listener-extraction-characterization.test.ts": 190, + "src/shared/agent-hook-listener-grok-completion.test.ts": 188, + "src/shared/agent-hook-listener-grok.test.ts": 210, + "src/shared/agent-hook-listener-hermes-codex-droid.test.ts": 181, + "src/shared/agent-hook-listener-interactive-prompts.test.ts": 166, + "src/shared/agent-hook-listener-jcode.test.ts": 192, + "src/shared/agent-hook-listener-omp-approval-ownership.test.ts": 190, + "src/shared/agent-hook-listener-opencode-reattribution.test.ts": 227, + "src/shared/agent-hook-listener-opencode-session-correlation.test.ts": 60, + "src/shared/agent-hook-listener-opencode-session-registry.test.ts": 67, + "src/shared/agent-hook-listener-opencode-tui-identity.test.ts": 196, + "src/shared/agent-hook-listener-pi-compatible.test.ts": 206, + "src/shared/agent-hook-listener-roster-retention.test.ts": 243, + "src/shared/agent-hook-listener-session-replacement.test.ts": 180, + "src/shared/agent-hook-listener-startless-child-lifecycle.test.ts": 213, + "src/shared/agent-hook-listener-transport.test.ts": 253, + "src/shared/agent-hook-listener/providers/dsh-events.test.ts": 189, + "src/shared/agent-hook-listener/providers/hermes-events.test.ts": 91, + "src/shared/agent-hook-listener/providers/jcode-turn-prompt.test.ts": 99, + "src/shared/agent-hook-listener/providers/kiro-events.test.ts": 170, + "src/shared/agent-hook-listener/providers/muse-events.test.ts": 195, + "src/shared/agent-hook-listener/providers/zcode-events.test.ts": 164, + "src/shared/agent-hook-listener/reverse-file-region-scan.test.ts": 44, + "src/shared/agent-hook-listener/transcript-reader.test.ts": 75, + "src/shared/agent-hook-relay.test.ts": 49, + "src/shared/agent-hook-request-body-memory.test.ts": 71, + "src/shared/agent-hook-scrub-safe-env.test.ts": 37, + "src/shared/agent-hook-spool-read.test.ts": 34, + "src/shared/agent-hook-status-cache.test.ts": 70, + "src/shared/agent-hook-transport-interference.test.ts": 41, + "src/shared/agent-image-paste.test.ts": 50, + "src/shared/agent-kind.test.ts": 273, + "src/shared/agent-launch-operation.test.ts": 258, + "src/shared/agent-launch-preferences.test.ts": 29, + "src/shared/agent-launch-remote.test.ts": 40, + "src/shared/agent-lead-status-fold.test.ts": 42, + "src/shared/agent-main-agent-verdict.test.ts": 65, + "src/shared/agent-model-probe-spec.test.ts": 57, + "src/shared/agent-notification-id.test.ts": 40, + "src/shared/agent-process-presence-platform.test.ts": 35, + "src/shared/agent-process-presence-probe.test.ts": 56, + "src/shared/agent-process-recognition.test.ts": 175, + "src/shared/agent-prompt-injection.test.ts": 57, + "src/shared/agent-resume-argv-drop.test.ts": 129, + "src/shared/agent-resume-launch-command.test.ts": 202, + "src/shared/agent-row-conversation-name.test.ts": 68, + "src/shared/agent-scratch-worktrees.test.ts": 51, + "src/shared/agent-session-account-home.test.ts": 265, + "src/shared/agent-session-approval-blocked-path.test.ts": 40, + "src/shared/agent-session-approval-subject.test.ts": 233, + "src/shared/agent-session-attachments.test.ts": 39, + "src/shared/agent-session-availability.test.ts": 259, + "src/shared/agent-session-background-task-state-admission.test.ts": 224, + "src/shared/agent-session-context-clear.test.ts": 321, + "src/shared/agent-session-context-usage-schema.test.ts": 225, + "src/shared/agent-session-conversation-name.test.ts": 54, + "src/shared/agent-session-definitive-refusal.test.ts": 318, + "src/shared/agent-session-failure-words.test.ts": 390, + "src/shared/agent-session-failure.test.ts": 273, + "src/shared/agent-session-journal-body-admission.test.ts": 277, + "src/shared/agent-session-journal-producer.test.ts": 31, + "src/shared/agent-session-journal-schemas.test.ts": 285, + "src/shared/agent-session-journal-submission-schema.test.ts": 179, + "src/shared/agent-session-lease-adjudication.test.ts": 48, + "src/shared/agent-session-legacy-handoff-lease.test.ts": 276, + "src/shared/agent-session-message-source.test.ts": 229, + "src/shared/agent-session-mutation-envelope.test.ts": 239, + "src/shared/agent-session-operation-ledger.test.ts": 352, + "src/shared/agent-session-option-catalog-grok.test.ts": 107, + "src/shared/agent-session-option-catalog-omp.test.ts": 80, + "src/shared/agent-session-option-catalog-opencode.test.ts": 59, + "src/shared/agent-session-option-catalog-removers.test.ts": 67, + "src/shared/agent-session-option-catalog.test.ts": 90, + "src/shared/agent-session-provider-context.test.ts": 233, + "src/shared/agent-session-provider-handle.test.ts": 90, + "src/shared/agent-session-question-answer.test.ts": 41, + "src/shared/agent-session-question-free-text.test.ts": 41, + "src/shared/agent-session-record-stored-form.test.ts": 250, + "src/shared/agent-session-refusal-notice-stop.test.ts": 224, + "src/shared/agent-session-refusal-notice.test.ts": 401, + "src/shared/agent-session-refusal-retry.test.ts": 38, + "src/shared/agent-session-registered-agents.test.ts": 236, + "src/shared/agent-session-restart-continuation.test.ts": 31, + "src/shared/agent-session-resume-cursor.test.ts": 128, + "src/shared/agent-session-resume.test.ts": 80, + "src/shared/agent-session-sign-in.test.ts": 265, + "src/shared/agent-session-status-snapshot-fold.test.ts": 36, + "src/shared/agent-session-stopped-history.test.ts": 209, + "src/shared/agent-session-stored-agent.test.ts": 208, + "src/shared/agent-session-thread-goal.test.ts": 41, + "src/shared/agent-session-turn-record.test.ts": 220, + "src/shared/agent-session-wire-refusals.test.ts": 235, + "src/shared/agent-session-write-failure.test.ts": 253, + "src/shared/agent-skill-sharing-contract.test.ts": 230, + "src/shared/agent-skill-sharing-gate.test.ts": 252, + "src/shared/agent-startup-plan-inputs-extra-agent-args.test.ts": 101, + "src/shared/agent-startup-plan-inputs.test.ts": 123, + "src/shared/agent-status-child-work-admission-activity.test.ts": 428, + "src/shared/agent-status-child-work-admission-collision.test.ts": 309, + "src/shared/agent-status-child-work-admission-parse.test.ts": 508, + "src/shared/agent-status-child-work-admission-sparse.test.ts": 313, + "src/shared/agent-status-child-work-admission.test.ts": 415, + "src/shared/agent-status-child-work-legacy-golden.test.ts": 50, + "src/shared/agent-status-child-work-legality.test.ts": 281, + "src/shared/agent-status-child-work-lifetime.test.ts": 408, + "src/shared/agent-status-child-work-liveness.test.ts": 34, + "src/shared/agent-status-child-work-projection.test.ts": 57, + "src/shared/agent-status-child-work-reconciliation.test.ts": 687, + "src/shared/agent-status-child-work-stop-acknowledged.test.ts": 290, + "src/shared/agent-status-child-work-view-wire.test.ts": 39, + "src/shared/agent-status-child-work-view.test.ts": 347, + "src/shared/agent-status-child-work.test.ts": 294, + "src/shared/agent-status-legacy-adapter.test.ts": 40, + "src/shared/agent-status-observation.test.ts": 47, + "src/shared/agent-status-osc-pending-retention.test.ts": 107, + "src/shared/agent-status-osc-scan-budget.test.ts": 232, + "src/shared/agent-status-osc-split-frame-scan-budget.test.ts": 109, "src/shared/agent-status-osc.test.ts": 98, - "src/shared/agent-status-run-alias-index.test.ts": 327, - "src/shared/agent-status-run-capability.test.ts": 87, - "src/shared/agent-status-run.test.ts": 64, - "src/shared/agent-status-serving-readiness.test.ts": 45, - "src/shared/agent-status-store-bounds.test.ts": 1432, - "src/shared/agent-status-store-byte-budget.test.ts": 1322, - "src/shared/agent-status-store-in-place.test.ts": 3761, - "src/shared/agent-status-store-parent.test.ts": 262, - "src/shared/agent-status-store-reopen.test.ts": 301, - "src/shared/agent-status-store-scaling.test.ts": 771, + "src/shared/agent-status-run-alias-index.test.ts": 291, + "src/shared/agent-status-run-capability.test.ts": 247, + "src/shared/agent-status-run.test.ts": 42, + "src/shared/agent-status-serving-readiness.test.ts": 37, + "src/shared/agent-status-store-bounds.test.ts": 1319, + "src/shared/agent-status-store-byte-budget.test.ts": 1366, + "src/shared/agent-status-store-in-place.test.ts": 2452, + "src/shared/agent-status-store-parent.test.ts": 258, + "src/shared/agent-status-store-reopen.test.ts": 308, + "src/shared/agent-status-store-scaling.test.ts": 716, "src/shared/agent-status-store.test.ts": 408, - "src/shared/agent-status-subject.test.ts": 317, - "src/shared/agent-status-transport-envelope.test.ts": 332, - "src/shared/agent-status-types.test.ts": 163, - "src/shared/agent-stop-display-status.test.ts": 51, - "src/shared/agent-tab-title.test.ts": 53, - "src/shared/agent-terminal-status-equivalence.test.ts": 40, - "src/shared/agent-title-agy-gemini-collision.test.ts": 84, - "src/shared/agent-title-decoration.test.ts": 46, - "src/shared/agent-title-evidence.test.ts": 114, - "src/shared/agent-title-identity-characterization.test.ts": 75, - "src/shared/agent-transcript-poll-scheduler.test.ts": 165, - "src/shared/agent-tui-input-clear.test.ts": 59, - "src/shared/ai-vault-resume-command.test.ts": 65, - "src/shared/ai-vault-resume-preparation.test.ts": 48, - "src/shared/ai-vault-scan-error-message.test.ts": 61, - "src/shared/ai-vault-search-agent-history.test.ts": 291, - "src/shared/ai-vault-search-contract.test.ts": 258, - "src/shared/ai-vault-search-query-operators.test.ts": 70, - "src/shared/ai-vault-search-scope-contract.test.ts": 247, - "src/shared/ai-vault-search-settings.test.ts": 214, - "src/shared/ai-vault-search-status-fallback.test.ts": 278, - "src/shared/ai-vault-session-depth.test.ts": 66, - "src/shared/ai-vault-session-filters.test.ts": 173, - "src/shared/ai-vault-types.test.ts": 54, - "src/shared/antigravity-source-control.test.ts": 317, - "src/shared/antigravity-transcript-reference.test.ts": 59, - "src/shared/app-version.test.ts": 54, - "src/shared/artifact-sharing-gate.test.ts": 312, - "src/shared/automation-cron-dialect.test.ts": 77, - "src/shared/automation-cron-input-validation.test.ts": 66, - "src/shared/automation-execution-target.test.ts": 50, - "src/shared/automation-extra-agent-args.test.ts": 166, - "src/shared/automation-host-filter.test.ts": 68, - "src/shared/automation-legacy-list-partition.test.ts": 53, - "src/shared/automation-list-response.test.ts": 53, - "src/shared/automation-list-scope.test.ts": 96, - "src/shared/automation-owner-key.test.ts": 62, - "src/shared/automation-owner-precondition.test.ts": 80, - "src/shared/automation-run-identity.test.ts": 40, - "src/shared/automation-run-retention.test.ts": 87, - "src/shared/automation-schedule-drift.test.ts": 97, - "src/shared/automation-schedules.test.ts": 154, - "src/shared/automation-usage-summary.test.ts": 48, - "src/shared/background-task-roster.test.ts": 98, - "src/shared/base-ref-search-result.test.ts": 46, - "src/shared/binary-file-extensions.test.ts": 52, - "src/shared/bounded-map.test.ts": 73, - "src/shared/branch-name-from-work.test.ts": 59, - "src/shared/branch-prefix.test.ts": 63, - "src/shared/browser-annotation-viewport-lifecycle.test.ts": 186, - "src/shared/browser-client-automation-protocol.test.ts": 240, - "src/shared/browser-client-host-id-argument.test.ts": 52, - "src/shared/browser-client-host-protocol.test.ts": 320, + "src/shared/agent-status-subject.test.ts": 256, + "src/shared/agent-status-transport-envelope.test.ts": 345, + "src/shared/agent-status-types.test.ts": 122, + "src/shared/agent-stop-display-status.test.ts": 27, + "src/shared/agent-tab-title.test.ts": 34, + "src/shared/agent-terminal-status-equivalence.test.ts": 39, + "src/shared/agent-title-agy-gemini-collision.test.ts": 51, + "src/shared/agent-title-decoration.test.ts": 26, + "src/shared/agent-title-evidence.test.ts": 164, + "src/shared/agent-title-identity-characterization.test.ts": 57, + "src/shared/agent-transcript-poll-scheduler.test.ts": 136, + "src/shared/agent-tui-input-clear.test.ts": 47, + "src/shared/ai-vault-resume-command.test.ts": 61, + "src/shared/ai-vault-resume-preparation.test.ts": 34, + "src/shared/ai-vault-scan-error-message.test.ts": 37, + "src/shared/ai-vault-search-agent-history.test.ts": 279, + "src/shared/ai-vault-search-contract.test.ts": 214, + "src/shared/ai-vault-search-query-operators.test.ts": 60, + "src/shared/ai-vault-search-scope-contract.test.ts": 258, + "src/shared/ai-vault-search-settings.test.ts": 208, + "src/shared/ai-vault-search-status-fallback.test.ts": 290, + "src/shared/ai-vault-session-depth.test.ts": 44, + "src/shared/ai-vault-session-filters.test.ts": 178, + "src/shared/ai-vault-types.test.ts": 31, + "src/shared/ansi-escape-sequences-consumers.test.ts": 113, + "src/shared/ansi-escape-sequences.test.ts": 267, + "src/shared/antigravity-source-control.test.ts": 234, + "src/shared/antigravity-transcript-reference.test.ts": 45, + "src/shared/app-version.test.ts": 34, + "src/shared/artifact-sharing-gate.test.ts": 262, + "src/shared/automatic-work-run-marker.test.ts": 181, + "src/shared/automation-cron-dialect.test.ts": 56, + "src/shared/automation-cron-input-validation.test.ts": 58, + "src/shared/automation-execution-target.test.ts": 38, + "src/shared/automation-extra-agent-args.test.ts": 214, + "src/shared/automation-host-filter.test.ts": 50, + "src/shared/automation-legacy-list-partition.test.ts": 45, + "src/shared/automation-list-response.test.ts": 54, + "src/shared/automation-list-scope.test.ts": 265, + "src/shared/automation-owner-key.test.ts": 41, + "src/shared/automation-owner-precondition.test.ts": 51, + "src/shared/automation-run-identity.test.ts": 35, + "src/shared/automation-run-retention.test.ts": 68, + "src/shared/automation-schedule-drift.test.ts": 70, + "src/shared/automation-schedules.test.ts": 170, + "src/shared/automation-usage-summary.test.ts": 33, + "src/shared/background-task-roster.test.ts": 92, + "src/shared/base-ref-search-result.test.ts": 23, + "src/shared/binary-file-extensions.test.ts": 31, + "src/shared/bounded-map.test.ts": 50, + "src/shared/branch-name-from-work.test.ts": 51, + "src/shared/branch-prefix.test.ts": 51, + "src/shared/browser-annotation-viewport-lifecycle.test.ts": 199, + "src/shared/browser-annotation-viewport-live-target.test.ts": 187, + "src/shared/browser-client-automation-protocol.test.ts": 241, + "src/shared/browser-client-host-id-argument.test.ts": 36, + "src/shared/browser-client-host-protocol.test.ts": 356, "src/shared/browser-client-host-reconciliation-protocol.test.ts": 254, - "src/shared/browser-client-hosting-eligibility.test.ts": 67, - "src/shared/browser-cookie-import-sources.test.ts": 57, - "src/shared/browser-grab-types.test.ts": 56, - "src/shared/browser-network-tunnel-protocol.test.ts": 45, - "src/shared/browser-network-tunnel-stream-framing.test.ts": 1622, - "src/shared/browser-screencast-protocol.test.ts": 108, - "src/shared/browser-url.test.ts": 308, - "src/shared/bundled-ripgrep.test.ts": 37, - "src/shared/cheap-process-table-snapshot.test.ts": 77, - "src/shared/check-job-log-byte-cap.test.ts": 94, - "src/shared/check-job-log-retention.test.ts": 222, - "src/shared/check-job-log-tail-slice.test.ts": 71, - "src/shared/claimed-agent-pty-owner.test.ts": 71, - "src/shared/claude-agent-teams-tmux-compat.test.ts": 62, - "src/shared/claude-background-task-kind.test.ts": 42, - "src/shared/claude-background-task-status.test.ts": 260, - "src/shared/claude-model-list-probe.test.ts": 57, - "src/shared/claude-owed-notification-resource-contract.test.ts": 183, - "src/shared/claude-profile-activation-contract.test.ts": 68, - "src/shared/claude-shell-function.test.ts": 196, - "src/shared/claude-sign-in-link.test.ts": 47, - "src/shared/claude-statusline-rate-limits.test.ts": 47, - "src/shared/claude-subagent-roster.test.ts": 111, - "src/shared/claude-subagent-row-lifecycle.test.ts": 205, - "src/shared/claude-terminal-interrupt.test.ts": 54, - "src/shared/cli-argument-boundary.test.ts": 54, - "src/shared/cli-workspace-provenance.test.ts": 50, - "src/shared/client-environment-info.test.ts": 42, - "src/shared/clipboard-image.test.ts": 40, - "src/shared/clipboard-text.test.ts": 86, - "src/shared/closed-terminal-tab-tombstones.test.ts": 242, - "src/shared/cloud-service-url.test.ts": 45, - "src/shared/coalescing-keyed-runner.test.ts": 112, - "src/shared/codebuddy-agent.test.ts": 234, - "src/shared/codex-auth-errors.test.ts": 55, - "src/shared/codex-composer-ready-scanner.test.ts": 97, - "src/shared/codex-pet-sprite-defaults.test.ts": 70, - "src/shared/codex-reset-credit-attempt-ledger.test.ts": 213, - "src/shared/codex-reset-credit-scope.test.ts": 54, - "src/shared/codex-shared-server-command.test.ts": 65, - "src/shared/codex-startup-delivery.test.ts": 85, - "src/shared/codex-status-transcript-line.test.ts": 83, - "src/shared/codex-subagent-poll-scheduler.test.ts": 57, - "src/shared/codex-subagent-rollout-lifecycle.test.ts": 172, - "src/shared/codex-subagent-roster.test.ts": 60, - "src/shared/codex-subagent-transcript.test.ts": 100, - "src/shared/codex-turn-transcript.test.ts": 46, - "src/shared/combined-diff-file-tree-width.test.ts": 46, - "src/shared/command-code-output-retention.test.ts": 109, - "src/shared/command-code-output-status.test.ts": 205, - "src/shared/command-code-turn-boundary.test.ts": 42, - "src/shared/command-environment.test.ts": 86, - "src/shared/command-token-scanner.test.ts": 54, - "src/shared/commit-message-agent-spec.test.ts": 118, - "src/shared/commit-message-generation.test.ts": 62, - "src/shared/commit-message-omp.test.ts": 390, - "src/shared/commit-message-plan.test.ts": 117, - "src/shared/commit-message-prompt.test.ts": 87, - "src/shared/computer-awake-mode.test.ts": 43, - "src/shared/computer-use-error-recovery.test.ts": 59, - "src/shared/computer-use-key-spec.test.ts": 59, - "src/shared/constants.test.ts": 282, - "src/shared/contextual-tours.test.ts": 67, - "src/shared/conversation-name-action.test.ts": 263, - "src/shared/conversation-name-generation.test.ts": 50, - "src/shared/crash-reporting.test.ts": 80, - "src/shared/credit-count-format.test.ts": 58, - "src/shared/cross-platform-path-guards.test.ts": 943, - "src/shared/cross-platform-path.test.ts": 55, - "src/shared/currency-format.test.ts": 47, - "src/shared/daemon-adoption-telemetry.test.ts": 349, - "src/shared/doc-preview-file-access.test.ts": 93, - "src/shared/doc-preview-scheme.test.ts": 44, - "src/shared/draft-paste-ready-scanner-grok-trace-replay.test.ts": 75, - "src/shared/draft-paste-ready-scanner.test.ts": 92, - "src/shared/dsb-headless-command.test.ts": 55, - "src/shared/dsb-native-one-shot-recognition.test.ts": 89, - "src/shared/dsb-terminal-title.test.ts": 105, - "src/shared/dsh-launch-command.test.ts": 115, - "src/shared/dsh-real-cli.test.ts": 73, - "src/shared/dsh-startup.test.ts": 140, - "src/shared/dsh-terminal-title.test.ts": 90, - "src/shared/emoji-shortcode-catalog.lazy.test.ts": 79, - "src/shared/emulator-keyboard-frame.test.ts": 61, - "src/shared/emulator-touch-frame.test.ts": 45, - "src/shared/ephemeral-setup-terminal-worktree-id.test.ts": 55, - "src/shared/ephemeral-vm-recipe-checkout-mode.test.ts": 261, - "src/shared/ephemeral-vm-recipe-doctor.test.ts": 51, - "src/shared/ephemeral-vm-recipe-output-capture.test.ts": 260, - "src/shared/ephemeral-vm-recipe-process.test.ts": 180, - "src/shared/ephemeral-vm-recipe-repo-url.test.ts": 52, - "src/shared/ephemeral-vm-recipes.test.ts": 291, - "src/shared/ephemeral-vm-runtime-feature-sorting.test.ts": 350, - "src/shared/ephemeral-vm-runtime-store-rollback.test.ts": 548, - "src/shared/ephemeral-vm-runtime-store.test.ts": 314, - "src/shared/event-loop-yield.test.ts": 69, - "src/shared/execution-host-registry-shared-control-connect.test.ts": 82, - "src/shared/execution-host-registry.test.ts": 91, - "src/shared/execution-host.test.ts": 68, - "src/shared/export-let-function-initializer-ban.test.ts": 2472, - "src/shared/external-automation-jobs-file.test.ts": 73, - "src/shared/external-worktree-inbox.test.ts": 71, - "src/shared/feature-education-telemetry.test.ts": 47, - "src/shared/feature-interactions.test.ts": 44, - "src/shared/feature-tips.test.ts": 66, - "src/shared/feature-wall-tour-depth.test.ts": 44, - "src/shared/feedback-image-limits.test.ts": 47, - "src/shared/file-explorer-display-root.test.ts": 53, - "src/shared/file-inventory-budget.test.ts": 54, - "src/shared/file-link-location.test.ts": 56, - "src/shared/file-name-sort.test.ts": 51, - "src/shared/fish-binary-requirement.test.ts": 46, - "src/shared/folder-workspace-execution-host.test.ts": 84, - "src/shared/folder-workspace-worktree.test.ts": 89, - "src/shared/folder-workspaces.test.ts": 82, - "src/shared/foreground-process-ancestry-parity.test.ts": 259, - "src/shared/foreground-process-selection.test.ts": 85, - "src/shared/foreground-wrapper-agent.test.ts": 87, - "src/shared/freebuff-launch.test.ts": 147, - "src/shared/freebuff-startup.test.ts": 120, - "src/shared/generated-code-path.test.ts": 53, - "src/shared/git-admission-node-compatibility.test.ts": 73, - "src/shared/git-binary-compatibility.test.ts": 123, - "src/shared/git-blob-absence.test.ts": 63, - "src/shared/git-branch-cleanup.test.ts": 73, - "src/shared/git-branch-compare-head.test.ts": 47, - "src/shared/git-branch-line-total-soft-deadline.test.ts": 3606, - "src/shared/git-branch-line-total.test.ts": 102, - "src/shared/git-capability-cache.test.ts": 59, - "src/shared/git-change-list.test.ts": 115, - "src/shared/git-check-ignore-stdio.test.ts": 47, - "src/shared/git-clone-failure-message.test.ts": 55, - "src/shared/git-config-snapshot-runner.test.ts": 68, - "src/shared/git-configured-branch-target.test.ts": 55, - "src/shared/git-cquoted-path.test.ts": 45, - "src/shared/git-diff-transport-budget.test.ts": 483, - "src/shared/git-discard-path-safety.test.ts": 68, - "src/shared/git-exec-mutation.test.ts": 40, - "src/shared/git-fetch-head-capability.test.ts": 45, - "src/shared/git-fetch-head-lock-key-derivation.test.ts": 123, - "src/shared/git-fetch-head-lock.test.ts": 162, - "src/shared/git-fork-sync.test.ts": 85, - "src/shared/git-history-graph.test.ts": 70, - "src/shared/git-history-message-allocation.test.ts": 49, - "src/shared/git-history-real-repository.test.ts": 465, - "src/shared/git-history-ref-display.test.ts": 63, - "src/shared/git-history.test.ts": 69, - "src/shared/git-lock-contention.test.ts": 48, - "src/shared/git-merge-tree-capability.test.ts": 53, - "src/shared/git-metadata-path.test.ts": 70, - "src/shared/git-push-target-validation.test.ts": 50, - "src/shared/git-remote-error.test.ts": 74, - "src/shared/git-remote-identity.test.ts": 61, - "src/shared/git-remote-url-index.test.ts": 58, - "src/shared/git-resolution-config-snapshot.test.ts": 73, - "src/shared/git-rev-list-output.test.ts": 58, - "src/shared/git-ssh-policy-env.test.ts": 204, - "src/shared/git-status-branch-line-total-cache.test.ts": 83, - "src/shared/git-status-conflict-entries.test.ts": 60, - "src/shared/git-status-limit.test.ts": 46, - "src/shared/git-status-line-stat-inputs.test.ts": 51, - "src/shared/git-status-line-stats-cache.test.ts": 82, - "src/shared/git-status-read-lease-expiry.test.ts": 79, - "src/shared/git-status-upstream-ref.test.ts": 58, - "src/shared/git-tracked-pathspecs.test.ts": 163, - "src/shared/git-uncommitted-line-stats.test.ts": 94, - "src/shared/git-upstream-status.test.ts": 53, - "src/shared/git-worktree-admin.test.ts": 130, - "src/shared/git-worktree-operation-lock.test.ts": 59, - "src/shared/github/account-binding.test.ts": 65, - "src/shared/github/api-availability.test.ts": 54, - "src/shared/github/owner-slug.test.ts": 49, - "src/shared/github/project-board-columns.test.ts": 65, - "src/shared/github/project-identity.test.ts": 46, - "src/shared/github/project-ref-input.test.ts": 52, - "src/shared/github/project-roadmap-timeline.test.ts": 70, - "src/shared/github/pull-request-auto-merge-availability.test.ts": 59, - "src/shared/github/pull-request-for-branch-outcome.test.ts": 46, - "src/shared/github/pull-request-merge-methods.test.ts": 49, - "src/shared/github/repository-identity-key.test.ts": 46, - "src/shared/gitlab-job-log-excerpt.test.ts": 80, - "src/shared/gitlab-job-trace-check-details.test.ts": 67, - "src/shared/gitlab-pipeline-checks.test.ts": 53, - "src/shared/gitlab-projects.test.ts": 50, - "src/shared/grok-model-list-probe.test.ts": 69, - "src/shared/grok-session-paths.test.ts": 93, - "src/shared/handled-wire-discriminant.test.ts": 48, - "src/shared/harness-injected-user-turns.test.ts": 64, - "src/shared/hermes-run-ref-retention.test.ts": 66, - "src/shared/hermes-session-run-index.test.ts": 293, - "src/shared/hermes-startup-query.windows.test.ts": 49, - "src/shared/home-or-filesystem-root.test.ts": 50, - "src/shared/hook-command-source-policy.test.ts": 43, - "src/shared/host-authority.test.ts": 66, - "src/shared/host-balanced-listing-scaling.test.ts": 74, - "src/shared/host-display-resolution.test.ts": 59, - "src/shared/host-operation-ownership.test.ts": 758, - "src/shared/host-setting-overrides.test.ts": 53, - "src/shared/hosted-review-creation-providers.test.ts": 52, - "src/shared/hosted-review-github.test.ts": 98, - "src/shared/hosted-review-refs.test.ts": 144, - "src/shared/image-data-uri.test.ts": 57, - "src/shared/image-paste-following-text.test.ts": 47, - "src/shared/import-copy-name.test.ts": 56, - "src/shared/in-flight-promise-dedupe.test.ts": 80, - "src/shared/jcode-runtime-dir.test.ts": 54, - "src/shared/jcode-session-files.test.ts": 58, - "src/shared/jcode-terminal-title.test.ts": 50, - "src/shared/jira-issue-url.test.ts": 47, - "src/shared/jira-search-input-jql.test.ts": 71, - "src/shared/json-text-structure-limit.test.ts": 70, - "src/shared/json-token-reader.test.ts": 309, - "src/shared/keybindings-conflicts.test.ts": 97, - "src/shared/keybindings-default-bindings.test.ts": 115, - "src/shared/keybindings-digit-index.test.ts": 96, - "src/shared/keybindings-double-tap.test.ts": 83, + "src/shared/browser-client-hosting-eligibility.test.ts": 244, + "src/shared/browser-cookie-import-sources.test.ts": 41, + "src/shared/browser-grab-types.test.ts": 37, + "src/shared/browser-network-tunnel-protocol.test.ts": 39, + "src/shared/browser-network-tunnel-stream-framing.test.ts": 1577, + "src/shared/browser-screencast-protocol.test.ts": 40, + "src/shared/browser-url.test.ts": 319, + "src/shared/bundled-ripgrep.test.ts": 24, + "src/shared/cheap-process-table-snapshot.test.ts": 46, + "src/shared/check-job-log-byte-cap.test.ts": 51, + "src/shared/check-job-log-excerpt.test.ts": 116, + "src/shared/check-job-log-marker-candidates.test.ts": 135, + "src/shared/check-job-log-performance-harness.test.ts": 514, + "src/shared/check-job-log-retention.test.ts": 431, + "src/shared/check-job-log-tail-slice.test.ts": 62, + "src/shared/check-job-log-text-performance.test.ts": 93, + "src/shared/claimed-agent-pty-owner.test.ts": 55, + "src/shared/claude-agent-teams-tmux-compat.test.ts": 35, + "src/shared/claude-background-task-kind.test.ts": 34, + "src/shared/claude-background-task-status.test.ts": 224, + "src/shared/claude-model-list-probe.test.ts": 49, + "src/shared/claude-owed-notification-resource-contract.test.ts": 162, + "src/shared/claude-profile-activation-contract.test.ts": 188, + "src/shared/claude-shell-function.test.ts": 184, + "src/shared/claude-sign-in-link.test.ts": 29, + "src/shared/claude-statusline-rate-limits.test.ts": 38, + "src/shared/claude-subagent-roster.test.ts": 67, + "src/shared/claude-subagent-row-lifecycle.test.ts": 195, + "src/shared/claude-terminal-interrupt.test.ts": 36, + "src/shared/cli-argument-boundary.test.ts": 47, + "src/shared/cli-workspace-provenance.test.ts": 29, + "src/shared/client-environment-info.test.ts": 30, + "src/shared/clipboard-image.test.ts": 32, + "src/shared/clipboard-text.test.ts": 68, + "src/shared/closed-terminal-tab-tombstones.test.ts": 219, + "src/shared/cloud-service-url.test.ts": 30, + "src/shared/coalescing-keyed-runner.test.ts": 99, + "src/shared/codebuddy-agent.test.ts": 219, + "src/shared/codex-auth-errors.test.ts": 44, + "src/shared/codex-capacity-notice.test.ts": 58, + "src/shared/codex-cli-installation.test.ts": 44, + "src/shared/codex-composer-ready-scanner.test.ts": 79, + "src/shared/codex-pet-sprite-defaults.test.ts": 61, + "src/shared/codex-reset-credit-attempt-ledger.test.ts": 224, + "src/shared/codex-reset-credit-scope.test.ts": 38, + "src/shared/codex-shared-server-command.test.ts": 59, + "src/shared/codex-startup-delivery.test.ts": 58, + "src/shared/codex-status-transcript-line.test.ts": 58, + "src/shared/codex-subagent-poll-scheduler.test.ts": 35, + "src/shared/codex-subagent-rollout-lifecycle.test.ts": 152, + "src/shared/codex-subagent-roster.test.ts": 46, + "src/shared/codex-subagent-transcript.test.ts": 92, + "src/shared/codex-turn-transcript.test.ts": 31, + "src/shared/combined-diff-file-tree-width.test.ts": 34, + "src/shared/command-code-output-retention.test.ts": 81, + "src/shared/command-code-output-status.test.ts": 178, + "src/shared/command-code-turn-boundary.test.ts": 27, + "src/shared/command-environment.test.ts": 69, + "src/shared/command-token-scanner.test.ts": 38, + "src/shared/commit-message-agent-spec.test.ts": 138, + "src/shared/commit-message-generation.test.ts": 33, + "src/shared/commit-message-omp.test.ts": 286, + "src/shared/commit-message-plan.test.ts": 103, + "src/shared/commit-message-prompt.test.ts": 63, + "src/shared/computer-awake-mode.test.ts": 27, + "src/shared/computer-use-error-recovery.test.ts": 40, + "src/shared/computer-use-key-spec.test.ts": 41, + "src/shared/constants.test.ts": 286, + "src/shared/contextual-tours.test.ts": 35, + "src/shared/conversation-name-action.test.ts": 232, + "src/shared/conversation-name-generation.test.ts": 40, + "src/shared/crash-reporting.test.ts": 68, + "src/shared/credit-count-format.test.ts": 28, + "src/shared/cross-platform-path-guards.test.ts": 833, + "src/shared/cross-platform-path.test.ts": 63, + "src/shared/currency-format.test.ts": 35, + "src/shared/daemon-adoption-telemetry.test.ts": 275, + "src/shared/darwin-process-table-capture.test.ts": 52, + "src/shared/darwin-process-table-inventory-drain.test.ts": 55, + "src/shared/darwin-terminal-device-stats.test.ts": 37, + "src/shared/darwin-terminal-names-real-ps.test.ts": 29, + "src/shared/darwin-terminal-names.test.ts": 278, + "src/shared/default-agent-settings.test.ts": 32, + "src/shared/device-key-proof.test.ts": 85, + "src/shared/doc-preview-file-access.test.ts": 76, + "src/shared/doc-preview-scheme.test.ts": 34, + "src/shared/draft-paste-ready-scanner-grok-trace-replay.test.ts": 63, + "src/shared/draft-paste-ready-scanner.test.ts": 94, + "src/shared/dsb-headless-command.test.ts": 51, + "src/shared/dsb-native-one-shot-recognition.test.ts": 81, + "src/shared/dsb-terminal-title.test.ts": 108, + "src/shared/dsh-launch-command.test.ts": 78, + "src/shared/dsh-real-cli.test.ts": 57, + "src/shared/dsh-startup.test.ts": 109, + "src/shared/dsh-terminal-title.test.ts": 44, + "src/shared/electron-isolated-profile-switches.test.ts": 29, + "src/shared/emoji-shortcode-catalog.lazy.test.ts": 84, + "src/shared/emulator-keyboard-frame.test.ts": 47, + "src/shared/emulator-touch-frame.test.ts": 25, + "src/shared/ephemeral-setup-terminal-worktree-id.test.ts": 47, + "src/shared/ephemeral-vm-recipe-checkout-mode.test.ts": 225, + "src/shared/ephemeral-vm-recipe-doctor.test.ts": 37, + "src/shared/ephemeral-vm-recipe-output-capture.test.ts": 257, + "src/shared/ephemeral-vm-recipe-process.test.ts": 172, + "src/shared/ephemeral-vm-recipe-repo-url.test.ts": 32, + "src/shared/ephemeral-vm-recipes.test.ts": 251, + "src/shared/ephemeral-vm-runtime-feature-sorting.test.ts": 308, + "src/shared/ephemeral-vm-runtime-store-rollback.test.ts": 500, + "src/shared/ephemeral-vm-runtime-store.test.ts": 224, + "src/shared/event-loop-yield.test.ts": 64, + "src/shared/execution-host-registry-shared-control-connect.test.ts": 238, + "src/shared/execution-host-registry.test.ts": 224, + "src/shared/execution-host.test.ts": 40, + "src/shared/export-let-function-initializer-ban.test.ts": 2090, + "src/shared/external-automation-jobs-file.test.ts": 64, + "src/shared/external-worktree-inbox.test.ts": 45, + "src/shared/feature-education-telemetry.test.ts": 28, + "src/shared/feature-interactions.test.ts": 40, + "src/shared/feature-tips.test.ts": 45, + "src/shared/feature-wall-tour-depth.test.ts": 33, + "src/shared/feedback-image-limits.test.ts": 34, + "src/shared/file-explorer-display-root.test.ts": 49, + "src/shared/file-inventory-budget.test.ts": 24, + "src/shared/file-link-location.test.ts": 41, + "src/shared/file-name-sort.test.ts": 35, + "src/shared/fish-binary-requirement.test.ts": 37, + "src/shared/folder-workspace-execution-host.test.ts": 65, + "src/shared/folder-workspace-worktree.test.ts": 87, + "src/shared/folder-workspaces.test.ts": 67, + "src/shared/foreground-process-ancestry-parity.test.ts": 270, + "src/shared/foreground-process-selection.test.ts": 74, + "src/shared/foreground-wrapper-agent.test.ts": 78, + "src/shared/freebuff-launch.test.ts": 130, + "src/shared/freebuff-startup.test.ts": 118, + "src/shared/generated-code-path.test.ts": 35, + "src/shared/git-admission-node-compatibility.test.ts": 48, + "src/shared/git-binary-compatibility.test.ts": 132, + "src/shared/git-blob-absence.test.ts": 51, + "src/shared/git-branch-cleanup.test.ts": 55, + "src/shared/git-branch-compare-head.test.ts": 31, + "src/shared/git-branch-line-total-soft-deadline.test.ts": 63, + "src/shared/git-branch-line-total.test.ts": 105, + "src/shared/git-capability-cache.test.ts": 33, + "src/shared/git-change-list.test.ts": 129, + "src/shared/git-check-ignore-stdio.test.ts": 37, + "src/shared/git-clone-failure-message.test.ts": 38, + "src/shared/git-config-snapshot-runner.test.ts": 51, + "src/shared/git-configured-branch-target.test.ts": 45, + "src/shared/git-cquoted-path.test.ts": 39, + "src/shared/git-diff-transport-budget.test.ts": 461, + "src/shared/git-discard-path-safety.test.ts": 55, + "src/shared/git-fetch-head-capability.test.ts": 28, + "src/shared/git-fetch-head-lock-key-derivation.test.ts": 107, + "src/shared/git-fetch-head-lock.test.ts": 150, + "src/shared/git-fork-sync.test.ts": 59, + "src/shared/git-history-graph.test.ts": 58, + "src/shared/git-history-message-allocation.test.ts": 52, + "src/shared/git-history-real-repository.test.ts": 431, + "src/shared/git-history-ref-display.test.ts": 48, + "src/shared/git-history.test.ts": 67, + "src/shared/git-lock-contention.test.ts": 32, + "src/shared/git-merge-tree-capability.test.ts": 38, + "src/shared/git-metadata-path.test.ts": 66, + "src/shared/git-push-target-validation.test.ts": 32, + "src/shared/git-remote-error.test.ts": 67, + "src/shared/git-remote-identity.test.ts": 33, + "src/shared/git-remote-url-index.test.ts": 50, + "src/shared/git-resolution-config-snapshot.test.ts": 58, + "src/shared/git-rev-list-output.test.ts": 39, + "src/shared/git-ssh-policy-env.test.ts": 168, + "src/shared/git-status-branch-line-total-cache.test.ts": 54, + "src/shared/git-status-conflict-entries.test.ts": 35, + "src/shared/git-status-limit.test.ts": 43, + "src/shared/git-status-line-stat-inputs.test.ts": 36, + "src/shared/git-status-line-stats-cache.test.ts": 53, + "src/shared/git-status-read-lease-expiry.test.ts": 63, + "src/shared/git-status-upstream-ref.test.ts": 45, + "src/shared/git-tracked-pathspecs.test.ts": 119, + "src/shared/git-uncommitted-line-stats.test.ts": 75, + "src/shared/git-upstream-status.test.ts": 49, + "src/shared/git-worktree-admin.test.ts": 131, + "src/shared/git-worktree-operation-lock.test.ts": 28, + "src/shared/github/account-binding.test.ts": 26, + "src/shared/github/api-availability.test.ts": 33, + "src/shared/github/owner-slug.test.ts": 30, + "src/shared/github/project-board-columns.test.ts": 44, + "src/shared/github/project-identity.test.ts": 24, + "src/shared/github/project-ref-input.test.ts": 39, + "src/shared/github/project-roadmap-timeline.test.ts": 66, + "src/shared/github/pull-request-auto-merge-availability.test.ts": 33, + "src/shared/github/pull-request-for-branch-outcome.test.ts": 33, + "src/shared/github/pull-request-merge-methods.test.ts": 29, + "src/shared/github/repository-identity-key.test.ts": 32, + "src/shared/gitlab-job-log-excerpt.test.ts": 89, + "src/shared/gitlab-job-trace-check-details.test.ts": 57, + "src/shared/gitlab-pipeline-checks.test.ts": 40, + "src/shared/gitlab-projects.test.ts": 31, + "src/shared/grok-model-list-probe.test.ts": 63, + "src/shared/grok-session-paths.test.ts": 120, + "src/shared/handled-wire-discriminant.test.ts": 35, + "src/shared/harness-injected-user-turns.test.ts": 50, + "src/shared/hermes-startup-query.windows.test.ts": 39, + "src/shared/home-or-filesystem-root.test.ts": 36, + "src/shared/hook-command-source-policy.test.ts": 25, + "src/shared/host-authority.test.ts": 35, + "src/shared/host-automatic-work-budget.test.ts": 39, + "src/shared/host-balanced-listing-scaling.test.ts": 72, + "src/shared/host-display-resolution.test.ts": 50, + "src/shared/host-operation-ownership.test.ts": 746, + "src/shared/host-setting-overrides.test.ts": 50, + "src/shared/host-spawn-admission.test.ts": 170, + "src/shared/hosted-review-creation-providers.test.ts": 26, + "src/shared/hosted-review-github.test.ts": 28, + "src/shared/hosted-review-refs.test.ts": 114, + "src/shared/image-data-uri.test.ts": 41, + "src/shared/image-paste-following-text.test.ts": 29, + "src/shared/import-copy-name.test.ts": 39, + "src/shared/in-flight-promise-dedupe.test.ts": 62, + "src/shared/jcode-runtime-dir.test.ts": 37, + "src/shared/jcode-session-files.test.ts": 52, + "src/shared/jcode-terminal-title.test.ts": 28, + "src/shared/jira-issue-summary-generation.test.ts": 58, + "src/shared/jira-issue-url.test.ts": 36, + "src/shared/jira-jql-predicate.test.ts": 38, + "src/shared/jira-preset-jql.test.ts": 159, + "src/shared/jira-project-reference.test.ts": 278, + "src/shared/jira-review-title.test.ts": 93, + "src/shared/jira-saved-filters.test.ts": 34, + "src/shared/jira-search-input-jql.test.ts": 68, + "src/shared/jira-transition-fields.test.ts": 41, + "src/shared/json-text-structure-limit.test.ts": 63, + "src/shared/json-token-reader.test.ts": 224, + "src/shared/keybindings-conflicts.test.ts": 83, + "src/shared/keybindings-default-bindings.test.ts": 91, + "src/shared/keybindings-digit-index.test.ts": 106, + "src/shared/keybindings-double-tap.test.ts": 64, "src/shared/keybindings-keyboard-layout.test.ts": 72, - "src/shared/keybindings-parse-cache.test.ts": 70, - "src/shared/keybindings-parsing.test.ts": 91, - "src/shared/keybindings-terminal-context.test.ts": 76, - "src/shared/keybindings-unassigned-actions.test.ts": 85, - "src/shared/kiro-home.test.ts": 46, - "src/shared/linear/inline-media.test.ts": 50, - "src/shared/linear/issue-attribute-filter.test.ts": 55, - "src/shared/linear/issue-view-resume-state.test.ts": 182, - "src/shared/linear/links.test.ts": 68, - "src/shared/linear/workspace-types.test.ts": 59, - "src/shared/linux-proc-port-scan-limits.test.ts": 53, - "src/shared/local-account-runtime.test.ts": 279, - "src/shared/local-build-compatibility.test.ts": 58, - "src/shared/local-windows-terminal-runtime.test.ts": 52, - "src/shared/localhost-worktree-labels.test.ts": 58, - "src/shared/loose-ref-count.test.ts": 139, - "src/shared/machine-name.test.ts": 44, - "src/shared/macos-symbolic-hotkeys.test.ts": 75, - "src/shared/main-agent-status-parity.test.ts": 224, - "src/shared/managed-agent-command-token.test.ts": 53, - "src/shared/managed-orcad-execution-host.test.ts": 85, - "src/shared/managed-orcad-ssh-owner.test.ts": 56, - "src/shared/manual-repo-order.test.ts": 56, - "src/shared/map-settled-with-concurrency.test.ts": 54, - "src/shared/map-with-concurrency.test.ts": 106, - "src/shared/markdown-document-listing-limits.test.ts": 45, - "src/shared/markdown-toc-panel-width.test.ts": 48, - "src/shared/mcp-config.test.ts": 95, - "src/shared/mobile-e2ee-v2-contract.test.ts": 59, - "src/shared/mobile-e2ee-v2-framing.test.ts": 65, - "src/shared/mobile-file-directory-limit.test.ts": 48, - "src/shared/mobile-markdown-document.test.ts": 61, - "src/shared/mobile-pairing-connection-mode.test.ts": 59, - "src/shared/mobile-pairing-custom-address.test.ts": 62, - "src/shared/mobile-push-contract.test.ts": 48, - "src/shared/mobile-relay-close-codes.test.ts": 42, - "src/shared/mobile-relay-mint-failure.test.ts": 43, - "src/shared/mobile-relay-pairing-offer.test.ts": 246, - "src/shared/mobile-relay-phone-protocol.test.ts": 223, - "src/shared/mobile-web-bundle/bundle-range-rpc-contract.test.ts": 241, - "src/shared/mobile-web-bundle/bundle-rpc-contract.test.ts": 254, - "src/shared/mobile-web-bundle/manifest-contract.test.ts": 284, - "src/shared/model-id-label.test.ts": 46, - "src/shared/modifier-double-tap-detector.test.ts": 62, - "src/shared/muse-session-log.test.ts": 63, - "src/shared/native-chat-activity-tail-line.test.ts": 268, - "src/shared/native-chat-agent-profiles.test.ts": 58, - "src/shared/native-chat-agent-support.test.ts": 54, - "src/shared/native-chat-appearance-settings.test.ts": 56, - "src/shared/native-chat-ask-fifo.test.ts": 218, - "src/shared/native-chat-ask-row.test.ts": 62, - "src/shared/native-chat-ask.test.ts": 69, - "src/shared/native-chat-background-task-row.test.ts": 60, - "src/shared/native-chat-command-envelope.test.ts": 49, - "src/shared/native-chat-command-output.test.ts": 55, - "src/shared/native-chat-context-usage.test.ts": 52, - "src/shared/native-chat-cut-turn-notice.test.ts": 342, - "src/shared/native-chat-edit-normalize.test.ts": 326, - "src/shared/native-chat-href-routing.test.ts": 58, - "src/shared/native-chat-image-transcript-markers.test.ts": 85, - "src/shared/native-chat-live-line.test.ts": 74, - "src/shared/native-chat-merge-pairs.test.ts": 343, - "src/shared/native-chat-orca-stop-cut.test.ts": 273, - "src/shared/native-chat-provider-frame-summary.test.ts": 49, - "src/shared/native-chat-provider-retry-runs.test.ts": 340, - "src/shared/native-chat-reasoning-order.test.ts": 283, - "src/shared/native-chat-reasoning-row.test.ts": 78, - "src/shared/native-chat-scope-cache.test.ts": 47, - "src/shared/native-chat-session-option-commands.test.ts": 79, - "src/shared/native-chat-session-option-defaults.test.ts": 95, - "src/shared/native-chat-session-option-snapshot.test.ts": 126, - "src/shared/native-chat-session-option-state.test.ts": 57, - "src/shared/native-chat-shell-environment.test.ts": 53, - "src/shared/native-chat-slash-commands.omp.test.ts": 48, - "src/shared/native-chat-slash-commands.test.ts": 59, - "src/shared/native-chat-stopped-send-turn.test.ts": 336, - "src/shared/native-chat-stream-unsubscribe.test.ts": 50, - "src/shared/native-chat-streaming.test.ts": 46, - "src/shared/native-chat-subagent-group-header.test.ts": 60, - "src/shared/native-chat-subagent-summary.test.ts": 58, - "src/shared/native-chat-task-list.test.ts": 58, - "src/shared/native-chat-tool-activity.test.ts": 57, - "src/shared/native-chat-tool-attribution-allocation.test.ts": 102, - "src/shared/native-chat-tool-fold-attribution.test.ts": 52, - "src/shared/native-chat-tool-icon.test.ts": 73, - "src/shared/native-chat-tool-identity.test.ts": 66, - "src/shared/native-chat-tool-line-identity.test.ts": 42, - "src/shared/native-chat-tool-pair-limit.test.ts": 128, - "src/shared/native-chat-tool-pairing.performance.test.ts": 500, - "src/shared/native-chat-tool-pairing.test.ts": 53, - "src/shared/native-chat-tool-preview-prefix.test.ts": 52, - "src/shared/native-chat-tool-preview-scan.test.ts": 52, - "src/shared/native-chat-tool-result-orphans.test.ts": 282, - "src/shared/native-chat-tool-run-outcome.test.ts": 81, - "src/shared/native-chat-tool-run-sentence.test.ts": 64, - "src/shared/native-chat-tool-summary.test.ts": 80, - "src/shared/native-chat-transcript-projection.test.ts": 268, - "src/shared/native-chat-transcript-retention.test.ts": 57, - "src/shared/native-chat-turn-activity.test.ts": 230, - "src/shared/native-chat-turn-fold.test.ts": 57, - "src/shared/native-chat-turn-grouping.test.ts": 231, - "src/shared/native-chat-turn-membership-derived-tool-row.test.ts": 340, - "src/shared/native-chat-turn-membership.test.ts": 287, - "src/shared/native-chat-turn-status.test.ts": 75, - "src/shared/native-chat-types.test.ts": 55, - "src/shared/native-chat-unverifiable-turn-status.test.ts": 257, - "src/shared/native-chat-upgrade-tip-audience.test.ts": 45, - "src/shared/native-chat-visual-directive.test.ts": 64, - "src/shared/native-chat-visual-height-governor.test.ts": 289, - "src/shared/native-chat-visual-shell.test.ts": 274, - "src/shared/native-chat-work-run.test.ts": 310, - "src/shared/native-file-drop.test.ts": 60, - "src/shared/nested-repo-telemetry-schema.test.ts": 299, - "src/shared/nested-repo-telemetry.test.ts": 50, - "src/shared/nested-worker-depth.test.ts": 51, - "src/shared/network-proxy.test.ts": 42, + "src/shared/keybindings-parse-cache.test.ts": 68, + "src/shared/keybindings-parsing.test.ts": 69, + "src/shared/keybindings-terminal-context.test.ts": 57, + "src/shared/keybindings-unassigned-actions.test.ts": 74, + "src/shared/kiro-home.test.ts": 33, + "src/shared/linear/inline-media.test.ts": 35, + "src/shared/linear/issue-attribute-filter.test.ts": 43, + "src/shared/linear/issue-view-resume-state.test.ts": 71, + "src/shared/linear/links.test.ts": 47, + "src/shared/linear/workspace-types.test.ts": 39, + "src/shared/linux-wayland-session.test.ts": 34, + "src/shared/local-account-runtime.test.ts": 275, + "src/shared/local-build-compatibility.test.ts": 38, + "src/shared/local-windows-terminal-runtime.test.ts": 25, + "src/shared/localhost-worktree-labels.test.ts": 34, + "src/shared/loose-ref-count.test.ts": 113, + "src/shared/machine-name.test.ts": 26, + "src/shared/macos-symbolic-hotkeys.test.ts": 88, + "src/shared/main-agent-status-parity.test.ts": 308, + "src/shared/managed-agent-command-token.test.ts": 28, + "src/shared/managed-orcad-execution-host.test.ts": 269, + "src/shared/managed-orcad-ssh-owner.test.ts": 29, + "src/shared/manual-repo-order.test.ts": 32, + "src/shared/map-settled-with-concurrency.test.ts": 51, + "src/shared/map-with-concurrency.test.ts": 88, + "src/shared/markdown-document-listing-limits.test.ts": 36, + "src/shared/markdown-toc-panel-width.test.ts": 34, + "src/shared/mcp-config.test.ts": 73, + "src/shared/mobile-e2ee-v2-contract.test.ts": 42, + "src/shared/mobile-e2ee-v2-framing.test.ts": 39, + "src/shared/mobile-e2ee-v2-node-key-schedule.test.ts": 101, + "src/shared/mobile-markdown-document.test.ts": 39, + "src/shared/mobile-pairing-connection-mode.test.ts": 34, + "src/shared/mobile-pairing-custom-address.test.ts": 36, + "src/shared/mobile-push-contract.test.ts": 26, + "src/shared/mobile-relay-close-codes.test.ts": 39, + "src/shared/mobile-relay-mint-failure.test.ts": 28, + "src/shared/mobile-relay-pairing-offer.test.ts": 294, + "src/shared/mobile-relay-phone-protocol.test.ts": 241, + "src/shared/mobile-web-bundle/bundle-range-rpc-contract.test.ts": 245, + "src/shared/mobile-web-bundle/bundle-rpc-contract.test.ts": 256, + "src/shared/mobile-web-bundle/manifest-contract.test.ts": 336, + "src/shared/model-id-label.test.ts": 39, + "src/shared/modifier-double-tap-detector.test.ts": 37, + "src/shared/muse-session-log.test.ts": 60, + "src/shared/native-chat-activity-tail-line.test.ts": 256, + "src/shared/native-chat-agent-profiles.test.ts": 48, + "src/shared/native-chat-agent-support.test.ts": 33, + "src/shared/native-chat-appearance-settings.test.ts": 37, + "src/shared/native-chat-ask-fifo.test.ts": 216, + "src/shared/native-chat-ask-row.test.ts": 56, + "src/shared/native-chat-ask.test.ts": 51, + "src/shared/native-chat-background-task-row.test.ts": 45, + "src/shared/native-chat-command-envelope.test.ts": 37, + "src/shared/native-chat-command-output.test.ts": 47, + "src/shared/native-chat-context-usage.test.ts": 35, + "src/shared/native-chat-cut-turn-notice.test.ts": 364, + "src/shared/native-chat-edit-normalize.test.ts": 419, + "src/shared/native-chat-href-routing.test.ts": 64, + "src/shared/native-chat-image-transcript-markers.test.ts": 83, + "src/shared/native-chat-live-line.test.ts": 67, + "src/shared/native-chat-merge-pairs.test.ts": 339, + "src/shared/native-chat-orca-stop-cut.test.ts": 243, + "src/shared/native-chat-provider-frame-summary.test.ts": 36, + "src/shared/native-chat-provider-retry-runs.test.ts": 322, + "src/shared/native-chat-reasoning-order.test.ts": 269, + "src/shared/native-chat-reasoning-row.test.ts": 53, + "src/shared/native-chat-scope-cache.test.ts": 36, + "src/shared/native-chat-session-option-commands.test.ts": 81, + "src/shared/native-chat-session-option-defaults.test.ts": 70, + "src/shared/native-chat-session-option-snapshot.test.ts": 104, + "src/shared/native-chat-session-option-state.test.ts": 50, + "src/shared/native-chat-shell-environment.test.ts": 32, + "src/shared/native-chat-slash-commands.omp.test.ts": 38, + "src/shared/native-chat-slash-commands.test.ts": 45, + "src/shared/native-chat-stopped-send-turn.test.ts": 348, + "src/shared/native-chat-stream-unsubscribe.test.ts": 59, + "src/shared/native-chat-streaming.test.ts": 34, + "src/shared/native-chat-subagent-group-header.test.ts": 38, + "src/shared/native-chat-subagent-items.test.ts": 274, + "src/shared/native-chat-subagent-summary.test.ts": 54, + "src/shared/native-chat-task-list.test.ts": 45, + "src/shared/native-chat-tool-activity.test.ts": 49, + "src/shared/native-chat-tool-attribution-allocation.test.ts": 112, + "src/shared/native-chat-tool-fold-attribution.test.ts": 39, + "src/shared/native-chat-tool-icon.test.ts": 83, + "src/shared/native-chat-tool-identity.test.ts": 64, + "src/shared/native-chat-tool-line-identity.test.ts": 33, + "src/shared/native-chat-tool-pair-limit.test.ts": 75, + "src/shared/native-chat-tool-pairing.performance.test.ts": 463, + "src/shared/native-chat-tool-pairing.test.ts": 41, + "src/shared/native-chat-tool-preview-prefix.test.ts": 31, + "src/shared/native-chat-tool-preview-scan.test.ts": 37, + "src/shared/native-chat-tool-result-orphans.test.ts": 294, + "src/shared/native-chat-tool-run-outcome.test.ts": 58, + "src/shared/native-chat-tool-run-sentence.test.ts": 52, + "src/shared/native-chat-tool-summary.test.ts": 61, + "src/shared/native-chat-transcript-projection.test.ts": 250, + "src/shared/native-chat-transcript-retention.test.ts": 56, + "src/shared/native-chat-turn-activity.test.ts": 216, + "src/shared/native-chat-turn-fold.test.ts": 42, + "src/shared/native-chat-turn-grouping.test.ts": 212, + "src/shared/native-chat-turn-membership-derived-tool-row.test.ts": 328, + "src/shared/native-chat-turn-membership.test.ts": 253, + "src/shared/native-chat-turn-status.test.ts": 69, + "src/shared/native-chat-unverifiable-turn-status.test.ts": 245, + "src/shared/native-chat-upgrade-tip-audience.test.ts": 31, + "src/shared/native-chat-visual-directive.test.ts": 62, + "src/shared/native-chat-visual-height-governor.test.ts": 236, + "src/shared/native-chat-visual-shell.test.ts": 247, + "src/shared/native-chat-work-run.test.ts": 278, + "src/shared/native-file-drop.test.ts": 65, + "src/shared/nested-repo-telemetry-schema.test.ts": 261, + "src/shared/nested-repo-telemetry.test.ts": 40, + "src/shared/nested-worker-depth.test.ts": 35, + "src/shared/network-proxy.test.ts": 33, "src/shared/network/manual-address.test.ts": 60, - "src/shared/network/server-share-address.test.ts": 58, - "src/shared/new-workspace-dialog-repo.test.ts": 51, - "src/shared/new-workspace/smart-workspace-linear-intent.test.ts": 67, - "src/shared/new-workspace/smart-workspace-url-source-results.test.ts": 77, - "src/shared/new-workspace/work-item-lookup-text.test.ts": 59, - "src/shared/new-workspace/workspace-source.test.ts": 65, - "src/shared/new-workspace/worktree-create-retry-policy.test.ts": 49, - "src/shared/node-bounded-file-reader-sync.test.ts": 53, - "src/shared/node-bounded-file-reader.test.ts": 68, - "src/shared/node-bounded-json-stringify.test.ts": 77, - "src/shared/node-file-content-equality.test.ts": 44, - "src/shared/node-pty-spawn-helper.test.ts": 48, - "src/shared/node-readable-text.test.ts": 72, - "src/shared/node-source-copy-content-equality.test.ts": 65, - "src/shared/notebook-venv-location.test.ts": 46, - "src/shared/nul-delimited-fields.test.ts": 45, - "src/shared/nvm-default-alias.test.ts": 90, - "src/shared/omp-draft-launch-shell.test.ts": 247, - "src/shared/omp-draft-launch-windows.test.ts": 122, - "src/shared/omp-fresh-launch-shell.test.ts": 176, - "src/shared/omp-fresh-launch.test.ts": 130, - "src/shared/omp-owner-state-title.test.ts": 91, - "src/shared/omp-pi-semantic-title-preservation.test.ts": 107, - "src/shared/omp-source-control-discovery.test.ts": 287, - "src/shared/onboarding-tour-telemetry-events.test.ts": 342, - "src/shared/open-in-applications.test.ts": 54, - "src/shared/opencode-cli-version.test.ts": 64, - "src/shared/opencode-config-directory.test.ts": 44, - "src/shared/opencode-generation-command.test.ts": 82, - "src/shared/opencode-generation-output.test.ts": 230, - "src/shared/opencode-headless-command.test.ts": 63, - "src/shared/opencode-installed-plugin.test.ts": 61, - "src/shared/opencode-launch-command.test.ts": 61, - "src/shared/opencode-permission-status.test.ts": 213, - "src/shared/opencode-plugin-atomic-write.test.ts": 84, - "src/shared/opencode-plugin-permissions.test.ts": 117, - "src/shared/opencode-startup-prompt.test.ts": 178, - "src/shared/opencode-terminal-title.test.ts": 50, - "src/shared/opencode-tui-config-registration.test.ts": 85, - "src/shared/opencode-tui-plugin-install.test.ts": 59, - "src/shared/opencode2-resume-launch-mode.test.ts": 158, - "src/shared/openssh-default-shell.test.ts": 59, - "src/shared/orca-session-address.test.ts": 291, - "src/shared/orca-yaml-alias-bounds.test.ts": 398, - "src/shared/orca-yaml-bounds.test.ts": 216, - "src/shared/orcad-artifacts.test.ts": 85, - "src/shared/orcad-build-identity.test.ts": 61, - "src/shared/orcad-migration-dormant-session-validation.test.ts": 344, - "src/shared/orcad-migration-manifest-contract.test.ts": 358, - "src/shared/orcad-migration-manifest.test.ts": 432, - "src/shared/orcad-migration-scrollback.test.ts": 271, - "src/shared/orcad-migration-staged-catalog-validation.test.ts": 381, - "src/shared/orcad-profile-preflight.test.ts": 217, - "src/shared/orcad-stop-request.test.ts": 217, - "src/shared/orchestration-ask-timeout.test.ts": 49, - "src/shared/orchestration-check-output.test.ts": 66, - "src/shared/orchestration-compatibility-evidence.test.ts": 53, - "src/shared/orchestration-dispatch-refusal-contract.test.ts": 88, - "src/shared/orchestration-fleet-attention.test.ts": 46, - "src/shared/orchestration-fleet-evidence-clock.test.ts": 63, - "src/shared/orchestration-fleet-projection.test.ts": 105, - "src/shared/orchestration-rpc-contract.test.ts": 87, - "src/shared/orchestration-task-display.test.ts": 40, - "src/shared/orchestration-task-summary.test.ts": 53, - "src/shared/orchestration-timing-budgets.test.ts": 49, - "src/shared/osc-title-scan-tail-retention.test.ts": 82, - "src/shared/osc-title-scan-tail.test.ts": 51, - "src/shared/osc52-clipboard-settings.test.ts": 66, - "src/shared/own-retained-string.test.ts": 181, - "src/shared/pairing-address-auto-selection.test.ts": 54, - "src/shared/pairing-local-ui-fields.test.ts": 46, - "src/shared/pairing.test.ts": 249, - "src/shared/pane-agent-identity-adapter.test.ts": 95, - "src/shared/pane-agent-identity-resolver.test.ts": 89, - "src/shared/pane-agent-identity-title-corpus.test.ts": 73, - "src/shared/pane-agent-owner.test.ts": 52, - "src/shared/pane-key-alias.test.ts": 52, - "src/shared/physical-exit-tracker.test.ts": 51, - "src/shared/pi-agent-kind.test.ts": 68, - "src/shared/pi-overlay-ui-settings.test.ts": 50, - "src/shared/pi-state-title-marker.test.ts": 76, - "src/shared/plugins/plugin-consent-fingerprint.test.ts": 290, - "src/shared/plugins/plugin-consent-request.test.ts": 215, - "src/shared/plugins/plugin-content-pack-contributions.test.ts": 292, - "src/shared/plugins/plugin-install-lockfile.test.ts": 285, - "src/shared/plugins/plugin-kill-list.test.ts": 298, + "src/shared/network/server-share-address.test.ts": 41, + "src/shared/new-workspace-dialog-repo.test.ts": 39, + "src/shared/new-workspace/smart-workspace-linear-intent.test.ts": 44, + "src/shared/new-workspace/smart-workspace-url-source-results.test.ts": 43, + "src/shared/new-workspace/work-item-lookup-text.test.ts": 47, + "src/shared/new-workspace/workspace-source.test.ts": 57, + "src/shared/new-workspace/worktree-create-retry-policy.test.ts": 35, + "src/shared/node-bounded-file-reader-sync.test.ts": 60, + "src/shared/node-bounded-file-reader.test.ts": 39, + "src/shared/node-bounded-json-stringify.test.ts": 47, + "src/shared/node-file-content-equality.test.ts": 25, + "src/shared/node-pty-spawn-helper.test.ts": 43, + "src/shared/node-readable-text.test.ts": 45, + "src/shared/node-source-copy-content-equality.test.ts": 55, + "src/shared/notebook-venv-location.test.ts": 38, + "src/shared/nul-delimited-fields.test.ts": 24, + "src/shared/nvm-default-alias.test.ts": 56, + "src/shared/omp-draft-launch-shell.test.ts": 229, + "src/shared/omp-draft-launch-windows.test.ts": 137, + "src/shared/omp-fresh-launch-shell.test.ts": 166, + "src/shared/omp-fresh-launch.test.ts": 122, + "src/shared/omp-owner-state-title.test.ts": 92, + "src/shared/omp-pi-semantic-title-preservation.test.ts": 89, + "src/shared/omp-source-control-discovery.test.ts": 289, + "src/shared/onboarding-tour-telemetry-events.test.ts": 248, + "src/shared/open-in-applications.test.ts": 26, + "src/shared/opencode-cli-version.test.ts": 38, + "src/shared/opencode-config-directory.test.ts": 37, + "src/shared/opencode-generation-command.test.ts": 60, + "src/shared/opencode-generation-output.test.ts": 201, + "src/shared/opencode-headless-command.test.ts": 46, + "src/shared/opencode-installed-plugin.test.ts": 34, + "src/shared/opencode-launch-command.test.ts": 55, + "src/shared/opencode-permission-status.test.ts": 204, + "src/shared/opencode-plugin-atomic-write.test.ts": 65, + "src/shared/opencode-plugin-permissions.test.ts": 99, + "src/shared/opencode-startup-prompt.test.ts": 128, + "src/shared/opencode-terminal-title.test.ts": 29, + "src/shared/opencode-tui-config-registration.test.ts": 60, + "src/shared/opencode-tui-plugin-install.test.ts": 63, + "src/shared/opencode2-resume-launch-mode.test.ts": 110, + "src/shared/openssh-default-shell.test.ts": 45, + "src/shared/orca-session-address.test.ts": 250, + "src/shared/orca-yaml-alias-bounds.test.ts": 410, + "src/shared/orca-yaml-bounds.test.ts": 169, + "src/shared/orcad-artifacts.test.ts": 84, + "src/shared/orcad-build-identity.test.ts": 46, + "src/shared/orcad-migration-dormant-session-validation.test.ts": 314, + "src/shared/orcad-migration-manifest-contract.test.ts": 351, + "src/shared/orcad-migration-manifest.test.ts": 393, + "src/shared/orcad-migration-scrollback.test.ts": 240, + "src/shared/orcad-migration-source-kind.test.ts": 362, + "src/shared/orcad-migration-staged-catalog-validation.test.ts": 299, + "src/shared/orcad-profile-preflight.test.ts": 220, + "src/shared/orcad-stop-request.test.ts": 239, + "src/shared/orchestration-ask-timeout.test.ts": 30, + "src/shared/orchestration-check-output.test.ts": 216, + "src/shared/orchestration-compatibility-evidence.test.ts": 34, + "src/shared/orchestration-dispatch-refusal-contract.test.ts": 72, + "src/shared/orchestration-fleet-attention.test.ts": 39, + "src/shared/orchestration-fleet-evidence-clock.test.ts": 48, + "src/shared/orchestration-fleet-projection.test.ts": 104, + "src/shared/orchestration-rpc-contract.test.ts": 260, + "src/shared/orchestration-task-display.test.ts": 48, + "src/shared/orchestration-task-summary.test.ts": 34, + "src/shared/orchestration-timing-budgets.test.ts": 35, + "src/shared/osc-title-scan-tail-retention.test.ts": 75, + "src/shared/osc-title-scan-tail.test.ts": 26, + "src/shared/osc52-clipboard-settings.test.ts": 40, + "src/shared/own-retained-string.test.ts": 159, + "src/shared/paired-e2ee-client-handshake.test.ts": 116, + "src/shared/paired-e2ee-device-key-auth.test.ts": 92, + "src/shared/paired-runtime-connection.test.ts": 224, + "src/shared/pairing-address-auto-selection.test.ts": 38, + "src/shared/pairing-local-ui-fields.test.ts": 38, + "src/shared/pairing.test.ts": 235, + "src/shared/pane-agent-identity-adapter.test.ts": 76, + "src/shared/pane-agent-identity-resolver.test.ts": 62, + "src/shared/pane-agent-identity-title-corpus.test.ts": 70, + "src/shared/pane-agent-owner.test.ts": 45, + "src/shared/pane-key-alias.test.ts": 40, + "src/shared/physical-exit-tracker.test.ts": 32, + "src/shared/pi-agent-kind.test.ts": 69, + "src/shared/pi-state-title-marker.test.ts": 77, + "src/shared/plugins/plugin-consent-fingerprint.test.ts": 216, + "src/shared/plugins/plugin-consent-request.test.ts": 207, + "src/shared/plugins/plugin-content-pack-contributions.test.ts": 260, + "src/shared/plugins/plugin-install-lockfile.test.ts": 213, + "src/shared/plugins/plugin-kill-list.test.ts": 318, "src/shared/plugins/plugin-language-pack-artifact.test.ts": 177, - "src/shared/plugins/plugin-manifest.test.ts": 265, - "src/shared/plugins/plugin-marketplace.test.ts": 314, - "src/shared/plugins/plugin-panel-call-admission.test.ts": 246, - "src/shared/plugins/plugin-panel-message-budget-traversal.test.ts": 297, - "src/shared/plugins/plugin-panel-message-budget.test.ts": 278, - "src/shared/plugins/plugin-panel-pong-frame.test.ts": 243, - "src/shared/plugins/plugin-panel-shell.test.ts": 343, - "src/shared/plugins/plugin-path-safety.test.ts": 304, - "src/shared/plugins/plugin-vm-recipe-artifact.test.ts": 342, - "src/shared/posix-wait-status.test.ts": 50, - "src/shared/powershell-native-argument.test.ts": 33, - "src/shared/powershell-native-argument.windows.test.ts": 65, - "src/shared/pr-bot-author-overrides.test.ts": 54, - "src/shared/pr-check-severity-order.test.ts": 56, - "src/shared/pr-check-status.test.ts": 50, - "src/shared/pr-comment-audience.test.ts": 54, - "src/shared/pr-comment-groups.test.ts": 43, - "src/shared/pr-comment-time.test.ts": 45, - "src/shared/preferred-git-remote.test.ts": 53, - "src/shared/priority-semaphore.test.ts": 63, - "src/shared/process-output-field-scanner.test.ts": 55, - "src/shared/process-table-index.test.ts": 176, - "src/shared/process-table-snapshot.test.ts": 175, - "src/shared/profile-state-build-compatibility.test.ts": 52, - "src/shared/profile-state-recovery-command.test.ts": 248, - "src/shared/profile-state-telemetry-schema.test.ts": 295, - "src/shared/project-catalog-row-normalization.test.ts": 65, - "src/shared/project-execution-runtime.test.ts": 62, - "src/shared/project-groups.test.ts": 148, - "src/shared/project-host-setup-projection.test.ts": 99, - "src/shared/project-identity-succession.test.ts": 81, - "src/shared/promise-settlement-waiters.test.ts": 169, - "src/shared/protocol-compat.test.ts": 88, - "src/shared/pty-delivery-diagnostics.test.ts": 55, - "src/shared/pty-liveness-verdict.test.ts": 46, - "src/shared/pty-owner-backend.test.ts": 45, - "src/shared/pty-owner-color-query-colors.test.ts": 66, - "src/shared/pty-running-work-probe.test.ts": 61, - "src/shared/pty-slave-line-discipline-echo.test.ts": 64, - "src/shared/pty-startup-ingress-kitty.test.ts": 106, - "src/shared/pty-startup-ingress-lifetime-colors.test.ts": 90, - "src/shared/pty-startup-ingress-live-query-reply.test.ts": 119, - "src/shared/pty-startup-ingress.test.ts": 155, - "src/shared/pty-startup-reply-echo-shapes.test.ts": 59, - "src/shared/published-pane-agent-identity.test.ts": 90, - "src/shared/pull-request-generation.test.ts": 71, - "src/shared/qoder-agent.test.ts": 293, - "src/shared/qoder-cn-launch-recipe.test.ts": 147, - "src/shared/quick-open-filter.renderer-safety.test.ts": 59, - "src/shared/quick-open-filter.test.ts": 93, - "src/shared/quick-open-fuzzy-scan.test.ts": 996, - "src/shared/quick-open-git-directory-collapse.test.ts": 88, - "src/shared/quick-open-listing-limits.test.ts": 45, - "src/shared/quick-open-query-target.test.ts": 64, - "src/shared/quick-open-recent-candidates.test.ts": 63, - "src/shared/quick-open-transport-budget.test.ts": 55, - "src/shared/raster-image-base64-preview.test.ts": 391, - "src/shared/raster-image-dimensions.test.ts": 58, - "src/shared/raster-image-preview-limits.test.ts": 63, - "src/shared/rate-limit-reset-format.test.ts": 46, - "src/shared/react-update-depth-attribution.test.ts": 46, - "src/shared/reconnect-jitter.test.ts": 42, - "src/shared/relay-frame-buffer.test.ts": 275, - "src/shared/relay-optional-artifacts.test.ts": 53, - "src/shared/relay-version-marker.test.ts": 51, - "src/shared/release-channel.test.ts": 75, - "src/shared/remote-foreground-evidence.test.ts": 64, - "src/shared/remote-pairing-address.test.ts": 268, - "src/shared/remote-pairing-verification.test.ts": 64, - "src/shared/remote-rpc-content-budget.test.ts": 105, - "src/shared/remote-runtime-client-error-classification.test.ts": 50, - "src/shared/remote-runtime-client.test.ts": 5122, - "src/shared/remote-runtime-connect-bound.test.ts": 558, - "src/shared/remote-runtime-memory-limits.test.ts": 144, - "src/shared/remote-runtime-outbound-admission.test.ts": 1054, - "src/shared/remote-runtime-pty-id.test.ts": 45, - "src/shared/remote-runtime-request-connection-stale.test.ts": 312, - "src/shared/remote-runtime-request-connection.test.ts": 410, - "src/shared/remote-runtime-request-frames.test.ts": 208, - "src/shared/remote-runtime-request-response-router.test.ts": 244, - "src/shared/remote-runtime-request-websocket.test.ts": 300, - "src/shared/remote-runtime-shared-control-connection.test.ts": 3555, - "src/shared/remote-runtime-shared-control-keepalive-refresh.test.ts": 279, - "src/shared/remote-runtime-shared-control-reconnect.test.ts": 263, - "src/shared/remote-runtime-shared-control-retired-request-ids.test.ts": 42, - "src/shared/remote-runtime-shared-control-socket-generation.test.ts": 221, - "src/shared/remote-runtime-shared-control-standing-intent.test.ts": 956, - "src/shared/remote-runtime-shared-control-subscription-close.test.ts": 255, - "src/shared/remote-runtime-shared-control-subscription-start.test.ts": 267, - "src/shared/remote-runtime-shared-control-subscriptions.test.ts": 277, - "src/shared/remote-runtime-socket-liveness.test.ts": 58, - "src/shared/remote-runtime-subscription-connect-bound.test.ts": 652, - "src/shared/remote-runtime-subscription-frame-router.test.ts": 249, - "src/shared/remote-runtime-subscription-request.test.ts": 699, - "src/shared/remote-runtime-tailscale-hint.test.ts": 59, - "src/shared/remote-runtime-transport-error-agreement.test.ts": 219, - "src/shared/renderer-restart-preparation.test.ts": 169, - "src/shared/repo-badge-color.test.ts": 310, - "src/shared/repo-icon.test.ts": 80, - "src/shared/repo-pack-index-scheduling.test.ts": 86, - "src/shared/repo-ref-maintenance.test.ts": 507, - "src/shared/repo-search-limits.test.ts": 49, - "src/shared/repro-13889-claude-quarter-circle-busy-title.test.ts": 105, - "src/shared/repro-7732-gitlab-job-id-dropped.test.ts": 51, - "src/shared/repro-8478-opencode-native-title-icon.test.ts": 86, - "src/shared/require-tui-agent-config.test.ts": 60, - "src/shared/resolved-worktree-lineage.test.ts": 60, - "src/shared/retired-pty-incarnations.test.ts": 50, - "src/shared/returned-draft-text.test.ts": 45, - "src/shared/review-head-tracking-ref.test.ts": 50, - "src/shared/review-refresh-policy.test.ts": 47, - "src/shared/ripgrep-dense-match-json.test.ts": 186, - "src/shared/ripgrep-filename-decoder.test.ts": 51, - "src/shared/ripgrep-line-decoding.test.ts": 388, - "src/shared/ripgrep-match-offsets.test.ts": 132, - "src/shared/ripgrep-process-availability.test.ts": 80, - "src/shared/ripgrep-search-diagnostics.test.ts": 72, - "src/shared/rpc-contract/agent-launch-params.test.ts": 408, - "src/shared/rpc-contract/automation-params-extra-agent-args.test.ts": 256, - "src/shared/rpc-contract/structured-agent-session-agent-params.test.ts": 292, - "src/shared/rpc-contract/terminal-create-shell-param.test.ts": 219, - "src/shared/rpc-contract/worktree-review-metadata-compatibility.test.ts": 277, - "src/shared/runtime-environment-managed-orcad-store.test.ts": 332, - "src/shared/runtime-environment-sidecar.test.ts": 340, - "src/shared/runtime-environment-ssh-access-store.test.ts": 416, - "src/shared/runtime-environment-store.test.ts": 353, - "src/shared/runtime-environments-ssh-access.test.ts": 247, - "src/shared/runtime-host-pairing-identity.test.ts": 52, - "src/shared/runtime-host-status-owner.test.ts": 83, - "src/shared/runtime-listing-host-scope.test.ts": 57, - "src/shared/runtime-navigation.test.ts": 47, - "src/shared/runtime-rpc-call-queue-retention.test.ts": 279, - "src/shared/runtime-rpc-call-queue.test.ts": 87, - "src/shared/runtime-source-env.test.ts": 51, - "src/shared/runtime-workspace-file-owner.test.ts": 50, - "src/shared/runtime-workspace-window-availability.test.ts": 49, - "src/shared/search-match-count.test.ts": 43, - "src/shared/search-subprocess-lines.test.ts": 99, - "src/shared/secret-at-rest-protection.test.ts": 50, - "src/shared/secret-store.test.ts": 43, - "src/shared/secure-file-coarse-ctime.test.ts": 61, - "src/shared/secure-file-fsync-flags.test.ts": 98, - "src/shared/secure-file.test.ts": 736, - "src/shared/secure-path-hardening-cache.test.ts": 59, - "src/shared/secure-path-hardening-retry-budget.test.ts": 91, - "src/shared/secure-path-windows-acl.win32.test.ts": 68, - "src/shared/serve-option-validation.test.ts": 63, - "src/shared/session-tab-placement.test.ts": 53, - "src/shared/setup-agent-sequencing-codex-no-daemon.test.ts": 1135, - "src/shared/setup-agent-sequencing.test.ts": 5411, - "src/shared/setup-agent-sequencing.windows.test.ts": 54, - "src/shared/setup-runner-command.test.ts": 80, - "src/shared/setup-script-imports.test.ts": 118, - "src/shared/setup-script-package-manager-suggestion.test.ts": 65, - "src/shared/setup-script-shebang.test.ts": 60, - "src/shared/setup-script-telemetry-events.test.ts": 297, - "src/shared/setup-script-telemetry.test.ts": 49, - "src/shared/sha256.test.ts": 61, - "src/shared/shell-foreground-snapshot.test.ts": 173, - "src/shared/shell-process-detection.test.ts": 55, - "src/shared/shell-process-readiness.test.ts": 68, - "src/shared/skill-bundle-install-contract.test.ts": 218, - "src/shared/skill-bundle-name.test.ts": 50, - "src/shared/skill-delete-contract.test.ts": 241, - "src/shared/skill-deletion-eligibility.test.ts": 56, - "src/shared/skill-install-contract.test.ts": 220, - "src/shared/skill-install-failure.test.ts": 202, - "src/shared/skill-metadata.test.ts": 47, - "src/shared/skill-package-manifest.test.ts": 350, - "src/shared/skill-path-containment.test.ts": 53, - "src/shared/skills-cli-agent-keys.test.ts": 49, - "src/shared/source-control-ai-action-recipes.test.ts": 306, - "src/shared/source-control-ai-action-variables.test.ts": 64, - "src/shared/source-control-ai-actions.test.ts": 66, - "src/shared/source-control-ai-policy-regression.test.ts": 281, - "src/shared/source-control-ai-recipe-save.test.ts": 276, - "src/shared/source-control-ai.test.ts": 278, - "src/shared/source-control-create-review-intent.test.ts": 60, - "src/shared/source-control-group-order.test.ts": 49, + "src/shared/plugins/plugin-manifest.test.ts": 248, + "src/shared/plugins/plugin-marketplace.test.ts": 325, + "src/shared/plugins/plugin-panel-call-admission.test.ts": 238, + "src/shared/plugins/plugin-panel-message-budget-traversal.test.ts": 255, + "src/shared/plugins/plugin-panel-message-budget.test.ts": 265, + "src/shared/plugins/plugin-panel-pong-frame.test.ts": 252, + "src/shared/plugins/plugin-panel-shell.test.ts": 380, + "src/shared/plugins/plugin-path-safety.test.ts": 229, + "src/shared/plugins/plugin-vm-recipe-artifact.test.ts": 203, + "src/shared/posix-wait-status.test.ts": 31, + "src/shared/powershell-native-argument.test.ts": 24, + "src/shared/powershell-native-argument.windows.test.ts": 40, + "src/shared/pr-bot-author-overrides.test.ts": 53, + "src/shared/pr-check-severity-order.test.ts": 40, + "src/shared/pr-check-status.test.ts": 39, + "src/shared/pr-comment-audience.test.ts": 51, + "src/shared/pr-comment-groups.test.ts": 26, + "src/shared/pr-comment-time.test.ts": 41, + "src/shared/preferred-git-remote.test.ts": 39, + "src/shared/priority-semaphore.test.ts": 45, + "src/shared/process-growth-breaker.test.ts": 35, + "src/shared/process-output-field-scanner.test.ts": 41, + "src/shared/process-table-capture-parsing.test.ts": 42, + "src/shared/process-table-index.test.ts": 184, + "src/shared/process-table-snapshot.test.ts": 162, + "src/shared/profile-state-build-compatibility.test.ts": 35, + "src/shared/profile-state-recovery-command.test.ts": 222, + "src/shared/profile-state-telemetry-schema.test.ts": 307, + "src/shared/project-catalog-row-normalization.test.ts": 51, + "src/shared/project-execution-runtime.test.ts": 55, + "src/shared/project-groups.test.ts": 125, + "src/shared/project-host-setup-projection.test.ts": 86, + "src/shared/project-identity-succession.test.ts": 69, + "src/shared/promise-settlement-waiters.test.ts": 183, + "src/shared/protocol-compat.test.ts": 193, + "src/shared/pty-delivery-diagnostics.test.ts": 35, + "src/shared/pty-liveness-verdict.test.ts": 38, + "src/shared/pty-owner-backend.test.ts": 37, + "src/shared/pty-owner-color-query-colors.test.ts": 58, + "src/shared/pty-running-work-probe.test.ts": 40, + "src/shared/pty-slave-line-discipline-echo.test.ts": 38, + "src/shared/pty-startup-ingress-kitty.test.ts": 120, + "src/shared/pty-startup-ingress-lifetime-colors.test.ts": 92, + "src/shared/pty-startup-ingress-live-query-reply.test.ts": 118, + "src/shared/pty-startup-ingress.test.ts": 167, + "src/shared/pty-startup-reply-echo-shapes.test.ts": 58, + "src/shared/published-pane-agent-identity.test.ts": 87, + "src/shared/pull-request-generation.test.ts": 47, + "src/shared/qoder-agent.test.ts": 295, + "src/shared/qoder-cn-launch-recipe.test.ts": 118, + "src/shared/quick-open-filter.renderer-safety.test.ts": 49, + "src/shared/quick-open-filter.test.ts": 94, + "src/shared/quick-open-fuzzy-scan.test.ts": 1098, + "src/shared/quick-open-git-directory-collapse.test.ts": 73, + "src/shared/quick-open-listing-limits.test.ts": 29, + "src/shared/quick-open-query-target.test.ts": 63, + "src/shared/quick-open-recent-candidates.test.ts": 40, + "src/shared/quick-open-subprocess-path-accumulator.test.ts": 24, + "src/shared/quick-open-transport-budget.test.ts": 34, + "src/shared/raster-image-base64-preview.test.ts": 395, + "src/shared/raster-image-dimensions.test.ts": 39, + "src/shared/raster-image-preview-limits.test.ts": 32, + "src/shared/rate-limit-reset-format.test.ts": 36, + "src/shared/react-update-depth-attribution.test.ts": 27, + "src/shared/reconnect-jitter.test.ts": 28, + "src/shared/relay-frame-buffer.test.ts": 273, + "src/shared/relay-version-marker.test.ts": 50, + "src/shared/release-channel.test.ts": 56, + "src/shared/remote-foreground-evidence.test.ts": 44, + "src/shared/remote-pairing-address.test.ts": 239, + "src/shared/remote-pairing-verification.test.ts": 202, + "src/shared/remote-rpc-content-budget.test.ts": 95, + "src/shared/remote-runtime-client-error-classification.test.ts": 42, + "src/shared/remote-runtime-client.test.ts": 5160, + "src/shared/remote-runtime-connect-bound.test.ts": 572, + "src/shared/remote-runtime-memory-limits.test.ts": 120, + "src/shared/remote-runtime-outbound-admission.test.ts": 1044, + "src/shared/remote-runtime-pty-id.test.ts": 38, + "src/shared/remote-runtime-request-connection-stale.test.ts": 311, + "src/shared/remote-runtime-request-connection.test.ts": 393, + "src/shared/remote-runtime-request-frames.test.ts": 280, + "src/shared/remote-runtime-request-response-router.test.ts": 243, + "src/shared/remote-runtime-request-websocket.test.ts": 406, + "src/shared/remote-runtime-shared-control-connection.test.ts": 3965, + "src/shared/remote-runtime-shared-control-keepalive-refresh.test.ts": 287, + "src/shared/remote-runtime-shared-control-reconnect.test.ts": 257, + "src/shared/remote-runtime-shared-control-retired-request-ids.test.ts": 40, + "src/shared/remote-runtime-shared-control-socket-generation.test.ts": 197, + "src/shared/remote-runtime-shared-control-standing-intent.test.ts": 961, + "src/shared/remote-runtime-shared-control-subscription-close.test.ts": 261, + "src/shared/remote-runtime-shared-control-subscription-start.test.ts": 308, + "src/shared/remote-runtime-shared-control-subscriptions.test.ts": 275, + "src/shared/remote-runtime-socket-liveness.test.ts": 37, + "src/shared/remote-runtime-subscription-connect-bound.test.ts": 658, + "src/shared/remote-runtime-subscription-frame-router.test.ts": 282, + "src/shared/remote-runtime-subscription-request.test.ts": 668, + "src/shared/remote-runtime-tailscale-hint.test.ts": 47, + "src/shared/remote-runtime-transport-error-agreement.test.ts": 297, + "src/shared/renderer-restart-preparation.test.ts": 163, + "src/shared/repo-badge-color.test.ts": 233, + "src/shared/repo-icon.test.ts": 75, + "src/shared/repo-pack-index-scheduling.test.ts": 67, + "src/shared/repo-ref-maintenance.test.ts": 409, + "src/shared/repo-search-limits.test.ts": 45, + "src/shared/repro-13889-claude-quarter-circle-busy-title.test.ts": 87, + "src/shared/repro-7732-gitlab-job-id-dropped.test.ts": 31, + "src/shared/repro-8478-opencode-native-title-icon.test.ts": 69, + "src/shared/require-tui-agent-config.test.ts": 32, + "src/shared/resolved-worktree-lineage.test.ts": 55, + "src/shared/retired-pty-incarnations.test.ts": 35, + "src/shared/returned-draft-text.test.ts": 34, + "src/shared/review-head-tracking-ref.test.ts": 36, + "src/shared/review-refresh-policy.test.ts": 42, + "src/shared/ripgrep-dense-match-json.test.ts": 122, + "src/shared/ripgrep-filename-decoder.test.ts": 35, + "src/shared/ripgrep-line-decoding.test.ts": 467, + "src/shared/ripgrep-match-offsets.test.ts": 100, + "src/shared/ripgrep-process-availability.test.ts": 66, + "src/shared/ripgrep-search-diagnostics.test.ts": 68, + "src/shared/rpc-contract/agent-launch-params-account-env.test.ts": 452, + "src/shared/rpc-contract/agent-launch-params-native-chat-default.test.ts": 434, + "src/shared/rpc-contract/agent-launch-params.test.ts": 420, + "src/shared/rpc-contract/agent-session-params-account-env.test.ts": 262, + "src/shared/rpc-contract/agent-session-spawn-intent-params.test.ts": 241, + "src/shared/rpc-contract/automation-params-extra-agent-args.test.ts": 247, + "src/shared/rpc-contract/jira-transition-update.test.ts": 209, + "src/shared/rpc-contract/structured-agent-session-agent-params.test.ts": 259, + "src/shared/rpc-contract/terminal-create-shell-param.test.ts": 283, + "src/shared/rpc-contract/worktree-review-metadata-compatibility.test.ts": 236, + "src/shared/runtime-environment-managed-orcad-store.test.ts": 383, + "src/shared/runtime-environment-sidecar.test.ts": 391, + "src/shared/runtime-environment-ssh-access-store.test.ts": 493, + "src/shared/runtime-environment-store-unreadable-records-interleaving.test.ts": 1049, + "src/shared/runtime-environment-store-unreadable-records.test.ts": 334, + "src/shared/runtime-environment-store.test.ts": 323, + "src/shared/runtime-environment-wsl-host-store.test.ts": 522, + "src/shared/runtime-environments-listed-remote-server.test.ts": 199, + "src/shared/runtime-environments-ssh-access.test.ts": 273, + "src/shared/runtime-host-pairing-identity.test.ts": 33, + "src/shared/runtime-host-status-owner.test.ts": 64, + "src/shared/runtime-listing-host-scope.test.ts": 42, + "src/shared/runtime-navigation.test.ts": 34, + "src/shared/runtime-rpc-call-queue-retention.test.ts": 256, + "src/shared/runtime-rpc-call-queue.test.ts": 62, + "src/shared/runtime-source-env.test.ts": 24, + "src/shared/runtime-workspace-file-owner.test.ts": 29, + "src/shared/runtime-workspace-window-availability.test.ts": 43, + "src/shared/search-match-count.test.ts": 37, + "src/shared/search-subprocess-lines.test.ts": 90, + "src/shared/secret-at-rest-protection.test.ts": 38, + "src/shared/secret-store.test.ts": 26, + "src/shared/secure-file-coarse-ctime.test.ts": 38, + "src/shared/secure-file-fsync-flags.test.ts": 52, + "src/shared/secure-file-windows.test.ts": 183, + "src/shared/secure-file.test.ts": 66, + "src/shared/secure-path-hardening-cache.test.ts": 38, + "src/shared/secure-path-hardening-retry-budget.test.ts": 70, + "src/shared/secure-path-windows-acl.win32.test.ts": 51, + "src/shared/serve-option-validation.test.ts": 44, + "src/shared/session-tab-placement.test.ts": 45, + "src/shared/setup-agent-sequencing-codex-no-daemon.test.ts": 1123, + "src/shared/setup-agent-sequencing.test.ts": 4293, + "src/shared/setup-agent-sequencing.windows.test.ts": 42, + "src/shared/setup-runner-command.test.ts": 63, + "src/shared/setup-script-imports.test.ts": 94, + "src/shared/setup-script-package-manager-suggestion.test.ts": 44, + "src/shared/setup-script-shebang.test.ts": 52, + "src/shared/setup-script-telemetry-events.test.ts": 253, + "src/shared/setup-script-telemetry.test.ts": 34, + "src/shared/sha256.test.ts": 58, + "src/shared/shell-foreground-snapshot.test.ts": 221, + "src/shared/shell-process-detection.test.ts": 44, + "src/shared/shell-process-readiness.test.ts": 65, + "src/shared/skill-bundle-install-contract.test.ts": 225, + "src/shared/skill-bundle-name.test.ts": 30, + "src/shared/skill-delete-contract.test.ts": 240, + "src/shared/skill-deletion-eligibility.test.ts": 45, + "src/shared/skill-install-contract.test.ts": 235, + "src/shared/skill-install-failure.test.ts": 235, + "src/shared/skill-metadata.test.ts": 28, + "src/shared/skill-package-manifest.test.ts": 322, + "src/shared/skill-path-containment.test.ts": 34, + "src/shared/skills-cli-agent-keys.test.ts": 26, + "src/shared/source-control-ai-action-recipes.test.ts": 277, + "src/shared/source-control-ai-action-variables.test.ts": 45, + "src/shared/source-control-ai-actions.test.ts": 48, + "src/shared/source-control-ai-policy-regression.test.ts": 227, + "src/shared/source-control-ai-recipe-save.test.ts": 266, + "src/shared/source-control-ai.test.ts": 264, + "src/shared/source-control-create-review-intent.test.ts": 48, + "src/shared/source-control-group-order.test.ts": 33, "src/shared/source-control-launch-agent-selection.test.ts": 289, - "src/shared/source-control-primary-action-decision.test.ts": 62, - "src/shared/source-control-push-failure.test.ts": 68, - "src/shared/source-control-recovery-agent-command.test.ts": 77, - "src/shared/source-scan/source-tree-scan.test.ts": 100, - "src/shared/source-scan/source-tree-walk.test.ts": 71, - "src/shared/ssh-pending-pty-kill.test.ts": 55, - "src/shared/ssh-pty-id.test.ts": 49, - "src/shared/ssh-remote-pty-lease-predicates.test.ts": 43, - "src/shared/ssh-retained-payload-admission.test.ts": 78, - "src/shared/ssh-target-generation.test.ts": 57, - "src/shared/stable-pane-id.test.ts": 46, - "src/shared/startup-command-staging.node-pty.test.ts": 2051, - "src/shared/startup-command-staging.test.ts": 143, - "src/shared/startup-command-submission.test.ts": 42, - "src/shared/startup-line-prompt-carry.test.ts": 175, - "src/shared/status-bar-compact-change-notice.test.ts": 60, - "src/shared/status-bar-usage-mode.test.ts": 49, - "src/shared/string-chunk-compaction.test.ts": 63, - "src/shared/structural-value-equality.test.ts": 52, - "src/shared/structured-agent-session-agent-status.test.ts": 71, - "src/shared/structured-agent-session-child-work-legacy.test.ts": 65, - "src/shared/structured-agent-session-coalescer.test.ts": 215, - "src/shared/structured-agent-session-composer.test.ts": 82, - "src/shared/structured-agent-session-context-usage.test.ts": 226, - "src/shared/structured-agent-session-create.test.ts": 86, - "src/shared/structured-agent-session-dispatch-rejection.test.ts": 283, - "src/shared/structured-agent-session-draft-hand-off.test.ts": 46, - "src/shared/structured-agent-session-first-frame-picker.test.ts": 86, - "src/shared/structured-agent-session-first-prompt.test.ts": 227, - "src/shared/structured-agent-session-item-retention.test.ts": 941, - "src/shared/structured-agent-session-latest-request.test.ts": 353, - "src/shared/structured-agent-session-live-turn.test.ts": 248, - "src/shared/structured-agent-session-message-projection-stopped-before-start.test.ts": 315, - "src/shared/structured-agent-session-message-projection.test.ts": 267, - "src/shared/structured-agent-session-model-catalog-application.test.ts": 77, - "src/shared/structured-agent-session-mutation.test.ts": 260, - "src/shared/structured-agent-session-opening-send.test.ts": 233, - "src/shared/structured-agent-session-option-codec.test.ts": 40, - "src/shared/structured-agent-session-option-picks.test.ts": 80, - "src/shared/structured-agent-session-options.test.ts": 78, + "src/shared/source-control-primary-action-decision.test.ts": 45, + "src/shared/source-control-push-failure.test.ts": 37, + "src/shared/source-control-recovery-agent-command.test.ts": 48, + "src/shared/source-scan/source-tree-scan.test.ts": 78, + "src/shared/source-scan/source-tree-walk.test.ts": 48, + "src/shared/ssh-pending-pty-kill.test.ts": 30, + "src/shared/ssh-pty-id.test.ts": 42, + "src/shared/ssh-remote-pty-lease-predicates.test.ts": 28, + "src/shared/ssh-retained-payload-admission.test.ts": 62, + "src/shared/ssh-target-generation.test.ts": 47, + "src/shared/stable-pane-id.test.ts": 40, + "src/shared/startup-command-staging.node-pty.test.ts": 2025, + "src/shared/startup-command-staging.test.ts": 162, + "src/shared/startup-command-submission.test.ts": 35, + "src/shared/startup-line-prompt-carry.test.ts": 296, + "src/shared/status-bar-compact-change-notice.test.ts": 30, + "src/shared/status-bar-usage-mode.test.ts": 41, + "src/shared/structural-value-equality.test.ts": 38, + "src/shared/structured-agent-session-agent-status.test.ts": 39, + "src/shared/structured-agent-session-child-work-legacy.test.ts": 40, + "src/shared/structured-agent-session-coalescer.test.ts": 268, + "src/shared/structured-agent-session-composer.test.ts": 64, + "src/shared/structured-agent-session-context-usage.test.ts": 242, + "src/shared/structured-agent-session-create.test.ts": 207, + "src/shared/structured-agent-session-dispatch-rejection.test.ts": 280, + "src/shared/structured-agent-session-draft-hand-off.test.ts": 36, + "src/shared/structured-agent-session-first-frame-picker.test.ts": 99, + "src/shared/structured-agent-session-first-prompt.test.ts": 253, + "src/shared/structured-agent-session-item-retention.test.ts": 879, + "src/shared/structured-agent-session-latest-request.test.ts": 326, + "src/shared/structured-agent-session-live-turn.test.ts": 243, + "src/shared/structured-agent-session-message-projection-stopped-before-start.test.ts": 355, + "src/shared/structured-agent-session-message-projection.test.ts": 346, + "src/shared/structured-agent-session-model-catalog-application.test.ts": 92, + "src/shared/structured-agent-session-mutation.test.ts": 248, + "src/shared/structured-agent-session-opening-send.test.ts": 211, + "src/shared/structured-agent-session-option-codec.test.ts": 33, + "src/shared/structured-agent-session-option-picks.test.ts": 85, + "src/shared/structured-agent-session-option-view.test.ts": 95, + "src/shared/structured-agent-session-options.test.ts": 93, + "src/shared/structured-agent-session-permission-option.test.ts": 96, "src/shared/structured-agent-session-projection-sender.test.ts": 307, - "src/shared/structured-agent-session-projection.test.ts": 414, - "src/shared/structured-agent-session-reasoning-projection.test.ts": 308, - "src/shared/structured-agent-session-reducer.queued-messages.test.ts": 228, - "src/shared/structured-agent-session-reducer.test.ts": 274, - "src/shared/structured-agent-session-rejection-words.test.ts": 209, - "src/shared/structured-agent-session-send-evidence.test.ts": 225, - "src/shared/structured-agent-session-send-mutation.test.ts": 305, - "src/shared/structured-agent-session-status-started-at.test.ts": 291, - "src/shared/structured-agent-session-status-tool-line.test.ts": 324, - "src/shared/structured-agent-session-subagent-roster.test.ts": 64, - "src/shared/structured-agent-session-turn-bars.test.ts": 255, - "src/shared/structured-agent-session-turn-timing-retention.test.ts": 355, - "src/shared/structured-agent-session-turn-timing.test.ts": 301, - "src/shared/structured-agent-session-unknown-item.test.ts": 260, - "src/shared/structured-agent-turn-clock-anchor.test.ts": 290, - "src/shared/structured-chat-naming-eligibility.test.ts": 236, - "src/shared/structured-native-chat-launch-route.test.ts": 87, - "src/shared/structured-session-background-tasks-view.test.ts": 67, - "src/shared/structured-session-foreground-commands.test.ts": 230, - "src/shared/subprocess-stdin-write.test.ts": 49, - "src/shared/synthetic-agent-title.test.ts": 52, - "src/shared/tab-title-resolution.test.ts": 60, - "src/shared/tailnet-address.test.ts": 45, - "src/shared/task-providers.test.ts": 57, - "src/shared/task-query.test.ts": 61, - "src/shared/task-source-context.test.ts": 211, - "src/shared/telemetry-common-props.test.ts": 271, - "src/shared/telemetry-events-feature-education.test.ts": 298, - "src/shared/telemetry-events.test.ts": 361, - "src/shared/telemetry-feature-wall-events.test.ts": 290, - "src/shared/telemetry-orca-cli-feature-tip.test.ts": 307, - "src/shared/telemetry-workspace-create-schemas.test.ts": 242, - "src/shared/terminal-bell-detector.test.ts": 45, - "src/shared/terminal-color-scheme-protocol.test.ts": 45, - "src/shared/terminal-composer-draft.test.ts": 72, - "src/shared/terminal-custom-themes.test.ts": 54, + "src/shared/structured-agent-session-projection.test.ts": 397, + "src/shared/structured-agent-session-reasoning-projection.test.ts": 289, + "src/shared/structured-agent-session-reducer.queued-messages.test.ts": 306, + "src/shared/structured-agent-session-reducer.test.ts": 271, + "src/shared/structured-agent-session-rejection-words.test.ts": 289, + "src/shared/structured-agent-session-send-evidence.test.ts": 237, + "src/shared/structured-agent-session-send-mutation.test.ts": 265, + "src/shared/structured-agent-session-send-settlement.test.ts": 38, + "src/shared/structured-agent-session-status-started-at.test.ts": 394, + "src/shared/structured-agent-session-status-tool-line.test.ts": 317, + "src/shared/structured-agent-session-subagent-roster.test.ts": 53, + "src/shared/structured-agent-session-turn-bars.test.ts": 273, + "src/shared/structured-agent-session-turn-timing-retention.test.ts": 314, + "src/shared/structured-agent-session-turn-timing.test.ts": 279, + "src/shared/structured-agent-session-unknown-item.test.ts": 296, + "src/shared/structured-agent-turn-clock-anchor.test.ts": 273, + "src/shared/structured-chat-naming-eligibility.test.ts": 273, + "src/shared/structured-native-chat-launch-route.test.ts": 249, + "src/shared/structured-session-background-tasks-view.test.ts": 42, + "src/shared/structured-session-foreground-commands.test.ts": 213, + "src/shared/subprocess-stdin-write.test.ts": 32, + "src/shared/synthetic-agent-title.test.ts": 39, + "src/shared/tab-title-resolution.test.ts": 34, + "src/shared/tailnet-address.test.ts": 33, + "src/shared/task-providers.test.ts": 43, + "src/shared/task-query.test.ts": 55, + "src/shared/task-source-context.test.ts": 212, + "src/shared/telemetry-common-props.test.ts": 258, + "src/shared/telemetry-events-feature-education.test.ts": 256, + "src/shared/telemetry-events.test.ts": 359, + "src/shared/telemetry-feature-wall-events.test.ts": 339, + "src/shared/telemetry-orca-cli-feature-tip.test.ts": 285, + "src/shared/telemetry-workspace-create-schemas.test.ts": 293, + "src/shared/terminal-bell-detector.test.ts": 36, + "src/shared/terminal-color-scheme-protocol.test.ts": 37, + "src/shared/terminal-composer-draft.test.ts": 46, + "src/shared/terminal-custom-themes.test.ts": 35, "src/shared/terminal-escape-introducer.test.ts": 55, - "src/shared/terminal-exit-cause.test.ts": 51, - "src/shared/terminal-file-url-target.test.ts": 42, - "src/shared/terminal-fonts.test.ts": 49, - "src/shared/terminal-github-pr-link-detector.test.ts": 71, - "src/shared/terminal-input.test.ts": 90, - "src/shared/terminal-kitty-keyboard-mode-tracker.test.ts": 91, - "src/shared/terminal-kitty-keyboard-tail-retention.test.ts": 212, - "src/shared/terminal-leaf-move.test.ts": 43, - "src/shared/terminal-line-height-settings.test.ts": 55, - "src/shared/terminal-mode-2031-final-state.test.ts": 102, - "src/shared/terminal-mode-reset-profiles.test.ts": 84, - "src/shared/terminal-mouse-encoding.test.ts": 72, - "src/shared/terminal-osc-color-override-tracker.test.ts": 71, - "src/shared/terminal-osc-color-reply.test.ts": 45, - "src/shared/terminal-osc133-carry-retention.test.ts": 340, - "src/shared/terminal-output-side-effects.test.ts": 103, - "src/shared/terminal-pane-placement.test.ts": 328, - "src/shared/terminal-partial-escape-tail-ground-scan.test.ts": 61, - "src/shared/terminal-partial-escape-tail.fuzz.test.ts": 1435, - "src/shared/terminal-partial-escape-tail.test.ts": 61, - "src/shared/terminal-query-reply.test.ts": 112, - "src/shared/terminal-quick-commands.test.ts": 56, - "src/shared/terminal-reply-query-scan.test.ts": 56, - "src/shared/terminal-restore-parity-writes.test.ts": 135, - "src/shared/terminal-scrollback-policy.test.ts": 55, - "src/shared/terminal-selection-gutter.test.ts": 47, - "src/shared/terminal-session-state-save-failure.test.ts": 46, - "src/shared/terminal-startup-cwd.test.ts": 308, - "src/shared/terminal-stream-json-byte-length.test.ts": 65, - "src/shared/terminal-stream-protocol.test.ts": 63, - "src/shared/terminal-surface-close-target.test.ts": 51, - "src/shared/terminal-synchronized-output-scan.test.ts": 80, - "src/shared/terminal-theme-selection.test.ts": 53, - "src/shared/terminal-title-agent-type.test.ts": 90, - "src/shared/terminal-title-classification-corpus.test.ts": 152, + "src/shared/terminal-exit-cause.test.ts": 49, + "src/shared/terminal-file-url-target.test.ts": 37, + "src/shared/terminal-fonts.test.ts": 32, + "src/shared/terminal-github-pr-link-detector.test.ts": 60, + "src/shared/terminal-input.test.ts": 70, + "src/shared/terminal-kitty-keyboard-mode-tracker.test.ts": 73, + "src/shared/terminal-kitty-keyboard-tail-retention.test.ts": 225, + "src/shared/terminal-leaf-move.test.ts": 33, + "src/shared/terminal-line-height-settings.test.ts": 28, + "src/shared/terminal-mode-2031-final-state.test.ts": 108, + "src/shared/terminal-mode-reset-profiles.test.ts": 83, + "src/shared/terminal-mouse-encoding.test.ts": 44, + "src/shared/terminal-osc-color-override-tracker.test.ts": 53, + "src/shared/terminal-osc-color-reply.test.ts": 34, + "src/shared/terminal-osc133-carry-retention.test.ts": 392, + "src/shared/terminal-output-side-effects.test.ts": 93, + "src/shared/terminal-pane-placement.test.ts": 332, + "src/shared/terminal-pane-title-reset.test.ts": 68, + "src/shared/terminal-partial-escape-tail-ground-scan.test.ts": 45, + "src/shared/terminal-partial-escape-tail.fuzz.test.ts": 1349, + "src/shared/terminal-partial-escape-tail.test.ts": 53, + "src/shared/terminal-query-reply.test.ts": 96, + "src/shared/terminal-quick-commands.test.ts": 53, + "src/shared/terminal-reply-query-scan.test.ts": 42, + "src/shared/terminal-restore-parity-writes.test.ts": 127, + "src/shared/terminal-scrollback-policy.test.ts": 35, + "src/shared/terminal-selection-gutter.test.ts": 32, + "src/shared/terminal-session-state-save-failure.test.ts": 37, + "src/shared/terminal-startup-cwd.test.ts": 270, + "src/shared/terminal-stream-json-byte-length.test.ts": 60, + "src/shared/terminal-stream-protocol.test.ts": 40, + "src/shared/terminal-surface-close-target.test.ts": 54, + "src/shared/terminal-synchronized-output-scan.test.ts": 82, + "src/shared/terminal-theme-selection.test.ts": 45, + "src/shared/terminal-title-agent-type.test.ts": 63, + "src/shared/terminal-title-classification-corpus.test.ts": 160, "src/shared/terminal-title-classification-memo.test.ts": 95, - "src/shared/terminal-view-attributes.test.ts": 60, - "src/shared/terminal-viewport.test.ts": 189, - "src/shared/terminal-wait-blocked-reason-legacy-alias.test.ts": 48, - "src/shared/terminal-zero-dimensions-diagnostic.test.ts": 47, - "src/shared/test-code-path.test.ts": 54, - "src/shared/text-search-dense-matches.test.ts": 450, - "src/shared/text-search-glob-patterns.test.ts": 243, - "src/shared/text-search-invalid-filename.test.ts": 62, - "src/shared/text-search-paths.test.ts": 68, - "src/shared/text-search-snippet-boundaries.test.ts": 66, - "src/shared/text-search-snippet-retention.test.ts": 603, - "src/shared/text-search.test.ts": 129, - "src/shared/timer-delay.test.ts": 54, - "src/shared/tmux-agent-hook-owner.test.ts": 467, - "src/shared/tmux-client-attachment.test.ts": 64, - "src/shared/tmux-hook-refresh-retirement.test.ts": 621, - "src/shared/tmux-host-attachment-probe.test.ts": 45, - "src/shared/tui-agent-config.test.ts": 58, - "src/shared/tui-agent-fork-startup.test.ts": 112, - "src/shared/tui-agent-launch-command-argument-values.test.ts": 118, - "src/shared/tui-agent-launch-command-override.test.ts": 61, - "src/shared/tui-agent-launch-command-pick-flags.test.ts": 97, - "src/shared/tui-agent-launch-defaults.test.ts": 96, - "src/shared/tui-agent-permissions.test.ts": 42, - "src/shared/tui-agent-prompt-transport.test.ts": 147, - "src/shared/tui-agent-rest-signal.test.ts": 64, - "src/shared/tui-agent-selection.test.ts": 62, - "src/shared/tui-agent-startup-copilot-resume.test.ts": 117, - "src/shared/tui-agent-startup-hermes.test.ts": 145, - "src/shared/tui-agent-startup-kiro-resume.test.ts": 65, - "src/shared/tui-agent-startup-session-options.test.ts": 135, - "src/shared/tui-agent-startup-shell.test.ts": 129, - "src/shared/tui-agent-startup.test.ts": 191, - "src/shared/ui-language.test.ts": 41, - "src/shared/ui-locale.test.ts": 45, - "src/shared/ui-zoom-level.test.ts": 55, - "src/shared/updated-at-order.test.ts": 104, - "src/shared/updater-windows-signature-check.test.ts": 43, - "src/shared/usage-percentage-display-change-notice.test.ts": 51, - "src/shared/usage-percentage-display.test.ts": 44, - "src/shared/utf8-byte-limits.test.ts": 149, - "src/shared/uuid-v4.test.ts": 47, - "src/shared/vscode-remote-ssh-launcher.test.ts": 53, - "src/shared/window-shortcut-policy-agent-dashboard.test.ts": 72, - "src/shared/window-shortcut-policy.test.ts": 119, - "src/shared/windows-breakaway-launcher.test.ts": 82, - "src/shared/windows-breakaway-previous-stderr.test.ts": 67, - "src/shared/windows-cmd-runner-delayed-launch.test.ts": 43, - "src/shared/windows-command-line-budget.test.ts": 278, - "src/shared/windows-console-input.test.ts": 65, - "src/shared/windows-environment-expansion.test.ts": 108, - "src/shared/windows-interactive-login-spawn.test.ts": 64, - "src/shared/windows-long-path-git-args.test.ts": 49, - "src/shared/windows-security-descriptor.test.ts": 57, - "src/shared/windows-terminal-shell.test.ts": 53, - "src/shared/windows-transient-lock-removal.test.ts": 1054, - "src/shared/worker-terminal-host-scope.test.ts": 98, - "src/shared/worker-transcript-background-task-text.test.ts": 54, - "src/shared/worker-transcript-text-scaling.test.ts": 57, - "src/shared/workspace-attachment-mutation.test.ts": 112, - "src/shared/workspace-attachment-normalization.test.ts": 67, - "src/shared/workspace-attachment-origins.test.ts": 53, - "src/shared/workspace-attachments.test.ts": 291, - "src/shared/workspace-cleanup-applied-filters.test.ts": 76, - "src/shared/workspace-cleanup-browse-state-persistence.test.ts": 61, - "src/shared/workspace-cleanup-ui-state.test.ts": 50, - "src/shared/workspace-cleanup.test.ts": 49, - "src/shared/workspace-doc-history.test.ts": 70, - "src/shared/workspace-layout/tab-group-moves.test.ts": 58, - "src/shared/workspace-layout/workspace-layout-command-model.test.ts": 470, - "src/shared/workspace-layout/workspace-layout-command-persistence.test.ts": 339, - "src/shared/workspace-layout/workspace-layout-commands.test.ts": 187, - "src/shared/workspace-layout/workspace-layout-fact-locations.test.ts": 442, - "src/shared/workspace-layout/workspace-layout-legacy-persistence.test.ts": 359, - "src/shared/workspace-layout/workspace-layout-load-normalization.test.ts": 379, - "src/shared/workspace-layout/workspace-layout-minted-ids.test.ts": 41, - "src/shared/workspace-layout/workspace-layout-model-rules.test.ts": 340, - "src/shared/workspace-layout/workspace-layout-published.test.ts": 303, - "src/shared/workspace-layout/workspace-layout-round-trip.test.ts": 345, - "src/shared/workspace-layout/workspace-layout-stream-frames.test.ts": 275, - "src/shared/workspace-layout/workspace-layout-transitions.test.ts": 89, - "src/shared/workspace-linked-item-equality.test.ts": 45, - "src/shared/workspace-linked-item-source-context.test.ts": 62, - "src/shared/workspace-name.test.ts": 78, - "src/shared/workspace-reference-identity.test.ts": 96, - "src/shared/workspace-review-checks.test.ts": 86, - "src/shared/workspace-session-browser-history.test.ts": 307, - "src/shared/workspace-session-partition-owner.test.ts": 37, - "src/shared/workspace-session-salvage-equivalence.test.ts": 472, - "src/shared/workspace-session-salvage.test.ts": 951, - "src/shared/workspace-session-schema-field-coverage.test.ts": 345, - "src/shared/workspace-session-schema.sleeping-agent.test.ts": 377, - "src/shared/workspace-session-schema.test.ts": 379, - "src/shared/workspace-session-tab-focus-schema.test.ts": 360, - "src/shared/workspace-session-terminal-buffers.test.ts": 316, - "src/shared/workspace-session-terminal-schema.test.ts": 313, - "src/shared/workspace-session-terminal-tab-close.test.ts": 315, - "src/shared/workspace-session-validation-work.test.ts": 377, - "src/shared/workspace-space-compaction.test.ts": 61, - "src/shared/workspace-space-entry-traversal.test.ts": 849, - "src/shared/workspace-space-scan-budget.test.ts": 49, - "src/shared/workspace-statuses.test.ts": 63, - "src/shared/worktree-execution-host-resolution.test.ts": 106, - "src/shared/worktree-name-suggestion.test.ts": 72, - "src/shared/worktree-scan-failure.test.ts": 44, - "src/shared/worktree/archive-failure-blocking-capability.test.ts": 68, - "src/shared/worktree/base-ref.test.ts": 63, - "src/shared/worktree/card-properties.test.ts": 55, - "src/shared/worktree/catalog-version.test.ts": 45, - "src/shared/worktree/create-preparation.test.ts": 47, - "src/shared/worktree/github-pr-suppression.test.ts": 66, - "src/shared/worktree/host-context-labels.test.ts": 50, - "src/shared/worktree/host-qualified-identity.test.ts": 50, - "src/shared/worktree/id.test.ts": 71, - "src/shared/worktree/identity.test.ts": 57, - "src/shared/worktree/local-base-branch-fast-forward-real-git.test.ts": 1130, - "src/shared/worktree/local-base-branch-fast-forward.test.ts": 98, - "src/shared/worktree/ownership-configured-base-visibility.test.ts": 69, - "src/shared/worktree/ownership-worktree-base-path.test.ts": 56, - "src/shared/worktree/ownership.test.ts": 251, - "src/shared/worktree/removal-fence-error.test.ts": 54, - "src/shared/worktree/removal-force-classification.test.ts": 52, - "src/shared/worktree/retired-name-cache.test.ts": 52, - "src/shared/worktree/retired-name-registry.test.ts": 306, - "src/shared/worktree/sort-order-update.test.ts": 42, + "src/shared/terminal-view-attributes.test.ts": 58, + "src/shared/terminal-viewport.test.ts": 206, + "src/shared/terminal-wait-blocked-reason-legacy-alias.test.ts": 39, + "src/shared/terminal-zero-dimensions-diagnostic.test.ts": 25, + "src/shared/test-code-path.test.ts": 29, + "src/shared/text-search-dense-matches.test.ts": 532, + "src/shared/text-search-glob-patterns.test.ts": 277, + "src/shared/text-search-invalid-filename.test.ts": 45, + "src/shared/text-search-paths.test.ts": 51, + "src/shared/text-search-snippet-boundaries.test.ts": 53, + "src/shared/text-search-snippet-retention.test.ts": 562, + "src/shared/text-search.test.ts": 139, + "src/shared/timer-delay.test.ts": 51, + "src/shared/tmux-agent-hook-owner.test.ts": 472, + "src/shared/tmux-client-attachment.test.ts": 35, + "src/shared/tmux-hook-refresh-retirement.test.ts": 602, + "src/shared/tmux-host-attachment-probe.test.ts": 38, + "src/shared/tui-agent-config.test.ts": 47, + "src/shared/tui-agent-fork-startup.test.ts": 148, + "src/shared/tui-agent-launch-command-argument-values.test.ts": 72, + "src/shared/tui-agent-launch-command-override.test.ts": 40, + "src/shared/tui-agent-launch-command-pick-flags.test.ts": 70, + "src/shared/tui-agent-launch-defaults.test.ts": 69, + "src/shared/tui-agent-permissions.test.ts": 52, + "src/shared/tui-agent-prompt-transport.test.ts": 139, + "src/shared/tui-agent-rest-signal.test.ts": 69, + "src/shared/tui-agent-selection.test.ts": 36, + "src/shared/tui-agent-startup-copilot-resume.test.ts": 119, + "src/shared/tui-agent-startup-hermes.test.ts": 165, + "src/shared/tui-agent-startup-kiro-resume.test.ts": 55, + "src/shared/tui-agent-startup-session-options.test.ts": 119, + "src/shared/tui-agent-startup-shell.test.ts": 120, + "src/shared/tui-agent-startup.test.ts": 175, + "src/shared/ui-language.test.ts": 23, + "src/shared/ui-locale.test.ts": 32, + "src/shared/ui-zoom-level.test.ts": 31, + "src/shared/updated-at-order.test.ts": 84, + "src/shared/updater-windows-signature-check.test.ts": 31, + "src/shared/usage-percentage-display-change-notice.test.ts": 35, + "src/shared/usage-percentage-display.test.ts": 34, + "src/shared/utf8-byte-limits.test.ts": 137, + "src/shared/uuid-v4.test.ts": 34, + "src/shared/vscode-remote-ssh-launcher.test.ts": 32, + "src/shared/web-origin-security.test.ts": 29, + "src/shared/window-shortcut-policy-agent-dashboard.test.ts": 70, + "src/shared/window-shortcut-policy-ime-process.test.ts": 66, + "src/shared/window-shortcut-policy.test.ts": 96, + "src/shared/windows-breakaway-launcher.test.ts": 69, + "src/shared/windows-breakaway-previous-stderr.test.ts": 40, + "src/shared/windows-cmd-runner-delayed-launch.test.ts": 37, + "src/shared/windows-command-line-budget.test.ts": 250, + "src/shared/windows-console-input.test.ts": 29, + "src/shared/windows-environment-expansion.test.ts": 83, + "src/shared/windows-interactive-login-spawn.test.ts": 41, + "src/shared/windows-long-path-git-args.test.ts": 35, + "src/shared/windows-path-access.test.ts": 50, + "src/shared/windows-secure-dacl.test.ts": 42, + "src/shared/windows-security-addon.test.ts": 37, + "src/shared/windows-security-addon.win32.test.ts": 33, + "src/shared/windows-terminal-shell.test.ts": 41, + "src/shared/windows-transient-lock-removal.test.ts": 825, + "src/shared/worker-terminal-host-scope.test.ts": 64, + "src/shared/worker-transcript-background-task-text.test.ts": 37, + "src/shared/worker-transcript-text-scaling.test.ts": 45, + "src/shared/workspace-attachment-mutation.test.ts": 113, + "src/shared/workspace-attachment-normalization.test.ts": 87, + "src/shared/workspace-attachment-origins.test.ts": 47, + "src/shared/workspace-attachments.test.ts": 318, + "src/shared/workspace-cleanup-applied-filters.test.ts": 51, + "src/shared/workspace-cleanup-browse-state-persistence.test.ts": 46, + "src/shared/workspace-cleanup-ui-state.test.ts": 34, + "src/shared/workspace-cleanup.test.ts": 41, + "src/shared/workspace-doc-history.test.ts": 50, + "src/shared/workspace-layout/old-client/old-client-method-translation.test.ts": 463, + "src/shared/workspace-layout/old-client/old-client-pane-layout-translation.test.ts": 340, + "src/shared/workspace-layout/old-client/old-client-session-tabs-projection.test.ts": 147, + "src/shared/workspace-layout/tab-group-moves.test.ts": 48, + "src/shared/workspace-layout/terminal-row-order.test.ts": 250, + "src/shared/workspace-layout/workspace-layout-command-model.test.ts": 488, + "src/shared/workspace-layout/workspace-layout-command-persistence.test.ts": 303, + "src/shared/workspace-layout/workspace-layout-commands.test.ts": 145, + "src/shared/workspace-layout/workspace-layout-editor-rekey.test.ts": 142, + "src/shared/workspace-layout/workspace-layout-fact-locations.test.ts": 449, + "src/shared/workspace-layout/workspace-layout-initial-surface.test.ts": 81, + "src/shared/workspace-layout/workspace-layout-legacy-persistence.test.ts": 507, + "src/shared/workspace-layout/workspace-layout-load-normalization.test.ts": 378, + "src/shared/workspace-layout/workspace-layout-minted-ids.test.ts": 37, + "src/shared/workspace-layout/workspace-layout-model-rules.test.ts": 310, + "src/shared/workspace-layout/workspace-layout-pane-status.test.ts": 52, + "src/shared/workspace-layout/workspace-layout-process-rules.test.ts": 129, + "src/shared/workspace-layout/workspace-layout-published.test.ts": 307, + "src/shared/workspace-layout/workspace-layout-round-trip.test.ts": 339, + "src/shared/workspace-layout/workspace-layout-stream-frames.test.ts": 294, + "src/shared/workspace-layout/workspace-layout-transitions.test.ts": 139, + "src/shared/workspace-layout/workspace-layout-view-tab-closes.test.ts": 68, + "src/shared/workspace-layout/workspace-layout-views.test.ts": 113, + "src/shared/workspace-linked-item-equality.test.ts": 28, + "src/shared/workspace-linked-item-source-context.test.ts": 57, + "src/shared/workspace-name.test.ts": 61, + "src/shared/workspace-reference-identity.test.ts": 60, + "src/shared/workspace-review-checks.test.ts": 84, + "src/shared/workspace-session-browser-history.test.ts": 301, + "src/shared/workspace-session-partition-owner.test.ts": 23, + "src/shared/workspace-session-salvage-equivalence.test.ts": 561, + "src/shared/workspace-session-salvage.test.ts": 1048, + "src/shared/workspace-session-schema-field-coverage.test.ts": 313, + "src/shared/workspace-session-schema.sleeping-agent.test.ts": 340, + "src/shared/workspace-session-schema.test.ts": 374, + "src/shared/workspace-session-tab-focus-schema.test.ts": 337, + "src/shared/workspace-session-terminal-buffers.test.ts": 346, + "src/shared/workspace-session-terminal-schema.test.ts": 323, + "src/shared/workspace-session-terminal-tab-close.test.ts": 283, + "src/shared/workspace-session-validation-work.test.ts": 351, + "src/shared/workspace-space-compaction.test.ts": 48, + "src/shared/workspace-space-entry-traversal.test.ts": 758, + "src/shared/workspace-space-scan-budget.test.ts": 42, + "src/shared/workspace-statuses.test.ts": 62, + "src/shared/worktree-execution-host-resolution.test.ts": 59, + "src/shared/worktree-name-suggestion.test.ts": 56, + "src/shared/worktree-scan-failure.test.ts": 30, + "src/shared/worktree/archive-failure-blocking-capability.test.ts": 212, + "src/shared/worktree/base-ref.test.ts": 45, + "src/shared/worktree/card-properties.test.ts": 30, + "src/shared/worktree/catalog-version.test.ts": 35, + "src/shared/worktree/create-preparation.test.ts": 38, + "src/shared/worktree/github-pr-suppression.test.ts": 229, + "src/shared/worktree/host-context-labels.test.ts": 33, + "src/shared/worktree/host-qualified-identity.test.ts": 38, + "src/shared/worktree/id.test.ts": 62, + "src/shared/worktree/identity.test.ts": 30, + "src/shared/worktree/local-base-branch-fast-forward-real-git.test.ts": 756, + "src/shared/worktree/local-base-branch-fast-forward.test.ts": 106, + "src/shared/worktree/ownership-configured-base-visibility.test.ts": 72, + "src/shared/worktree/ownership-worktree-base-path.test.ts": 44, + "src/shared/worktree/ownership.test.ts": 200, + "src/shared/worktree/removal-fence-error.test.ts": 33, + "src/shared/worktree/removal-force-classification.test.ts": 45, + "src/shared/worktree/retired-name-cache.test.ts": 44, + "src/shared/worktree/retired-name-registry.test.ts": 412, + "src/shared/worktree/sort-order-update.test.ts": 31, "src/shared/worktree/visibility-sources.test.ts": 65, - "src/shared/ws-outbound-backpressure-queue.test.ts": 79, - "src/shared/wsl-login-shell-command.test.ts": 105, - "src/shared/wsl-paths.test.ts": 64, - "src/shared/zcode-missing-tui.test.ts": 56, - "src/shared/zcode-orchestration-contract.test.ts": 161, + "src/shared/ws-outbound-backpressure-queue.test.ts": 70, + "src/shared/wsl-host-deployment-store.test.ts": 409, + "src/shared/wsl-login-shell-command.test.ts": 84, + "src/shared/wsl-move/wsl-move-dispositions.test.ts": 529, + "src/shared/wsl-paths.test.ts": 53, + "src/shared/zcode-missing-tui.test.ts": 39, + "src/shared/zcode-orchestration-contract.test.ts": 155, "src/shared/zod-salvage-absence.test.ts": 208, - "src/wsl-guest/agent-hook-canonical-status.test.ts": 2544, - "src/wsl-guest/agent-hook-envelope-publication.test.ts": 385, + "src/wsl-guest/agent-hook-canonical-status.test.ts": 2718, + "src/wsl-guest/agent-hook-envelope-publication.test.ts": 333, "src/wsl-guest/agent-hook-integration.test.ts": 1405, - "src/wsl-guest/agent-hook-interrupt-reconciliation.test.ts": 540, - "src/wsl-guest/agent-hook-opencode-tui-identity.test.ts": 638, - "src/wsl-guest/agent-hook-server-claude-owed-notification-expiry.test.ts": 700, - "src/wsl-guest/agent-hook-server-codex-subagent-transcript.test.ts": 1475, - "src/wsl-guest/agent-hook-server-codex-turn-interruption.test.ts": 1828, - "src/wsl-guest/agent-hook-server.test.ts": 1582, - "src/wsl-guest/dispatcher-bulk-admission.test.ts": 415, - "src/wsl-guest/dispatcher-bulk-failed-admission.test.ts": 262, - "src/wsl-guest/dispatcher-bulk-retirement-admission.test.ts": 3089, - "src/wsl-guest/dispatcher-capacity-degradation.test.ts": 314, - "src/wsl-guest/dispatcher-client-close-cause.test.ts": 257, - "src/wsl-guest/dispatcher-client-writer.test.ts": 86, - "src/wsl-guest/dispatcher-frame-guard-regressions.test.ts": 280, - "src/wsl-guest/dispatcher-json-payload.test.ts": 343, - "src/wsl-guest/dispatcher-notification-ownership.test.ts": 218, - "src/wsl-guest/dispatcher-per-connection-state-baseline.test.ts": 266, - "src/wsl-guest/dispatcher-rpc-cancel-id-coercion.test.ts": 245, - "src/wsl-guest/dispatcher-silent-client-reaper.test.ts": 279, - "src/wsl-guest/dispatcher-structured-error.test.ts": 275, - "src/wsl-guest/dispatcher-timeout.test.ts": 247, - "src/wsl-guest/dispatcher-writer-admission.test.ts": 111, - "src/wsl-guest/dispatcher.test.ts": 414, - "src/wsl-guest/legacy-relay-publication-ledger.test.ts": 50, - "src/wsl-guest/omp-config-root.test.ts": 177, - "src/wsl-guest/opencode-startup-prompt-install.test.ts": 139, - "src/wsl-guest/plugin-overlay-env.test.ts": 98, - "src/wsl-guest/plugin-overlay.test.ts": 164, - "src/wsl-guest/plugin-source-limit.test.ts": 46, - "src/wsl-guest/preflight-handler.test.ts": 84, - "src/wsl-guest/protocol-backpressure.test.ts": 109, - "src/wsl-guest/protocol-handshake.test.ts": 59, - "src/wsl-guest/protocol-json-payload.test.ts": 143, - "src/wsl-guest/relay-agent-presence.test.ts": 398, - "src/wsl-guest/relay-command-env.test.ts": 77, - "src/wsl-guest/relay-hot-path-operation-counts.test.ts": 279, - "src/wsl-guest/wsl-agent-hook-relay.test.ts": 454, - "src/wsl-guest/wsl-hook-fs-bridge.test.ts": 80, - "src/wsl-guest/wsl-install-plugins-handler.test.ts": 147, - "tests/e2e/accumulated-workspace-state-builder.unit.test.ts": 120, - "tests/e2e/alternate-screen-fixture-script.unit.test.ts": 60, - "tests/e2e/codex-child-approval-activity-row.unit.test.ts": 4279, - "tests/e2e/completed-worker-retirement-resume.unit.test.ts": 14396, - "tests/e2e/cursor-quota-transport.unit.test.ts": 1102, - "tests/e2e/daemon-running-work-probe.unit.test.ts": 315, - "tests/e2e/folder-upgrade-identity-persistence.unit.test.ts": 2238, - "tests/e2e/github-work-item-search-burst.unit.test.ts": 1180, - "tests/e2e/global-setup-cli-artifact.unit.test.ts": 55, - "tests/e2e/global-teardown.unit.test.ts": 124, - "tests/e2e/helpers/alt-screen-frame.unit.test.ts": 96, - "tests/e2e/helpers/client-hosted-browser-fixture.unit.test.ts": 752, - "tests/e2e/helpers/electron-crashpad-cleanup.unit.test.ts": 49, - "tests/e2e/helpers/electron-home-isolation.unit.test.ts": 51, - "tests/e2e/helpers/electron-launch-args.unit.test.ts": 57, - "tests/e2e/helpers/electron-main-evaluate-retry.unit.test.ts": 75, - "tests/e2e/helpers/electron-process-shutdown.unit.test.ts": 69, - "tests/e2e/helpers/fake-agent-command-override.unit.test.ts": 66, - "tests/e2e/helpers/fake-agent-paste-end-scanner.unit.test.ts": 68, - "tests/e2e/helpers/git-status-retry-barrier.unit.test.ts": 59, - "tests/e2e/helpers/golden-source-control.unit.test.ts": 645, - "tests/e2e/helpers/headless-paired-runtime-serve-readiness.unit.test.ts": 56, - "tests/e2e/helpers/paired-client-runtime-environment.unit.test.ts": 59, - "tests/e2e/helpers/paired-client-window-reveal.unit.test.ts": 179, - "tests/e2e/helpers/persisted-profile-state.unit.test.ts": 470, - "tests/e2e/helpers/remote-skill-cloud-fixture.unit.test.ts": 421, - "tests/e2e/helpers/streaming-terminal-cleanup.unit.test.ts": 51, - "tests/e2e/helpers/terminal-pty-write-spy.unit.test.ts": 64, - "tests/e2e/host-cold-park-remote-subscriber.unit.test.ts": 7476, - "tests/e2e/host-guest-paint-retention-remote-viewer.unit.test.ts": 5043, - "tests/e2e/native-chat-ssh-delivery.unit.test.ts": 5842, - "tests/e2e/omp-child-session-recovery.unit.test.ts": 1983, - "tests/e2e/omp-retired-host-renderer.unit.test.ts": 1794, - "tests/e2e/omp-risk-remote-review.unit.test.ts": 965, - "tests/e2e/orca-restart-navigation.unit.test.ts": 537, - "tests/e2e/orchestration-run-pagination.unit.test.ts": 52, - "tests/e2e/paced-terminal-typing.unit.test.ts": 475, - "tests/e2e/paired-runtime-rejected-input-remount.unit.test.ts": 4522, - "tests/e2e/remote-agent-completion-authority.unit.test.ts": 1490, - "tests/e2e/remote-terminal-input-delivery-model.unit.test.ts": 18232, - "tests/e2e/remote-terminal-tab-retirement.unit.test.ts": 5251, - "tests/e2e/restored-terminal-input-readiness.unit.test.ts": 71, - "tests/e2e/runtime-graph-publication-probe.unit.test.ts": 65, - "tests/e2e/session-tabs-decorative-title-fanout.unit.test.ts": 5357, - "tests/e2e/session-tabs-empty-inventory-daemon-oracle.unit.test.ts": 1142, - "tests/e2e/session-tabs-rich-status-boundaries.unit.test.ts": 5281, - "tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts": 362, - "tests/e2e/structured-agent-session-read-owner.unit.test.ts": 2988, - "tests/e2e/structured-attention-explicit-read.unit.test.ts": 13645, - "tests/e2e/structured-attention-headless-delivery.unit.test.ts": 259, - "tests/e2e/structured-attention-host-read-retirement.unit.test.ts": 3238, - "tests/e2e/structured-attention-prompt-relay.unit.test.ts": 17392, - "tests/e2e/structured-attention-read-owner-lifecycle.unit.test.ts": 17537, - "tests/e2e/structured-attention-read-retirement.unit.test.ts": 13824, - "tests/e2e/structured-attention-relay-settlement.unit.test.ts": 19420, - "tests/e2e/structured-chat-owner-status-activation.unit.test.ts": 3376, - "tests/e2e/structured-native-chat-routing-authority.unit.test.ts": 1170, - "tests/e2e/structured-session-reopened-tab-close.unit.test.ts": 25072, - "tests/e2e/sustained-agent-typing-load-scripts.unit.test.ts": 2142, - "tests/e2e/terminal-account-restart.unit.test.ts": 2678, - "tests/e2e/terminal-binding-crash-hydration.unit.test.ts": 1477, - "tests/e2e/terminal-foreground-confirmation.unit.test.ts": 477, - "tests/e2e/terminal-layout-parity-snapshot.unit.test.ts": 91, - "tests/e2e/terminal-perf-presentation.unit.test.ts": 64, - "tests/e2e/terminal-probe-input-sequence.unit.test.ts": 42, - "tests/e2e/terminal-split-activation-latency-artifact.unit.test.ts": 64, - "tests/e2e/terminal-split-activation-latency-report.unit.test.ts": 70, - "tests/tools/omp-close-lifecycle.test.mjs": 283, - "tests/tools/omp-config-root-local-shell.test.mjs": 249, - "tests/tools/omp-config-root-relay-shell.test.mjs": 162, - "tests/tools/relay-bench/phone-e2ee-desktop-parity.test.mjs": 114, - "tests/tools/relay-bench/region-probe-replay.test.mjs": 84, - "tests/tools/relay-bench/relay-bench-invocation.test.mjs": 102, - "tests/tools/relay-bench/relay-bench-state-file.test.mjs": 65, - "tests/tools/relay-bench/relay-phone-connect-bench.test.mjs": 89, - "tests/tools/win-update-e2e/app-driver.test.mjs": 541, - "tests/tools/win-update-e2e/app-shutdown.test.mjs": 644, - "tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-oracle.test.mjs": 59, - "tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-probe.test.mjs": 253, - "tests/tools/windows-pty-native-capability-smoke/run.test.mjs": 57 + "src/wsl-guest/agent-hook-interrupt-reconciliation.test.ts": 611, + "src/wsl-guest/agent-hook-opencode-tui-identity.test.ts": 688, + "src/wsl-guest/agent-hook-server-claude-owed-notification-expiry.test.ts": 816, + "src/wsl-guest/agent-hook-server-codex-subagent-transcript.test.ts": 1462, + "src/wsl-guest/agent-hook-server-codex-turn-interruption.test.ts": 1576, + "src/wsl-guest/agent-hook-server.test.ts": 1487, + "src/wsl-guest/dispatcher-bulk-admission.test.ts": 524, + "src/wsl-guest/dispatcher-bulk-failed-admission.test.ts": 236, + "src/wsl-guest/dispatcher-bulk-retirement-admission.test.ts": 2530, + "src/wsl-guest/dispatcher-capacity-degradation.test.ts": 392, + "src/wsl-guest/dispatcher-client-close-cause.test.ts": 240, + "src/wsl-guest/dispatcher-client-writer.test.ts": 73, + "src/wsl-guest/dispatcher-frame-guard-regressions.test.ts": 204, + "src/wsl-guest/dispatcher-json-payload.test.ts": 266, + "src/wsl-guest/dispatcher-notification-ownership.test.ts": 245, + "src/wsl-guest/dispatcher-per-connection-state-baseline.test.ts": 255, + "src/wsl-guest/dispatcher-rpc-cancel-id-coercion.test.ts": 252, + "src/wsl-guest/dispatcher-silent-client-reaper.test.ts": 260, + "src/wsl-guest/dispatcher-structured-error.test.ts": 266, + "src/wsl-guest/dispatcher-timeout.test.ts": 249, + "src/wsl-guest/dispatcher-writer-admission.test.ts": 110, + "src/wsl-guest/dispatcher.test.ts": 476, + "src/wsl-guest/legacy-relay-publication-ledger.test.ts": 35, + "src/wsl-guest/omp-config-root.test.ts": 212, + "src/wsl-guest/opencode-startup-prompt-install.test.ts": 181, + "src/wsl-guest/plugin-overlay-env.test.ts": 116, + "src/wsl-guest/plugin-overlay.test.ts": 176, + "src/wsl-guest/plugin-source-limit.test.ts": 29, + "src/wsl-guest/preflight-handler.test.ts": 77, + "src/wsl-guest/protocol-backpressure.test.ts": 106, + "src/wsl-guest/protocol-handshake.test.ts": 47, + "src/wsl-guest/protocol-json-payload.test.ts": 123, + "src/wsl-guest/relay-agent-presence.test.ts": 471, + "src/wsl-guest/relay-command-env.test.ts": 63, + "src/wsl-guest/relay-hot-path-operation-counts.test.ts": 286, + "src/wsl-guest/wsl-agent-hook-relay.test.ts": 469, + "src/wsl-guest/wsl-hook-fs-bridge.test.ts": 63, + "src/wsl-guest/wsl-install-plugins-handler.test.ts": 175, + "tests/e2e/accumulated-workspace-state-builder.unit.test.ts": 97, + "tests/e2e/agent-launch-cli.unit.test.ts": 2368, + "tests/e2e/agent-launch-setup-choice.unit.test.ts": 2109, + "tests/e2e/alternate-screen-fixture-script.unit.test.ts": 41, + "tests/e2e/claude-auto-mode-host-catalog.unit.test.ts": 2987, + "tests/e2e/codex-child-approval-activity-row.unit.test.ts": 4090, + "tests/e2e/completed-worker-retirement-resume.unit.test.ts": 16736, + "tests/e2e/cursor-quota-transport.unit.test.ts": 1224, + "tests/e2e/daemon-running-work-probe.unit.test.ts": 259, + "tests/e2e/folder-upgrade-identity-persistence.unit.test.ts": 1871, + "tests/e2e/github-work-item-search-burst.unit.test.ts": 1226, + "tests/e2e/global-setup-cli-artifact.unit.test.ts": 45, + "tests/e2e/global-teardown.unit.test.ts": 111, + "tests/e2e/helpers/alt-screen-frame.unit.test.ts": 87, + "tests/e2e/helpers/client-hosted-browser-fixture.unit.test.ts": 858, + "tests/e2e/helpers/electron-crashpad-cleanup.unit.test.ts": 36, + "tests/e2e/helpers/electron-home-isolation.unit.test.ts": 229, + "tests/e2e/helpers/electron-launch-args.unit.test.ts": 44, + "tests/e2e/helpers/electron-main-evaluate-retry.unit.test.ts": 42, + "tests/e2e/helpers/electron-process-shutdown.unit.test.ts": 4342, + "tests/e2e/helpers/fake-agent-command-override.unit.test.ts": 41, + "tests/e2e/helpers/fake-agent-paste-end-scanner.unit.test.ts": 49, + "tests/e2e/helpers/git-status-retry-barrier.unit.test.ts": 40, + "tests/e2e/helpers/golden-source-control.unit.test.ts": 531, + "tests/e2e/helpers/headless-paired-runtime-serve-readiness.unit.test.ts": 47, + "tests/e2e/helpers/paired-client-runtime-environment.unit.test.ts": 44, + "tests/e2e/helpers/paired-client-window-reveal.unit.test.ts": 48, + "tests/e2e/helpers/persisted-profile-state.unit.test.ts": 407, + "tests/e2e/helpers/remote-skill-cloud-fixture.unit.test.ts": 349, + "tests/e2e/helpers/streaming-terminal-cleanup.unit.test.ts": 30, + "tests/e2e/helpers/terminal-pty-write-spy.unit.test.ts": 33, + "tests/e2e/helpers/workspace-layout-oracle-feed.unit.test.ts": 77, + "tests/e2e/host-agent-launch-settings-hook-bookkeeping.unit.test.ts": 4389, + "tests/e2e/host-cold-park-remote-subscriber.unit.test.ts": 5953, + "tests/e2e/host-guest-paint-retention-remote-viewer.unit.test.ts": 4969, + "tests/e2e/native-chat-ssh-delivery.unit.test.ts": 6131, + "tests/e2e/omp-child-session-recovery.unit.test.ts": 1804, + "tests/e2e/omp-retired-host-renderer.unit.test.ts": 1991, + "tests/e2e/omp-risk-remote-review.unit.test.ts": 876, + "tests/e2e/orca-restart-navigation.unit.test.ts": 310, + "tests/e2e/orchestration-run-pagination.unit.test.ts": 32, + "tests/e2e/paced-terminal-typing.unit.test.ts": 379, + "tests/e2e/paired-runtime-rejected-input-remount.unit.test.ts": 4236, + "tests/e2e/remote-agent-completion-authority.unit.test.ts": 1509, + "tests/e2e/remote-terminal-input-delivery-model.unit.test.ts": 32257, + "tests/e2e/remote-terminal-tab-retirement.unit.test.ts": 4857, + "tests/e2e/restored-terminal-input-readiness.unit.test.ts": 57, + "tests/e2e/runtime-graph-publication-probe.unit.test.ts": 40, + "tests/e2e/session-tabs-decorative-title-fanout.unit.test.ts": 4631, + "tests/e2e/session-tabs-empty-inventory-daemon-oracle.unit.test.ts": 828, + "tests/e2e/session-tabs-rich-status-boundaries.unit.test.ts": 5112, + "tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts": 689, + "tests/e2e/structured-agent-session-read-owner.unit.test.ts": 4296, + "tests/e2e/structured-attention-headless-delivery.unit.test.ts": 221, + "tests/e2e/structured-attention-host-read-retirement.unit.test.ts": 2723, + "tests/e2e/structured-attention-prompt-relay.unit.test.ts": 28835, + "tests/e2e/structured-attention-read-retirement.unit.test.ts": 8627, + "tests/e2e/structured-chat-owner-status-activation.unit.test.ts": 2817, + "tests/e2e/structured-native-chat-routing-authority.unit.test.ts": 1325, + "tests/e2e/structured-session-reopened-tab-close.unit.test.ts": 30040, + "tests/e2e/structured-session-tab-close-stale-frame.unit.test.ts": 14249, + "tests/e2e/sustained-agent-typing-load-scripts.unit.test.ts": 2153, + "tests/e2e/terminal-account-restart.unit.test.ts": 3437, + "tests/e2e/terminal-binding-crash-hydration.unit.test.ts": 1349, + "tests/e2e/terminal-foreground-confirmation.unit.test.ts": 483, + "tests/e2e/terminal-layout-parity-snapshot.unit.test.ts": 41, + "tests/e2e/terminal-perf-presentation.unit.test.ts": 57, + "tests/e2e/terminal-probe-input-sequence.unit.test.ts": 40, + "tests/e2e/terminal-split-activation-latency-artifact.unit.test.ts": 44, + "tests/e2e/terminal-split-activation-latency-report.unit.test.ts": 116, + "tests/e2e/worktree-status-warning.unit.test.ts": 1920, + "tests/tools/omp-close-lifecycle.test.mjs": 277, + "tests/tools/omp-config-root-local-shell.test.mjs": 216, + "tests/tools/omp-config-root-relay-shell.test.mjs": 234, + "tests/tools/relay-bench/phone-e2ee-desktop-parity.test.mjs": 131, + "tests/tools/relay-bench/region-probe-replay.test.mjs": 56, + "tests/tools/relay-bench/relay-bench-invocation.test.mjs": 67, + "tests/tools/relay-bench/relay-bench-state-file.test.mjs": 53, + "tests/tools/relay-bench/relay-phone-connect-bench.test.mjs": 77, + "tests/tools/win-update-e2e/app-driver.test.mjs": 300, + "tests/tools/win-update-e2e/app-shutdown.test.mjs": 799, + "tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-oracle.test.mjs": 44, + "tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-probe.test.mjs": 213, + "tests/tools/windows-pty-native-capability-smoke/run.test.mjs": 43 } }, "e2e": { diff --git a/config/scripts/ci-shard-timings.md b/config/scripts/ci-shard-timings.md index db79660a183..eee0dc34bf9 100644 --- a/config/scripts/ci-shard-timings.md +++ b/config/scripts/ci-shard-timings.md @@ -1,6 +1,6 @@ # Timing-based CI shards -The eight unit shards and fourteen general E2E shards use longest-processing-time +Unit shards and general E2E shards use longest-processing-time assignment of whole files to the currently lightest shard. Ties use file path and then shard index, independent of filesystem enumeration and locale. Unknown, zero, or invalid durations use the baseline's positive median (1 second when no @@ -46,7 +46,32 @@ age as specs change. Full CI runs on the existing runner classes are required to measure elapsed-time and occupancy improvements, including discovery overhead. No retries, assertions, coverage exclusions, runner classes or shard counts changed. -## Current unit refresh: September 27, 2026 +## Current unit refresh: October 14, 2026 + +The baseline imports all five successful ARM unit shards from +[run 38646706136, attempt 1](https://github.com/stablyai/orca/actions/runs/38646706136/attempts/1). +The tested merge was `8d7f46198b99432efd104ac7803174173b367c66`, using Node +24.21.0, Bun 1.4.3, `ubuntu-24.04-arm`, and four workers per shard. All 12,314 +files appear exactly once: 12,163 passed and 151 skipped, with zero unhandled +errors. Artifact digests, all 20 report hashes, source/runtime provenance, full +discovery, and each saved assignment were checked before importing. The E2E +weights remain unchanged. + +| Shard | Job ID | Measured worker-seconds | Job seconds | +| --- | --- | ---: | ---: | +| 1 | 116137259336 | 1,966.470 | 568 | +| 2 | 116137259460 | 2,080.624 | 604 | +| 3 | 116137259318 | 2,121.495 | 611 | +| 4 | 116137259783 | 2,094.487 | 614 | +| 5 | 116137259540 | 2,190.636 | 641 | + +Retrospectively assigning those measurements projects 2,090.726–2,090.747 +worker-seconds per shard, instead of 1,966.470–2,190.636. That is a scheduling +projection, not a hosted speedup: module times overlap across workers, and total +runner occupancy was 50.63 minutes. Runner capacity, selection, workers, and the +assignment algorithm are unchanged. Later source changes need fresh validation. + +## Historical unit refresh: September 27, 2026 The baseline imports every successful Node 24 shard from [run 36294142683, attempt 1](https://github.com/stablyai/orca/actions/runs/36294142683/attempts/1). diff --git a/config/scripts/ci-unit-module-cache-contents.mjs b/config/scripts/ci-unit-module-cache-contents.mjs new file mode 100644 index 00000000000..f0f1ffa9a75 --- /dev/null +++ b/config/scripts/ci-unit-module-cache-contents.mjs @@ -0,0 +1,217 @@ +import { createHash } from 'node:crypto' +import { constants } from 'node:fs' +import { lstat, open, readdir } from 'node:fs/promises' +import { join } from 'node:path' +import { checkModuleTrailer } from './ci-unit-module-cache-trailer.mjs' + +export const MODULE_CACHE_MANIFEST = '_orca_manifest.json' +export const MODULE_CACHE_LIMITS = Object.freeze({ + entries: 50_000, + totalBytes: 2 ** 30, + entryBytes: 32 * 2 ** 20 +}) +const METADATA = '_metadata.json' +const MODULE_NAME = /^[a-f0-9]{40}$/ +const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) +const fail = (message) => { + throw new Error(`Unit module cache: ${message}`) +} + +async function readRegularFile(path) { + const before = await lstat(path) + if (!before.isFile()) { + fail(`not a regular file: ${path}`) + } + const file = await open( + path, + constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0) + ) + try { + const info = await file.stat() + if (!info.isFile() || info.size > MODULE_CACHE_LIMITS.entryBytes) { + fail(`invalid file size or type: ${path}`) + } + const buffer = Buffer.alloc(info.size + 1) + let bytes = 0 + while (bytes < buffer.length) { + const result = await file.read(buffer, bytes, buffer.length - bytes, bytes) + if (!result.bytesRead) { + break + } + bytes += result.bytesRead + } + if (bytes !== info.size) { + fail(`file changed while reading: ${path}`) + } + return buffer.subarray(0, bytes) + } finally { + await file.close() + } +} + +function checkLockfileHash(lockfileHash) { + if (typeof lockfileHash !== 'string' || !/^[a-f0-9]{8}$/.test(lockfileHash)) { + fail('invalid expected lockfile hash') + } +} + +export async function inventoryModuleCache({ path, lockfileHash }) { + checkLockfileHash(lockfileHash) + if (!(await lstat(path)).isDirectory()) { + fail('cache root is not a regular directory') + } + const names = (await readdir(path)).sort() + if (names.length > MODULE_CACHE_LIMITS.entries) { + fail('too many cache entries') + } + if (!names.includes(METADATA)) { + fail('cache metadata is missing') + } + let totalBytes = 0 + let manifestBytes = 0 + const entries = Array.from({ length: names.length }) + let next = 0 + const workers = await Promise.allSettled( + Array.from({ length: Math.min(4, names.length) }, async () => { + while (next < names.length) { + const index = next++ + const name = names[index] + if (name !== METADATA && name !== MODULE_CACHE_MANIFEST && !MODULE_NAME.test(name)) { + fail(`invalid cache entry name: ${name}`) + } + const buffer = await readRegularFile(join(path, name)) + if (name === MODULE_CACHE_MANIFEST) { + manifestBytes = buffer.length + } else { + totalBytes += buffer.length + } + if (totalBytes + manifestBytes > MODULE_CACHE_LIMITS.totalBytes) { + fail('cache byte limit exceeded') + } + if (name === MODULE_CACHE_MANIFEST) { + continue + } + if (name === METADATA) { + const metadata = JSON.parse(buffer.toString('utf8')) + if (!isRecord(metadata) || metadata.lockfileHash !== lockfileHash) { + fail('cache lockfile hash mismatch') + } + } else { + checkModuleTrailer(buffer) + } + entries[index] = { + name, + bytes: buffer.length, + sha256: createHash('sha256').update(buffer).digest('hex') + } + } + }) + ) + const failure = workers.find((result) => result.status === 'rejected') + if (failure) { + throw failure.reason + } + return { entries: entries.filter(Boolean), totalBytes } +} + +function checkIdentity({ key, format, versions, sourceSha, lockfileHash }) { + checkLockfileHash(lockfileHash) + if (typeof key !== 'string' || !key || typeof format !== 'string' || !format) { + fail('invalid cache identity') + } + if ( + !isRecord(versions) || + !['node', 'bun', 'vitest'].every((name) => typeof versions[name] === 'string' && versions[name]) + ) { + fail('invalid cache versions') + } + if (sourceSha !== undefined && (typeof sourceSha !== 'string' || !MODULE_NAME.test(sourceSha))) { + fail('invalid source SHA') + } +} + +export async function sealModuleCache(options) { + checkIdentity(options) + if (!options.sourceSha) { + fail('source SHA is required when sealing') + } + const inventory = await inventoryModuleCache(options) + const manifest = { + version: 1, + key: options.key, + format: options.format, + versions: options.versions, + sourceSha: options.sourceSha, + lockfileHash: options.lockfileHash, + ...inventory + } + const content = `${JSON.stringify(manifest)}\n` + if ( + Buffer.byteLength(content) > MODULE_CACHE_LIMITS.entryBytes || + inventory.totalBytes + Buffer.byteLength(content) > MODULE_CACHE_LIMITS.totalBytes || + inventory.entries.length + 1 > MODULE_CACHE_LIMITS.entries + ) { + fail('sealed cache exceeds limits') + } + const file = await open( + join(options.path, MODULE_CACHE_MANIFEST), + constants.O_WRONLY | + constants.O_CREAT | + (constants.O_NOFOLLOW ?? 0) | + (constants.O_NONBLOCK ?? 0), + 0o600 + ) + try { + if (!(await file.stat()).isFile()) { + fail('manifest is not a regular file') + } + await file.truncate(0) + await file.writeFile(content) + } finally { + await file.close() + } + return manifest +} + +export async function validateModuleCache(options) { + checkIdentity(options) + const manifest = JSON.parse( + (await readRegularFile(join(options.path, MODULE_CACHE_MANIFEST))).toString('utf8') + ) + if (!isRecord(manifest)) { + fail('invalid cache manifest') + } + checkIdentity(manifest) + if ( + !manifest.sourceSha || + manifest.version !== 1 || + manifest.key !== options.key || + manifest.format !== options.format || + manifest.lockfileHash !== options.lockfileHash || + (options.sourceSha !== undefined && manifest.sourceSha !== options.sourceSha) || + !['node', 'bun', 'vitest'].every((name) => manifest.versions[name] === options.versions[name]) + ) { + fail('cache manifest identity mismatch') + } + const inventory = await inventoryModuleCache(options) + if ( + manifest.totalBytes !== inventory.totalBytes || + !Array.isArray(manifest.entries) || + manifest.entries.length !== inventory.entries.length + ) { + fail('cache inventory mismatch') + } + for (let index = 0; index < inventory.entries.length; index++) { + const expected = manifest.entries[index] + const actual = inventory.entries[index] + if ( + !isRecord(expected) || + expected.name !== actual.name || + expected.bytes !== actual.bytes || + expected.sha256 !== actual.sha256 + ) { + fail(`cache content mismatch: ${actual.name}`) + } + } + return manifest +} diff --git a/config/scripts/ci-unit-module-cache-contents.test.mjs b/config/scripts/ci-unit-module-cache-contents.test.mjs new file mode 100644 index 00000000000..195540360df --- /dev/null +++ b/config/scripts/ci-unit-module-cache-contents.test.mjs @@ -0,0 +1,313 @@ +import { + mkdtemp, + mkdir, + readFile, + rename, + rm, + symlink, + truncate, + writeFile +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + inventoryModuleCache, + MODULE_CACHE_LIMITS, + MODULE_CACHE_MANIFEST, + sealModuleCache, + validateModuleCache +} from './ci-unit-module-cache-contents.mjs' + +const roots = [] +const filename = 'a'.repeat(40) +const identity = { + key: 'unit-module-v1-Linux-ARM64-runtime-config-lock', + format: 'vitest-fs-1.0.0-beta.8', + versions: { node: '24.21.0', bun: '1.4.3', vitest: '5.0.3' }, + sourceSha: 'b'.repeat(40), + lockfileHash: '0028fbb2' +} + +function moduleTable() { + return [ + { + file: '1', + id: '1', + url: '2', + imports: '3', + mappings: false, + deps: '4', + dynamicDeps: '5', + staticMocks: '6', + moduleType: '7' + }, + '/workspace/src/main.ts', + '/src/main.ts', + { urls: '8', ids: '9' }, + ['10'], + [], + ['11'], + 'esm', + ['10'], + { '/dependency.ts': '12' }, + '/dependency.ts', + { method: '13', specifier: '10', hasFactory: true, factoryLoadsOriginal: false }, + '/workspace/dependency.ts', + 'mock' + ] +} + +const moduleCode = (table = moduleTable()) => + `export const value = 1\n//# vitestCache=${Buffer.from(JSON.stringify(table)).toString('base64')}` + +async function fixture() { + const path = await mkdtemp(join(tmpdir(), 'orca-unit-module-cache-')) + roots.push(path) + await writeFile(join(path, filename), moduleCode()) + await writeFile( + join(path, '_metadata.json'), + JSON.stringify({ lockfileHash: identity.lockfileHash }) + ) + return { path, ...identity } +} + +async function changeManifest(options, change) { + const path = join(options.path, MODULE_CACHE_MANIFEST) + const manifest = JSON.parse(await readFile(path, 'utf8')) + change(manifest) + await writeFile(path, JSON.stringify(manifest)) +} + +afterEach(async () => { + await Promise.all(roots.splice(0).map((path) => rm(path, { recursive: true, force: true }))) +}) + +describe('unit module cache contents', () => { + it('seals all transformed files and Vitest metadata with exact content hashes', async () => { + const options = await fixture() + const inventory = await inventoryModuleCache(options) + expect(inventory.entries.map((entry) => entry.name)).toEqual(['_metadata.json', filename]) + expect(inventory.entries.every((entry) => /^[a-f0-9]{64}$/.test(entry.sha256))).toBe(true) + expect(inventory.totalBytes).toBe( + inventory.entries.reduce((sum, entry) => sum + entry.bytes, 0) + ) + const manifest = await sealModuleCache(options) + expect(manifest).toMatchObject({ version: 1, ...identity, ...inventory }) + expect(await validateModuleCache(options)).toEqual(manifest) + const restoreOptions = { ...options, sourceSha: undefined } + expect(await validateModuleCache(restoreOptions)).toEqual(manifest) + }) + + it('accepts absent optional module type, dependency lists and mocks', async () => { + const options = await fixture() + await writeFile( + join(options.path, filename), + moduleCode([ + { id: '1', url: '2', file: null, imports: '3', mappings: false }, + '\u0000virtual-module', + '/@id/virtual-module', + { urls: '4', ids: '5' }, + [], + {} + ]) + ) + await sealModuleCache(options) + await expect(validateModuleCache(options)).resolves.toMatchObject({ key: identity.key }) + }) + + it.each(['missing', 'modified', 'extra', 'renamed', 'metadata'])( + 'rejects %s sealed contents', + async (change) => { + const options = await fixture() + await sealModuleCache(options) + const path = join(options.path, filename) + if (change === 'missing') { + await rm(path) + } + if (change === 'modified') { + await writeFile(path, moduleCode().replace('value = 1', 'value = 2')) + } + if (change === 'extra') { + await writeFile(join(options.path, 'c'.repeat(40)), moduleCode()) + } + if (change === 'renamed') { + await rename(path, join(options.path, 'c'.repeat(40))) + } + if (change === 'metadata') { + await writeFile( + join(options.path, '_metadata.json'), + JSON.stringify({ lockfileHash: identity.lockfileHash, changed: true }) + ) + } + await expect(validateModuleCache(options)).rejects.toThrow( + /cache (inventory|content) mismatch/ + ) + } + ) + + it.each([ + ['key', 'different-key'], + ['format', 'different-format'], + ['sourceSha', 'd'.repeat(40)], + ['lockfileHash', '12345678'], + ['version', 2], + ['versions', { ...identity.versions, bun: '1.4.4' }] + ])('rejects mismatched manifest %s', async (field, value) => { + const options = await fixture() + await sealModuleCache(options) + await changeManifest(options, (manifest) => { + manifest[field] = value + }) + await expect(validateModuleCache(options)).rejects.toThrow(/identity mismatch/) + }) + + it.each(['missing', 'malformed', 'mismatched'])( + 'rejects %s Vitest metadata before sealing', + async (kind) => { + const options = await fixture() + const path = join(options.path, '_metadata.json') + if (kind === 'missing') { + await rm(path) + } + if (kind === 'malformed') { + await writeFile(path, '{') + } + if (kind === 'mismatched') { + await writeFile(path, '{"lockfileHash":"12345678"}') + } + await expect(sealModuleCache(options)).rejects.toThrow() + } + ) + + it.each(['.tmp-partial', 'invalid-module.js', 'B'.repeat(40)])( + 'rejects invalid filename %s', + async (name) => { + const options = await fixture() + await writeFile(join(options.path, name), moduleCode()) + await expect(inventoryModuleCache(options)).rejects.toThrow('invalid cache entry name') + } + ) + + it('rejects a directory masquerading as a transformed file', async () => { + const options = await fixture() + await mkdir(join(options.path, 'c'.repeat(40))) + await expect(inventoryModuleCache(options)).rejects.toThrow('not a regular file') + }) + + it.skipIf(process.platform === 'win32')( + 'rejects symlinked modules, manifests and cache roots', + async () => { + const options = await fixture() + const outside = await fixture() + await symlink(join(outside.path, filename), join(options.path, 'c'.repeat(40))) + await expect(inventoryModuleCache(options)).rejects.toThrow('not a regular file') + await rm(join(options.path, 'c'.repeat(40))) + await symlink(join(outside.path, filename), join(options.path, MODULE_CACHE_MANIFEST)) + await expect(sealModuleCache(options)).rejects.toThrow('not a regular file') + await symlink(options.path, join(outside.path, 'linked-root')) + await expect( + inventoryModuleCache({ ...options, path: join(outside.path, 'linked-root') }) + ).rejects.toThrow('cache root') + } + ) + + it('rejects oversized entries before allocating their contents', async () => { + const options = await fixture() + await truncate(join(options.path, filename), MODULE_CACHE_LIMITS.entryBytes + 1) + await expect(inventoryModuleCache(options)).rejects.toThrow('invalid file size') + }) + + it.each([ + ['missing trailer', 'export const value = 1'], + ['invalid base64', 'x\n//# vitestCache=***'], + ['noncanonical base64', 'x\n//# vitestCache=W10'], + ['invalid JSON', 'x\n//# vitestCache=ew=='], + ['plain JSON object', `x\n//# vitestCache=${Buffer.from('{}').toString('base64')}`] + ])('rejects %s', async (_label, code) => { + const options = await fixture() + await writeFile(join(options.path, filename), code) + await expect(sealModuleCache(options)).rejects.toThrow() + }) + + it.each([ + [ + 'out-of-range reference', + (table) => { + table[0].id = '999' + } + ], + [ + 'noncanonical reference', + (table) => { + table[0].id = '01' + } + ], + [ + 'numeric id', + (table) => { + table[0].id = 1 + } + ], + [ + 'cyclic imports', + (table) => { + table[0].imports = '0' + } + ], + [ + 'invalid mappings', + (table) => { + table[0].mappings = null + } + ], + [ + 'invalid module type', + (table) => { + table[7] = 'unknown' + } + ], + [ + 'invalid import URL', + (table) => { + table[8][0] = false + } + ], + [ + 'invalid import ids', + (table) => { + table[9]['/dependency.ts'] = 4 + } + ], + [ + 'invalid dependencies', + (table) => { + table[4][0] = false + } + ], + [ + 'invalid static mocks', + (table) => { + table[11].hasFactory = 1 + } + ], + [ + 'inline object', + (table) => { + table[0].imports = {} + } + ], + [ + 'unsafe property', + (table) => { + table[0] = JSON.parse('{"__proto__":"0"}') + } + ] + ])('rejects %s in module metadata', async (_label, mutate) => { + const options = await fixture() + const table = moduleTable() + mutate(table) + await writeFile(join(options.path, filename), moduleCode(table)) + await expect(sealModuleCache(options)).rejects.toThrow() + }) +}) diff --git a/config/scripts/ci-unit-module-cache-trailer.mjs b/config/scripts/ci-unit-module-cache-trailer.mjs new file mode 100644 index 00000000000..e83212c716d --- /dev/null +++ b/config/scripts/ci-unit-module-cache-trailer.mjs @@ -0,0 +1,118 @@ +const TRAILER = '\n//# vitestCache=' +const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) +const fail = (message) => { + throw new Error(`Unit module cache: ${message}`) +} + +export function checkModuleTrailer(buffer) { + const text = buffer.toString('utf8') + if (!Buffer.from(text).equals(buffer)) { + fail('module is not valid UTF-8') + } + const marker = text.lastIndexOf(TRAILER) + if (marker === -1) { + fail('module trailer is missing') + } + const encoded = text.slice(marker + TRAILER.length) + const decoded = Buffer.from(encoded, 'base64') + if (!encoded || decoded.toString('base64') !== encoded) { + fail('module trailer is not canonical base64') + } + const json = decoded.toString('utf8') + if (!Buffer.from(json).equals(decoded)) { + fail('module metadata is not valid UTF-8') + } + const table = JSON.parse(json) + if (!Array.isArray(table) || !isRecord(table[0])) { + fail('module metadata is not a flatted object table') + } + const resolve = (value) => { + if (typeof value !== 'string') { + return value + } + if ( + !/^(0|[1-9][0-9]*)$/.test(value) || + !Number.isSafeInteger(Number(value)) || + Number(value) >= table.length + ) { + fail('invalid flatted reference') + } + return table[Number(value)] + } + // Check table references directly; the cache only needs the documented metadata fields. + for (const slot of table) { + if (typeof slot === 'string') { + continue + } + if (!slot || typeof slot !== 'object') { + fail('invalid flatted table slot') + } + if (Object.hasOwn(slot, '__proto__')) { + fail('unsafe flatted property') + } + for (const value of Object.values(slot)) { + if (value !== null && typeof value === 'object') { + fail('inline object in flatted table') + } + if (typeof value === 'number' && !Number.isFinite(value)) { + fail('invalid flatted number') + } + resolve(value) + } + } + const fields = Object.fromEntries( + Object.entries(table[0]).map(([name, value]) => [name, resolve(value)]) + ) + const strings = (value) => + Array.isArray(value) && value.every((item) => typeof resolve(item) === 'string') + if (typeof fields.id !== 'string' || typeof fields.url !== 'string') { + fail('invalid module id or url') + } + if (fields.file != null && typeof fields.file !== 'string') { + fail('invalid module file') + } + if (typeof fields.mappings !== 'boolean') { + fail('invalid module mappings') + } + if ( + fields.moduleType !== undefined && + fields.moduleType !== 'cjs' && + fields.moduleType !== 'esm' + ) { + fail('invalid module type') + } + if (!isRecord(fields.imports)) { + fail('invalid module imports') + } + const urls = resolve(fields.imports.urls) + const ids = resolve(fields.imports.ids) + if ( + !strings(urls) || + !isRecord(ids) || + !Object.values(ids).every((id) => typeof resolve(id) === 'string') + ) { + fail('invalid module import references') + } + for (const name of ['deps', 'dynamicDeps']) { + if (fields[name] !== undefined && !strings(fields[name])) { + fail(`invalid module ${name}`) + } + } + if (fields.staticMocks != null) { + if (!Array.isArray(fields.staticMocks)) { + fail('invalid module static mocks') + } + for (const reference of fields.staticMocks) { + const mock = resolve(reference) + if ( + !isRecord(mock) || + typeof resolve(mock.method) !== 'string' || + typeof resolve(mock.specifier) !== 'string' || + typeof mock.hasFactory !== 'boolean' || + typeof mock.factoryLoadsOriginal !== 'boolean' + ) { + fail('invalid module static mock') + } + } + } +} diff --git a/config/scripts/ci-unit-module-cache.mjs b/config/scripts/ci-unit-module-cache.mjs new file mode 100644 index 00000000000..f7e76882dc9 --- /dev/null +++ b/config/scripts/ci-unit-module-cache.mjs @@ -0,0 +1,239 @@ +import { createHash } from 'node:crypto' +import { + appendFileSync, + existsSync, + globSync, + readFileSync, + realpathSync, + rmSync, + statSync +} from 'node:fs' +import { createRequire } from 'node:module' +import { join, resolve } from 'node:path' +import { performance } from 'node:perf_hooks' +import { pathToFileURL } from 'node:url' +import { runProcessSync } from '@orca/process-host' +import { collectNodeServerInputs } from './node-server-change-scope.mjs' +import { workspacePackageManifests } from './workspace-source-exports.mjs' +import { writeAssignment } from './ci-shard-assignment.mjs' +import { sealModuleCache, validateModuleCache } from './ci-unit-module-cache-contents.mjs' + +const ROOT = resolve(import.meta.dirname, '../..') +const IDENTITY_REPORT = 'ci-shards/unit-module-cache-identity.json' +const CACHE_REPORT = 'ci-shards/unit-module-cache.json' +const CACHE_FORMAT = '1.0.0-beta.8' + +function digestInputs(root, files) { + const hash = createHash('sha256') + for (const file of [...new Set(files)].sort()) { + if (!existsSync(join(root, file))) { + hash.update(`${file}\0missing\0`) + continue + } + const bytes = readFileSync(join(root, file)) + hash.update(`${file}\0${bytes.length}\0`).update(bytes) + } + return hash.digest('hex') +} + +export async function moduleCachePolicyHash(root = ROOT) { + const runnerScripts = globSync('config/scripts/vitest*.{mjs,ts}', { cwd: root }).filter( + (file) => !file.includes('.test.') + ) + const configInputs = await collectNodeServerInputs({ + root, + entryPoints: [ + 'config/vitest.config.ts', + 'config/scripts/run-vitest.mjs', + 'config/scripts/ci-unit-timing-reporter.mjs', + ...runnerScripts + ] + }) + return digestInputs(root, [ + ...configInputs, + ...workspacePackageManifests(root), + ...globSync(['config/patches/**', '**/tsconfig*.json'], { + cwd: root, + exclude: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/out/**', 'mobile/vendor/**'] + }).filter((file) => statSync(join(root, file)).isFile()), + 'package.json', + 'pnpm-lock.yaml', + 'pnpm-workspace.yaml', + '.npmrc', + '.pnpmfile.cjs', + 'config/.bun-version', + 'config/scripts/ci-unit-module-cache.mjs', + 'config/scripts/ci-unit-module-cache-contents.mjs', + 'config/scripts/ci-unit-module-cache-trailer.mjs' + ]) +} + +export function moduleCacheIdentity({ + root = ROOT, + cacheRoot = process.env.RUNNER_TEMP, + policyHash, + installedLockHash, + versions, + shard, + platform = process.platform, + arch = process.arch, + nodeEnv = process.env.NODE_ENV ?? 'test', + namespace = '' +}) { + if ( + !cacheRoot || + !/^[a-f0-9]{64}$/.test(policyHash ?? '') || + !/^[a-f0-9]{64}$/.test(installedLockHash ?? '') || + ![versions?.node, versions?.bun, versions?.vitest].every((version) => + /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(version ?? '') + ) || + !Number.isInteger(shard?.index) || + !Number.isInteger(shard?.count) || + shard.index < 1 || + shard.index > shard.count + ) { + throw new Error('Unit module cache identity is incomplete') + } + const identity = { + format: CACHE_FORMAT, + root: realpathSync(root), + platform, + arch, + versions, + policyHash, + installedLockHash, + nodeEnv, + shard, + namespace + } + const digest = createHash('sha256').update(JSON.stringify(identity)).digest('hex') + return { + ...identity, + key: `unit-module-v1-${platform}-${arch}-n${versions.node}-b${versions.bun}-v${versions.vitest}-s${shard.index}of${shard.count}-${digest}`, + path: join(resolve(cacheRoot), 'orca-vitest-module-cache') + } +} + +export async function resolveModuleCacheIdentity({ + root = ROOT, + cacheRoot = process.env.RUNNER_TEMP, + shard, + namespace = '' +} = {}) { + const require = createRequire(join(root, 'package.json')) + const bun = runProcessSync({ program: 'bun', args: ['--version'], cwd: root, timeoutMs: 10_000 }) + if (bun.code !== 0) { + throw new Error('Bun version is unavailable') + } + return moduleCacheIdentity({ + root, + cacheRoot, + shard, + namespace, + policyHash: await moduleCachePolicyHash(root), + installedLockHash: createHash('sha256') + .update(readFileSync(join(root, 'node_modules', '.pnpm', 'lock.yaml'))) + .digest('hex'), + versions: { + node: process.versions.node, + bun: bun.stdout.trim(), + vitest: require('vitest/package.json').version + } + }) +} + +export async function prepareModuleCache(identity, { restoreHit = false } = {}) { + const started = performance.now() + try { + if (!restoreHit) { + throw new Error('No exact cache was restored') + } + const manifest = await validateModuleCache({ + path: identity.path, + key: identity.key, + format: identity.format, + versions: identity.versions, + lockfileHash: identity.installedLockHash.slice(0, 8) + }) + return { + available: true, + entries: manifest.entries.length, + bytes: manifest.totalBytes, + producerSourceSha: manifest.sourceSha, + validationMs: performance.now() - started + } + } catch (error) { + rmSync(identity.path, { recursive: true, force: true }) + return { + available: false, + reason: String(error), + validationMs: performance.now() - started + } + } +} + +export async function finishModuleCache(identity, { sourceSha } = {}) { + const started = performance.now() + const manifest = await sealModuleCache({ + ...identity, + sourceSha, + lockfileHash: identity.installedLockHash.slice(0, 8) + }) + return { + entries: manifest.entries.length, + bytes: manifest.totalBytes, + sealMs: performance.now() - started + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const phase = process.argv[2] + const output = (values) => { + if (process.env.GITHUB_OUTPUT) { + for (const [key, value] of Object.entries(values)) { + appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`) + } + } + } + if (phase === 'identity') { + try { + const identity = await resolveModuleCacheIdentity({ + shard: { + index: Number(process.env.ORCA_UNIT_SHARD_INDEX), + count: Number(process.env.ORCA_UNIT_SHARD_COUNT) + }, + namespace: process.env.ORCA_UNIT_CACHE_NAMESPACE ?? '' + }) + writeAssignment(IDENTITY_REPORT, identity) + output({ key: identity.key, path: identity.path }) + if (process.env.GITHUB_ENV) { + appendFileSync(process.env.GITHUB_ENV, `ORCA_UNIT_MODULE_CACHE_PATH=${identity.path}\n`) + } + } catch (error) { + console.warn(`Unit module cache unavailable: ${String(error)}`) + } + } else { + const identity = JSON.parse(readFileSync(IDENTITY_REPORT, 'utf8')) + if (phase === 'prepare') { + const result = await prepareModuleCache(identity, { + restoreHit: process.env.ORCA_UNIT_CACHE_RESTORE_HIT === 'true' + }) + writeAssignment(CACHE_REPORT, { key: identity.key, mode: 'restore', ...result }) + console.log(JSON.stringify(result)) + } else if (phase === 'seal') { + try { + const result = await finishModuleCache(identity, { + sourceSha: process.env.ORCA_SHARD_SOURCE_SHA ?? process.env.GITHUB_SHA + }) + const previous = JSON.parse(readFileSync(CACHE_REPORT, 'utf8')) + writeAssignment(CACHE_REPORT, { ...previous, produced: result }) + output({ ready: true }) + console.log(JSON.stringify(result)) + } catch (error) { + console.warn(`Unit module cache was not saved: ${String(error)}`) + } + } else { + throw new Error(`Unknown unit module cache phase: ${phase}`) + } + } +} diff --git a/config/scripts/ci-unit-module-cache.test.mjs b/config/scripts/ci-unit-module-cache.test.mjs new file mode 100644 index 00000000000..59270e0b247 --- /dev/null +++ b/config/scripts/ci-unit-module-cache.test.mjs @@ -0,0 +1,167 @@ +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { parse } from 'yaml' +import { + finishModuleCache, + moduleCacheIdentity, + prepareModuleCache +} from './ci-unit-module-cache.mjs' + +const temporary = [] +afterEach(() => { + for (const directory of temporary.splice(0)) { + rmSync(directory, { recursive: true, force: true }) + } +}) + +function options() { + const directory = mkdtempSync(join(tmpdir(), 'orca-unit-cache-identity-')) + temporary.push(directory) + return { + root: directory, + cacheRoot: directory, + policyHash: 'a'.repeat(64), + installedLockHash: 'b'.repeat(64), + versions: { node: '24.21.0', bun: '1.4.3', vitest: '5.0.3' }, + shard: { index: 1, count: 5 }, + platform: 'linux', + arch: 'arm64', + nodeEnv: 'test', + namespace: '' + } +} + +describe('persistent unit module cache boundary', () => { + it('separates hosts, runtimes, config, lockfiles, roots, shards and experiments', () => { + const original = options() + const identity = moduleCacheIdentity(original) + const anotherRoot = options().root + for (const update of [ + { root: anotherRoot }, + { policyHash: 'c'.repeat(64) }, + { installedLockHash: 'd'.repeat(64) }, + { platform: 'win32' }, + { arch: 'x64' }, + { nodeEnv: 'production' }, + { namespace: 'comparison' }, + { shard: { index: 2, count: 5 } }, + { shard: { index: 1, count: 10 } }, + { versions: { ...original.versions, node: '26.6.0' } }, + { versions: { ...original.versions, bun: '1.4.4' } }, + { versions: { ...original.versions, vitest: '5.0.4' } } + ]) { + const next = moduleCacheIdentity({ ...original, ...update }) + expect(next.key).not.toBe(identity.key) + expect(next.path).toBe(identity.path) + } + expect(moduleCacheIdentity(original)).toEqual(identity) + }) + + it.each([ + { cacheRoot: '' }, + { policyHash: '' }, + { installedLockHash: '' }, + { versions: { node: '24', bun: '1.4.3', vitest: '5.0.3' } }, + { shard: { index: 0, count: 5 } }, + { shard: { index: 6, count: 5 } } + ])('refuses an incomplete identity %j', (update) => { + expect(() => moduleCacheIdentity({ ...options(), ...update })).toThrow('incomplete') + }) + + it.each([false, true])( + 'starts cold and discards unusable restored bytes (hit %s)', + async (hit) => { + const identity = moduleCacheIdentity(options()) + mkdirSync(identity.path) + writeFileSync(join(identity.path, 'untrusted'), 'throw Error("must not execute")') + const prepared = await prepareModuleCache(identity, { restoreHit: hit }) + expect(prepared.available).toBe(false) + expect(prepared.validationMs).toBeGreaterThanOrEqual(0) + expect(() => readFileSync(join(identity.path, 'untrusted'))).toThrow() + } + ) + it('restores a matching cache from an earlier source without copying evaluated state', async () => { + const identity = moduleCacheIdentity(options()) + mkdirSync(identity.path) + writeFileSync( + join(identity.path, '_metadata.json'), + JSON.stringify({ lockfileHash: identity.installedLockHash.slice(0, 8) }) + ) + const metadata = [ + { id: '1', url: '2', file: '1', imports: '3', mappings: false }, + join(identity.root, 'source.ts'), + '/source.ts', + { urls: '4', ids: '5' }, + [], + {} + ] + writeFileSync( + join(identity.path, 'a'.repeat(40)), + `export const value = 1\n//# vitestCache=${Buffer.from(JSON.stringify(metadata)).toString('base64')}` + ) + const produced = await finishModuleCache(identity, { sourceSha: 'a'.repeat(40) }) + expect(produced.entries).toBe(2) + const prepared = await prepareModuleCache( + { ...identity, sourceSha: 'b'.repeat(40) }, + { restoreHit: true } + ) + expect(prepared).toMatchObject({ + available: true, + entries: produced.entries, + bytes: produced.bytes, + producerSourceSha: 'a'.repeat(40) + }) + }) +}) + +it('uses an optional exact restore after install and saves successful scoped producers', () => { + const workflow = parse(readFileSync('.github/workflows/unit-tests.yml', 'utf8')) + const steps = workflow.jobs.test.steps + const dependencies = steps.findIndex( + (step) => step.uses === './.github/actions/install-node-dependencies' + ) + const identity = steps.findIndex((step) => step.id === 'module-cache') + const restore = steps.findIndex((step) => step.id === 'module-cache-restore') + const prepare = steps.findIndex((step) => step.run?.endsWith('ci-unit-module-cache.mjs prepare')) + const tests = steps.findIndex((step) => step.name === 'Test shard') + const seal = steps.find((step) => step.id === 'module-cache-seal') + const save = steps.find((step) => step.uses === 'actions/cache/save@v5') + expect(identity).toBeGreaterThan(dependencies) + expect(restore).toBeGreaterThan(identity) + expect(prepare).toBeGreaterThan(restore) + expect(tests).toBeGreaterThan(prepare) + expect(steps[restore].uses).toBe('actions/cache/restore@v5') + expect(steps[restore].with['restore-keys']).toBeUndefined() + expect(steps[restore]['continue-on-error']).toBe(true) + expect(seal.if).toContain('success()') + expect(seal.if).toContain("github.event_name == 'pull_request'") + expect(seal.if).toContain("github.ref == 'refs/heads/main'") + expect(seal.if).toContain("github.event_name != 'pull_request'") + expect(seal.if).toContain("inputs.module_cache_seed && github.event_name == 'workflow_dispatch'") + expect(save.if).toBe("steps.module-cache-seal.outputs.ready == 'true'") + expect(save.with.key).toBe(steps[restore].with.key) + expect(workflow.on.workflow_call.inputs.module_cache.default).toBe('off') + expect(workflow.on.workflow_call.inputs.run_relay_integration.default).toBe(true) +}) + +it('runs full same-source controls serially with five four-worker shards and included producer costs', () => { + const workflow = parse(readFileSync('.github/workflows/unit-module-cache-benchmark.yml', 'utf8')) + const { baseline, producer, warm } = workflow.jobs + expect(producer.needs).toBe('baseline') + expect(warm.needs).toBe('producer') + for (const job of [baseline, producer, warm]) { + expect(job.uses).toBe('./.github/workflows/unit-tests.yml') + expect(job.with.node_versions).toBe('["24.21.0"]') + expect(job.with.runner).toBe('ubuntu-24.04-arm') + expect(JSON.parse(job.with.shards)).toEqual( + Array.from({ length: 5 }, (_, index) => ({ index: index + 1, count: 5 })) + ) + expect(job.with.phase_diagnostics).toBe(true) + } + expect(producer.with.module_cache).toBe('cold') + expect(producer.with.module_cache_seed).toBe(true) + expect(warm.with.module_cache).toBe('warm') + expect(warm.with.module_cache_namespace).toBe(producer.with.module_cache_namespace) +}) diff --git a/config/scripts/ci-unit-timing-reporter.mjs b/config/scripts/ci-unit-timing-reporter.mjs index 820a101d830..e3ac8fcede4 100644 --- a/config/scripts/ci-unit-timing-reporter.mjs +++ b/config/scripts/ci-unit-timing-reporter.mjs @@ -1,8 +1,114 @@ -import { relative } from 'node:path' +import { relative, resolve } from 'node:path' import { appendFileSync, readFileSync } from 'node:fs' import { writeAssignment } from './ci-shard-assignment.mjs' import { auditUnitSelection } from './ci-unit-selection.mjs' +export const UNIT_PHASE_FILE_LIMIT = 50_000 +const phaseNames = [ + 'environmentSetupDuration', + 'prepareDuration', + 'collectDuration', + 'setupDuration', + 'duration' +] + +const nonnegativeNumber = (value) => (Number.isFinite(value) && value >= 0 ? value : null) +const emptyPhases = () => Object.fromEntries(phaseNames.map((name) => [name, 0])) +const emptyImportSample = () => ({ entries: 0, selfTimeMs: 0, totalTimeMs: 0 }) +const workerCounters = (state) => ({ + workersSpawned: Number.isInteger(state?.workersSpawned) + ? nonnegativeNumber(state.workersSpawned) + : null, + startupTimeMs: nonnegativeNumber(state?.startupTime) +}) + +export function buildUnitPhaseReport(ctx, modules, errors, reason, baseline) { + const files = [] + const projects = new Map() + const totals = emptyPhases() + const phaseSamples = emptyPhases() + for (const module of modules) { + const diagnostic = module.diagnostic() + const project = module.project?.name ?? 'unknown' + if (!projects.has(project)) { + projects.set(project, { + project, + files: 0, + phases: emptyPhases(), + phaseSamples: emptyPhases(), + sampledImports: emptyImportSample() + }) + } + const aggregate = projects.get(project) + aggregate.files += 1 + const phases = {} + for (const name of phaseNames) { + const value = nonnegativeNumber(diagnostic?.[name]) + phases[name] = value + if (value !== null) { + aggregate.phases[name] += value + aggregate.phaseSamples[name] += 1 + totals[name] += value + phaseSamples[name] += 1 + } + } + const imports = diagnostic?.importDurations + if (imports && typeof imports === 'object') { + for (const id in imports) { + if (!Object.hasOwn(imports, id)) { + continue + } + const selfTime = nonnegativeNumber(imports[id]?.selfTime) + const totalTime = nonnegativeNumber(imports[id]?.totalTime) + if (selfTime !== null && totalTime !== null) { + aggregate.sampledImports.entries += 1 + aggregate.sampledImports.selfTimeMs += selfTime + aggregate.sampledImports.totalTimeMs += totalTime + } + } + } + if (files.length < UNIT_PHASE_FILE_LIMIT) { + files.push({ + file: relative(ctx.config.root, module.moduleId).replaceAll('\\', '/'), + project, + workerId: nonnegativeNumber(diagnostic?.workerId), + concurrencyId: nonnegativeNumber(diagnostic?.concurrencyId), + phases + }) + } + } + const counters = workerCounters(ctx.state) + return { + metric: 'module-phase-diagnostics-v1', + nodeVersion: process.env.ORCA_TEST_NODE_VERSION ?? process.versions.node, + ...(process.versions.bun ? { bunVersion: process.versions.bun } : {}), + vitestVersion: ctx.version ?? null, + shard: ctx.config.shard ?? { index: 1, count: 1 }, + status: reason, + unhandledErrors: errors.length, + units: 'milliseconds', + phaseTime: 'summed-worker-time', + fileLimit: UNIT_PHASE_FILE_LIMIT, + filesSeen: modules.length, + filesOmitted: modules.length - files.length, + workerTotals: { + scope: baseline ? 'test-run' : 'vitest-session', + ...Object.fromEntries( + Object.entries(counters).map(([name, value]) => [ + name, + value === null || (baseline && baseline[name] === null) + ? null + : nonnegativeNumber(value - (baseline?.[name] ?? 0)) + ]) + ) + }, + phases: totals, + phaseSamples, + projects: [...projects.values()], + files + } +} + export function moduleDuration(diagnostic) { return Math.max( 1, @@ -21,6 +127,12 @@ export default class UnitTimingReporter { this.ctx = ctx } + onTestRunStart() { + if (process.env.ORCA_UNIT_PHASE_REPORT) { + this.workerBaseline = workerCounters(this.ctx.state) + } + } + onTestRunEnd(modules, errors, reason) { const results = Object.fromEntries( modules.map((module) => [ @@ -61,5 +173,17 @@ export default class UnitTimingReporter { } catch { // Timing evidence remains useful when selection planning was unavailable. } + const phaseReport = process.env.ORCA_UNIT_PHASE_REPORT + const timingReport = process.env.ORCA_UNIT_TIMING_REPORT ?? 'ci-shards/unit-timings.json' + if (phaseReport && resolve(phaseReport) !== resolve(timingReport)) { + try { + writeAssignment( + phaseReport, + buildUnitPhaseReport(this.ctx, modules, errors, reason, this.workerBaseline) + ) + } catch { + // Optional diagnostics must not change the test verdict. + } + } } } diff --git a/config/scripts/ci-unit-timing-reporter.test.mjs b/config/scripts/ci-unit-timing-reporter.test.mjs index b4964449a9c..4e5e5cc59ce 100644 --- a/config/scripts/ci-unit-timing-reporter.test.mjs +++ b/config/scripts/ci-unit-timing-reporter.test.mjs @@ -1,9 +1,13 @@ -import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { importUnitTimingReports } from './ci-unit-timing-import.mjs' -import UnitTimingReporter, { moduleDuration } from './ci-unit-timing-reporter.mjs' +import UnitTimingReporter, { + buildUnitPhaseReport, + moduleDuration, + UNIT_PHASE_FILE_LIMIT +} from './ci-unit-timing-reporter.mjs' const report = (index, timings) => ({ metric: 'module-duration-v1', @@ -38,6 +42,7 @@ describe('unit worker timing evidence', () => { const previous = process.env.ORCA_UNIT_TIMING_REPORT process.env.ORCA_UNIT_TIMING_REPORT = join(directory, 'unit-timings.json') vi.stubEnv('ORCA_TEST_NODE_VERSION', nodeVersion) + vi.stubEnv('ORCA_UNIT_PHASE_REPORT', undefined) try { const reporter = new UnitTimingReporter() reporter.onInit({ config: { root: process.cwd(), shard: { index: 1, count: 8 } } }) @@ -121,3 +126,217 @@ describe('unit worker timing evidence', () => { expect(() => importUnitTimingReports([first, report(2, { b: -1 })])).toThrow('timing') }) }) + +const phaseDiagnostic = (overrides = {}) => ({ + environmentSetupDuration: 1.5, + prepareDuration: 2.5, + collectDuration: 30.25, + setupDuration: 4.5, + duration: 5.25, + workerId: 7, + concurrencyId: 2, + importDurations: {}, + ...overrides +}) +const phaseModule = (file, diagnostic = phaseDiagnostic(), project = 'bun') => ({ + moduleId: resolve(file), + project: { name: project }, + state: () => 'passed', + diagnostic: () => diagnostic +}) +const phaseContext = (state) => ({ + config: { root: process.cwd(), shard: { index: 1, count: 5 } }, + version: '5.0.3', + state +}) + +function withReporterOutputs(run) { + const directory = mkdtempSync(join(tmpdir(), 'orca-unit-phases-')) + const timingPath = join(directory, 'unit-timings.json') + const phasePath = join(directory, 'unit-phase-diagnostics.json') + vi.stubEnv('ORCA_UNIT_TIMING_REPORT', timingPath) + vi.stubEnv('ORCA_UNIT_PHASE_REPORT', undefined) + vi.stubEnv('ORCA_UNIT_SELECTION_PLAN', join(directory, 'missing-selection.json')) + vi.stubEnv('ORCA_SHARD_SOURCE_SHA', 'phase-source') + vi.stubEnv('GITHUB_RUN_ID', 'phase-run') + vi.stubEnv('GITHUB_RUN_ATTEMPT', '3') + try { + run({ timingPath, phasePath }) + } finally { + vi.unstubAllEnvs() + rmSync(directory, { recursive: true, force: true }) + } +} + +describe('optional unit phase diagnostics', () => { + it('leaves the timing report identical and writes nothing extra when disabled', () => { + withReporterOutputs(({ timingPath, phasePath }) => { + const modules = [ + phaseModule( + 'src/example.test.ts', + phaseDiagnostic({ importDurations: { dependency: { selfTime: 100, totalTime: 200 } } }) + ) + ] + const reporter = new UnitTimingReporter() + reporter.onInit(phaseContext({ workersSpawned: 3, startupTime: 500 })) + reporter.onTestRunEnd(modules, [], 'passed') + const timing = readFileSync(timingPath, 'utf8') + expect(existsSync(phasePath)).toBe(false) + + vi.stubEnv('ORCA_UNIT_PHASE_REPORT', phasePath) + reporter.onTestRunEnd(modules, [], 'passed') + expect(readFileSync(timingPath, 'utf8')).toBe(timing) + expect(JSON.parse(timing).timings).toEqual({ 'src/example.test.ts': 44 }) + expect(JSON.parse(readFileSync(phasePath, 'utf8'))).toMatchObject({ + sourceSha: 'phase-source', + runId: 'phase-run', + runAttempt: '3', + metric: 'module-phase-diagnostics-v1', + vitestVersion: '5.0.3', + shard: { index: 1, count: 5 }, + status: 'passed', + unhandledErrors: 0, + workerTotals: { scope: 'vitest-session', workersSpawned: 3, startupTimeMs: 500 } + }) + }) + }) + + it('records fractions and project totals without retaining sampled import records', () => { + const report = buildUnitPhaseReport( + phaseContext(), + [ + phaseModule( + 'src/first.test.ts', + phaseDiagnostic({ + importDurations: { + first: { selfTime: 2, totalTime: 9, importer: 'src/private-account.ts' }, + second: { selfTime: 3.5, totalTime: 5.5 }, + invalid: { selfTime: Number.NaN, totalTime: 4 } + } + }) + ), + phaseModule('src/second.test.ts'), + phaseModule('src/node.test.ts', phaseDiagnostic({ workerId: 9 }), 'node-runtime') + ], + [{ message: 'test error' }], + 'failed' + ) + expect(report.files[0]).toEqual({ + file: 'src/first.test.ts', + project: 'bun', + workerId: 7, + concurrencyId: 2, + phases: { + environmentSetupDuration: 1.5, + prepareDuration: 2.5, + collectDuration: 30.25, + setupDuration: 4.5, + duration: 5.25 + } + }) + expect(report.projects).toMatchObject([ + { + project: 'bun', + files: 2, + phases: { collectDuration: 60.5 }, + phaseSamples: { collectDuration: 2 }, + sampledImports: { entries: 2, selfTimeMs: 5.5, totalTimeMs: 14.5 } + }, + { project: 'node-runtime', files: 1, sampledImports: { entries: 0 } } + ]) + expect(report.phases.collectDuration).toBe(90.75) + expect(report).toMatchObject({ + phaseTime: 'summed-worker-time', + status: 'failed', + unhandledErrors: 1 + }) + expect(JSON.stringify(report)).not.toContain('private-account') + expect(JSON.stringify(report)).not.toContain('importDurations') + }) + + it('reports successful worker startup counters for the current run separately', () => { + withReporterOutputs(({ phasePath }) => { + vi.stubEnv('ORCA_UNIT_PHASE_REPORT', phasePath) + const state = { workersSpawned: 2, startupTime: 10 } + const reporter = new UnitTimingReporter() + reporter.onInit(phaseContext(state)) + reporter.onTestRunStart() + state.workersSpawned = 5 + state.startupTime = 60.25 + reporter.onTestRunEnd([phaseModule('src/example.test.ts')], [], 'passed') + expect(JSON.parse(readFileSync(phasePath, 'utf8')).workerTotals).toEqual({ + scope: 'test-run', + workersSpawned: 3, + startupTimeMs: 50.25 + }) + }) + }) + + it('caps file detail while keeping aggregates from omitted files', () => { + const module = phaseModule('src/repeated.test.ts') + const modules = Array.from({ length: UNIT_PHASE_FILE_LIMIT }, () => module) + modules.push( + phaseModule( + 'src/omitted.test.ts', + phaseDiagnostic({ + collectDuration: 11, + importDurations: { dependency: { selfTime: 2, totalTime: 3 } } + }) + ) + ) + const report = buildUnitPhaseReport(phaseContext(), modules, [], 'passed') + expect(report.files).toHaveLength(UNIT_PHASE_FILE_LIMIT) + expect(report).toMatchObject({ + fileLimit: UNIT_PHASE_FILE_LIMIT, + filesSeen: UNIT_PHASE_FILE_LIMIT + 1, + filesOmitted: 1, + phases: { collectDuration: UNIT_PHASE_FILE_LIMIT * 30.25 + 11 }, + projects: [ + { + files: UNIT_PHASE_FILE_LIMIT + 1, + sampledImports: { entries: 1, selfTimeMs: 2, totalTimeMs: 3 } + } + ] + }) + }) + + it('marks unavailable counters, phases and worker IDs without inventing measurements', () => { + const report = buildUnitPhaseReport( + phaseContext({ workersSpawned: -1, startupTime: Number.NaN }), + [phaseModule('src/unavailable.test.ts', {})], + [], + 'interrupted' + ) + expect(report.workerTotals).toEqual({ + scope: 'vitest-session', + workersSpawned: null, + startupTimeMs: null + }) + expect(report.files[0]).toMatchObject({ + workerId: null, + concurrencyId: null, + phases: { collectDuration: null } + }) + expect(report.phaseSamples.collectDuration).toBe(0) + expect(report.status).toBe('interrupted') + }) + + it.each(['unwritable', 'same as timing'])( + 'preserves timing evidence when the optional output is %s', + (target) => { + withReporterOutputs(({ timingPath }) => { + const reporter = new UnitTimingReporter() + reporter.onInit(phaseContext()) + const modules = [phaseModule('src/example.test.ts')] + reporter.onTestRunEnd(modules, [], 'passed') + const timing = readFileSync(timingPath, 'utf8') + vi.stubEnv( + 'ORCA_UNIT_PHASE_REPORT', + target === 'unwritable' ? `${timingPath}/unwritable.json` : timingPath + ) + expect(() => reporter.onTestRunEnd(modules, [], 'passed')).not.toThrow() + expect(readFileSync(timingPath, 'utf8')).toBe(timing) + }) + } + ) +}) diff --git a/config/scripts/orcad-e2e-routing.test.mjs b/config/scripts/orcad-e2e-routing.test.mjs index 7ccc061aab2..e1838741ccf 100644 --- a/config/scripts/orcad-e2e-routing.test.mjs +++ b/config/scripts/orcad-e2e-routing.test.mjs @@ -90,7 +90,7 @@ it('builds the e2e app when only a build-dependent orcad spec is requested', () e2e_needs_build: true }) } - expect(jobs['orcad-ssh-docker'].needs).toEqual(['build', 'prepare-native-cache']) + expect(jobs['orcad-ssh-docker'].needs).toEqual(['build']) }) it('runs every managed-SSH spec in one sharded lane, only when one is routed', () => { diff --git a/config/scripts/pr-auxiliary-preflight-admission.test.mjs b/config/scripts/pr-auxiliary-preflight-admission.test.mjs index e52617fbca4..ff69c2c67c1 100644 --- a/config/scripts/pr-auxiliary-preflight-admission.test.mjs +++ b/config/scripts/pr-auxiliary-preflight-admission.test.mjs @@ -251,7 +251,15 @@ describe('Mobile CI tool preflight routing', () => { it('keeps root shard execution enabled for callers that do not opt out', () => { expect(unit.on.workflow_call.inputs.run_shards.default).toBe(true) expect(evaluate(unit.jobs.test.if, { inputs: { run_shards: true } })).toBe(true) - expect(unit.jobs.relay_integration.if).toBeUndefined() + const relayDefault = unit.on.workflow_call.inputs.run_relay_integration.default + expect(relayDefault).toBe(true) + expect( + evaluate(unit.jobs.relay_integration.if, { inputs: { run_relay_integration: relayDefault } }) + ).toBe(true) + expect( + evaluate(unit.jobs.relay_integration.if, { inputs: { run_relay_integration: false } }) + ).toBe(false) + expect(workflow.jobs.test.with.run_relay_integration).toBeUndefined() expect(unit.jobs.relay_integration.needs).toBeUndefined() }) diff --git a/config/scripts/release-cut-token-permissions.test.mjs b/config/scripts/release-cut-token-permissions.test.mjs index a7dc7b8f160..e1b3292d117 100644 --- a/config/scripts/release-cut-token-permissions.test.mjs +++ b/config/scripts/release-cut-token-permissions.test.mjs @@ -14,7 +14,6 @@ const EXPECTED_MATRIX = { '.github/workflows/e2e.yml#orcad-serve-mode-switch': { contents: 'read' }, '.github/workflows/e2e.yml#orcad-serve-mode-switch-windows': { contents: 'read' }, '.github/workflows/e2e.yml#orcad-ssh-docker': { contents: 'read' }, - '.github/workflows/e2e.yml#prepare-native-cache': { contents: 'read' }, '.github/workflows/e2e.yml#ssh-browser-network-route': { contents: 'read' }, '.github/workflows/homebrew-bump.yml#bump-cask': { contents: 'read' }, '.github/workflows/node-server-tests.yml#changes': { contents: 'read' }, diff --git a/config/vitest.config.ts b/config/vitest.config.ts index 7c0ac6e84fe..1485f61565d 100644 --- a/config/vitest.config.ts +++ b/config/vitest.config.ts @@ -30,6 +30,9 @@ const testOptions = { environment: 'node', clearMocks: false, fsModuleCache: true, + ...(process.env.ORCA_UNIT_MODULE_CACHE_PATH + ? { fsModuleCachePath: process.env.ORCA_UNIT_MODULE_CACHE_PATH } + : {}), env: { ORCA_VITEST_RUNTIME: 'node' }, server: { deps: { inline: ['zod'] } }, // Node's storage globals and V8 retention checks require the existing child flags. diff --git a/src/main/browser/browser-popup-compatibility-cdp-fixture.ts b/src/main/browser/browser-popup-compatibility-cdp-fixture.ts index e291a1677ac..96ac766eb04 100644 --- a/src/main/browser/browser-popup-compatibility-cdp-fixture.ts +++ b/src/main/browser/browser-popup-compatibility-cdp-fixture.ts @@ -6,10 +6,12 @@ import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { join } from 'node:path' import { chromium, type Browser, type Page } from 'playwright' -import { afterAll, afterEach, beforeAll, expect, vi } from 'vitest' +import { afterAll, afterEach, beforeAll, beforeEach, expect, vi } from 'vitest' import { build as buildVite } from 'vite' import { waitForBrowserCdpEndpoint } from './browser-session-ua-cdp-collector' +import { popupFixtureHtml } from './browser-popup-compatibility-page-fixture' import { resolveElectronProbeLaunch } from './electron-probe-display-launch' +import { startHiddenRendererFrameCapture } from './hidden-renderer-frame-capture-fixture' // Hidden renderer startup and CDP events can exceed the default one-second poll on CI. vi.setConfig({ expect: { poll: { timeout: 10_000 } } }) @@ -35,6 +37,7 @@ export let browser: Browser | null = null export let origin = '' let stderr = '' const pages = new Map() +const stopFrameCaptures = new Map Promise>() const statusPath = join(root, 'status.json') export const artifactDir = process.env.ORCA_POPUP_FIXTURE_ARTIFACT_DIR export const snapshots: ReturnType[] = [] @@ -77,25 +80,12 @@ export function status(): { return JSON.parse(readFileSync(statusPath, 'utf8')) } -function fixtureHtml(): string { - return ` - - - - - -
-
-
- Docs - OAuth link - Background - Popup link - - ` -} - +const cdpConnected = Promise.withResolvers() const server = createServer((request, response) => { + if (request.url === '/fixture-cdp-connected') { + void cdpConnected.promise.then(() => response.writeHead(204).end()) + return + } const chunks: Buffer[] = [] request.on('data', (chunk: Buffer) => chunks.push(chunk)) request.on('end', () => { @@ -127,7 +117,7 @@ const server = createServer((request, response) => { response.setHeader('Content-Type', 'text/html') response.end( ['/local', '/route', '/local-control', '/route-control'].includes(path) - ? fixtureHtml() + ? popupFixtureHtml(origin) : `Hostile synthetic title${path}` ) }) @@ -213,6 +203,7 @@ beforeAll(async () => { }) await waitForBrowserCdpEndpoint(cdpPort) browser = await chromium.connectOverCDP(`http://127.0.0.1:${cdpPort}`) + cdpConnected.resolve() await waitForFixture(() => { if (!existsSync(statusPath)) { return false @@ -246,6 +237,17 @@ beforeAll(async () => { } }, 60_000) +beforeEach(async () => { + for (const page of pages.values()) { + if (!stopFrameCaptures.has(page)) { + stopFrameCaptures.set( + page, + await startHiddenRendererFrameCapture(page, (hasFrame) => expect.poll(hasFrame).toBe(true)) + ) + } + } +}) + afterEach(async () => { const route = pages.get('route') if (route && !route.isClosed()) { @@ -258,6 +260,8 @@ afterEach(async () => { }) afterAll(async () => { + cdpConnected.resolve() + await Promise.allSettled([...stopFrameCaptures.values()].map((stop) => stop())) if (artifactDir && existsSync(statusPath)) { mkdirSync(artifactDir, { recursive: true }) writeFileSync( diff --git a/src/main/browser/browser-popup-compatibility-electron-fixture.ts b/src/main/browser/browser-popup-compatibility-electron-fixture.ts index bc48843b4f9..1196ff997e6 100644 --- a/src/main/browser/browser-popup-compatibility-electron-fixture.ts +++ b/src/main/browser/browser-popup-compatibility-electron-fixture.ts @@ -286,6 +286,8 @@ async function run(): Promise { writeStatus() } }) + // Playwright must attach before Electron changes guest targets from pages to webviews. + await fetch(`${fixtureOrigin}/fixture-cdp-connected`) await createHost('local', localPartition) phase = 'local' writeStatus() diff --git a/src/main/browser/browser-popup-compatibility-page-fixture.ts b/src/main/browser/browser-popup-compatibility-page-fixture.ts new file mode 100644 index 00000000000..66869c8fafd --- /dev/null +++ b/src/main/browser/browser-popup-compatibility-page-fixture.ts @@ -0,0 +1,17 @@ +export function popupFixtureHtml(origin: string): string { + return ` + + + + + +
+
+
+ Docs + OAuth link + Background + Popup link + + ` +} diff --git a/src/main/browser/hidden-renderer-frame-capture-fixture.ts b/src/main/browser/hidden-renderer-frame-capture-fixture.ts new file mode 100644 index 00000000000..8019bdd8e10 --- /dev/null +++ b/src/main/browser/hidden-renderer-frame-capture-fixture.ts @@ -0,0 +1,47 @@ +import type { Page } from 'playwright' + +export async function startHiddenRendererFrameCapture( + page: Page, + waitForFirstFrame: (hasFrame: () => boolean) => Promise +): Promise<() => Promise> { + const cdp = await page.context().newCDPSession(page) + let stopped = false + const handlePageClose = (): void => { + void stop().catch(() => {}) + } + const stop = async (): Promise => { + if (stopped) { + return + } + stopped = true + try { + await cdp.send('Page.stopScreencast') + } finally { + page.off('close', handlePageClose) + cdp.off('Page.screencastFrame', acknowledgeFrame) + await cdp.detach() + } + } + let hasFrame = false + const acknowledgeFrame = (frame: { sessionId: number }): void => { + hasFrame = true + void cdp.send('Page.screencastFrameAck', { sessionId: frame.sessionId }).catch(() => {}) + } + cdp.on('Page.screencastFrame', acknowledgeFrame) + page.once('close', handlePageClose) + try { + // Keep hidden renderers painting without revealing their native windows. + await cdp.send('Page.enable') + await cdp.send('Page.startScreencast', { + format: 'jpeg', + quality: 20, + maxWidth: 640, + maxHeight: 480 + }) + await waitForFirstFrame(() => hasFrame) + return stop + } catch (error) { + await stop().catch(() => {}) + throw error + } +} diff --git a/src/main/runtime/mobile-subscribe-idle-pane-hydrate.test.ts b/src/main/runtime/mobile-subscribe-idle-pane-hydrate.test.ts index 023d6d3ba09..fb35d9e9e94 100644 --- a/src/main/runtime/mobile-subscribe-idle-pane-hydrate.test.ts +++ b/src/main/runtime/mobile-subscribe-idle-pane-hydrate.test.ts @@ -1,3 +1,4 @@ +import './rpc/unused-default-rpc-methods.test-fixture' /** * A phone subscribing to an idle reattached PTY must get its first snapshot on the phone grid. * diff --git a/src/main/runtime/opencode-wsl-folder-launch-host.test.ts b/src/main/runtime/opencode-wsl-folder-launch-host.test.ts index 32e8293a7d6..8a94f8cf48a 100644 --- a/src/main/runtime/opencode-wsl-folder-launch-host.test.ts +++ b/src/main/runtime/opencode-wsl-folder-launch-host.test.ts @@ -1,3 +1,4 @@ +import './rpc/unused-default-rpc-methods.test-fixture' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' import { RpcDispatcher } from './rpc/dispatcher' diff --git a/src/main/runtime/rpc/dispatcher-transport-parity.test.ts b/src/main/runtime/rpc/dispatcher-transport-parity.test.ts index e9eb767fd2c..b7f9e6a12a8 100644 --- a/src/main/runtime/rpc/dispatcher-transport-parity.test.ts +++ b/src/main/runtime/rpc/dispatcher-transport-parity.test.ts @@ -1,3 +1,4 @@ +import './unused-default-rpc-methods.test-fixture' import { afterEach, describe, expect, it, vi } from 'vitest' import { z } from 'zod' import { ORCHESTRATION_CONTRACT_VERSION } from '../../../shared/protocol-version' diff --git a/src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts b/src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts index 00ed70257c9..9811f3e42ad 100644 --- a/src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts +++ b/src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts @@ -1,3 +1,4 @@ +import '../unused-default-rpc-methods.test-fixture' /** * A launch's record across a host restart, against the real durable ledger. * diff --git a/src/main/runtime/rpc/methods/managed-server.test.ts b/src/main/runtime/rpc/methods/managed-server.test.ts index f25bb93eafc..1b7e97f248a 100644 --- a/src/main/runtime/rpc/methods/managed-server.test.ts +++ b/src/main/runtime/rpc/methods/managed-server.test.ts @@ -1,3 +1,4 @@ +import '../unused-default-rpc-methods.test-fixture' import { afterEach, describe, expect, it, vi } from 'vitest' import type { OrcaRuntimeService } from '../../orca-runtime' import { diff --git a/src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts b/src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts index 1e23163efe9..9e5c53c32b3 100644 --- a/src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts +++ b/src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts @@ -1,3 +1,4 @@ +import '../unused-default-rpc-methods.test-fixture' /** * The behaviours that only exist while a read is genuinely in flight or genuinely failing: the * per-connection cap, an abort that arrives mid-read, and a verify whose open throws. All three go diff --git a/src/main/runtime/rpc/methods/mobile-web-bundle-window-refusals.test.ts b/src/main/runtime/rpc/methods/mobile-web-bundle-window-refusals.test.ts index b66507b5479..944b8584544 100644 --- a/src/main/runtime/rpc/methods/mobile-web-bundle-window-refusals.test.ts +++ b/src/main/runtime/rpc/methods/mobile-web-bundle-window-refusals.test.ts @@ -1,3 +1,4 @@ +import '../unused-default-rpc-methods.test-fixture' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' diff --git a/src/main/runtime/rpc/methods/mobile-web-bundle.test.ts b/src/main/runtime/rpc/methods/mobile-web-bundle.test.ts index c17a40f865b..377b1de04ba 100644 --- a/src/main/runtime/rpc/methods/mobile-web-bundle.test.ts +++ b/src/main/runtime/rpc/methods/mobile-web-bundle.test.ts @@ -1,3 +1,4 @@ +import '../unused-default-rpc-methods.test-fixture' import { mkdirSync, mkdtempSync, rmSync, truncateSync, unlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' diff --git a/src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts index e2b33bc9bb6..f2a79711711 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts @@ -1,3 +1,4 @@ +import '../unused-default-rpc-methods.test-fixture' import { mkdtemp, readFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' diff --git a/src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher.test.ts b/src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher.test.ts index 564eaed649a..3dfe2f824bc 100644 --- a/src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher.test.ts +++ b/src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher.test.ts @@ -1,3 +1,4 @@ +import './unused-default-rpc-methods.test-fixture' import { createHash } from 'node:crypto' import { afterEach, describe, expect, it, vi } from 'vitest' import type Database from '../../sqlite/sync-database' diff --git a/src/main/startup/codex-hook-startup-wiring.test.ts b/src/main/startup/codex-hook-startup-wiring.test.ts index f142448b98e..a62cb6fb007 100644 --- a/src/main/startup/codex-hook-startup-wiring.test.ts +++ b/src/main/startup/codex-hook-startup-wiring.test.ts @@ -32,26 +32,49 @@ vi.mock('./main-process-runtime-service', () => ({ }), configureRuntimeServices: vi.fn() })) -vi.mock('../star-nag/service') -vi.mock('../browser/agent-browser-bridge') -vi.mock('../emulator/emulator-bridge') -vi.mock('../runtime/rpc/dispatcher') +vi.mock('../star-nag/service', () => ({ + StarNagService: class { + start = vi.fn() + registerIpcHandlers = vi.fn() + } +})) +vi.mock('../browser/agent-browser-bridge', () => ({ + AgentBrowserBridge: class { + sweepOrphanedSessions = vi.fn() + } +})) +vi.mock('../emulator/emulator-bridge', () => ({ EmulatorBridge: class {} })) +vi.mock('../runtime/rpc/dispatcher', () => ({ RpcDispatcher: class {} })) vi.mock('../browser/browser-manager', () => ({ browserManager: {} })) -vi.mock('../browser/browser-client-page-automation-runtime') -vi.mock('../crash-reporting/process-gone-diagnostics') -vi.mock('./main-window-lifecycle-flags') -vi.mock('./gpu-lifecycle') +vi.mock('../browser/browser-client-page-automation-runtime', () => ({ + configureBrowserClientPageAutomationRuntime: vi.fn() +})) +vi.mock('../crash-reporting/process-gone-diagnostics', () => ({ + startPreGoneCrashSampling: vi.fn() +})) +vi.mock('./main-window-lifecycle-flags', () => ({ recordProcessGoneCrash: vi.fn() })) +vi.mock('./gpu-lifecycle', () => ({ handleGpuChildCrash: vi.fn() })) vi.mock('./configure-process', () => ({ shouldInstallManagedHooks: () => false })) -vi.mock('../agent-hooks/install-telemetry') -vi.mock('./main-process-observers') -vi.mock('./main-process-account-services') -vi.mock('./main-process-automations') -vi.mock('./main-process-plugins') -vi.mock('../worktree-trash') -vi.mock('./worktree-removal-records-load') -vi.mock('./first-window-deferral') -vi.mock('./startup-diagnostics') -vi.mock('../opencode/opencode-status-plugin-startup-refresh') +vi.mock('../agent-hooks/install-telemetry', () => ({ recordManagedHookInstallFailure: vi.fn() })) +vi.mock('./main-process-observers', () => ({ initializeMainProcessObservers: vi.fn() })) +vi.mock('./main-process-account-services', () => ({ + initializeMainProcessAccountServices: vi.fn() +})) +vi.mock('./main-process-automations', () => ({ initializeMainProcessAutomations: vi.fn() })) +vi.mock('./main-process-plugins', () => ({ initializeMainProcessPlugins: vi.fn() })) +vi.mock('../worktree-trash', () => ({ + collectWorktreeTrashSweepRoots: vi.fn(), + sweepStaleWorktreeTrash: vi.fn() +})) +vi.mock('./worktree-removal-records-load', () => ({ loadWorktreeRemovalRecordsForStore: vi.fn() })) +vi.mock('./first-window-deferral', () => ({ runAfterFirstWindowShown: vi.fn() })) +vi.mock('./startup-diagnostics', () => ({ logStartupMilestone: vi.fn() })) +vi.mock('../opencode/opencode-status-plugin-startup-refresh', () => ({ + refreshInstalledOpenCodeStatusPlugins: vi.fn() +})) +vi.mock('../orcad/orcad-artifact-cache-retention', () => ({ + pruneDesktopOrcadArtifactCache: vi.fn() +})) import { initializeReadyRuntimeServices } from './main-process-ready-runtime' diff --git a/src/main/startup/main-process-runtime-launch-activation.test.ts b/src/main/startup/main-process-runtime-launch-activation.test.ts index 80bd815606e..516342f877b 100644 --- a/src/main/startup/main-process-runtime-launch-activation.test.ts +++ b/src/main/startup/main-process-runtime-launch-activation.test.ts @@ -102,6 +102,14 @@ vi.mock('../window/foreground-activation-policy', () => ({ isWindowlessLaunch: () => false, showWindowWithoutStealingFocus })) +vi.mock('../tray/system-tray', () => ({ + createSystemTray: vi.fn(), + setMacMenuBarIconVisible: vi.fn() +})) +vi.mock('../window/attach-main-window-services', () => ({ ensureAutoUpdaterConfigured: vi.fn() })) +vi.mock('../window/dashboard-popout-window', () => ({ getDashboardPopoutWindow: () => null })) +vi.mock('../window/createMainWindow', () => ({ loadMainWindow: vi.fn() })) +vi.mock('../window/renderer-recovery-prompt', () => ({ presentRendererRecoveryPrompt: vi.fn() })) vi.mock('./main-process-ready-foundation', () => ({ initializeReadyFoundation: vi.fn(async () => {}) diff --git a/src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx b/src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx index 9e49a3d6ed3..736c75979bf 100644 --- a/src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx +++ b/src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx @@ -19,6 +19,7 @@ vi.mock('@monaco-editor/react', () => ({ }, loader: { config: vi.fn() } })) +vi.mock('@/lib/monaco-setup', () => ({})) vi.mock('@/store', () => ({ useAppStore: (selector: (state: Record) => unknown) => selector({ diff --git a/src/renderer/src/components/editor/MonacoEditor.system-theme.test.tsx b/src/renderer/src/components/editor/MonacoEditor.system-theme.test.tsx index 9f1065591a4..d3ac9e63e13 100644 --- a/src/renderer/src/components/editor/MonacoEditor.system-theme.test.tsx +++ b/src/renderer/src/components/editor/MonacoEditor.system-theme.test.tsx @@ -17,6 +17,7 @@ vi.mock('@monaco-editor/react', () => ({ }, loader: { config: vi.fn() } })) +vi.mock('@/lib/monaco-setup', () => ({})) vi.mock('@/store', () => ({ useAppStore: (selector: (state: Record) => unknown) => selector({ diff --git a/src/renderer/src/components/right-sidebar/CommitArea.primary-icons.test.tsx b/src/renderer/src/components/right-sidebar/CommitArea.primary-icons.test.tsx deleted file mode 100644 index 202d746708d..00000000000 --- a/src/renderer/src/components/right-sidebar/CommitArea.primary-icons.test.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { renderToStaticMarkup } from 'react-dom/server' -import { TooltipProvider } from '@/components/ui/tooltip' -import { CommitArea } from './source-control/commit/commit-area' -import { resolvePrimaryAction, type PrimaryActionInputs } from './source-control-primary-action' -import { resolveDropdownItems } from './source-control-dropdown-items' -import type { DropdownActionKind } from './source-control-dropdown-item-types' - -// Why: split out from CommitArea.test.tsx so each file stays under the -// project's max-lines budget. These tests cover the directional-icon -// mapping for primary action kinds (push / pull / sync / publish); the -// commit-checkmark and core CommitArea behaviour live in the sibling file. - -function buildInputs(overrides: Partial = {}): PrimaryActionInputs { - return { - stagedCount: 1, - hasUnstagedChanges: false, - hasStageableChanges: false, - hasPartiallyStagedChanges: false, - hasMessage: true, - hasUnresolvedConflicts: false, - isCommitting: false, - isRemoteOperationActive: false, - upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 }, - ...overrides - } -} - -function baseProps(overrides: Partial = {}) { - const inputs = buildInputs(overrides) - return { - worktreeId: 'wt-1', - groupId: 'group-1', - commitMessage: 'feat: add commit area', - commitError: null as string | null, - commitFailureRecoveryPrompt: null as string | null, - pushRecovery: null, - remoteActionError: null as string | null, - isCommitting: inputs.isCommitting, - isFixingCommitFailureWithAI: false, - isFixingPushFailureWithAI: false, - sourceControlAiActionsVisible: true, - aiAgentConfigured: false, - isGenerating: false, - generateError: null as string | null, - stagedCount: inputs.stagedCount, - hasPartiallyStagedChanges: inputs.hasPartiallyStagedChanges, - hasUnresolvedConflicts: inputs.hasUnresolvedConflicts, - isRemoteOperationActive: inputs.isRemoteOperationActive, - inFlightRemoteOpKind: inputs.inFlightRemoteOpKind ?? null, - primaryAction: resolvePrimaryAction(inputs), - dropdownItems: resolveDropdownItems(inputs), - onCommitMessageChange: vi.fn(), - onGenerate: vi.fn(), - onCancelGenerate: vi.fn(), - onFixCommitFailureWithAI: vi.fn(), - onFixPushFailureWithAI: vi.fn(), - onPrimaryAction: vi.fn(), - onDropdownAction: vi.fn() as (kind: DropdownActionKind) => void - } -} - -function primaryButton(props: ReturnType): string { - const markup = renderToStaticMarkup( - - - - ) - const button = [...markup.matchAll(//g)] - .map((match) => match[0]) - .find((entry) => entry.includes('data-slot="button"')) - if (!button) { - throw new Error('primary button not found') - } - return button -} - -describe('CommitArea primary action icons', () => { - it('renders an up-arrow on a Push primary', () => { - expect( - primaryButton( - baseProps({ - stagedCount: 0, - hasMessage: false, - upstreamStatus: { hasUpstream: true, ahead: 1, behind: 0 } - }) - ) - ).toContain('lucide-arrow-up') - }) - - it('renders no directional icon on a Pull primary', () => { - const button = primaryButton( - baseProps({ - stagedCount: 0, - hasMessage: false, - upstreamStatus: { hasUpstream: true, ahead: 0, behind: 1 } - }) - ) - expect(button).not.toContain('lucide-arrow-up') - expect(button).not.toContain('lucide-arrow-down-up') - expect(button).not.toContain('lucide-cloud-upload') - }) - - it('renders a bidirectional arrow on a Sync primary', () => { - expect( - primaryButton( - baseProps({ - stagedCount: 0, - hasMessage: false, - upstreamStatus: { hasUpstream: true, ahead: 1, behind: 1 } - }) - ) - ).toContain('lucide-arrow-down-up') - }) - - it('renders a cloud-up icon on a Publish primary', () => { - expect( - primaryButton( - baseProps({ - stagedCount: 0, - hasMessage: false, - upstreamStatus: { hasUpstream: false, ahead: 0, behind: 0 } - }) - ) - ).toContain('lucide-cloud-upload') - }) - - it('renders a plus icon on a Stage All primary', () => { - expect( - primaryButton( - baseProps({ - stagedCount: 0, - hasUnstagedChanges: true, - hasStageableChanges: true, - hasMessage: false, - upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 } - }) - ) - ).toContain('lucide-plus') - }) -}) diff --git a/src/renderer/src/components/settings/ShortcutCommandBlock.test.tsx b/src/renderer/src/components/settings/ShortcutCommandBlock.test.tsx deleted file mode 100644 index 00a24ecfccf..00000000000 --- a/src/renderer/src/components/settings/ShortcutCommandBlock.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -// @vitest-environment happy-dom - -import { cleanup, render, screen } from '@testing-library/react' -import { afterEach, describe, expect, it, vi } from 'vitest' -import { getKeybindingDefinition } from '../../../../shared/keybindings' -import { TooltipProvider } from '@/components/ui/tooltip' -import { ShortcutCommandBlock } from './ShortcutCommandBlock' - -afterEach(cleanup) - -describe('ShortcutCommandBlock', () => { - it('wraps persistent conflict remediation instead of truncating it', () => { - const warning = 'Blocked by Mission Control. Remap here or change it in System Settings.' - const item = getKeybindingDefinition('tab.selectByIndex') - expect(item).not.toBeNull() - - render( - - - - ) - - const helper = screen.getByText(warning) - expect(helper.classList.contains('whitespace-normal')).toBe(true) - expect(helper.classList.contains('truncate')).toBe(false) - }) -}) diff --git a/src/renderer/src/components/settings/__snapshots__/terminal-preview-content.test.ts.snap b/src/renderer/src/components/settings/__snapshots__/terminal-preview-content.test.ts.snap deleted file mode 100644 index 1c8628b31ed..00000000000 --- a/src/renderer/src/components/settings/__snapshots__/terminal-preview-content.test.ts.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`PREVIEW_BUFFER > matches the locked snapshot 1`] = ` -"~/orca main * $ npm test -  PASS  src/preview.test.ts - ✓ renders sample output (3ms) - ✗ ligatures: => != >= <= === - -def total(xs: list[int]) -> int: - """Sum the values.""" - return sum(x for x in xs) - -~/orca main * $ git diff -@@ -1,2 +1,3 @@ --const size = 13 -+const size = 14 - -~/orca main * $ " -`; diff --git a/src/renderer/src/components/settings/appearance-interface-summary.test.ts b/src/renderer/src/components/settings/appearance-interface-summary.test.ts deleted file mode 100644 index e60f1bcae04..00000000000 --- a/src/renderer/src/components/settings/appearance-interface-summary.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { getDefaultSettings } from '../../../../shared/constants' -import { resolveInterfaceSectionSummary } from './appearance-interface-summary' - -describe('resolveInterfaceSectionSummary', () => { - it('includes theme, language, and font when the language setting is shown', () => { - const settings = { - ...getDefaultSettings('/tmp'), - theme: 'dark' as const, - uiLanguage: 'zh' as const, - appFontFamily: 'Inter' - } - - expect(resolveInterfaceSectionSummary(settings)).toBe('Dark · 中文(简体) · Inter') - }) - - it('falls back to the default font label when app font is empty', () => { - const settings = { - ...getDefaultSettings('/tmp'), - theme: 'light' as const, - uiLanguage: 'system' as const, - appFontFamily: '' - } - - expect(resolveInterfaceSectionSummary(settings)).toBe('Light · System · Default font') - }) -}) diff --git a/src/renderer/src/components/settings/terminal-preview-content.test.ts b/src/renderer/src/components/settings/terminal-preview-content.test.ts deleted file mode 100644 index 510bc43eb56..00000000000 --- a/src/renderer/src/components/settings/terminal-preview-content.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { PREVIEW_BUFFER } from './terminal-preview-content' - -describe('PREVIEW_BUFFER', () => { - it('contains the canonical content markers', () => { - // Why: strip ANSI before matching so re-coloring a marker (e.g. splitting - // "def render" into separate keyword/identifier spans) doesn't break this - // test — what matters is the visible text the user reads. - // eslint-disable-next-line no-control-regex - const stripped = PREVIEW_BUFFER.replace(/\x1b\[[0-9;]*m/g, '') - expect(stripped).toContain('npm test') - expect(stripped).toContain('PASS') - expect(stripped).toContain('ligatures: => != >= <= ===') - expect(stripped).toContain('git diff') - expect(stripped).toContain('def total') - }) - - it('ends without a trailing newline so the cursor parks on the prompt', () => { - expect(PREVIEW_BUFFER.endsWith('\n')).toBe(false) - expect(PREVIEW_BUFFER).toMatch(/\$ $/) - }) - - it('matches the locked snapshot', () => { - expect(PREVIEW_BUFFER).toMatchSnapshot() - }) -}) diff --git a/tests/e2e/helpers/electron-process-shutdown.ts b/tests/e2e/helpers/electron-process-shutdown.ts index ab912e8a235..f1ffaf3f66e 100644 --- a/tests/e2e/helpers/electron-process-shutdown.ts +++ b/tests/e2e/helpers/electron-process-shutdown.ts @@ -3,6 +3,7 @@ import { execFileSync } from 'node:child_process' import { existsSync, readFileSync, readdirSync } from 'node:fs' import path from 'node:path' import { cleanupE2ECrashpad } from './electron-crashpad-cleanup' +import { recordElectronShutdownTiming } from './electron-shutdown-timing' import type { ElectronApplication } from '@stablyai/playwright-test' const GRACEFUL_CLOSE_TIMEOUT_MS = 10_000 @@ -214,10 +215,35 @@ export async function forceQuitElectronAppForE2E( ).catch(() => undefined) } -export async function closeElectronAppForE2E(app: ElectronApplication): Promise { +export async function closeElectronAppForE2E( + app: ElectronApplication, + options: { cleanupDaemonsForUserDataDir?: string } = {} +): Promise { const proc = app.process() - const releasePipes = (): void => releaseExitedProcessPipes(proc) + const started = performance.now() + let rootExitMs: number | null = hasExited(proc) ? 0 : null + let pipesClosedMs: number | null = null + let outcome = 'closed' + let daemonCleanup: Promise | undefined + const startDaemonCleanup = (): void => { + if (options.cleanupDaemonsForUserDataDir && !daemonCleanup) { + daemonCleanup = cleanupE2EDaemons(options.cleanupDaemonsForUserDataDir) + void daemonCleanup.catch(() => undefined) + } + } + const releasePipes = (): void => { + if (hasExited(proc)) { + rootExitMs ??= performance.now() - started + // Linux daemon trees retain Chromium sockets that block Playwright's public close. + startDaemonCleanup() + } + releaseExitedProcessPipes(proc) + } + const pipesClosed = (): void => { + pipesClosedMs = performance.now() - started + } proc.once('exit', releasePipes) + proc.once('close', pipesClosed) releasePipes() try { await withTimeout(app.close(), GRACEFUL_CLOSE_TIMEOUT_MS, 'Timed out closing Electron app') @@ -228,13 +254,23 @@ export async function closeElectronAppForE2E(app: ElectronApplication): Promise< } } } catch { + outcome = 'fallback' if (proc) { await forceKillProcessTree(proc) } } finally { proc.off('exit', releasePipes) + proc.off('close', pipesClosed) releasePipes() + recordElectronShutdownTiming('close', started, { + pid: proc.pid, + outcome, + rootExitMs, + pipesClosedMs + }) } + startDaemonCleanup() + await daemonCleanup } function readDaemonPidFiles(userDataDir: string): number[] { @@ -268,8 +304,14 @@ export async function cleanupE2EDaemons(userDataDir: string): Promise { // Why: app quit intentionally leaves daemon PTYs alive for warm reattach. // E2E temp profiles are deleted after each test, so their detached daemons // must be stopped explicitly or CI accumulates orphan Electron/shell trees. - for (const pid of readDaemonPidFiles(userDataDir)) { - await forceKillPidTree(pid) + const started = performance.now() + const pids = readDaemonPidFiles(userDataDir) + try { + for (const pid of pids) { + await forceKillPidTree(pid) + } + cleanupE2ECrashpad(userDataDir) + } finally { + recordElectronShutdownTiming('daemon-cleanup', started, { daemonCount: pids.length }) } - cleanupE2ECrashpad(userDataDir) } diff --git a/tests/e2e/helpers/electron-process-shutdown.unit.test.ts b/tests/e2e/helpers/electron-process-shutdown.unit.test.ts index 19b65e9ea36..45143ef245e 100644 --- a/tests/e2e/helpers/electron-process-shutdown.unit.test.ts +++ b/tests/e2e/helpers/electron-process-shutdown.unit.test.ts @@ -158,6 +158,52 @@ describe('Electron shutdown with inherited pipes', () => { }) describe.skipIf(process.platform === 'win32')('owned POSIX daemon cleanup', () => { + it('preserves a running daemon when final cleanup is not requested', async () => { + const daemon = await ownedDaemon('exit') + const { proc, app } = exitedAppFixture() + proc.exitCode = 0 + await closeElectronAppForE2E(app) + expect(daemon.pids.every((pid) => !pidIsGone(pid))).toBe(true) + }) + + it('starts final daemon cleanup after app exit and awaits public close and all descendants', async () => { + const daemon = await ownedDaemon('descendant') + const { proc, app, close } = exitedAppFixture() + let finishPublicClose: () => void = () => {} + const publicClose = new Promise((resolve) => { + finishPublicClose = resolve + }) + let publicCloseFinished = false + close.mockImplementation(async () => { + await daemon.exited + await publicClose + publicCloseFinished = true + return [] + }) + let shutdownFinished = false + const closing = closeElectronAppForE2E(app, { + cleanupDaemonsForUserDataDir: daemon.profile + }).then(() => { + shutdownFinished = true + }) + expect(close).toHaveBeenCalledOnce() + expect(daemon.pids.every((pid) => !pidIsGone(pid))).toBe(true) + proc.exitCode = 0 + proc.emit('exit', 0, null) + const [, signal] = await daemon.exited + expect(signal).toBe('SIGTERM') + expect(pidIsGone(daemon.pids[1])).toBe(false) + expect(publicCloseFinished).toBe(false) + expect(shutdownFinished).toBe(false) + finishPublicClose() + await expect.poll(() => publicCloseFinished).toBe(true) + expect(shutdownFinished).toBe(false) + await closing + await expect.poll(() => daemon.pids.every(pidIsGone), { timeout: 5_000 }).toBe(true) + expect(close).toHaveBeenCalledOnce() + expect(proc.listenerCount('exit')).toBe(0) + }, 10_000) + it('returns when every captured process exits on SIGTERM', async () => { const daemon = await ownedDaemon('exit') const started = performance.now() diff --git a/tests/e2e/helpers/electron-shutdown-timing.ts b/tests/e2e/helpers/electron-shutdown-timing.ts new file mode 100644 index 00000000000..db66ba33cb3 --- /dev/null +++ b/tests/e2e/helpers/electron-shutdown-timing.ts @@ -0,0 +1,21 @@ +type ShutdownPhase = 'close' | 'daemon-cleanup' | 'profile-removal' + +export function recordElectronShutdownTiming( + phase: ShutdownPhase, + started: number, + details: Record = {} +): void { + if ( + process.env.ORCA_E2E_SHUTDOWN_TIMING !== '1' && + process.env.ORCA_E2E_FORWARD_APP_LOGS !== '1' + ) { + return + } + console.log( + `[e2e:shutdown] ${JSON.stringify({ + phase, + elapsedMs: Number((performance.now() - started).toFixed(3)), + ...details + })}` + ) +} diff --git a/tests/e2e/helpers/hidden-renderer-frame-capture.ts b/tests/e2e/helpers/hidden-renderer-frame-capture.ts index 41c427e9bb3..c4953cd0b20 100644 --- a/tests/e2e/helpers/hidden-renderer-frame-capture.ts +++ b/tests/e2e/helpers/hidden-renderer-frame-capture.ts @@ -1,45 +1,9 @@ import type { Page } from '@playwright/test' +import { startHiddenRendererFrameCapture as startCapture } from '../../../src/main/browser/hidden-renderer-frame-capture-fixture' import { expect } from './orca-app' export async function startHiddenRendererFrameCapture(page: Page): Promise<() => Promise> { - const cdp = await page.context().newCDPSession(page) - let stopped = false - const handlePageClose = (): void => { - void stop().catch(() => {}) - } - const stop = async (): Promise => { - if (stopped) { - return - } - stopped = true - try { - await cdp.send('Page.stopScreencast') - } finally { - page.off('close', handlePageClose) - cdp.off('Page.screencastFrame', acknowledgeFrame) - await cdp.detach() - } - } - let hasFrame = false - const acknowledgeFrame = (frame: { sessionId: number }): void => { - hasFrame = true - void cdp.send('Page.screencastFrameAck', { sessionId: frame.sessionId }).catch(() => {}) - } - cdp.on('Page.screencastFrame', acknowledgeFrame) - page.once('close', handlePageClose) - try { - // Keep hidden renderers painting without revealing their native windows. - await cdp.send('Page.enable') - await cdp.send('Page.startScreencast', { - format: 'jpeg', - quality: 20, - maxWidth: 640, - maxHeight: 480 - }) - await expect.poll(() => hasFrame).toBe(true) - return stop - } catch (error) { - await stop().catch(() => {}) - throw error - } + return startCapture(page, async (hasFrame) => { + await expect.poll(hasFrame).toBe(true) + }) } diff --git a/tests/e2e/helpers/orca-app.ts b/tests/e2e/helpers/orca-app.ts index f58f31bee89..a4b101b0e54 100644 --- a/tests/e2e/helpers/orca-app.ts +++ b/tests/e2e/helpers/orca-app.ts @@ -24,7 +24,8 @@ import { import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' import os from 'node:os' import path from 'node:path' -import { cleanupE2EDaemons, closeElectronAppForE2E } from './electron-process-shutdown' +import { closeElectronAppForE2E } from './electron-process-shutdown' +import { recordElectronShutdownTiming } from './electron-shutdown-timing' import { getOrcaElectronLaunchArgs } from './electron-launch-args' import { retryTransientMainEvaluate } from './electron-main-evaluate-retry' import { getE2ECompletedOnboardingProfile } from './e2e-completed-onboarding-profile' @@ -91,9 +92,11 @@ const ORCA_E2E_SLOWMO_MS = ((): number => { })() async function removeUserDataDirAfterShutdown(userDataDir: string): Promise { + const started = performance.now() for (let attempt = 0; attempt < 5; attempt += 1) { try { rmSync(userDataDir, { recursive: true, force: true }) + recordElectronShutdownTiming('profile-removal', started) return } catch (error) { if (attempt === 4) { @@ -251,16 +254,14 @@ export const test = base.extend({ ) assertElectronResolvedIsolatedHome(resolvedHome, homeIsolation) } catch (error) { - await closeElectronAppForE2E(app) - await cleanupE2EDaemons(userDataDir) + await closeElectronAppForE2E(app, { cleanupDaemonsForUserDataDir: userDataDir }) await removeUserDataDirAfterShutdown(userDataDir) throw error } await provideFixture(app) // Why: the Playwright close promise can settle before all Electron and PTY // descendants are gone in CI; worker teardown then hangs on open handles. - await closeElectronAppForE2E(app) - await cleanupE2EDaemons(userDataDir) + await closeElectronAppForE2E(app, { cleanupDaemonsForUserDataDir: userDataDir }) await removeUserDataDirAfterShutdown(userDataDir) }, diff --git a/tests/e2e/remote-terminal-input-delivery-model.unit.test.ts b/tests/e2e/remote-terminal-input-delivery-model.unit.test.ts index 79a67c1d99c..ffc7fe4a9a6 100644 --- a/tests/e2e/remote-terminal-input-delivery-model.unit.test.ts +++ b/tests/e2e/remote-terminal-input-delivery-model.unit.test.ts @@ -82,7 +82,7 @@ function attachModelPane( ReturnType['attach'] >[0]['callbacks'] ): ReturnType { - const call = vi.fn(async (request: { method: string }) => + const call = async (request: { method: string }) => request.method === 'terminal.resolvePane' ? { ok: true, @@ -96,7 +96,7 @@ function attachModelPane( } } : { ok: true, result: { terminal: { handle: MODEL_TERMINAL_HANDLE } } } - ) + vi.stubGlobal('window', { api: { runtimeEnvironments: { call, subscribe: link.subscribe } }, location: { search: '' } diff --git a/tests/e2e/terminal-windows-shell-paste-ownership.spec.ts b/tests/e2e/terminal-windows-shell-paste-ownership.spec.ts index f944e7686af..e7b5014e5ac 100644 --- a/tests/e2e/terminal-windows-shell-paste-ownership.spec.ts +++ b/tests/e2e/terminal-windows-shell-paste-ownership.spec.ts @@ -193,6 +193,8 @@ async function updateWindowsDefaultShellSetting( } test.describe('Windows terminal shell paste ownership', () => { + test.skip(process.platform !== 'win32', 'Windows terminal shell paste coverage is Windows-only') + async function skipWhenGitBashUnavailable(page: Page): Promise { const isAvailable = await page.evaluate(() => window.api.gitBash.isAvailable()) test.skip(!isAvailable, 'Git Bash is not available on this Windows host') @@ -203,8 +205,6 @@ test.describe('Windows terminal shell paste ownership', () => { orcaPage, testRepoPath }) => { - test.skip(process.platform !== 'win32', 'PowerShell paste coverage is Windows-only') - await waitForSessionReady(orcaPage) await waitForActiveWorktree(orcaPage) await ensureTerminalVisible(orcaPage) @@ -256,8 +256,6 @@ test.describe('Windows terminal shell paste ownership', () => { orcaPage, testRepoPath }) => { - test.skip(process.platform !== 'win32', 'cmd.exe paste coverage is Windows-only') - await waitForSessionReady(orcaPage) await waitForActiveWorktree(orcaPage) await ensureTerminalVisible(orcaPage) @@ -307,7 +305,6 @@ test.describe('Windows terminal shell paste ownership', () => { orcaPage, testRepoPath }) => { - test.skip(process.platform !== 'win32', 'Git Bash paste coverage is Windows-only') await skipWhenGitBashUnavailable(orcaPage) await waitForSessionReady(orcaPage) @@ -359,7 +356,6 @@ test.describe('Windows terminal shell paste ownership', () => { orcaPage, testRepoPath }) => { - test.skip(process.platform !== 'win32', 'WSL paste coverage is Windows-only') test.skip(!hasWslNodeRuntime(), 'WSL with node is not available on this Windows host') await waitForSessionReady(orcaPage) @@ -418,7 +414,6 @@ test.describe('Windows terminal shell paste ownership', () => { orcaPage, testRepoPath }) => { - test.skip(process.platform !== 'win32', 'WSL paste runtime retention is Windows-only') test.skip(!hasWslNodeRuntime(), 'WSL with node is not available on this Windows host') await waitForSessionReady(orcaPage) diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts index c583b8fd222..ee4bf330b9f 100644 --- a/tests/playwright.config.ts +++ b/tests/playwright.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from '@stablyai/playwright-test' +import { resolveBuildSourceCommit } from '../config/scripts/build-source-commit.mjs' /** * Playwright config for Orca E2E tests. @@ -14,6 +15,9 @@ import { defineConfig } from '@stablyai/playwright-test' */ export default defineConfig({ testDir: './e2e', + // Keep checkout provenance without fetching the PR base during test setup. + captureGitInfo: { commit: false, diff: false }, + metadata: { sourceCommit: resolveBuildSourceCommit() }, globalSetup: './e2e/global-setup.ts', globalTeardown: './e2e/global-teardown.ts', // Why: this suite launches a fresh Electron app and isolated userData dir per From d5324c96e3e6e88a6deb242ba122e09362c6c800 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Wed, 14 Oct 2026 17:21:11 -0700 Subject: [PATCH 28/53] fix(ssh): the SSH password dialog no longer closes itself and comes back (#28883) * fix(ssh): the SSH password dialog no longer closes itself and comes back A host that drops an unanswered login no longer replaces the open prompt with a new one, a background dial no longer replaces a connection whose reconnect is asking the user, one Cancel closes every open prompt for that host, and a paired client's automatic connect stays automatic. Co-Authored-By: Claude * fix(ssh): a typed password its reconnect has yet to use is not replaced by a background dial Co-Authored-By: Claude --------- Co-authored-by: m4air Co-authored-by: Claude --- src/main/ipc/ssh-passphrase.test.ts | 52 +++++- src/main/ipc/ssh-passphrase.ts | 20 ++- src/main/runtime/rpc/methods/ssh.test.ts | 12 +- src/main/runtime/rpc/methods/ssh.ts | 8 +- ...h-connection-manager-sign-in-reuse.test.ts | 3 + src/main/ssh/ssh-connection-manager.test.ts | 41 ++++- src/main/ssh/ssh-connection-manager.ts | 4 +- src/main/ssh/ssh-connection.ts | 53 ++++-- ...credential-prompt-login-grace-wire.test.ts | 152 ++++++++++++++++++ src/main/ssh/ssh-target-registry.ts | 17 +- .../src/web/preload-api/web-terminal-api.ts | 2 +- .../src/web/web-preload-api-ssh.test.ts | 5 +- .../rpc-params-catalog.generated.ts | 11 +- src/shared/rpc-contract/ssh-params.ts | 5 + 14 files changed, 350 insertions(+), 35 deletions(-) create mode 100644 src/main/ssh/ssh-credential-prompt-login-grace-wire.test.ts diff --git a/src/main/ipc/ssh-passphrase.test.ts b/src/main/ipc/ssh-passphrase.test.ts index ee53cc78d30..59e2900eb1f 100644 --- a/src/main/ipc/ssh-passphrase.test.ts +++ b/src/main/ipc/ssh-passphrase.test.ts @@ -1,6 +1,6 @@ -import type { BrowserWindow } from 'electron' +import { ipcMain, type BrowserWindow } from 'electron' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { requestCredential } from './ssh-passphrase' +import { registerCredentialHandler, requestCredential } from './ssh-passphrase' import { SSH_CREDENTIAL_TIMEOUT_MS } from '../ssh/ssh-connection-utils' import { isSshCredentialDeclined, @@ -76,4 +76,52 @@ describe('SSH credential requests', () => { requestId: request.requestId }) }) + + it('closes every other open prompt for the host the user cancelled', async () => { + registerCredentialHandler() + const handler = vi.mocked(ipcMain.handle).mock.calls.at(-1)![1] + const submit = (requestId: string, value: string | null): unknown => + handler(Object.create(null), { requestId, value }) + const window = credentialWindow() + const requestIds = (): string[] => + vi + .mocked(window.webContents.send) + .mock.calls.flatMap(([channel, request]) => + channel === 'ssh:credential-request' && + request && + typeof request === 'object' && + 'requestId' in request && + typeof request.requestId === 'string' + ? [request.requestId] + : [] + ) + const first = requestCredential( + () => window, + 'target-1', + 'password', + 'host', + undefined, + new AbortController().signal + ) + const queued = requestCredential( + () => window, + 'target-1', + 'password', + 'host', + undefined, + new AbortController().signal + ) + const otherHost = requestCredential(() => window, 'target-2', 'password', 'other') + const [firstId, queuedId, otherId] = requestIds() + + await submit(firstId, null) + + await expect(first).resolves.toBeNull() + await expect(queued).resolves.toBeNull() + expect(window.webContents.send).toHaveBeenCalledWith('ssh:credential-resolved', { + requestId: queuedId + }) + await submit(otherId, 'secret') + await expect(otherHost).resolves.toBe('secret') + }) }) diff --git a/src/main/ipc/ssh-passphrase.ts b/src/main/ipc/ssh-passphrase.ts index 54de11be4c3..6c9c720b10f 100644 --- a/src/main/ipc/ssh-passphrase.ts +++ b/src/main/ipc/ssh-passphrase.ts @@ -2,7 +2,10 @@ import { ipcMain, type BrowserWindow } from 'electron' import { randomUUID } from 'node:crypto' import { SSH_CREDENTIAL_TIMEOUT_MS, type SshCredentialKind } from '../ssh/ssh-connection-utils' import { recordSshCredentialDeclined } from '../ssh/ssh-credential-decline' -const pendingRequests = new Map void }>() +const pendingRequests = new Map< + string, + { targetId: string; resolve: (value: string | null) => void; close: () => void } +>() function notifyCredentialResolved( getMainWindow: () => BrowserWindow | null, @@ -37,14 +40,23 @@ export function requestCredential( const onAbort = (): void => finish(null) // Why: an abort is the connection's own teardown; only the user's Cancel (or silence) declines. const decline = (): void => { - if (pendingRequests.has(requestId)) { - recordSshCredentialDeclined(targetId, signal) + if (!pendingRequests.has(requestId)) { + return } + recordSshCredentialDeclined(targetId, signal) finish(null) + // Why: one Cancel answers the host; a queued prompt for it would otherwise open next. + for (const [otherId, other] of pendingRequests) { + if (otherId !== requestId && other.targetId === targetId) { + other.close() + } + } } timer = setTimeout(decline, SSH_CREDENTIAL_TIMEOUT_MS) pendingRequests.set(requestId, { - resolve: (value) => (value === null ? decline() : finish(value)) + targetId, + resolve: (value) => (value === null ? decline() : finish(value)), + close: () => finish(null) }) if (signal?.aborted) { finish(null) diff --git a/src/main/runtime/rpc/methods/ssh.test.ts b/src/main/runtime/rpc/methods/ssh.test.ts index b71602fadfa..3c6e6723084 100644 --- a/src/main/runtime/rpc/methods/ssh.test.ts +++ b/src/main/runtime/rpc/methods/ssh.test.ts @@ -59,10 +59,20 @@ describe('ssh RPC methods', () => { const response = await dispatcher.dispatch(makeRequest('ssh.connect', { targetId: 'ssh-1' })) - expect(connectRegisteredSshTargetMock).toHaveBeenCalledWith('ssh-1') + expect(connectRegisteredSshTargetMock).toHaveBeenCalledWith('ssh-1', { automatic: false }) expect(response).toMatchObject({ ok: true, result: { state } }) }) + it("keeps a paired client's automatic connect from lifting a cancelled sign-in", async () => { + connectRegisteredSshTargetMock.mockResolvedValueOnce(null) + const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService + const dispatcher = new RpcDispatcher({ runtime, methods: SSH_METHODS }) + + await dispatcher.dispatch(makeRequest('ssh.connect', { targetId: 'ssh-1', automatic: true })) + + expect(connectRegisteredSshTargetMock).toHaveBeenCalledWith('ssh-1', { automatic: true }) + }) + it('returns null when the target has no registered state yet', async () => { getRegisteredSshStateMock.mockReturnValueOnce(undefined) const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService diff --git a/src/main/runtime/rpc/methods/ssh.ts b/src/main/runtime/rpc/methods/ssh.ts index e2bce335a98..0f74474e3e8 100644 --- a/src/main/runtime/rpc/methods/ssh.ts +++ b/src/main/runtime/rpc/methods/ssh.ts @@ -18,6 +18,7 @@ import type { } from '../../../../shared/ssh-target-management' import { SshBrowseDir, + SshConnect, SshTarget, SshTargetAdd, SshTargetUpdate @@ -84,10 +85,13 @@ export const SSH_METHODS = [ defineMethod({ name: 'ssh.connect', permission: 'host-admin', - params: SshTarget, + params: SshConnect, handler: async (params) => { try { - return { state: getPublicSshState(await connectRegisteredSshTarget(params.targetId)) } + const state = await connectRegisteredSshTarget(params.targetId, { + automatic: params.automatic === true + }) + return { state: getPublicSshState(state) } } catch { const state = getRegisteredSshState(params.targetId) throw new Error(getPublicSshError(state?.status ?? 'error')) diff --git a/src/main/ssh/ssh-connection-manager-sign-in-reuse.test.ts b/src/main/ssh/ssh-connection-manager-sign-in-reuse.test.ts index 1569833a73f..9ad909dd278 100644 --- a/src/main/ssh/ssh-connection-manager-sign-in-reuse.test.ts +++ b/src/main/ssh/ssh-connection-manager-sign-in-reuse.test.ts @@ -119,6 +119,9 @@ describe('SshConnectionManager on a password host', () => { onCredentialRequest.mockImplementationOnce(() => new Promise(() => {})) emitSshEvent('keyboard-interactive', '', '', '', passwordPrompt, vi.fn()) await vi.waitFor(() => expect(onCredentialRequest).toHaveBeenCalledTimes(2)) + // A background dial may not close the dialog the user is answering. + await expect(manager.connect(createTarget())).rejects.toThrow('already in progress') + await manager.disconnectConnection('target-1', manager.getConnection('target-1')!) ssh2Mock.connectSequence = [authFailure(), 'ready'] await manager.connect(createTarget()) diff --git a/src/main/ssh/ssh-connection-manager.test.ts b/src/main/ssh/ssh-connection-manager.test.ts index 2c3a9ed30ae..9f5f99509a1 100644 --- a/src/main/ssh/ssh-connection-manager.test.ts +++ b/src/main/ssh/ssh-connection-manager.test.ts @@ -1,18 +1,23 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { SshTarget } from '../../shared/ssh-types' -const mockState = vi.hoisted(() => ({ - connectResults: [] as Promise[], - instances: [] as { - connect: ReturnType - disconnect: ReturnType - status: 'connecting' | 'connected' | 'disconnected' - }[] -})) +type MockConnectionRecord = { + connect: ReturnType + disconnect: ReturnType + status: 'connecting' | 'connected' | 'disconnected' + awaitingCredential: boolean +} + +const mockState = vi.hoisted(() => { + const connectResults: Promise[] = [] + const instances: MockConnectionRecord[] = [] + return { connectResults, instances } +}) vi.mock('./ssh-connection', () => ({ SshConnection: class MockSshConnection { status: 'connecting' | 'connected' | 'disconnected' = 'connecting' + awaitingCredential = false connect = vi.fn(async () => { await (mockState.connectResults.shift() ?? Promise.resolve()) this.status = 'connected' @@ -35,6 +40,10 @@ vi.mock('./ssh-connection', () => ({ getReusableCredentials(): null { return null } + + isAwaitingCredential(): boolean { + return this.awaitingCredential + } } })) @@ -160,6 +169,22 @@ describe('SshConnectionManager', () => { expect(manager.getConnection(target.id)).toBe(replacement) }) + it('leaves a connection whose own reconnect is asking the user alone', async () => { + const manager = new SshConnectionManager({ onStateChange: vi.fn() }) + const conn = await manager.connect(target) + const opened = mockState.instances.at(-1)! + const openedCount = mockState.instances.length + // Its reconnect after a drop has the credential dialog open. + opened.status = 'connecting' + opened.awaitingCredential = true + + await expect(manager.connect(target)).rejects.toThrow('already in progress') + + expect(opened.disconnect).not.toHaveBeenCalled() + expect(mockState.instances).toHaveLength(openedCount) + expect(manager.getConnection(target.id)).toBe(conn) + }) + it('clears the pool entry when the closed connection is still the registered one', async () => { const manager = new SshConnectionManager({ onStateChange: vi.fn() }) const conn = await manager.connect(target) diff --git a/src/main/ssh/ssh-connection-manager.ts b/src/main/ssh/ssh-connection-manager.ts index 2b7d04c71e1..24890afd9d0 100644 --- a/src/main/ssh/ssh-connection-manager.ts +++ b/src/main/ssh/ssh-connection-manager.ts @@ -50,7 +50,9 @@ export class SshConnectionManager { throw createSshCredentialDeclinedError(target.label) } - if (this.connectingTargets.has(target.id)) { + // Why: replacing a connection whose own reconnect is asking the user would close that dialog + // and open another one. + if (this.connectingTargets.has(target.id) || existing?.isAwaitingCredential()) { throw new Error(`Connection to ${target.label} is already in progress`) } diff --git a/src/main/ssh/ssh-connection.ts b/src/main/ssh/ssh-connection.ts index 85ad2150be3..c1df60eb8e9 100644 --- a/src/main/ssh/ssh-connection.ts +++ b/src/main/ssh/ssh-connection.ts @@ -195,6 +195,7 @@ export class SshConnection { // Only a sign-in the host accepted may be handed on; a typed or re-asked one is unproven until then. private signInVerified = false private keyboardInteractiveCancelled = false + private openCredentialPrompts = 0 // A rejected cached password must reach the user on the next round. private keyboardInteractivePasswordState = { passwordAutoAnswered: false } private hostKeyFingerprint: string | undefined @@ -276,6 +277,15 @@ export class SshConnection { return this.cachedPassphrase != null || this.cachedPassword != null } + /** The user is answering a prompt, or answered one this connection's own retry has yet to use. */ + isAwaitingCredential(): boolean { + if (this.openCredentialPrompts > 0) { + return true + } + const retrying = this.state.status === 'connecting' || this.state.status === 'reconnecting' + return retrying && !this.signInVerified && this.hasCachedCredential() + } + /** Null unless the host accepted it, so a wrong password is asked for again, not replayed. */ getReusableCredentials(): SshCachedCredentials | null { if (!this.signInVerified || !this.hasCachedCredential()) { @@ -495,13 +505,18 @@ export class SshConnection { if (!this.isCurrentConnectAttempt(connectGeneration)) { return undefined } - return this.callbacks.onCredentialRequest?.( - this.target.id, - kind, - detail, - echo, - this.credentialAbortController.signal - ) + this.openCredentialPrompts += 1 + try { + return await this.callbacks.onCredentialRequest?.( + this.target.id, + kind, + detail, + echo, + this.credentialAbortController.signal + ) + } finally { + this.openCredentialPrompts -= 1 + } } private async answerKeyboardInteractive( @@ -1100,6 +1115,7 @@ export class SshConnection { let settled = false let startupTimer: ReturnType | null = null let keyboardInteractiveRounds = 0 + let keyboardInteractiveAnswer: Promise | null = null const clearStartupTimer = (): void => { if (startupTimer) { clearTimeout(startupTimer) @@ -1201,7 +1217,7 @@ export class SshConnection { return } rearmStartupTimer(SSH_KEYBOARD_INTERACTIVE_READY_TIMEOUT_MS) - void this.answerKeyboardInteractive( + const answer = this.answerKeyboardInteractive( config.host || this.target.label, name, instructions, @@ -1224,7 +1240,7 @@ export class SshConnection { ), { level: 'client-authentication' } ) - onStartupError(cancelledError) + failStartup(cancelledError) } return } @@ -1234,6 +1250,13 @@ export class SshConnection { }, () => finish([]) ) + keyboardInteractiveAnswer = answer + const forgetAnswer = (): void => { + if (keyboardInteractiveAnswer === answer) { + keyboardInteractiveAnswer = null + } + } + void answer.then(forgetAnswer, forgetAnswer) } const cleanupStartupListeners = (): void => { @@ -1276,7 +1299,19 @@ export class SshConnection { resolve() } + // Why: a host that drops an unanswered login (sshd LoginGraceTime) must not close the open + // prompt for a retry that opens another; the user's answer decides, and a typed password is + // cached for that retry. const onStartupError = (err: Error): void => { + if (keyboardInteractiveAnswer && !settled) { + const fail = (): void => failStartup(err) + void keyboardInteractiveAnswer.then(fail, fail) + return + } + failStartup(err) + } + + const failStartup = (err: Error): void => { if (settled) { return } diff --git a/src/main/ssh/ssh-credential-prompt-login-grace-wire.test.ts b/src/main/ssh/ssh-credential-prompt-login-grace-wire.test.ts new file mode 100644 index 00000000000..da63cb87c96 --- /dev/null +++ b/src/main/ssh/ssh-credential-prompt-login-grace-wire.test.ts @@ -0,0 +1,152 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { Server, utils, type Connection } from 'ssh2' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { SshConnection } from './ssh-connection' +import { createCallbacks, createResolvedConfig, createTarget } from './ssh-connection-test-fixtures' +import { initSshHostKeyStoreFile } from './ssh-host-key-store' +import { resolveWithSshG } from './ssh-config-parser' +import type * as SshConfigParser from './ssh-config-parser' + +vi.mock('./ssh-config-parser', async (importOriginal) => ({ + ...(await importOriginal()), + resolveWithSshG: vi.fn() +})) + +const PASSWORD = 'fixture-password' +// Longer than the connection's 2s retry delay, so a retry would start while the prompt is open. +const USER_ANSWER_DELAY_MS = 3_000 + +/** + * A password host whose sshd gives up on an unanswered login (LoginGraceTime) before the user has + * typed. The open password dialog must stay the one the user answers: the dropped socket may not + * silently close it and open a new one. + */ +describe('a password prompt the server gives up on', () => { + let profile: string + let server: Server + let conn: SshConnection | undefined + const sockets = new Set() + + beforeEach(() => { + profile = mkdtempSync(join(tmpdir(), 'orca-login-grace-wire-')) + initSshHostKeyStoreFile(join(profile, 'host-keys.json')) + vi.stubEnv('SSH_AUTH_SOCK', '') + vi.mocked(resolveWithSshG).mockResolvedValue( + createResolvedConfig({ + hostname: '127.0.0.1', + identitiesOnly: true, + identityAgent: 'none', + proxyUseFdpass: false, + strictHostKeyChecking: 'no' + }) + ) + }) + + afterEach(async () => { + await conn?.disconnect() + conn = undefined + for (const socket of sockets) { + socket.end() + } + sockets.clear() + if (server) { + await new Promise((resolve) => server.close(() => resolve())) + } + vi.unstubAllEnvs() + rmSync(profile, { recursive: true, force: true }) + }) + + /** Every login gets a keyboard-interactive password prompt; the first is abandoned by the host. */ + async function startHost(): Promise { + let logins = 0 + server = new Server( + { hostKeys: [utils.generateKeyPairSync('ecdsa', { bits: 256 }).private] }, + (socket) => { + sockets.add(socket) + socket.on('error', () => {}) + socket.on('close', () => sockets.delete(socket)) + const login = ++logins + socket.on('authentication', (context) => { + if (context.method !== 'keyboard-interactive') { + context.reject(['keyboard-interactive']) + return + } + context.prompt([{ prompt: 'Password: ', echo: false }], (answers) => { + if (answers[0] === PASSWORD) { + context.accept() + } else { + context.reject(['keyboard-interactive']) + } + }) + if (login === 1) { + setTimeout(() => socket.end(), 100) + } + }) + socket.on('ready', () => socket.on('session', (accept) => accept())) + } + ) + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) + const address = server.address() + if (!address || typeof address === 'string') { + throw new Error('Fixture did not bind') + } + return address.port + } + + /** Behaves like the real dialog: an abort closes it, otherwise the user answers late. */ + function slowUser(answer: string | null) { + return vi.fn( + ( + _targetId: string, + _kind: string, + _detail: string, + _echo?: boolean, + signal?: AbortSignal + ): Promise => + new Promise((resolve) => { + const timer = setTimeout(() => resolve(answer), USER_ANSWER_DELAY_MS) + signal?.addEventListener('abort', () => { + clearTimeout(timer) + resolve(null) + }) + }) + ) + } + + it('keeps the open prompt and signs in with what the user typed', async () => { + const port = await startHost() + const onCredentialRequest = slowUser(PASSWORD) + conn = new SshConnection( + createTarget({ host: '127.0.0.1', port }), + createCallbacks({ onCredentialRequest }) + ) + + const connecting = conn.connect() + await vi.waitFor(() => expect(onCredentialRequest).toHaveBeenCalled()) + const answered = onCredentialRequest.mock.results[0].value + await answered + // Answered but not yet used by the retry: a background dial must not replace it. + await vi.waitFor(() => expect(conn?.isAwaitingCredential()).toBe(true), { timeout: 1_000 }) + await connecting + + expect(conn.getState().status).toBe('connected') + expect(onCredentialRequest).toHaveBeenCalledTimes(1) + expect(conn.isAwaitingCredential()).toBe(false) + }, 15_000) + + it('stops after the user cancels the prompt the server abandoned', async () => { + const port = await startHost() + const onCredentialRequest = slowUser(null) + conn = new SshConnection( + createTarget({ host: '127.0.0.1', port }), + createCallbacks({ onCredentialRequest }) + ) + + await expect(conn.connect()).rejects.toThrow(/cancelled/i) + + expect(conn.getState().status).toBe('auth-failed') + expect(onCredentialRequest).toHaveBeenCalledTimes(1) + }, 15_000) +}) diff --git a/src/main/ssh/ssh-target-registry.ts b/src/main/ssh/ssh-target-registry.ts index 5008f3b9fc9..91878606b30 100644 --- a/src/main/ssh/ssh-target-registry.ts +++ b/src/main/ssh/ssh-target-registry.ts @@ -24,7 +24,12 @@ import type { */ let sshStore: SshConnectionStore | null = null -let registeredConnectSshTarget: ((targetId: string) => Promise) | null = null +type ConnectSshTarget = ( + targetId: string, + options?: { automatic?: boolean } +) => Promise + +let registeredConnectSshTarget: ConnectSshTarget | null = null let registeredGetSshState: ((targetId: string) => SshConnectionState | undefined) | null = null export function setSshTargetRegistryStore(store: SshConnectionStore | null): void { @@ -36,20 +41,24 @@ export function getSshTargetRegistryStore(): SshConnectionStore | null { } export function setSshTargetRegistryHandlers(handlers: { - connect: ((targetId: string) => Promise) | null + connect: ConnectSshTarget | null getState: ((targetId: string) => SshConnectionState | undefined) | null }): void { registeredConnectSshTarget = handlers.connect registeredGetSshState = handlers.getState } -export async function connectRegisteredSshTarget(targetId: string): Promise { +/** `automatic`: no user asked for this connect, so a cancelled sign-in stays cancelled. */ +export async function connectRegisteredSshTarget( + targetId: string, + options?: { automatic?: boolean } +): Promise { if (!registeredConnectSshTarget) { // Why this still throws: a headless host that never registered handlers must fail // loudly rather than report a target as unreachable, which would read as `exited`. throw new Error('ssh_handlers_not_registered') } - return registeredConnectSshTarget(targetId) + return registeredConnectSshTarget(targetId, options) } export function getRegisteredSshState(targetId: string): SshConnectionState | undefined { diff --git a/src/renderer/src/web/preload-api/web-terminal-api.ts b/src/renderer/src/web/preload-api/web-terminal-api.ts index 579021b68f2..2e0432137b3 100644 --- a/src/renderer/src/web/preload-api/web-terminal-api.ts +++ b/src/renderer/src/web/preload-api/web-terminal-api.ts @@ -159,7 +159,7 @@ export function createSshApi(): NonNullable['ssh']> { connect: async (args) => { const { state } = await callRuntimeResult<{ state: SshConnectionState | null }>( 'ssh.connect', - { targetId: args.targetId } + { targetId: args.targetId, automatic: args.automatic } ) return state }, diff --git a/src/renderer/src/web/web-preload-api-ssh.test.ts b/src/renderer/src/web/web-preload-api-ssh.test.ts index 9e146cf81d6..87df206c8a8 100644 --- a/src/renderer/src/web/web-preload-api-ssh.test.ts +++ b/src/renderer/src/web/web-preload-api-ssh.test.ts @@ -63,9 +63,12 @@ describe('web SSH preload API', () => { expect(partial).toMatchObject({ providerEpoch: 'partial-provider-epoch' }) expect(partial).not.toHaveProperty('connectionGeneration') + // A background connect stays automatic on the host, so it cannot lift a cancelled sign-in. + await globals.window.api.ssh.connect({ targetId: 'ssh-1', automatic: true }) expect(runtimeCalls).toEqual([ { method: 'ssh.connect', params: { targetId: 'ssh-1' } }, - { method: 'ssh.getState', params: { targetId: 'ssh-1' } } + { method: 'ssh.getState', params: { targetId: 'ssh-1' } }, + { method: 'ssh.connect', params: { targetId: 'ssh-1', automatic: true } } ]) }) }) diff --git a/src/shared/rpc-contract/rpc-params-catalog.generated.ts b/src/shared/rpc-contract/rpc-params-catalog.generated.ts index f19fb603545..3c8d830db0e 100644 --- a/src/shared/rpc-contract/rpc-params-catalog.generated.ts +++ b/src/shared/rpc-contract/rpc-params-catalog.generated.ts @@ -505,7 +505,14 @@ import { DictationStart, SpeechModelAction } from './speech-params' -import { RepoAddRemote, SshBrowseDir, SshTarget, SshTargetAdd, SshTargetUpdate } from './ssh-params' +import { + RepoAddRemote, + SshBrowseDir, + SshConnect, + SshTarget, + SshTargetAdd, + SshTargetUpdate +} from './ssh-params' import { ContinueInterruptedParams } from './structured-agent-session-continue-params' import { AcknowledgeAttentionParams, @@ -1236,7 +1243,7 @@ export const RPC_PARAMS_BY_METHOD = { 'speech.models.list': null, 'ssh.addTarget': SshTargetAdd, 'ssh.browseDir': SshBrowseDir, - 'ssh.connect': SshTarget, + 'ssh.connect': SshConnect, 'ssh.getState': SshTarget, 'ssh.listEditableTargets': null, 'ssh.listRemovedTargetLabels': null, diff --git a/src/shared/rpc-contract/ssh-params.ts b/src/shared/rpc-contract/ssh-params.ts index c64481b4599..878c281e727 100644 --- a/src/shared/rpc-contract/ssh-params.ts +++ b/src/shared/rpc-contract/ssh-params.ts @@ -10,6 +10,11 @@ export const SshTarget = z.object({ targetId: z.string().min(1) }) +// Why `automatic`: a client's background connect must not lift the host's cancelled sign-in. +export const SshConnect = SshTarget.extend({ + automatic: z.boolean().optional() +}) + const boundedText = (max: number) => z.string().trim().min(1).max(max) // Why strict: an unknown key (a password, a passphrase, or a main-owned field such as a From a46dfac35ec58c3beeecfc0288b15ff8aa1a3405 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:10:38 -0700 Subject: [PATCH 29/53] chore(i18n): translate new keys to es/fr/ja/ko/zh Scan 2026-10-14: fill target-locale coverage for PRs merged since the 2026-10-13 scan (#28486). Adds the missing keys (es 98, fr/ja/ko/zh 125 each), updates the two changed WorktreeJumpPalette strings, and fixes pre-existing mistranslations: the ZCode missing-TUI notice (English in all five locales), fr settings search keywords (procuration/indigene), zh workspace-cleanup browse MT, 6 zh keys that still used Latin "Agent", and the zh AccountsPane credential-help cluster (leftover English and a mangled __Host-console_session token). Leaves the 11 pinned provider-credit-balance keys plus the 3 pinned Chat UI keys as English fallback by design. Records the scan in config/update-translation.txt. --- config/update-translation.txt | 50 ++++++ src/renderer/src/i18n/locales/es.json | 146 +++++++++++++-- src/renderer/src/i18n/locales/fr.json | 183 +++++++++++++++++-- src/renderer/src/i18n/locales/ja.json | 177 ++++++++++++++++-- src/renderer/src/i18n/locales/ko.json | 177 ++++++++++++++++-- src/renderer/src/i18n/locales/zh.json | 247 +++++++++++++++++++++----- 6 files changed, 887 insertions(+), 93 deletions(-) diff --git a/config/update-translation.txt b/config/update-translation.txt index ecf51cfd151..b49e70a5b53 100644 --- a/config/update-translation.txt +++ b/config/update-translation.txt @@ -200,3 +200,53 @@ Method (reproducible): blockers->阻碍者); 7 zh keys still use Latin "Agent"; zh ZCode TUI title/description left English; fr search keywords "indigène"/"procuration". +## Scan 2026-10-14 + +- HEAD scanned: 7244bf3e6f (origin/main). Base: e8cd6dcb184 — the previous scan's PR + (#28486) already merged upstream, so the branch was reset onto origin/main and this + window is e8cd6dcb184 .. HEAD. (No delta is lost: the branch's own commit 9c778181ad + was the source of e8cd6dcb184.) +- Gates before fix: catalog verify PASS, coverage --check PASS (13 allowlisted), extraction + PASS, runtime-catalog PASS. No unkeyed strings and en.json in sync; the gap was + target-locale coverage only. +- Delta since base: 154 new en keys, 33 removed, 2 changed en values. Missing per locale: + es 112, fr/ja/ko/zh 139. 14 of the 139 are the pinned English-fallback keys (11 + provider-credit-balance + 3 Chat UI), so the translatable delta is es 98 / others 125. + (es already carried the 27 `updates.installBlock.*` keys from a separate es PR.) +- Applied the 98/125 new keys x5 via apply-translation-delta.mjs (pipeline repaired ja + より新しい->より新規 and zh search keyword jira->Jira). Strictly additive: +598 keys. +- Changed-value review: the two `WorktreeJumpPalette` search placeholders gained "tasks"; + updated all five locales (ko/zh re-normalized to 워크스페이스/工作区 by the pipeline). +- Pre-existing (out-of-delta) fixes this pass, per the mandate to fix badly translated + strings: + - `auto.components.terminal.pane.zcode.missing.tui.{title,description}` were byte-English + in ALL five locales -> translated x5. + - fr settings search keywords: `procuration`->`proxy` (legal vs network sense), + `indigène`->`natif` x2. + - zh `components.workspace.cleanup.browse.*` bad MT repaired (~27 values: host->主机, + path->路径, ticket->工单/门票, blockers->阻塞项 [aligned with `chip.kind.blocker`], + blockerMode->阻塞项匹配, PR/先生->PR / MR, ahead/behind->领先/落后, review->评审, + leftover English "left" dropped, …). + - 6 zh keys moved Latin "Agent" -> 智能体 (Skills filter/detail, native-chat follow-ups, + aiVaultUnavailableForHost). + - zh `AccountsPane` credential-help cluster: leftover English "auth cookie"/"auth" and a + mangled `__Host-console_session` code token restored. +- Gates after fix: catalog verify PASS (all five 16062/16076; the 14 omissions are the + pinned keys), coverage --check PASS (13 allowlisted), extraction PASS, runtime-catalog + PASS, locale vitest suites PASS (33 files, 304 tests). +- Reviews (4 of max 4 rounds; round 0 = 5 parallel translators, then 5 reviewers/round): + R1 applied 51 (es 4: casing titles + ZCode; fr 6: nouvel elision + ZCode + 3 keywords; + ja 5: hostUpdateRequired 新しい->新規 pipeline artifact + ZCode + casing commas; ko 5: + 남은->남음, casing descriptions + ZCode; zh 31: browse MT + Agent + ZCode + + unresponsiveSummary). R2 applied 4 zh (browse sort.ahead/behind, chip.kind.review/ + reviewState). R3 applied 2 zh (preparingSummary "before restarting" fidelity, + blockerModeAny grammar); es/fr/ja/ko signed off clean. R4 applied 3 zh (AccountsPane + cluster); no other locale needed a 4th round. +- Remaining out-of-delta findings for a future pass (pre-existing, not from this window): + ja/zh/fr settings search-alias MT (ja トゥイ/アンシ, グーグル/クロム/リナックス/…; fr + worktree->arbre de travail, board->conseil, remote->télécommande; zh `网络`->任一 remains + mix) — the right fix is a batch addition to `SEARCH_KEYWORD_OVERRIDES`; fr + "un nouveau espace" (missing `nouvel`) in 5 pre-existing keys; ko + `newWorkspace.createDescription` 에이전트 repetition; zh `jiraColumnStoryPoints` 点数 + (story points) and `jira.transitions.chooseResolution` 解决方案 (Jira field is 解决结果). + diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 74e4cd79330..4bae3943288 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -196,7 +196,8 @@ "readFailed": "No se pudo leer la configuración de agentes. Comprueba la conexión con el host e inténtalo de nuevo.", "saveFailed": "No se pudo guardar la configuración de agentes. Comprueba la conexión con el host e inténtalo de nuevo.", "hostDefaults": "Valores predeterminados para nuevos agentes en el host seleccionado.", - "save": "Guardar" + "save": "Guardar", + "clearDefault": "Borrar agente predeterminado" }, "followSymlinkedDirectoriesKeywords": { "symlink": "enlace simbólico", @@ -299,7 +300,12 @@ "taskUrl": { "loadingHint": "Cargando {{value0}}…", "createHint": "Crear árbol de trabajo a partir de {{value0}}" - } + }, + "tasksHeader": "Tareas", + "taskCreate": "Crear espacio de trabajo", + "taskSearchLoading": "Buscando tareas…", + "taskSearchFailed": "No se pudieron buscar algunas fuentes de tareas", + "taskSearchRetry": "Reintentar" }, "auto": { "App": { @@ -1052,6 +1058,10 @@ } } } + }, + "aiVaultResumeRefusal": { + "openInChat": "Esta conversación de {{value0}} está abierta en un chat. Actualiza el historial de sesiones para abrirla.", + "ownershipUnknown": "Orca no pudo confirmar quién es el propietario de esta conversación de {{value0}}. Actualiza el historial de sesiones e inténtalo de nuevo." } }, "hooks": { @@ -2327,7 +2337,14 @@ "jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again.", "loadPageUnreachable": "La página {{value0}} está más allá de lo que la búsqueda de GitHub puede devolver.", "loadPageFailed": "No se pudo cargar la página {{value0}} desde GitHub.", - "loadPageNoMoreResults": "No hay más resultados en la página {{value0}}." + "loadPageNoMoreResults": "No hay más resultados en la página {{value0}}.", + "jiraColumnsPicker": "Columnas", + "jiraColumnParent": "Padre", + "jiraColumnSprint": "Sprint", + "jiraColumnStoryPoints": "Puntos", + "jiraColumnOriginalEstimate": "Estimación", + "jiraColumnRemainingEstimate": "Restante", + "jiraBoardEmptyColumn": "Sin incidencias" }, "Terminal": { "73768427cf": "Cerrar", @@ -2447,8 +2464,8 @@ "projectBadge": "Proyecto", "repoGroupBadge": "Grupo de repositorios", "pluginCommandFailed": "No se pudo ejecutar el comando del plugin.", - "27f10cca63": "Busca chats, terminales, espacios de trabajo, ajustes y acciones...", - "2770f02910": "Busca chats, terminales, espacios de trabajo, ajustes y acciones", + "27f10cca63": "Busca chats, terminales, espacios de trabajo, tareas, ajustes y acciones...", + "2770f02910": "Busca chats, terminales, espacios de trabajo, tareas, ajustes y acciones", "recentChatsTerminalsHeader": "Chats y terminales recientes", "lastActiveTime": "Última actividad hace {{value0}}" }, @@ -3244,8 +3261,8 @@ "zcode": { "missing": { "tui": { - "title": "This ZCode build has no terminal UI", - "description": "Orca's hooks installed correctly — the zcode on your PATH just cannot open a session. The ZCode desktop app bundles the agent runtime without its terminal UI. Install a zcode that ships the TUI, then run zcode outside Orca to confirm." + "title": "Esta compilación de ZCode no tiene interfaz de terminal", + "description": "Los hooks de Orca se instalaron correctamente — el zcode en tu PATH simplemente no puede abrir una sesión. La aplicación de escritorio de ZCode incluye el entorno de ejecución del agente sin su interfaz de terminal. Instala un zcode que incluya la TUI y luego ejecuta zcode fuera de Orca para confirmarlo." } } }, @@ -3442,7 +3459,8 @@ "sessionQueued": "Sesión en cola", "openSupportedWorkspace": "Abre un espacio de trabajo antes de reanudar una sesión.", "localSessionSshWorkspaceUnsupported": "El historial de esta sesión solo existe en este equipo. Para reanudarla, arrástrala a un workspace local. Los workspaces SSH no tienen acceso a ese historial.", - "sessionHostMismatchUnsupported": "Esta sesión pertenece a un host diferente. Suéltala en un espacio de trabajo del mismo host." + "sessionHostMismatchUnsupported": "Esta sesión pertenece a un host diferente. Suéltala en un espacio de trabajo del mismo host.", + "oldPayload": "Esta sesión se arrastró desde una ventana de Orca más antigua. Reanúdala desde el historial de sesiones." }, "TabGroupDropOverlay": { "paneColumnLabel": "Nueva división" @@ -7553,7 +7571,11 @@ "compareAgainstUpstreamDescription": "Elige qué base usa Control de código fuente de forma predeterminada para las comparaciones de cambios confirmados. El upstream de la rama sigue automáticamente la rama actual y recurre a la rama predeterminada del repositorio cuando no hay upstream. Aun así, puedes cambiar la base de comparación por worktree desde el panel Git de ese worktree. Los objetivos de Pull Request y rebase no cambian.", "compareBaseRepositoryDefault": "Predeterminada del repositorio", "compareBaseBranchUpstream": "Upstream de la rama", - "refreshLocalBaseDescription": "Cuando creas un espacio de trabajo, Orca actualiza la base remota y adelanta de forma segura tu rama local correspondiente, como main o master. Esto evita que comandos como git diff main...HEAD comparen con un historial obsoleto. Orca omite la actualización si esa rama tiene cambios sin commit o commits solo locales." + "refreshLocalBaseDescription": "Cuando creas un espacio de trabajo, Orca actualiza la base remota y adelanta de forma segura tu rama local correspondiente, como main o master. Esto evita que comandos como git diff main...HEAD comparen con un historial obsoleto. Orca omite la actualización si esa rama tiene cambios sin commit o commits solo locales.", + "jiraIssueKeyCasingTitle": "Mayúsculas y minúsculas de la clave de Jira", + "jiraIssueKeyCasingDescription": "Formato de mayúsculas de la clave de incidencia de Jira en los nombres de espacio de trabajo y de rama que Orca sugiere para nuevos espacios de trabajo vinculados a Jira, como PROJ-123-fix-login en lugar de proj-123-fix-login. Los espacios de trabajo existentes y los nombres que escribes nunca se modifican.", + "jiraIssueKeyCasingLowercase": "Minúsculas", + "jiraIssueKeyCasingPreserve": "Clave en mayúsculas" }, "HiddenExperimentalGroup": { "7c4e18d2f6": "Activa los gestos de captura del fallo de negrita en esta máquina: {{samplingShortcut}} inicia una ráfaga de muestreo; {{captureShortcut}} captura evidencia del panel en disco.", @@ -10028,7 +10050,13 @@ "defaultBranch": "rama predeterminada", "repositoryDefault": "predeterminada del repositorio", "branchUpstream": "upstream de la rama", - "diffBase": "base diferencial" + "diffBase": "base diferencial", + "jiraIssueKeyCasingTitle": "Mayúsculas y minúsculas de la clave de Jira", + "jiraIssueKeyCasingDescription": "Formato de mayúsculas de la clave de incidencia de Jira en los nombres de espacio de trabajo y de rama que Orca sugiere para nuevos espacios de trabajo vinculados a Jira, como PROJ-123-fix-login en lugar de proj-123-fix-login. Los espacios de trabajo existentes y los nombres que escribes nunca se modifican.", + "jira": "jira", + "issueKey": "clave de incidencia", + "uppercase": "mayúsculas", + "lowercase": "minúsculas" } }, "input": { @@ -19041,6 +19069,42 @@ }, "automations": { "runTerminalUnavailable": "La terminal de ejecución no está disponible." + }, + "jiraIssueTitleField": { + "generate": "Generar título a partir de la descripción", + "stop": "Detener la generación del título", + "canceled": "Generación cancelada.", + "webUnavailable": "La generación de títulos de Jira no está disponible en el cliente web.", + "hostUpdateRequired": "Generar títulos de Jira requiere un servidor de Orca más reciente. Actualiza el host de la conexión de Jira e inténtalo de nuevo.", + "generating": "Generando título. Haz clic para detener.", + "failed": "No se pudo generar un resumen a partir de la descripción." + }, + "agentCombobox": { + "clearSelection": "Borrar selección de agente" + }, + "newWorkspace": { + "agentOptional": "Agente [Opcional]", + "createDescription": "Elige el proyecto y el nombre del espacio de trabajo. Selecciona un agente si quieres iniciar uno." + }, + "JiraIssueWorkspace": { + "pendingChange": "Ya hay otro cambio de incidencia en curso." + }, + "jiraProjectPicker": { + "search": "Buscar proyectos de Jira", + "empty": "No se encontraron proyectos.", + "label": "Proyecto de Jira", + "all": "Todos los proyectos de Jira" + }, + "RepositoryJiraProjectSection": { + "saveFailed": "No se pudo guardar el proyecto de Jira predeterminado.", + "runtimeUpdate": "Actualiza este entorno de ejecución para guardar un proyecto de Jira predeterminado.", + "title": "Proyecto de Jira predeterminado", + "description": "Usa este proyecto de forma predeterminada para el selector de incidencias de Jira y la lista de Tareas. Puedes cambiarlo para cada worktree.", + "none": "Sin proyecto predeterminado", + "connect": "Conecta Jira en Integraciones para elegir un proyecto.", + "keywordJira": "jira", + "keywordDefaultProject": "proyecto predeterminado", + "keywordIssuePicker": "selector de incidencias" } }, "dashboardPopout": { @@ -19602,7 +19666,8 @@ } }, "common": { - "retry": "Reintentar" + "retry": "Reintentar", + "cancel": "Cancelar" }, "terminal": { "codexTerminalServerIsolationNotice": { @@ -19941,6 +20006,65 @@ "jql": "JQL del filtro de estado", "editJql": "Editar como JQL", "catalogHint": "Los estados provienen de las incidencias cargadas. Borra los estados para descubrir otros en esta vista." + }, + "connect": { + "scopedTokenGuide": "Los tokens con alcance necesitan read:jira-work, write:jira-work y read:jira-user. Deja el email vacío para usar autenticación Bearer.", + "oldHostGuide": "Este entorno de ejecución remoto requiere un email y un token de API sin alcance. Actualiza el entorno de ejecución remoto para usar tokens con alcance.", + "scopedDescription": "Usa la URL de un sitio de Jira Cloud y un token de API para explorar incidencias. Los tokens con alcance pueden omitir el email.", + "optionalEmail": "Email de Atlassian (opcional para tokens con alcance)" + }, + "workspace": { + "open": "Abrir el espacio de trabajo vinculado a {{key}}", + "count": "{{count}} espacios de trabajo vinculados a {{key}}", + "startAnother": "Iniciar un espacio de trabajo nuevo", + "empty": "Vincula una incidencia de Jira a un espacio de trabajo o elige otra vista.", + "openFailed": "No se pudo abrir el espacio de trabajo vinculado a esta incidencia de Jira.", + "mode": "Con espacio de trabajo", + "search": "Buscar incidencias vinculadas", + "single": "1 espacio de trabajo vinculado a {{key}}" + }, + "prompt": { + "dialogTitle": "Copiar prompt desde plantilla", + "template": "Plantilla", + "custom": "Plantilla personalizada", + "templateText": "Texto de la plantilla", + "variables": "Usa $jira-issue para el contexto de la incidencia y $other-prompt para tus instrucciones adicionales. Se guarda en este dispositivo.", + "other": "Instrucciones adicionales (opcional)", + "preview": "Vista previa", + "implementation": "Iniciar implementación", + "bug": "Investigar error", + "testPlan": "Escribir plan de pruebas", + "prSummary": "Redactar resumen de PR" + }, + "copy": { + "context": "Copiar contexto de {{key}}", + "actions": "Copiar contexto de la incidencia" + }, + "transitions": { + "changeStatus": "Cambiar estado: {{status}}", + "loadError": "No se pudieron cargar los cambios de estado.", + "retry": "Reintentar", + "loading": "Cargando cambios de estado…", + "completeInJira": "Completa “{{name}}” en Jira", + "unsupported": "Jira requiere campos que Orca aún no puede recopilar: {{fields}}.", + "chooseResolution": "Elige una resolución", + "comment": "Comentario", + "back": "Atrás", + "apply": "Cambiar estado", + "empty": "No hay cambios de estado disponibles." + }, + "board": { + "currentSprint": "Sprint actual", + "viewMode": "Vista de Jira", + "list": "Lista", + "board": "Tablero", + "moveIssue": "Mover {{key}}", + "moveTitle": "Mover {{key}} a {{column}}", + "moveDescription": "Jira controla qué cambios de estado están disponibles y qué campos requieren.", + "chooseTransition": "Elige un cambio de estado:", + "cancel": "Cancelar", + "noTransition": "No hay ningún cambio de estado a {{column}} disponible para {{key}}.", + "queryTooLong": "La consulta de Jira es demasiado larga. Acorta la consulta o borra los filtros." } } } diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 97e5b6b5048..5e069bfd51d 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -300,7 +300,8 @@ "readFailed": "Impossible de lire les paramètres des agents. Vérifiez la connexion à l’hôte et réessayez.", "saveFailed": "Impossible d’enregistrer les paramètres des agents. Vérifiez la connexion à l’hôte et réessayez.", "hostDefaults": "Paramètres par défaut des nouveaux agents sur l’hôte sélectionné.", - "save": "Enregistrer" + "save": "Enregistrer", + "clearDefault": "Effacer l'agent par défaut" }, "followSymlinkedDirectoriesKeywords": { "symlink": "lien symbolique", @@ -403,7 +404,12 @@ "taskUrl": { "loadingHint": "Chargement de {{value0}}…", "createHint": "Créer un worktree à partir de {{value0}}" - } + }, + "tasksHeader": "Tâches", + "taskCreate": "Créer un espace de travail", + "taskSearchLoading": "Recherche des tâches…", + "taskSearchFailed": "Certaines sources de tâches n'ont pas pu être recherchées", + "taskSearchRetry": "Réessayer" }, "auto": { "App": { @@ -1156,6 +1162,10 @@ } } } + }, + "aiVaultResumeRefusal": { + "openInChat": "Cette conversation {{value0}} est ouverte dans un chat. Actualisez l'historique des sessions pour l'ouvrir.", + "ownershipUnknown": "Orca n'a pas pu confirmer à qui appartient cette conversation {{value0}}. Actualisez l'historique des sessions et réessayez." } }, "hooks": { @@ -2435,7 +2445,14 @@ "linearModeHasWorktreeTooltip": "Tickets Linear liés à un espace de travail Orca", "linearEmptyHasWorktree": "Aucun ticket Linear n'est encore lié à un espace de travail Orca. Démarrez le travail depuis une issue Linear pour le voir ici.", "linearOpenAttachedWorkspace": "Ouvrir l'espace de travail attaché à {{value0}}", - "linearWorktreesColumn": "Espaces de travail" + "linearWorktreesColumn": "Espaces de travail", + "jiraColumnsPicker": "Colonnes", + "jiraColumnParent": "Parent", + "jiraColumnSprint": "Sprint", + "jiraColumnStoryPoints": "Points", + "jiraColumnOriginalEstimate": "Estimation", + "jiraColumnRemainingEstimate": "Restant", + "jiraBoardEmptyColumn": "Aucune issue" }, "Terminal": { "73768427cf": "Fermer", @@ -2556,8 +2573,8 @@ "repoGroupBadge": "Groupe de dépôts", "pluginCommandFailed": "Impossible d'exécuter la commande du plugin.", "recentChatsTerminalsHeader": "Chats et terminaux récents", - "27f10cca63": "Rechercher chats, terminaux, espaces de travail, paramètres et actions...", - "2770f02910": "Rechercher chats, terminaux, espaces de travail, paramètres et actions", + "27f10cca63": "Rechercher chats, terminaux, espaces de travail, tâches, paramètres et actions...", + "2770f02910": "Rechercher chats, terminaux, espaces de travail, tâches, paramètres et actions", "lastActiveTime": "Dernière activité il y a {{value0}}" }, "github": { @@ -3371,8 +3388,8 @@ "zcode": { "missing": { "tui": { - "title": "This ZCode build has no terminal UI", - "description": "Orca's hooks installed correctly — the zcode on your PATH just cannot open a session. The ZCode desktop app bundles the agent runtime without its terminal UI. Install a zcode that ships the TUI, then run zcode outside Orca to confirm." + "title": "Ce build ZCode n'a pas d'interface de terminal", + "description": "Les hooks d'Orca se sont installés correctement — le zcode présent dans votre PATH ne peut tout simplement pas ouvrir de session. L'application de bureau ZCode intègre le runtime de l'agent sans son interface de terminal. Installez un zcode qui inclut la TUI, puis exécutez zcode en dehors d'Orca pour confirmer." } } }, @@ -3550,7 +3567,8 @@ "sessionQueued": "Session en attente", "openSupportedWorkspace": "Ouvrez un espace de travail avant de reprendre une session.", "sessionHostMismatchUnsupported": "Cette session appartient à un autre hôte. Déposez-la sur un espace de travail du même hôte.", - "localSessionSshWorkspaceUnsupported": "L'historique de cette session est stocké sur cette machine ; la reprise est impossible dans un espace de travail SSH. Déposez-la plutôt sur un espace de travail local." + "localSessionSshWorkspaceUnsupported": "L'historique de cette session est stocké sur cette machine ; la reprise est impossible dans un espace de travail SSH. Déposez-la plutôt sur un espace de travail local.", + "oldPayload": "Cette session a été glissée depuis une fenêtre Orca plus ancienne. Reprenez-la plutôt depuis l'historique des sessions." }, "TabGroupDropOverlay": { "paneColumnLabel": "Nouvelle scission" @@ -7698,7 +7716,11 @@ "compareAgainstUpstreamDescription": "Choisissez la base que le contrôle de code source utilise par défaut pour comparer les changements commités. L'amont de la branche suit automatiquement la branche courante et revient à la branche par défaut du dépôt en l'absence d'amont. Vous pouvez toujours changer la base de comparaison d'un worktree depuis son panneau Git. Les cibles de pull request et de rebase ne changent pas.", "compareBaseRepositoryDefault": "Valeur par défaut du dépôt", "compareBaseBranchUpstream": "Amont de la branche", - "refreshLocalBaseDescription": "Quand vous créez un espace de travail, Orca actualise la base distante et fait avancer en toute sécurité votre branche locale correspondante (fast-forward), telle que main ou master. Cela évite que des commandes comme git diff main...HEAD comparent avec un historique périmé. Orca ignore la mise à jour si cette branche contient des modifications non commitées ou des commits uniquement locaux." + "refreshLocalBaseDescription": "Quand vous créez un espace de travail, Orca actualise la base distante et fait avancer en toute sécurité votre branche locale correspondante (fast-forward), telle que main ou master. Cela évite que des commandes comme git diff main...HEAD comparent avec un historique périmé. Orca ignore la mise à jour si cette branche contient des modifications non commitées ou des commits uniquement locaux.", + "jiraIssueKeyCasingTitle": "Casse de la clé Jira", + "jiraIssueKeyCasingDescription": "Casse de la clé de l'issue Jira dans les noms d'espace de travail et de branche qu'Orca suggère pour les nouveaux espaces de travail liés à Jira, par exemple PROJ-123-fix-login au lieu de proj-123-fix-login. Les espaces de travail existants et les noms que vous saisissez ne sont jamais modifiés.", + "jiraIssueKeyCasingLowercase": "Minuscules", + "jiraIssueKeyCasingPreserve": "Clé en majuscules" }, "InputPane": { "db15068196": "Activé par défaut sur Linux et macOS. Linux utilise le presse-papiers de sélection du système ; les autres plateformes utilisent un tampon privé.", @@ -9687,7 +9709,7 @@ }, "sshWorkspaceRouting": { "ssh": "ssh", - "proxy": "procuration", + "proxy": "proxy", "tunnel": "tunnel", "routing": "routage", "network": "réseau" @@ -9695,7 +9717,7 @@ "userAgent": { "identity": "identité", "userAgent": "agent utilisateur", - "native": "indigène", + "native": "natif", "cleaned": "nettoyé", "restart": "redémarrage" } @@ -9896,7 +9918,7 @@ "nativeChat": { "title": "Chat UI", "grok": "grok", - "native": "indigène", + "native": "natif", "chat": "chat", "claude": "claude", "codex": "codex", @@ -10178,7 +10200,13 @@ "localChanges": "modifications locales", "originMaster": "origin/master", "committedChanges": "modifications committées", - "diffBase": "base de différence" + "diffBase": "base de différence", + "jiraIssueKeyCasingTitle": "Casse de la clé Jira", + "jiraIssueKeyCasingDescription": "Casse de la clé de l'issue Jira dans les noms d'espace de travail et de branche qu'Orca suggère pour les nouveaux espaces de travail liés à Jira, par exemple PROJ-123-fix-login au lieu de proj-123-fix-login. Les espaces de travail existants et les noms que vous saisissez ne sont jamais modifiés.", + "jira": "jira", + "issueKey": "clé d'issue", + "uppercase": "majuscules", + "lowercase": "minuscules" } }, "input": { @@ -19145,6 +19173,42 @@ }, "automations": { "runTerminalUnavailable": "Le terminal d'exécution est indisponible." + }, + "jiraIssueTitleField": { + "generate": "Générer le titre à partir de la description", + "stop": "Arrêter la génération du titre", + "canceled": "Génération annulée.", + "webUnavailable": "La génération de titres Jira est indisponible dans le client web.", + "hostUpdateRequired": "La génération de titres Jira nécessite un serveur Orca plus récent. Mettez à jour l'hôte de la connexion Jira, puis réessayez.", + "generating": "Génération du titre. Cliquez pour arrêter.", + "failed": "Impossible de générer un résumé à partir de la description." + }, + "agentCombobox": { + "clearSelection": "Effacer la sélection d'agent" + }, + "newWorkspace": { + "agentOptional": "Agent (facultatif)", + "createDescription": "Choisissez le projet et le nom de l'espace de travail. Sélectionnez un agent si vous souhaitez en démarrer un." + }, + "JiraIssueWorkspace": { + "pendingChange": "Une autre modification d'issue est en cours." + }, + "jiraProjectPicker": { + "search": "Rechercher des projets Jira", + "empty": "Aucun projet trouvé.", + "label": "Projet Jira", + "all": "Tous les projets Jira" + }, + "RepositoryJiraProjectSection": { + "saveFailed": "Impossible d'enregistrer le projet Jira par défaut.", + "runtimeUpdate": "Mettez à jour ce runtime pour enregistrer un projet Jira par défaut.", + "title": "Projet Jira par défaut", + "description": "Faites de ce projet le projet par défaut du sélecteur d'issues Jira et de la liste Tâches. Vous pouvez le modifier pour chaque worktree.", + "none": "Aucun projet par défaut", + "connect": "Connectez Jira dans Intégrations pour choisir un projet.", + "keywordJira": "jira", + "keywordDefaultProject": "projet par défaut", + "keywordIssuePicker": "sélecteur d'issues" } }, "dashboardPopout": { @@ -19507,7 +19571,8 @@ "continueInCliNewTab": "Continuer dans la CLI dans un nouvel onglet" }, "common": { - "retry": "Réessayer" + "retry": "Réessayer", + "cancel": "Annuler" }, "fileExplorer": { "root": { @@ -19910,6 +19975,96 @@ "jql": "JQL du filtre de statut", "editJql": "Modifier en JQL", "catalogHint": "Les statuts proviennent des tickets chargés. Effacez les statuts pour en découvrir d’autres dans cette vue." + }, + "connect": { + "scopedTokenGuide": "Les jetons à portée limitée nécessitent read:jira-work, write:jira-work et read:jira-user. Laissez l'e-mail vide pour utiliser l'authentification Bearer.", + "oldHostGuide": "Ce runtime distant exige un e-mail et un jeton API non limité. Mettez à jour le runtime distant pour utiliser des jetons à portée limitée.", + "scopedDescription": "Utilisez l'URL d'un site Jira Cloud et un jeton API pour parcourir les issues. Les jetons à portée limitée peuvent omettre l'e-mail.", + "optionalEmail": "E-mail Atlassian (facultatif pour les jetons à portée limitée)" + }, + "workspace": { + "open": "Ouvrir l'espace de travail lié à {{key}}", + "count": "{{count}} espaces de travail liés à {{key}}", + "startAnother": "Démarrer un nouvel espace de travail", + "empty": "Liez une issue Jira à un espace de travail, ou choisissez une autre vue.", + "openFailed": "Impossible d'ouvrir l'espace de travail lié à cette issue Jira.", + "mode": "Avec espace de travail", + "search": "Rechercher les issues liées", + "single": "1 espace de travail lié à {{key}}" + }, + "prompt": { + "dialogTitle": "Copier le prompt depuis un modèle", + "template": "Modèle", + "custom": "Modèle personnalisé", + "templateText": "Texte du modèle", + "variables": "Utilisez $jira-issue pour le contexte de l'issue et $other-prompt pour vos instructions supplémentaires. Enregistré sur cet appareil.", + "other": "Instructions supplémentaires (facultatif)", + "preview": "Aperçu", + "implementation": "Lancer l'implémentation", + "bug": "Analyser le bug", + "testPlan": "Rédiger un plan de test", + "prSummary": "Rédiger un résumé de PR" + }, + "copy": { + "context": "Copier le contexte pour {{key}}", + "actions": "Copier le contexte de l'issue" + }, + "transitions": { + "changeStatus": "Changer le statut : {{status}}", + "loadError": "Impossible de charger les changements de statut.", + "retry": "Réessayer", + "loading": "Chargement des changements de statut…", + "completeInJira": "Terminez « {{name}} » dans Jira", + "unsupported": "Jira exige des champs qu'Orca ne peut pas encore collecter : {{fields}}.", + "chooseResolution": "Choisissez une résolution", + "comment": "Commentaire", + "back": "Retour", + "apply": "Changer le statut", + "empty": "Aucun changement de statut disponible." + }, + "board": { + "currentSprint": "Sprint en cours", + "viewMode": "Vue Jira", + "list": "Liste", + "board": "Tableau", + "moveIssue": "Déplacer {{key}}", + "moveTitle": "Déplacer {{key}} vers {{column}}", + "moveDescription": "Jira contrôle les changements de statut disponibles et les champs qu'ils exigent.", + "chooseTransition": "Choisissez un changement de statut :", + "cancel": "Annuler", + "noTransition": "Aucun changement de statut vers {{column}} n'est disponible pour {{key}}.", + "queryTooLong": "La requête Jira est trop longue. Raccourcissez la requête ou effacez les filtres." + } + }, + "updates": { + "installBlock": { + "instancesLabel": "Instances utilisant Orca", + "browserLabel": "Navigateur en arrière-plan", + "serverLabel": "Serveur en arrière-plan", + "windowLabel": "Autre fenêtre Orca", + "instanceLabel": "Autre instance Orca", + "processId": "PID {{pid}}", + "preparingTitle": "Préparation de votre mise à jour", + "preparingSummary": "Vérification des instances Orca en cours d'exécution avant de redémarrer.", + "later": "Plus tard", + "retainedDownload": "Orca reste ouvert. Pas besoin de retélécharger.", + "retryInstallation": "Réessayer l'installation", + "waitingLabel": "Mise à jour en attente", + "busyTitle": "Un autre Orca est en cours de mise à jour", + "busySummary": "Une mise à jour est déjà en cours d'installation par une autre instance Orca.", + "busyGuidance": "Attendez un instant, puis réessayez. Votre mise à jour téléchargée est prête.", + "unresponsiveTitle": "En attente des serveurs en arrière-plan", + "unresponsiveSummary": "Orca n'a pas pu terminer la mise en pause de ses serveurs en arrière-plan pour la mise à jour.", + "unresponsiveGuidance": "Arrêtez-le depuis le terminal ou le service qui l'a démarré, puis réessayez. Les appareils connectés seront déconnectés.", + "readyTitle": "Votre mise à jour est prête", + "serverSummary": "Un serveur Orca en arrière-plan utilise encore cette app.", + "instanceSummary": "Une autre instance Orca utilise encore cette app.", + "serverGuidance": "Arrêtez le serveur depuis le terminal ou le service qui l'a démarré, puis réessayez. Les appareils connectés seront déconnectés.", + "mixedGuidance": "Fermez les autres fenêtres Orca et arrêtez le serveur depuis le terminal ou le service qui l'a démarré. Les appareils connectés seront déconnectés. Réessayez ensuite.", + "instanceGuidance": "Quittez les autres instances Orca, puis réessayez. Votre mise à jour téléchargée restera prête.", + "waitingAriaLabel": "Mise à jour en attente d'autres instances Orca. Cliquez pour développer.", + "preparingLabel": "Préparation de la mise à jour", + "preparingAriaLabel": "Préparation de la mise à jour. Cliquez pour développer." } } } diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 388df378b4c..35a7f216487 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -196,7 +196,8 @@ "readFailed": "Agent設定を読み込めませんでした。ホストへの接続を確認して再試行してください。", "saveFailed": "Agent設定を保存できませんでした。ホストへの接続を確認して再試行してください。", "hostDefaults": "選択したホストで起動する新しいAgentの既定値。", - "save": "保存" + "save": "保存", + "clearDefault": "デフォルトの Agent をクリア" }, "followSymlinkedDirectoriesKeywords": { "symlink": "シンボリックリンク", @@ -299,7 +300,12 @@ "taskUrl": { "loadingHint": "{{value0}}を読み込んでいます…", "createHint": "{{value0}} からワークツリーを作成" - } + }, + "tasksHeader": "タスク", + "taskCreate": "ワークスペースを作成", + "taskSearchLoading": "タスクを検索しています…", + "taskSearchFailed": "一部のタスクソースを検索できませんでした", + "taskSearchRetry": "再試行" }, "auto": { "App": { @@ -1052,6 +1058,10 @@ } } } + }, + "aiVaultResumeRefusal": { + "openInChat": "この {{value0}} の会話はチャットで開かれています。開くにはセッション履歴を更新してください。", + "ownershipUnknown": "Orca はこの {{value0}} の会話の所有者を確認できませんでした。セッション履歴を更新して、再試行してください。" } }, "hooks": { @@ -2327,7 +2337,14 @@ "jiraLinkSourceUnavailable": "この Jira Issue をリンクできませんでした。Jira に再接続するか該当するサイトを選択してから、もう一度お試しください。", "loadPageUnreachable": "ページ {{value0}} は GitHub 検索が返せる範囲を超えています。", "loadPageFailed": "GitHub からページ {{value0}} を読み込めませんでした。", - "loadPageNoMoreResults": "ページ {{value0}} にはこれ以上結果がありません。" + "loadPageNoMoreResults": "ページ {{value0}} にはこれ以上結果がありません。", + "jiraColumnsPicker": "列", + "jiraColumnParent": "親", + "jiraColumnSprint": "スプリント", + "jiraColumnStoryPoints": "ポイント", + "jiraColumnOriginalEstimate": "見積もり", + "jiraColumnRemainingEstimate": "残り", + "jiraBoardEmptyColumn": "Issue はありません" }, "Terminal": { "73768427cf": "閉じる", @@ -2447,8 +2464,8 @@ "projectBadge": "プロジェクト", "repoGroupBadge": "リポジトリグループ", "pluginCommandFailed": "プラグインのコマンドを実行できませんでした。", - "27f10cca63": "チャット、ターミナル、ワークスペース、設定、操作を検索します…", - "2770f02910": "チャット、ターミナル、ワークスペース、設定、操作を検索します", + "27f10cca63": "チャット、ターミナル、ワークスペース、タスク、設定、操作を検索します…", + "2770f02910": "チャット、ターミナル、ワークスペース、タスク、設定、操作を検索します", "recentChatsTerminalsHeader": "最近のチャットとターミナル", "lastActiveTime": "最後にアクティブになったのは{{value0}}前です" }, @@ -3244,8 +3261,8 @@ "zcode": { "missing": { "tui": { - "title": "This ZCode build has no terminal UI", - "description": "Orca's hooks installed correctly — the zcode on your PATH just cannot open a session. The ZCode desktop app bundles the agent runtime without its terminal UI. Install a zcode that ships the TUI, then run zcode outside Orca to confirm." + "title": "この ZCode ビルドにはターミナル UI がありません", + "description": "Orca のフックは正しくインストールされていますが、PATH 上の zcode はセッションを開けません。ZCode デスクトップアプリには、ターミナル UI のない Agent ランタイムが同梱されています。TUI を含む zcode をインストールし、Orca の外部で zcode を実行して確認してください。" } } }, @@ -3442,7 +3459,8 @@ "sessionQueued": "セッションをキューに追加しました", "openSupportedWorkspace": "セッションを再開する前に、ワークスペースを開いてください。", "localSessionSshWorkspaceUnsupported": "このセッションの履歴はこのマシンに保存されているため、SSH ワークスペースでは再開できません。代わりにローカルワークスペースにドロップしてください。", - "sessionHostMismatchUnsupported": "このセッションは別のホストに属しています。同じホスト上のワークスペースにドロップしてください。" + "sessionHostMismatchUnsupported": "このセッションは別のホストに属しています。同じホスト上のワークスペースにドロップしてください。", + "oldPayload": "このセッションは古い Orca ウィンドウからドラッグされました。代わりにセッション履歴から再開してください。" }, "TabGroupDropOverlay": { "paneColumnLabel": "新規分割" @@ -7575,7 +7593,11 @@ "compareAgainstUpstreamDescription": "ソース管理がコミット済み変更の比較に既定で使うベースを選択します。ブランチの upstream は現在のブランチに自動的に追従し、upstream がない場合はリポジトリの既定ブランチにフォールバックします。比較ベースは各ワークツリーの Git パネルからワークツリーごとに変更できます。Pull Request やリベースの対象は変更されません。", "compareBaseRepositoryDefault": "リポジトリ既定", "compareBaseBranchUpstream": "ブランチの upstream", - "refreshLocalBaseDescription": "ワークスペースを作成すると、Orca はリモートのベースを更新し、main や master など、対応するローカルブランチを安全に fast-forward します。これにより、git diff main...HEAD などのコマンドが古い履歴と比較されるのを防ぎます。対象のブランチにコミットしていない変更やローカルのみのコミットがある場合、Orca は更新をスキップします。" + "refreshLocalBaseDescription": "ワークスペースを作成すると、Orca はリモートのベースを更新し、main や master など、対応するローカルブランチを安全に fast-forward します。これにより、git diff main...HEAD などのコマンドが古い履歴と比較されるのを防ぎます。対象のブランチにコミットしていない変更やローカルのみのコミットがある場合、Orca は更新をスキップします。", + "jiraIssueKeyCasingTitle": "Jira キーの大文字/小文字", + "jiraIssueKeyCasingDescription": "Orca が新規の Jira 連携ワークスペースに提案するワークスペース名とブランチ名で、Jira Issue キーを大文字と小文字のどちらで表記するかを指定します。たとえば proj-123-fix-login ではなく PROJ-123-fix-login のようになります。既存のワークスペースと入力した名前は変更されません。", + "jiraIssueKeyCasingLowercase": "小文字", + "jiraIssueKeyCasingPreserve": "大文字のキー" }, "HiddenExperimentalGroup": { "7c4e18d2f6": "このマシンで太字グリッチのキャプチャジェスチャーを有効にします。{{samplingShortcut}} でサンプリングバーストを開始し、{{captureShortcut}} でペインの証拠をディスクに保存します。", @@ -10050,7 +10072,13 @@ "defaultBranch": "デフォルトブランチ", "repositoryDefault": "リポジトリ既定", "branchUpstream": "ブランチ upstream", - "diffBase": "デフベース" + "diffBase": "デフベース", + "jiraIssueKeyCasingTitle": "Jira キーの大文字/小文字", + "jiraIssueKeyCasingDescription": "Orca が新規の Jira 連携ワークスペースに提案するワークスペース名とブランチ名で、Jira Issue キーを大文字と小文字のどちらで表記するかを指定します。たとえば proj-123-fix-login ではなく PROJ-123-fix-login のようになります。既存のワークスペースと入力した名前は変更されません。", + "jira": "Jira", + "issueKey": "Issue キー", + "uppercase": "大文字", + "lowercase": "小文字" } }, "input": { @@ -19041,6 +19069,42 @@ }, "automations": { "runTerminalUnavailable": "実行ターミナルを利用できません。" + }, + "jiraIssueTitleField": { + "generate": "説明からタイトルを生成", + "stop": "タイトルの生成を停止", + "canceled": "生成をキャンセルしました。", + "webUnavailable": "Web クライアントでは Jira のタイトル生成を利用できません。", + "hostUpdateRequired": "Jira のタイトルを生成するには、更新された Orca サーバーが必要です。Jira 接続のホストを更新して、再試行してください。", + "generating": "タイトルを生成しています。クリックして停止します。", + "failed": "説明から概要を生成できませんでした。" + }, + "agentCombobox": { + "clearSelection": "Agent の選択をクリア" + }, + "newWorkspace": { + "agentOptional": "Agent [オプション]", + "createDescription": "プロジェクトとワークスペース名を選択します。開始する場合は Agent を選択してください。" + }, + "JiraIssueWorkspace": { + "pendingChange": "別の Issue の変更が進行中です。" + }, + "jiraProjectPicker": { + "search": "Jira プロジェクトを検索", + "empty": "プロジェクトが見つかりませんでした。", + "label": "Jira プロジェクト", + "all": "すべての Jira プロジェクト" + }, + "RepositoryJiraProjectSection": { + "saveFailed": "デフォルトの Jira プロジェクトを保存できませんでした。", + "runtimeUpdate": "デフォルトの Jira プロジェクトを保存するには、このランタイムを更新してください。", + "title": "デフォルトの Jira プロジェクト", + "description": "Jira の Issue ピッカーとタスクリストのデフォルトをこのプロジェクトに設定します。ワークツリーごとに変更できます。", + "none": "デフォルトプロジェクトなし", + "connect": "プロジェクトを選択するには、連携で Jira に接続してください。", + "keywordJira": "Jira", + "keywordDefaultProject": "デフォルトプロジェクト", + "keywordIssuePicker": "Issue ピッカー" } }, "dashboardPopout": { @@ -19507,7 +19571,8 @@ "continueInCliNewTab": "新規タブで CLI を続行する" }, "common": { - "retry": "再試行" + "retry": "再試行", + "cancel": "キャンセル" }, "fileExplorer": { "root": { @@ -19910,6 +19975,96 @@ "jql": "ステータスフィルターの JQL", "editJql": "JQL として編集", "catalogHint": "ステータスは読み込まれた課題から取得されます。このビューで他のステータスを表示するには、選択をクリアしてください。" + }, + "connect": { + "scopedTokenGuide": "スコープ付きトークンには read:jira-work、write:jira-work、read:jira-user が必要です。Bearer 認証を使用するには、メールアドレスを空のままにします。", + "oldHostGuide": "このリモートランタイムには、メールアドレスとスコープなしの API トークンが必要です。スコープ付きトークンを使用するには、リモートランタイムを更新してください。", + "scopedDescription": "Jira Cloud のサイト URL と API トークンを使って Issue を閲覧します。スコープ付きトークンではメールアドレスを省略できます。", + "optionalEmail": "Atlassian のメールアドレス (スコープ付きトークンではオプション)" + }, + "workspace": { + "open": "{{key}} にリンクされたワークスペースを開く", + "count": "{{key}} にリンクされたワークスペース {{count}} 件", + "startAnother": "新規ワークスペースを開始", + "empty": "Jira Issue をワークスペースにリンクするか、別のビューを選択してください。", + "openFailed": "この Jira Issue にリンクされたワークスペースを開けませんでした。", + "mode": "ワークスペースあり", + "search": "リンクされた Issue を検索", + "single": "{{key}} にリンクされたワークスペース 1 件" + }, + "prompt": { + "dialogTitle": "テンプレートからプロンプトをコピー", + "template": "テンプレート", + "custom": "カスタムテンプレート", + "templateText": "テンプレートテキスト", + "variables": "$jira-issue は Issue のコンテキストに、$other-prompt は追加の指示に使用します。このデバイスに保存されます。", + "other": "追加の指示 (オプション)", + "preview": "プレビュー", + "implementation": "実装を開始", + "bug": "バグを調査", + "testPlan": "テストプランを作成", + "prSummary": "PR サマリーの下書きを作成" + }, + "copy": { + "context": "{{key}} のコンテキストをコピー", + "actions": "Issue のコンテキストをコピー" + }, + "transitions": { + "changeStatus": "ステータスを変更: {{status}}", + "loadError": "ステータス変更を読み込めませんでした。", + "retry": "再試行", + "loading": "ステータス変更を読み込んでいます…", + "completeInJira": "Jira で「{{name}}」を完了", + "unsupported": "Jira には、Orca がまだ取得できないフィールドが必要です: {{fields}}。", + "chooseResolution": "解決策を選択", + "comment": "コメント", + "back": "戻る", + "apply": "ステータスを変更", + "empty": "利用可能なステータス変更はありません。" + }, + "board": { + "currentSprint": "現在のスプリント", + "viewMode": "Jira ビュー", + "list": "リスト", + "board": "ボード", + "moveIssue": "{{key}} を移動", + "moveTitle": "{{key}} を {{column}} に移動", + "moveDescription": "利用できるステータス変更と必要なフィールドは Jira が制御します。", + "chooseTransition": "ステータス変更を選択:", + "cancel": "キャンセル", + "noTransition": "{{key}} では {{column}} へのステータス変更を利用できません。", + "queryTooLong": "Jira クエリが長すぎます。クエリを短くするか、フィルターをクリアしてください。" + } + }, + "updates": { + "installBlock": { + "instancesLabel": "Orca を使用中のインスタンス", + "browserLabel": "バックグラウンドブラウザ", + "serverLabel": "バックグラウンドサーバー", + "windowLabel": "別の Orca ウィンドウ", + "instanceLabel": "別の Orca インスタンス", + "processId": "PID {{pid}}", + "preparingTitle": "更新を準備しています", + "preparingSummary": "再起動する前に、実行中の Orca インスタンスを確認しています。", + "later": "後で", + "retainedDownload": "Orca は開いたままです。再度ダウンロードする必要はありません。", + "retryInstallation": "インストールを再試行", + "waitingLabel": "更新を待機中", + "busyTitle": "別の Orca が更新中です", + "busySummary": "別の Orca インスタンスがすでに更新をインストールしています。", + "busyGuidance": "しばらく待ってから、再試行してください。ダウンロード済みの更新は準備できています。", + "unresponsiveTitle": "バックグラウンドサーバーを待機中", + "unresponsiveSummary": "Orca は更新のためにバックグラウンドサーバーを一時停止できませんでした。", + "unresponsiveGuidance": "起動元のターミナルまたはサービスから停止して、再試行してください。接続中のデバイスは切断されます。", + "readyTitle": "更新の準備ができました", + "serverSummary": "バックグラウンドの Orca サーバーがこのアプリをまだ使用しています。", + "instanceSummary": "別の Orca インスタンスがこのアプリをまだ使用しています。", + "serverGuidance": "起動元のターミナルまたはサービスから停止して、再試行してください。接続中のデバイスは切断されます。", + "mixedGuidance": "他の Orca ウィンドウをすべて閉じ、起動元のターミナルまたはサービスからサーバーを停止してください。接続中のデバイスは切断されます。その後、再試行してください。", + "instanceGuidance": "他の Orca インスタンスを終了してから、再試行してください。ダウンロード済みの更新は準備されたままになります。", + "waitingAriaLabel": "他の Orca インスタンスを待機中の更新。クリックして展開します。", + "preparingLabel": "更新を準備中", + "preparingAriaLabel": "更新を準備中。クリックして展開します。" } } } diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index bc3184663f5..776f3fb9f6a 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -196,7 +196,8 @@ "readFailed": "에이전트 설정을 읽을 수 없습니다. 호스트 연결을 확인하고 다시 시도하세요.", "saveFailed": "에이전트 설정을 저장할 수 없습니다. 호스트 연결을 확인하고 다시 시도하세요.", "hostDefaults": "선택한 호스트의 새 에이전트 기본값입니다.", - "save": "저장" + "save": "저장", + "clearDefault": "기본 에이전트 지우기" }, "followSymlinkedDirectoriesKeywords": { "symlink": "심볼릭 링크", @@ -299,7 +300,12 @@ "taskUrl": { "loadingHint": "{{value0}} 로드 중…", "createHint": "{{value0}}에서 작업 트리 만들기" - } + }, + "tasksHeader": "작업", + "taskCreate": "워크스페이스 만들기", + "taskSearchLoading": "작업 검색 중…", + "taskSearchFailed": "일부 작업 소스를 검색할 수 없습니다", + "taskSearchRetry": "다시 시도" }, "auto": { "App": { @@ -1052,6 +1058,10 @@ } } } + }, + "aiVaultResumeRefusal": { + "openInChat": "이 {{value0}} 대화는 채팅에서 열려 있습니다. 열려면 세션 기록을 새로고침하세요.", + "ownershipUnknown": "Orca가 이 {{value0}} 대화의 소유자를 확인할 수 없습니다. 세션 기록을 새로고침한 후 다시 시도하세요." } }, "hooks": { @@ -2327,7 +2337,14 @@ "jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again.", "loadPageUnreachable": "{{value0}} 페이지는 GitHub 검색이 반환할 수 있는 범위를 벗어났습니다.", "loadPageFailed": "GitHub에서 {{value0}} 페이지를 불러오지 못했습니다.", - "loadPageNoMoreResults": "{{value0}} 페이지에 더 이상 결과가 없습니다." + "loadPageNoMoreResults": "{{value0}} 페이지에 더 이상 결과가 없습니다.", + "jiraColumnsPicker": "열", + "jiraColumnParent": "상위", + "jiraColumnSprint": "스프린트", + "jiraColumnStoryPoints": "포인트", + "jiraColumnOriginalEstimate": "예상", + "jiraColumnRemainingEstimate": "남음", + "jiraBoardEmptyColumn": "이슈 없음" }, "Terminal": { "73768427cf": "닫기", @@ -2447,8 +2464,8 @@ "projectBadge": "프로젝트", "repoGroupBadge": "리포지토리 그룹", "pluginCommandFailed": "플러그인 명령을 실행할 수 없습니다.", - "27f10cca63": "채팅, 터미널, 작업 공간, 설정 및 작업 검색...", - "2770f02910": "채팅, 터미널, 작업 공간, 설정 및 작업 검색", + "27f10cca63": "채팅, 터미널, 워크스페이스, 작업, 설정 및 작업 검색...", + "2770f02910": "채팅, 터미널, 워크스페이스, 작업, 설정 및 작업 검색", "recentChatsTerminalsHeader": "최근 채팅 및 터미널", "lastActiveTime": "마지막 활동: {{value0}} 전" }, @@ -3244,8 +3261,8 @@ "zcode": { "missing": { "tui": { - "title": "This ZCode build has no terminal UI", - "description": "Orca's hooks installed correctly — the zcode on your PATH just cannot open a session. The ZCode desktop app bundles the agent runtime without its terminal UI. Install a zcode that ships the TUI, then run zcode outside Orca to confirm." + "title": "이 ZCode 빌드에는 터미널 UI가 없습니다", + "description": "Orca 후크는 올바르게 설치되었습니다 — PATH에 있는 zcode는 세션을 열 수 없을 뿐입니다. ZCode 데스크톱 앱은 터미널 UI 없이 에이전트 런타임을 번들로 제공합니다. TUI가 포함된 zcode를 설치한 다음 Orca 외부에서 zcode를 실행하여 확인하세요." } } }, @@ -3442,7 +3459,8 @@ "sessionQueued": "세션이 대기열에 추가됨", "openSupportedWorkspace": "세션을 재개하기 전에 워크트리를 여세요.", "localSessionSshWorkspaceUnsupported": "이 세션의 기록은 이 컴퓨터에 저장되어 있어 SSH 워크스페이스에서는 재개할 수 없습니다. 대신 로컬 워크스페이스에 놓으세요.", - "sessionHostMismatchUnsupported": "이 세션은 다른 호스트에 속합니다. 같은 호스트의 워크스페이스에 놓으세요." + "sessionHostMismatchUnsupported": "이 세션은 다른 호스트에 속합니다. 같은 호스트의 워크스페이스에 놓으세요.", + "oldPayload": "이 세션은 이전 버전의 Orca 창에서 끌어왔습니다. 대신 세션 기록에서 재개하세요." }, "TabGroupDropOverlay": { "paneColumnLabel": "새 분할" @@ -7538,7 +7556,11 @@ "compareAgainstUpstreamDescription": "소스 제어가 커밋된 변경 사항 비교에 기본으로 사용할 기준을 선택합니다. 브랜치 업스트림은 현재 브랜치를 자동으로 따라가며, 업스트림이 없으면 저장소 기본 브랜치로 돌아갑니다. 비교 기준은 해당 worktree의 Git 패널에서 worktree별로 변경할 수 있습니다. Pull Request 및 리베이스 대상은 변경되지 않습니다.", "compareBaseRepositoryDefault": "저장소 기본값", "compareBaseBranchUpstream": "브랜치 업스트림", - "refreshLocalBaseDescription": "워크스페이스를 만들면 Orca가 원격 기준 브랜치를 새로고침하고 main이나 master 같은 해당 로컬 브랜치를 안전하게 앞으로 이동(fast-forward)합니다. 그러면 git diff main...HEAD 같은 명령이 오래된 이력과 비교하지 않습니다. 해당 브랜치에 커밋되지 않은 변경 사항이나 로컬에만 있는 커밋이 있으면 Orca는 업데이트를 건너뜁니다." + "refreshLocalBaseDescription": "워크스페이스를 만들면 Orca가 원격 기준 브랜치를 새로고침하고 main이나 master 같은 해당 로컬 브랜치를 안전하게 앞으로 이동(fast-forward)합니다. 그러면 git diff main...HEAD 같은 명령이 오래된 이력과 비교하지 않습니다. 해당 브랜치에 커밋되지 않은 변경 사항이나 로컬에만 있는 커밋이 있으면 Orca는 업데이트를 건너뜁니다.", + "jiraIssueKeyCasingTitle": "Jira 키 대소문자", + "jiraIssueKeyCasingDescription": "새로 Jira에 연결된 워크스페이스의 워크스페이스 이름과 브랜치 이름에서 Orca가 제안하는 Jira 이슈 키의 대소문자입니다(예: proj-123-fix-login 대신 PROJ-123-fix-login). 기존 워크스페이스와 직접 입력한 이름은 변경되지 않습니다.", + "jiraIssueKeyCasingLowercase": "소문자", + "jiraIssueKeyCasingPreserve": "대문자 키" }, "HiddenExperimentalGroup": { "7c4e18d2f6": "이 컴퓨터에서 굵게 표시 결함 캡처 제스처를 활성화합니다. {{samplingShortcut}}하면 샘플링 버스트가 시작되고, {{captureShortcut}}하면 창 증거를 디스크에 캡처합니다.", @@ -10013,7 +10035,13 @@ "defaultBranch": "기본 브랜치", "repositoryDefault": "저장소 기본값", "branchUpstream": "브랜치 업스트림", - "diffBase": "차이점 기반" + "diffBase": "차이점 기반", + "jiraIssueKeyCasingTitle": "Jira 키 대소문자", + "jiraIssueKeyCasingDescription": "새로 Jira에 연결된 워크스페이스의 워크스페이스 이름과 브랜치 이름에서 Orca가 제안하는 Jira 이슈 키의 대소문자입니다(예: proj-123-fix-login 대신 PROJ-123-fix-login). 기존 워크스페이스와 직접 입력한 이름은 변경되지 않습니다.", + "jira": "Jira", + "issueKey": "이슈 키", + "uppercase": "대문자", + "lowercase": "소문자" } }, "input": { @@ -19041,6 +19069,42 @@ }, "automations": { "runTerminalUnavailable": "실행 터미널을 사용할 수 없습니다." + }, + "jiraIssueTitleField": { + "generate": "설명에서 제목 생성", + "stop": "제목 생성 중지", + "canceled": "생성이 취소되었습니다.", + "webUnavailable": "웹 클라이언트에서는 Jira 제목 생성을 사용할 수 없습니다.", + "hostUpdateRequired": "Jira 제목을 생성하려면 더 새로운 Orca 서버가 필요합니다. Jira 연결의 호스트를 업데이트한 후 다시 시도하세요.", + "generating": "제목 생성 중입니다. 중지하려면 클릭하세요.", + "failed": "설명에서 요약을 생성하지 못했습니다." + }, + "agentCombobox": { + "clearSelection": "에이전트 선택 지우기" + }, + "newWorkspace": { + "agentOptional": "에이전트 [선택 사항]", + "createDescription": "프로젝트와 워크스페이스 이름을 선택하세요. 에이전트를 시작하려면 에이전트를 선택하세요." + }, + "JiraIssueWorkspace": { + "pendingChange": "다른 이슈 변경이 진행 중입니다." + }, + "jiraProjectPicker": { + "search": "Jira 프로젝트 검색", + "empty": "프로젝트를 찾을 수 없습니다.", + "label": "Jira 프로젝트", + "all": "모든 Jira 프로젝트" + }, + "RepositoryJiraProjectSection": { + "saveFailed": "기본 Jira 프로젝트를 저장하지 못했습니다.", + "runtimeUpdate": "기본 Jira 프로젝트를 저장하려면 이 런타임을 업데이트하세요.", + "title": "기본 Jira 프로젝트", + "description": "Jira 이슈 선택기와 작업 목록을 이 프로젝트로 기본 설정합니다. 워크트리마다 변경할 수 있습니다.", + "none": "기본 프로젝트 없음", + "connect": "프로젝트를 선택하려면 연동에서 Jira를 연결하세요.", + "keywordJira": "Jira", + "keywordDefaultProject": "기본 프로젝트", + "keywordIssuePicker": "이슈 선택기" } }, "dashboardPopout": { @@ -19507,7 +19571,8 @@ "continueInCliNewTab": "새 탭의 CLI에서 계속" }, "common": { - "retry": "다시 시도" + "retry": "다시 시도", + "cancel": "취소" }, "fileExplorer": { "root": { @@ -19910,6 +19975,96 @@ "jql": "상태 필터 JQL", "editJql": "JQL로 편집", "catalogHint": "상태는 불러온 이슈에서 가져옵니다. 이 보기에서 다른 상태를 찾으려면 선택을 지우세요." + }, + "connect": { + "scopedTokenGuide": "범위가 지정된 토큰에는 read:jira-work, write:jira-work, read:jira-user가 필요합니다. Bearer 인증을 사용하려면 이메일을 비워 두세요.", + "oldHostGuide": "이 원격 런타임에는 이메일과 범위가 지정되지 않은 API 토큰이 필요합니다. 범위가 지정된 토큰을 사용하려면 원격 런타임을 업데이트하세요.", + "scopedDescription": "Jira Cloud 사이트 URL과 API 토큰으로 이슈를 찾아보세요. 범위가 지정된 토큰은 이메일을 생략할 수 있습니다.", + "optionalEmail": "Atlassian 이메일(범위가 지정된 토큰은 선택 사항)" + }, + "workspace": { + "open": "{{key}}에 연결된 워크스페이스 열기", + "count": "{{key}}에 연결된 워크스페이스 {{count}}개", + "startAnother": "새 워크스페이스 시작", + "empty": "Jira 이슈를 워크스페이스에 연결하거나 다른 보기를 선택하세요.", + "openFailed": "이 Jira 이슈에 연결된 워크스페이스를 열 수 없습니다.", + "mode": "워크스페이스 있음", + "search": "연결된 이슈 검색", + "single": "{{key}}에 연결된 워크스페이스 1개" + }, + "prompt": { + "dialogTitle": "템플릿에서 프롬프트 복사", + "template": "템플릿", + "custom": "사용자 지정 템플릿", + "templateText": "템플릿 텍스트", + "variables": "이슈 컨텍스트에는 $jira-issue를, 추가 지침에는 $other-prompt를 사용하세요. 이 기기에 저장됩니다.", + "other": "추가 지침(선택 사항)", + "preview": "미리보기", + "implementation": "구현 시작", + "bug": "버그 조사", + "testPlan": "테스트 계획 작성", + "prSummary": "PR 요약 초안 작성" + }, + "copy": { + "context": "{{key}}의 컨텍스트 복사", + "actions": "이슈 컨텍스트 복사" + }, + "transitions": { + "changeStatus": "상태 변경: {{status}}", + "loadError": "상태 변경을 로드하지 못했습니다.", + "retry": "다시 시도", + "loading": "상태 변경 로드 중…", + "completeInJira": "Jira에서 “{{name}}” 완료", + "unsupported": "Jira에는 Orca가 아직 수집할 수 없는 필드가 필요합니다: {{fields}}.", + "chooseResolution": "해결 방법 선택", + "comment": "댓글", + "back": "뒤로", + "apply": "상태 변경", + "empty": "사용 가능한 상태 변경이 없습니다." + }, + "board": { + "currentSprint": "현재 스프린트", + "viewMode": "Jira 보기", + "list": "목록", + "board": "보드", + "moveIssue": "{{key}} 이동", + "moveTitle": "{{key}}을(를) {{column}}(으)로 이동", + "moveDescription": "Jira는 사용 가능한 상태 변경과 해당 변경에 필요한 필드를 제어합니다.", + "chooseTransition": "상태 변경을 선택하세요:", + "cancel": "취소", + "noTransition": "{{key}}에 대해 {{column}}(으)로 가는 상태 변경을 사용할 수 없습니다.", + "queryTooLong": "Jira 쿼리가 너무 깁니다. 쿼리를 줄이거나 필터를 지우세요." + } + }, + "updates": { + "installBlock": { + "instancesLabel": "Orca를 사용 중인 인스턴스", + "browserLabel": "백그라운드 브라우저", + "serverLabel": "백그라운드 서버", + "windowLabel": "다른 Orca 창", + "instanceLabel": "다른 Orca 인스턴스", + "processId": "PID {{pid}}", + "preparingTitle": "업데이트 준비 중", + "preparingSummary": "다시 시작하기 전에 실행 중인 Orca 인스턴스를 확인하는 중입니다.", + "later": "나중에", + "retainedDownload": "Orca는 계속 열려 있습니다. 다시 다운로드할 필요가 없습니다.", + "retryInstallation": "설치 다시 시도", + "waitingLabel": "업데이트 대기 중", + "busyTitle": "다른 Orca가 업데이트 중입니다", + "busySummary": "다른 Orca 인스턴스가 이미 업데이트를 설치하고 있습니다.", + "busyGuidance": "잠시 기다린 후 다시 시도하세요. 다운로드한 업데이트가 준비되어 있습니다.", + "unresponsiveTitle": "백그라운드 서버 대기 중", + "unresponsiveSummary": "Orca가 업데이트를 위해 백그라운드 서버 일시 중지를 완료하지 못했습니다.", + "unresponsiveGuidance": "시작한 터미널 또는 서비스에서 중지한 후 다시 시도하세요. 연결된 기기는 연결이 끊어집니다.", + "readyTitle": "업데이트가 준비되었습니다", + "serverSummary": "백그라운드 Orca 서버가 아직 이 앱을 사용 중입니다.", + "instanceSummary": "다른 Orca 인스턴스가 아직 이 앱을 사용 중입니다.", + "serverGuidance": "시작한 터미널 또는 서비스에서 서버를 중지한 후 다시 시도하세요. 연결된 기기는 연결이 끊어집니다.", + "mixedGuidance": "다른 Orca 창을 모두 닫고, 시작한 터미널 또는 서비스에서 서버를 중지하세요. 연결된 기기는 연결이 끊어집니다. 그런 다음 다시 시도하세요.", + "instanceGuidance": "다른 Orca 인스턴스를 종료한 후 다시 시도하세요. 다운로드한 업데이트는 준비된 상태로 유지됩니다.", + "waitingAriaLabel": "다른 Orca 인스턴스를 기다리는 업데이트입니다. 클릭하여 펼치세요.", + "preparingLabel": "업데이트 준비 중", + "preparingAriaLabel": "업데이트 준비 중입니다. 클릭하여 펼치세요." } } } diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 2822f090949..4360606e7e0 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -196,7 +196,8 @@ "readFailed": "无法读取智能体设置。请检查主机连接并重试。", "saveFailed": "无法保存智能体设置。请检查主机连接并重试。", "hostDefaults": "所选主机上新智能体的默认设置。", - "save": "保存" + "save": "保存", + "clearDefault": "清除默认智能体" }, "followSymlinkedDirectoriesKeywords": { "symlink": "符号链接", @@ -299,7 +300,12 @@ "taskUrl": { "loadingHint": "正在加载 {{value0}}...", "createHint": "从 {{value0}} 创建工作树" - } + }, + "tasksHeader": "任务", + "taskCreate": "创建工作区", + "taskSearchLoading": "正在搜索任务…", + "taskSearchFailed": "部分任务源无法搜索", + "taskSearchRetry": "重试" }, "auto": { "App": { @@ -365,7 +371,7 @@ "mobileScopeRejected": "此二维码仅授予受限(移动端)访问权限。若要使用完整 Web 应用,请从设置 → 远程 Orca 服务器 → 分享此 Orca 服务器 → 新链接打开浏览器访问链接。" }, "webPreloadApi": { - "aiVaultUnavailableForHost": "此执行主机不支持 Agent 会话历史记录。", + "aiVaultUnavailableForHost": "此执行主机不支持智能体会话历史。", "runtimeEnvironmentManuallyDisconnected": "运行时环境已手动断开连接。", "loopbackPairingBlocked": "此访问链接会指回此设备。", "remotePairingUnreachable": "无法连接 {{endpoint}} 上的 Orca。", @@ -1052,6 +1058,10 @@ } } } + }, + "aiVaultResumeRefusal": { + "openInChat": "此 {{value0}} 会话已在聊天中打开。请刷新会话历史以打开它。", + "ownershipUnknown": "Orca 无法确认此 {{value0}} 会话的归属。请刷新会话历史并重试。" } }, "hooks": { @@ -2327,7 +2337,14 @@ "jiraLinkSourceUnavailable": "无法关联此 Jira 议题。请重新连接 Jira 或选择匹配的站点,然后重试。", "loadPageUnreachable": "第 {{value0}} 页超出了 GitHub 搜索可返回的范围。", "loadPageFailed": "无法从 GitHub 加载第 {{value0}} 页。", - "loadPageNoMoreResults": "第 {{value0}} 页没有更多结果。" + "loadPageNoMoreResults": "第 {{value0}} 页没有更多结果。", + "jiraColumnsPicker": "列", + "jiraColumnParent": "父级", + "jiraColumnSprint": "冲刺", + "jiraColumnStoryPoints": "点数", + "jiraColumnOriginalEstimate": "预估", + "jiraColumnRemainingEstimate": "剩余", + "jiraBoardEmptyColumn": "没有议题" }, "Terminal": { "73768427cf": "关闭", @@ -2447,8 +2464,8 @@ "projectBadge": "项目", "repoGroupBadge": "仓库组", "pluginCommandFailed": "无法运行插件命令。", - "27f10cca63": "搜索聊天、终端、工作空间、设置和操作...", - "2770f02910": "搜索聊天、终端、工作空间、设置和操作", + "27f10cca63": "搜索聊天、终端、工作区、任务、设置和操作...", + "2770f02910": "搜索聊天、终端、工作区、任务、设置和操作", "recentChatsTerminalsHeader": "最近的聊天和终端", "lastActiveTime": "最后活跃时间 {{value0}} 前" }, @@ -3244,8 +3261,8 @@ "zcode": { "missing": { "tui": { - "title": "This ZCode build has no terminal UI", - "description": "Orca's hooks installed correctly — the zcode on your PATH just cannot open a session. The ZCode desktop app bundles the agent runtime without its terminal UI. Install a zcode that ships the TUI, then run zcode outside Orca to confirm." + "title": "此 ZCode 版本没有终端 UI", + "description": "Orca 的钩子已正确安装——但 PATH 中的 zcode 就是无法打开会话。ZCode 桌面应用打包了智能体运行时,却没有附带其终端 UI。请安装一个自带 TUI 的 zcode,然后在 Orca 之外运行 zcode 加以确认。" } } }, @@ -3442,7 +3459,8 @@ "sessionQueued": "会话已排队", "openSupportedWorkspace": "在恢复会话之前,请先打开一个工作区。", "localSessionSshWorkspaceUnsupported": "此会话的历史记录存储在本机上,因此无法在 SSH 工作区中恢复。请改为将其拖放到本地工作区。", - "sessionHostMismatchUnsupported": "此会话属于其他主机。请将其拖放到同一主机上的工作区。" + "sessionHostMismatchUnsupported": "此会话属于其他主机。请将其拖放到同一主机上的工作区。", + "oldPayload": "此会话是从较早版本的 Orca 窗口拖入的。请改从会话历史恢复。" }, "TabGroupDropOverlay": { "paneColumnLabel": "新建拆分" @@ -4546,7 +4564,7 @@ "35b9a724a0": "可用", "c13b82793c": "管理安装", "aee7b99cc6": "从链接安装", - "filterProvider": "按 Agent 筛选", + "filterProvider": "按智能体筛选", "filterSource": "按来源筛选", "allSources": "全部", "clearFilters": "清除筛选", @@ -4644,7 +4662,7 @@ "retryOther": "重试 {{count}} 技能" }, "filter": { - "allAgents": "所有 Agent", + "allAgents": "所有智能体", "sharedAgent": "共享 (.agents)" }, "SkillsSelectionHeader": { @@ -4656,7 +4674,7 @@ "deleteTitle": "选择要删除的技能" }, "SkillDetailDialog": { - "agents": "Agent", + "agents": "智能体", "updated": "已更新", "copy": "复制" }, @@ -4665,7 +4683,7 @@ "titleMany": "{{value0}} 个已安装的 Orca 技能已过期", "updateOne": "更新技能", "updateMany": "更新技能", - "description": "更新 {{value0}},让 Agents 遵循此版本 Orca 的最新说明。" + "description": "更新 {{value0}},让智能体遵循此版本 Orca 的最新说明。" }, "SkillFreshnessRow": { "statusUpdateAvailable": "有更新", @@ -6769,11 +6787,11 @@ "1fd1b1b6b4": "未设置 Cookie", "4e32e030b2": "存储在本地。Orca 仅将其发送到 platform.minimax.io 以刷新使用量。", "5e08b0fe57": "存储在本地,仅发送到 platform.minimax.io 以刷新使用量。", - "79418c782a": "在浏览器中打开 platform.minimax.io/console/usage 并登录,然后从 DevTools(网络 → 任一 remains 请求 → Cookie)复制 Cookie 请求头。", + "79418c782a": "在浏览器中打开 platform.minimax.io/console/usage 并登录,然后从 DevTools(Network → 任意 remains 请求 → Cookie)复制 Cookie 请求头。", "f5d8d2a6a1": "在浏览器中打开 platform.minimax.io/console/usage 并登录。", - "d5267cce63": ")。 auth cookie 仍然涵盖工作区发现;仅 auth 不足以使用。 OpenCode Go 身份验证基于 Web,并在 Windows 和 WSL 终端之间共享。", + "d5267cce63": ")。认证 cookie 仍然涵盖工作区发现;仅凭认证不足以获取使用情况。OpenCode Go 认证基于 Web,可在 Windows 与 WSL 终端间共享。", "62ab430f94": "从浏览器的 DevTools → Network → 任何 opencode.ai 请求粘贴完整的 Cookie 标头,包括 __Host-console_session(例如", - "37b4b4a3f7": "授权=…; __主机控制台_会话=...", + "37b4b4a3f7": "auth=…; __Host-console_session=…", "0335bd31d5": "粘贴完整的 opencode.ai Cookie 标头,包括 __Host-console_session,以进行速率限制提取。", "codexLoginLinkPending": "Codex 在您的浏览器中打开了此登录链接。复制它以在其他地方(私人窗口或其他配置文件)完成登录。", "codexLoginLinkCopied": "已复制", @@ -7538,7 +7556,11 @@ "compareAgainstUpstreamDescription": "选择源代码管理在比较已提交变更时默认使用的基准。分支上游会自动跟随当前分支;如果没有上游,则回退到仓库默认分支。你仍然可以在对应 worktree 的 Git 面板中按 worktree 更改对比基准。Pull Request 和变基目标不会改变。", "compareBaseRepositoryDefault": "仓库默认", "compareBaseBranchUpstream": "分支上游", - "refreshLocalBaseDescription": "创建工作区时,Orca 会刷新远程基准分支,并安全地快进对应的本地分支,例如 main 或 master。这样可以避免 git diff main...HEAD 等命令与过时的历史记录进行比较。如果该分支有未提交的更改或仅在本地的提交,Orca 会跳过更新。" + "refreshLocalBaseDescription": "创建工作区时,Orca 会刷新远程基准分支,并安全地快进对应的本地分支,例如 main 或 master。这样可以避免 git diff main...HEAD 等命令与过时的历史记录进行比较。如果该分支有未提交的更改或仅在本地的提交,Orca 会跳过更新。", + "jiraIssueKeyCasingTitle": "Jira 键大小写", + "jiraIssueKeyCasingDescription": "Orca 为新的 Jira 关联工作区建议的工作区和分支名称中 Jira 议题键的大小写,例如使用 PROJ-123-fix-login 而非 proj-123-fix-login。现有工作区和你输入的名称绝不会更改。", + "jiraIssueKeyCasingLowercase": "小写", + "jiraIssueKeyCasingPreserve": "大写键" }, "HiddenExperimentalGroup": { "7c4e18d2f6": "在本机启用粗体故障捕获手势:{{samplingShortcut}} 开始采样突发;{{captureShortcut}} 将窗格证据捕获到磁盘。", @@ -10013,7 +10035,13 @@ "defaultBranch": "默认分支", "repositoryDefault": "仓库默认", "branchUpstream": "分支上游", - "diffBase": "差异基础" + "diffBase": "差异基础", + "jiraIssueKeyCasingTitle": "Jira 键大小写", + "jiraIssueKeyCasingDescription": "Orca 为新的 Jira 关联工作区建议的工作区和分支名称中 Jira 议题键的大小写,例如使用 PROJ-123-fix-login 而非 proj-123-fix-login。现有工作区和你输入的名称绝不会更改。", + "jira": "Jira", + "issueKey": "议题键", + "uppercase": "大写", + "lowercase": "小写" } }, "input": { @@ -18685,8 +18713,8 @@ "status": "状态", "agent": "智能体", "git": "Git", - "review": "审查", - "reviewState": "审查状态", + "review": "评审", + "reviewState": "评审状态", "reviewProvider": "提供方", "ticket": "工单", "ticketSource": "工单来源", @@ -18717,28 +18745,28 @@ "filters": "过滤器", "showingCount": "显示 {{value1}} 的 {{value0}}", "clearFilters": "清除过滤器", - "checkingGit": "检查 git 状态: {{value0}} left", + "checkingGit": "检查 git 状态:剩余 {{value0}}", "facet": { "git": "Git", "review": "评审", - "ticket": "门票", + "ticket": "工单", "location": "位置", "safety": "安全", "activity": "活动", - "size": "磁盘上的大小", + "size": "磁盘大小", "status": "工作区状态", "agent": "智能体", - "context": "当地背景" + "context": "本地上下文" }, - "minAhead": "提前承诺≥", + "minAhead": "提交领先 ≥", "minBehind": "提交落后 ≥", "branchQuery": "分支包含", "prunable": "可修剪的", "locked": "锁定", - "reviewPresence": "PR/先生", + "reviewPresence": "PR / MR", "reviewProvider": "提供者", - "ticketPresence": "联票", - "host": "主持人", + "ticketPresence": "关联工单", + "host": "主机", "repo": "存储库", "pathPrefix": "路径开始于", "tier": { @@ -18746,12 +18774,12 @@ "review": "待评审", "protected": "受保护" }, - "blockerMode": "拦网比赛", - "blockerModeAny": "有没有", + "blockerMode": "阻塞项匹配", + "blockerModeAny": "有任意一个", "blockerModeNone": "没有", - "blockers": "阻碍者", + "blockers": "阻塞项", "dismissed": "被忽略", - "selectableOnly": "我现在只能删除工作区", + "selectableOnly": "仅显示我现在可删除的工作区", "idleSignal": { "lastVisited": "未打开", "lastActivity": "无活动", @@ -18768,7 +18796,7 @@ "pinned": "已固定", "unread": "未读", "comment": "有评论", - "retainedDoneAgents": "智能体成绩单完成", + "retainedDoneAgents": "已完成的智能体记录", "contextPresence": "打开选项卡、终端、评论", "completelyEmpty": "没有什么可失去的", "noWorkspaces": "未找到工作区。", @@ -18781,25 +18809,25 @@ "lastActivity": "上次活动", "lastVisited": "上次打开", "created": "已创建", - "size": "尺寸", + "size": "大小", "name": "工作区", "repo": "存储库", - "path": "小路", - "host": "主持人", + "path": "路径", + "host": "主机", "workspaceStatus": "状态", "agent": "智能体", "git": "Git", - "ahead": "前进", - "behind": "在后面", + "ahead": "领先", + "behind": "落后", "branch": "分支", "review": "评审", - "ticket": "票", - "localContext": "开放的背景", + "ticket": "工单", + "localContext": "打开上下文", "tier": "安全", - "blockerCount": "阻碍者" + "blockerCount": "阻塞项" }, "git": { - "clean": "干净的", + "clean": "干净", "dirty": "未提交的更改", "unpushed": "未推送的提交", "unknown": "未检查" @@ -18807,7 +18835,7 @@ "agent": { "working": "处理中", "permission": "等着你", - "idle": "闲置的" + "idle": "闲置" }, "review": { "open": "打开", @@ -18824,7 +18852,7 @@ }, "triState": { "any": "任何", - "only": "仅有的", + "only": "仅", "exclude": "排除" }, "presence": { @@ -19009,7 +19037,7 @@ }, "nativeChat": { "queueFollowUpsTitle": "将后续消息加入队列", - "queueFollowUpsCopy": "Agent 工作时发送的消息会以卡片形式等待,你可以立即发送、编辑或删除。带图片的消息会立即发送。", + "queueFollowUpsCopy": "智能体工作时发送的消息会以卡片形式等待,你可以立即发送、编辑或删除。带图片的消息会立即发送。", "queueFollowUpsToggleLabel": "切换后续消息队列", "inlineVisualsTitle": "内联可视化", "inlineVisualsCopy": "允许智能体在其回复中显示图表、示意图和原型图。适用于新启动的聊天。", @@ -19041,6 +19069,42 @@ }, "automations": { "runTerminalUnavailable": "运行终端不可用。" + }, + "jiraIssueTitleField": { + "generate": "根据描述生成标题", + "stop": "停止生成标题", + "canceled": "生成已取消。", + "webUnavailable": "Jira 标题生成在 Web 客户端中不可用。", + "hostUpdateRequired": "生成 Jira 标题需要较新版本的 Orca 服务器。请更新 Jira 连接的主机并重试。", + "generating": "正在生成标题。点击停止。", + "failed": "无法根据描述生成摘要。" + }, + "agentCombobox": { + "clearSelection": "清除智能体选择" + }, + "newWorkspace": { + "agentOptional": "智能体 [可选]", + "createDescription": "选择项目和工作区名称。如果想启动智能体,请选择一个。" + }, + "JiraIssueWorkspace": { + "pendingChange": "另一个议题更改正在进行中。" + }, + "jiraProjectPicker": { + "search": "搜索 Jira 项目", + "empty": "未找到项目。", + "label": "Jira 项目", + "all": "所有 Jira 项目" + }, + "RepositoryJiraProjectSection": { + "saveFailed": "无法保存默认 Jira 项目。", + "runtimeUpdate": "请更新此运行时以保存默认 Jira 项目。", + "title": "默认 Jira 项目", + "description": "将 Jira 议题选择器和任务列表默认设为此项目。你可以为每个工作树单独更改。", + "none": "无默认项目", + "connect": "在集成中连接 Jira 以选择项目。", + "keywordJira": "Jira", + "keywordDefaultProject": "默认项目", + "keywordIssuePicker": "议题选择器" } }, "dashboardPopout": { @@ -19507,7 +19571,8 @@ "continueInCliNewTab": "在新选项卡中继续使用 CLI" }, "common": { - "retry": "重试" + "retry": "重试", + "cancel": "取消" }, "fileExplorer": { "root": { @@ -19910,6 +19975,96 @@ "jql": "状态筛选器 JQL", "editJql": "作为 JQL 编辑", "catalogHint": "状态来自已加载的议题。清除状态选择以查看此视图中的其他状态。" + }, + "connect": { + "scopedTokenGuide": "作用域令牌需要 read:jira-work、write:jira-work 和 read:jira-user。将邮箱留空以使用 Bearer 身份验证。", + "oldHostGuide": "此远程运行时需要邮箱和不带作用域的 API 令牌。请更新远程运行时以使用作用域令牌。", + "scopedDescription": "使用 Jira Cloud 站点 URL 和 API 令牌浏览议题。作用域令牌可以省略邮箱。", + "optionalEmail": "Atlassian 邮箱(作用域令牌可选)" + }, + "workspace": { + "open": "打开关联到 {{key}} 的工作区", + "count": "{{count}} 个关联到 {{key}} 的工作区", + "startAnother": "启动新工作区", + "empty": "将 Jira 议题关联到工作区,或选择其他视图。", + "openFailed": "无法打开关联到此 Jira 议题的工作区。", + "mode": "有工作区", + "search": "搜索关联的议题", + "single": "1 个关联到 {{key}} 的工作区" + }, + "prompt": { + "dialogTitle": "从模板复制提示词", + "template": "模板", + "custom": "自定义模板", + "templateText": "模板文本", + "variables": "使用 $jira-issue 表示议题上下文,使用 $other-prompt 表示你的附加说明。保存在此设备上。", + "other": "附加说明(可选)", + "preview": "预览", + "implementation": "开始实现", + "bug": "调查缺陷", + "testPlan": "编写测试计划", + "prSummary": "起草 PR 摘要" + }, + "copy": { + "context": "复制 {{key}} 的上下文", + "actions": "复制议题上下文" + }, + "transitions": { + "changeStatus": "更改状态:{{status}}", + "loadError": "无法加载状态更改。", + "retry": "重试", + "loading": "正在加载状态更改…", + "completeInJira": "在 Jira 中完成“{{name}}”", + "unsupported": "Jira 需要 Orca 目前无法收集的字段:{{fields}}。", + "chooseResolution": "选择解决方案", + "comment": "评论", + "back": "返回", + "apply": "更改状态", + "empty": "没有可用的状态更改。" + }, + "board": { + "currentSprint": "当前冲刺", + "viewMode": "Jira 视图", + "list": "列表", + "board": "看板", + "moveIssue": "移动 {{key}}", + "moveTitle": "将 {{key}} 移动到 {{column}}", + "moveDescription": "Jira 控制哪些状态更改可用,以及这些更改需要哪些字段。", + "chooseTransition": "选择状态更改:", + "cancel": "取消", + "noTransition": "{{key}} 没有可转到 {{column}} 的状态更改。", + "queryTooLong": "Jira 查询过长。请缩短查询或清除筛选条件。" + } + }, + "updates": { + "installBlock": { + "instancesLabel": "正在使用 Orca 的实例", + "browserLabel": "后台浏览器", + "serverLabel": "后台服务器", + "windowLabel": "其他 Orca 窗口", + "instanceLabel": "其他 Orca 实例", + "processId": "PID {{pid}}", + "preparingTitle": "正在准备你的更新", + "preparingSummary": "重启前检查正在运行的 Orca 实例。", + "later": "稍后", + "retainedDownload": "Orca 保持打开。无需重新下载。", + "retryInstallation": "重试安装", + "waitingLabel": "更新等待中", + "busyTitle": "另一个 Orca 正在更新", + "busySummary": "另一个 Orca 实例已在安装更新。", + "busyGuidance": "请稍候片刻再重试。你下载的更新已就绪。", + "unresponsiveTitle": "正在等待后台服务器", + "unresponsiveSummary": "Orca 未能为更新暂停其后台服务器。", + "unresponsiveGuidance": "请从启动它的终端或服务中停止它,然后重试。已连接的设备将断开连接。", + "readyTitle": "你的更新已就绪", + "serverSummary": "某个后台 Orca 服务器仍在使用此应用。", + "instanceSummary": "另一个 Orca 实例仍在使用此应用。", + "serverGuidance": "请从启动服务器的终端或服务中停止它,然后重试。已连接的设备将断开连接。", + "mixedGuidance": "关闭所有其他 Orca 窗口,并从启动服务器的终端或服务中停止服务器。已连接的设备将断开连接。然后重试。", + "instanceGuidance": "退出其他 Orca 实例,然后重试。你下载的更新将保持就绪。", + "waitingAriaLabel": "更新正在等待其他 Orca 实例。点击展开。", + "preparingLabel": "正在准备更新", + "preparingAriaLabel": "正在准备更新。点击展开。" } } } From a140cc4b8912af1825697f4d7695c1d41e7bfd31 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:32:50 -0700 Subject: [PATCH 30/53] chore(i18n): address review threads on the translation scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ko Worktree Jump Palette: render "actions" as 동작 so it is distinct from the tasks entry (작업) in the same list. - zh DevTools cookie instruction: clarify the filter phrase to `任意包含 remains 的请求`, keeping the literal `remains` request name. --- config/update-translation.txt | 5 ++++- src/renderer/src/i18n/locales/ko.json | 4 ++-- src/renderer/src/i18n/locales/zh.json | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/config/update-translation.txt b/config/update-translation.txt index b49e70a5b53..d7f594320ff 100644 --- a/config/update-translation.txt +++ b/config/update-translation.txt @@ -241,7 +241,10 @@ Method (reproducible): unresponsiveSummary). R2 applied 4 zh (browse sort.ahead/behind, chip.kind.review/ reviewState). R3 applied 2 zh (preparingSummary "before restarting" fidelity, blockerModeAny grammar); es/fr/ja/ko signed off clean. R4 applied 3 zh (AccountsPane - cluster); no other locale needed a 4th round. + cluster); no other locale needed a 4th round. Post-review addressed 2 CodeRabbit + threads: ko palette "actions" -> 동작 (distinct from tasks 작업), and the zh DevTools + filter phrase clarified to `任意包含 remains 的请求` (the literal `remains` stays — it is + the request name users filter for, per `87f814af6f`). - Remaining out-of-delta findings for a future pass (pre-existing, not from this window): ja/zh/fr settings search-alias MT (ja トゥイ/アンシ, グーグル/クロム/リナックス/…; fr worktree->arbre de travail, board->conseil, remote->télécommande; zh `网络`->任一 remains diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 776f3fb9f6a..fe158c85245 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -2464,8 +2464,8 @@ "projectBadge": "프로젝트", "repoGroupBadge": "리포지토리 그룹", "pluginCommandFailed": "플러그인 명령을 실행할 수 없습니다.", - "27f10cca63": "채팅, 터미널, 워크스페이스, 작업, 설정 및 작업 검색...", - "2770f02910": "채팅, 터미널, 워크스페이스, 작업, 설정 및 작업 검색", + "27f10cca63": "채팅, 터미널, 워크스페이스, 작업, 설정 및 동작 검색...", + "2770f02910": "채팅, 터미널, 워크스페이스, 작업, 설정 및 동작 검색", "recentChatsTerminalsHeader": "최근 채팅 및 터미널", "lastActiveTime": "마지막 활동: {{value0}} 전" }, diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 4360606e7e0..ae37fe069d8 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -6778,7 +6778,7 @@ "selectedEndpointStorage": "保存在本地,并发送到所选的 MiniMax 端点以刷新用量。", "openSelectedConsole": "在浏览器中打开 {{url}} 并登录。", "cookieSelectedEndpoint": "保存在本地,并发送到所选的 MiniMax 端点以刷新用量。", - "copySelectedConsoleCookie": "打开所选控制台并登录,然后从开发者工具复制 Cookie 请求标头(Network → 任意 remains 请求 → Cookie)。", + "copySelectedConsoleCookie": "打开所选控制台并登录,然后从开发者工具复制 Cookie 请求标头(Network → 任意包含 remains 的请求 → Cookie)。", "apiKeySelectedEndpoint": "粘贴 MiniMax 控制台 → API 密钥中的密钥。保存在本地,并发送到所选的 MiniMax 端点以刷新用量。API 密钥优先于 Cookie。", "apiKeyInstructions": "从 MiniMax 控制台 → API 密钥中复制密钥。已保存的 API 密钥优先于 Cookie;使用“忘记密钥”切换回 Cookie。", "endpointOverseas": "海外 (platform.minimax.io)", @@ -6787,7 +6787,7 @@ "1fd1b1b6b4": "未设置 Cookie", "4e32e030b2": "存储在本地。Orca 仅将其发送到 platform.minimax.io 以刷新使用量。", "5e08b0fe57": "存储在本地,仅发送到 platform.minimax.io 以刷新使用量。", - "79418c782a": "在浏览器中打开 platform.minimax.io/console/usage 并登录,然后从 DevTools(Network → 任意 remains 请求 → Cookie)复制 Cookie 请求头。", + "79418c782a": "在浏览器中打开 platform.minimax.io/console/usage 并登录,然后从 DevTools(Network → 任意包含 remains 的请求 → Cookie)复制 Cookie 请求头。", "f5d8d2a6a1": "在浏览器中打开 platform.minimax.io/console/usage 并登录。", "d5267cce63": ")。认证 cookie 仍然涵盖工作区发现;仅凭认证不足以获取使用情况。OpenCode Go 认证基于 Web,可在 Windows 与 WSL 终端间共享。", "62ab430f94": "从浏览器的 DevTools → Network → 任何 opencode.ai 请求粘贴完整的 Cookie 标头,包括 __Host-console_session(例如", From 9195d2aff5904444b5cb5f996282b3f2d3673e24 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:36:01 -0400 Subject: [PATCH 31/53] refactor(terminal): park and quit capture keeps the stored pane layout (#28882) * refactor(terminal): park and quit capture keeps the stored pane layout The capture that runs on quit, sleep and park rebuilt the whole pane tree from the DOM. The tab's stored layout already follows every pane change, so the capture now writes only what it alone knows onto that layout: the scrollback buffers, the pruned scrollback refs, and the live PTY bindings with the focus repair that goes with them. Co-Authored-By: Claude Opus 5.5 * fix(terminal): keep a stored leaf's scrollback and binding while its pane is unmounted Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .../terminal-shutdown-layout-capture.test.ts | 231 +++++++++--------- .../terminal-shutdown-layout-capture.ts | 87 +++---- .../use-terminal-pane-title-effects.ts | 18 +- 3 files changed, 152 insertions(+), 184 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts index 7e75da603ee..9867d1a5f70 100644 --- a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from '../../../../shared/terminal-scrollback-limits' import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' import { getUtf8ByteLength } from '../../../../shared/utf8-byte-limits' @@ -33,61 +33,21 @@ vi.mock('@/lib/pane-manager/pane-terminal-output-scheduler', () => ({ flushTerminalOutput: mocks.flushTerminalOutput })) -class MockHTMLElement { - classList: { contains: (cls: string) => boolean } - dataset: Record - children: MockHTMLElement[] - style: Record - firstElementChild: MockHTMLElement | null - - constructor(opts: { - classList?: string[] - dataset?: Record - children?: MockHTMLElement[] - style?: Record - firstElementChild?: MockHTMLElement | null - }) { - const classes = opts.classList ?? [] - this.classList = { contains: (cls: string) => classes.includes(cls) } - this.dataset = opts.dataset ?? {} - this.children = opts.children ?? [] - this.style = opts.style ?? {} - this.firstElementChild = opts.firstElementChild ?? null - } -} - -beforeAll(() => { - ;(globalThis as unknown as Record).HTMLElement = MockHTMLElement -}) - beforeEach(() => { mocks.flushTerminalOutput.mockReset() }) -function mockRootForPane(paneId: number, leafId: string = LEAF_ID): HTMLDivElement { - const pane = new MockHTMLElement({ - classList: ['pane'], - dataset: { paneId: String(paneId), leafId } - }) - return new MockHTMLElement({ firstElementChild: pane }) as unknown as HTMLDivElement +const SINGLE_PANE_LAYOUT: TerminalLayoutSnapshot = { + root: { type: 'leaf', leafId: LEAF_ID }, + activeLeafId: LEAF_ID, + expandedLeafId: null } -function mockRootForSplit(firstPaneId = 1, secondPaneId = 2): HTMLDivElement { - const first = new MockHTMLElement({ - classList: ['pane'], - dataset: { paneId: String(firstPaneId), leafId: LEAF_ID }, - style: { flex: '1' } - }) - const second = new MockHTMLElement({ - classList: ['pane'], - dataset: { paneId: String(secondPaneId), leafId: LEAF_ID_2 }, - style: { flex: '1' } - }) - const split = new MockHTMLElement({ - classList: ['pane-split'], - children: [first, second] - }) - return new MockHTMLElement({ firstElementChild: split }) as unknown as HTMLDivElement +const SPLIT_ROOT: TerminalLayoutSnapshot['root'] = { + type: 'split', + direction: 'vertical', + first: { type: 'leaf', leafId: LEAF_ID }, + second: { type: 'leaf', leafId: LEAF_ID_2 } } describe('captureTerminalShutdownLayout', () => { @@ -104,12 +64,10 @@ describe('captureTerminalShutdownLayout', () => { } try { const layout = captureTerminalShutdownLayout({ - manager: { getPanes: () => [pane], getActivePane: () => pane } as never, - container: mockRootForPane(1), - expandedPaneId: null, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the capture reads only the mocked pane fields. + manager: { getPanes: () => [pane] } as never, paneTransports: new Map(), - paneTitlesByPaneId: {}, - existingLayout: undefined + existingLayout: SINGLE_PANE_LAYOUT }) expect(layout.buffersByLeafId).toEqual({ [LEAF_ID]: `${contents}${CURSOR_HOME}` }) expect(pane.serializeAddon.serialize).toHaveBeenCalledExactlyOnceWith({ scrollback: 5_000 }) @@ -139,12 +97,10 @@ describe('captureTerminalShutdownLayout', () => { serializeAddon: { serialize } }) const layout = captureTerminalShutdownLayout({ - manager: { getPanes: () => [pane], getActivePane: () => pane } as never, - container: mockRootForPane(1), - expandedPaneId: null, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the capture reads only the mocked pane fields. + manager: { getPanes: () => [pane] } as never, paneTransports: new Map(), - paneTitlesByPaneId: {}, - existingLayout: undefined + existingLayout: SINGLE_PANE_LAYOUT }) const expected = delta <= 0 ? contents : 'short' expect(layout.buffersByLeafId).toEqual({ [LEAF_ID]: `${expected}${CURSOR_HOME}` }) @@ -175,8 +131,7 @@ describe('captureTerminalShutdownLayout', () => { } } const manager = { - getPanes: vi.fn(() => [pane]), - getActivePane: vi.fn(() => pane) + getPanes: vi.fn(() => [pane]) } mocks.flushTerminalOutput.mockImplementation((target: typeof terminal) => { expect(target).toBe(terminal) @@ -186,12 +141,12 @@ describe('captureTerminalShutdownLayout', () => { const layout = captureTerminalShutdownLayout({ manager: manager as never, - container: mockRootForPane(1, LEAF_ID), - expandedPaneId: null, paneTransports: new Map([[1, { getPtyId: vi.fn(() => 'pty-1') }]]), - paneTitlesByPaneId: { 1: 'build logs' }, - existingLayout: undefined, - chatLeafId: LEAF_ID + existingLayout: { + ...SINGLE_PANE_LAYOUT, + titlesByLeafId: { [LEAF_ID]: 'build logs' }, + chatLeafId: LEAF_ID + } }) expect(order).toEqual(['flush', 'serialize']) @@ -218,20 +173,17 @@ describe('captureTerminalShutdownLayout', () => { } } const manager = { - getPanes: vi.fn(() => [pane]), - getActivePane: vi.fn(() => pane) + getPanes: vi.fn(() => [pane]) } const layout = captureTerminalShutdownLayout({ manager: manager as never, - container: mockRootForPane(1), - expandedPaneId: null, paneTransports: new Map([[1, { getPtyId: vi.fn(() => 'pty-1') }]]), - paneTitlesByPaneId: { 1: 'local shell' }, existingLayout: { - root: null, + root: SINGLE_PANE_LAYOUT.root, activeLeafId: null, expandedLeafId: null, + titlesByLeafId: { [LEAF_ID]: 'local shell' }, buffersByLeafId: { [LEAF_ID]: 'previous-local-scrollback' } }, captureBuffers: false @@ -259,17 +211,13 @@ describe('captureTerminalShutdownLayout', () => { } } const manager = { - getPanes: vi.fn(() => [pane]), - getActivePane: vi.fn(() => pane) + getPanes: vi.fn(() => [pane]) } const layout = captureTerminalShutdownLayout({ manager: manager as never, - container: mockRootForPane(1), - expandedPaneId: null, paneTransports: new Map([[1, { getPtyId: vi.fn(() => 'pty-1') }]]), - paneTitlesByPaneId: { 1: 'ssh shell' }, - existingLayout: undefined + existingLayout: SINGLE_PANE_LAYOUT }) const buffer = layout.buffersByLeafId?.[LEAF_ID] ?? '' @@ -299,17 +247,13 @@ describe('captureTerminalShutdownLayout', () => { serializeAddon: { serialize } } const manager = { - getPanes: vi.fn(() => [pane]), - getActivePane: vi.fn(() => pane) + getPanes: vi.fn(() => [pane]) } const layout = captureTerminalShutdownLayout({ manager: manager as never, - container: mockRootForPane(1), - expandedPaneId: null, paneTransports: new Map([[1, { getPtyId: vi.fn(() => 'pty-1') }]]), - paneTitlesByPaneId: { 1: 'ssh shell' }, - existingLayout: undefined + existingLayout: SINGLE_PANE_LAYOUT }) // Full pass plus at most MAX_SCROLLBACK_FIT_PROBES fit probes; a bisection took 10+. @@ -338,17 +282,13 @@ describe('captureTerminalShutdownLayout', () => { serializeAddon: { serialize } } const manager = { - getPanes: vi.fn(() => [pane]), - getActivePane: vi.fn(() => pane) + getPanes: vi.fn(() => [pane]) } const layout = captureTerminalShutdownLayout({ manager: manager as never, - container: mockRootForPane(1), - expandedPaneId: null, paneTransports: new Map([[1, { getPtyId: vi.fn(() => 'pty-1') }]]), - paneTitlesByPaneId: { 1: 'ssh shell' }, - existingLayout: undefined + existingLayout: SINGLE_PANE_LAYOUT }) const bytes = getUtf8ByteLength(layout.buffersByLeafId?.[LEAF_ID] ?? '') @@ -369,20 +309,17 @@ describe('captureTerminalShutdownLayout', () => { } } const manager = { - getPanes: vi.fn(() => [pane]), - getActivePane: vi.fn(() => pane) + getPanes: vi.fn(() => [pane]) } const layout = captureTerminalShutdownLayout({ manager: manager as never, - container: mockRootForPane(1), - expandedPaneId: null, paneTransports: new Map([[1, { getPtyId: vi.fn(() => 'pty-1') }]]), - paneTitlesByPaneId: { 1: 'local shell' }, existingLayout: { - root: null, + root: SINGLE_PANE_LAYOUT.root, activeLeafId: null, expandedLeafId: null, + titlesByLeafId: { [LEAF_ID]: 'local shell' }, buffersByLeafId: { [LEAF_ID]: 'previous-scrollback' }, scrollbackRefsByLeafId: { [LEAF_ID]: 'v1-previous' } }, @@ -416,18 +353,14 @@ describe('captureTerminalShutdownLayout', () => { } } const manager = { - getPanes: vi.fn(() => [deadPane, livePane]), - getActivePane: vi.fn(() => deadPane) + getPanes: vi.fn(() => [deadPane, livePane]) } const layout = captureTerminalShutdownLayout({ manager: manager as never, - container: mockRootForSplit(1, 2), - expandedPaneId: null, paneTransports: new Map([[2, { getPtyId: vi.fn(() => 'pty-live') }]]), - paneTitlesByPaneId: {}, existingLayout: { - root: null, + root: SPLIT_ROOT, activeLeafId: LEAF_ID, expandedLeafId: null, ptyIdsByLeafId: { [LEAF_ID]: 'pty-stale', [LEAF_ID_2]: 'pty-live' } @@ -463,21 +396,17 @@ describe('captureTerminalShutdownLayout', () => { } } const manager = { - getPanes: vi.fn(() => [firstPane, secondPane]), - getActivePane: vi.fn(() => secondPane) + getPanes: vi.fn(() => [firstPane, secondPane]) } const layout = captureTerminalShutdownLayout({ manager: manager as never, - container: mockRootForSplit(1, 2), - expandedPaneId: null, paneTransports: new Map([ [1, { getPtyId: vi.fn(() => null) }], [2, { getPtyId: vi.fn(() => null) }] ]), - paneTitlesByPaneId: {}, existingLayout: { - root: null, + root: SPLIT_ROOT, activeLeafId: LEAF_ID_2, expandedLeafId: null, ptyIdsByLeafId: { [LEAF_ID]: 'pty-first', [LEAF_ID_2]: 'pty-second' } @@ -512,17 +441,13 @@ describe('captureTerminalShutdownLayout', () => { } } const manager = { - getPanes: vi.fn(() => [paneWithoutPty, paneWithPty]), - getActivePane: vi.fn(() => paneWithoutPty) + getPanes: vi.fn(() => [paneWithoutPty, paneWithPty]) } const layout = captureTerminalShutdownLayout({ manager: manager as never, - container: mockRootForSplit(), - expandedPaneId: null, paneTransports: new Map([[2, { getPtyId: vi.fn(() => 'pty-2') }]]), - paneTitlesByPaneId: {}, - existingLayout: undefined + existingLayout: { root: SPLIT_ROOT, activeLeafId: LEAF_ID, expandedLeafId: null } }) expect(layout.activeLeafId).toBe(LEAF_ID_2) @@ -550,18 +475,14 @@ describe('captureTerminalShutdownLayout', () => { } } const manager = { - getPanes: vi.fn(() => [paneWithoutPty, paneWithPty]), - getActivePane: vi.fn(() => paneWithoutPty) + getPanes: vi.fn(() => [paneWithoutPty, paneWithPty]) } const layout = captureTerminalShutdownLayout({ manager: manager as never, - container: mockRootForSplit(), - expandedPaneId: null, paneTransports: new Map([[2, { getPtyId: vi.fn(() => 'pty-2') }]]), - paneTitlesByPaneId: {}, existingLayout: { - root: null, + root: SPLIT_ROOT, activeLeafId: LEAF_ID, expandedLeafId: null, ptyIdsByLeafId: { [LEAF_ID]: 'stale-pty', [LEAF_ID_2]: 'pty-2' } @@ -571,4 +492,72 @@ describe('captureTerminalShutdownLayout', () => { expect(layout.activeLeafId).toBe(LEAF_ID_2) expect(layout.ptyIdsByLeafId).toEqual({ [LEAF_ID_2]: 'pty-2' }) }) + it('keeps the stored structure instead of rebuilding it from the panes', async () => { + const { captureTerminalShutdownLayout } = await import('./terminal-shutdown-layout-capture') + const panes = [LEAF_ID, LEAF_ID_2].map((leafId, index) => ({ + id: index + 1, + leafId, + stablePaneId: leafId, + terminal: mockTerminal(1_000), + serializeAddon: { serialize: vi.fn(() => '') } + })) + const existingLayout: TerminalLayoutSnapshot = { + root: { + type: 'split', + direction: 'horizontal', + first: { type: 'leaf', leafId: LEAF_ID }, + second: { type: 'leaf', leafId: LEAF_ID_2 }, + ratio: 0.3 + }, + activeLeafId: LEAF_ID_2, + expandedLeafId: LEAF_ID_2, + titlesByLeafId: { [LEAF_ID]: 'server' } + } + + const layout = captureTerminalShutdownLayout({ + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the capture reads only the mocked pane fields. + manager: { getPanes: () => panes } as never, + paneTransports: new Map([ + [1, { getPtyId: () => 'pty-1' }], + [2, { getPtyId: () => 'pty-2' }] + ]), + existingLayout + }) + + expect(layout).toEqual({ + ...existingLayout, + ptyIdsByLeafId: { [LEAF_ID]: 'pty-1', [LEAF_ID_2]: 'pty-2' } + }) + }) + it('keeps the buffer and binding of a stored leaf whose pane is not mounted yet', async () => { + const { captureTerminalShutdownLayout } = await import('./terminal-shutdown-layout-capture') + const pane = { + id: 1, + leafId: LEAF_ID, + stablePaneId: LEAF_ID, + terminal: mockTerminal(1_000), + serializeAddon: { serialize: vi.fn(() => 'mounted scrollback') } + } + + const layout = captureTerminalShutdownLayout({ + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the capture reads only the mocked pane fields. + manager: { getPanes: () => [pane] } as never, + paneTransports: new Map([[1, { getPtyId: () => 'pty-1' }]]), + existingLayout: { + root: SPLIT_ROOT, + activeLeafId: LEAF_ID, + expandedLeafId: null, + ptyIdsByLeafId: { [LEAF_ID]: 'pty-1', [LEAF_ID_2]: 'pty-2' }, + buffersByLeafId: { [LEAF_ID_2]: 'unmounted scrollback' }, + scrollbackRefsByLeafId: { [LEAF_ID_2]: 'v1-unmounted' } + } + }) + + expect(layout.buffersByLeafId).toEqual({ + [LEAF_ID]: `mounted scrollback${CURSOR_HOME}`, + [LEAF_ID_2]: 'unmounted scrollback' + }) + expect(layout.scrollbackRefsByLeafId).toEqual({ [LEAF_ID_2]: 'v1-unmounted' }) + expect(layout.ptyIdsByLeafId).toEqual({ [LEAF_ID]: 'pty-1', [LEAF_ID_2]: 'pty-2' }) + }) }) diff --git a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts index a16e2434b4b..7e527325755 100644 --- a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts +++ b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts @@ -2,8 +2,8 @@ import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-typ import type { ManagedPane } from '@/lib/pane-manager/pane-manager' import type { PtyTransport } from './pty-transport-types' import { flushTerminalOutput } from '@/lib/pane-manager/pane-terminal-output-scheduler' -import { serializeTerminalLayout } from './layout-serialization' import { mergeCapturedLeafState } from './merge-captured-leaf-state' +import { collectLeafIds } from './terminal-pane-layout-tree' import { resolveTerminalLayoutActiveLeafId } from './terminal-layout-leaf-ids' import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from '../../../../shared/terminal-scrollback-limits' import { serializeWithAbsoluteCursor } from '../../../../shared/terminal-serialize-absolute-cursor' @@ -15,17 +15,13 @@ type ShutdownPane = Pick> - paneTitlesByPaneId: Record - existingLayout: TerminalLayoutSnapshot | undefined - chatLeafId?: string | null + /** The tab's stored layout; its structure is kept as is, never re-read from the DOM. */ + existingLayout: TerminalLayoutSnapshot /** Merge prior for buffers, resolved across both scrollback homes; defaults to the shared layout's. */ priorBuffersByLeafId?: Record captureBuffers?: boolean @@ -94,13 +90,9 @@ function serializeWithinSessionScrollbackByteLimit( export function captureTerminalShutdownLayout({ manager, - container, - expandedPaneId, paneTransports, - paneTitlesByPaneId, existingLayout, - chatLeafId, - priorBuffersByLeafId = existingLayout?.buffersByLeafId, + priorBuffersByLeafId = existingLayout.buffersByLeafId, captureBuffers = true, clearedScrollbackLeafIds }: CaptureTerminalShutdownLayoutArgs): TerminalLayoutSnapshot { @@ -135,19 +127,18 @@ export function captureTerminalShutdownLayout({ } } - const activePaneId = manager.getActivePane()?.id ?? panes[0]?.id ?? null - const layout = serializeTerminalLayout( - container, - activePaneId, - expandedPaneId, - new Map(panes.map((pane) => [pane.id, pane.leafId])) - ) - const currentLeafIds = new Set(panes.map((p) => p.leafId)) - if (chatLeafId && panes.some((pane) => pane.leafId === chatLeafId)) { - layout.chatLeafId = chatLeafId - } + // Why the stored tree, not the mounted panes: reconciliation can leave a stored leaf unmounted. + const currentLeafIds = new Set(collectLeafIds(existingLayout.root)) const livePtyIdsByLeafId: Record = {} const preservedPtyIdsByLeafId: Record = {} + const mountedLeafIds = new Set(panes.map((p) => p.leafId)) + for (const leafId of currentLeafIds) { + const priorPtyId = existingLayout.ptyIdsByLeafId?.[leafId] + // Why: reconciliation inserts an unmounted leaf only while it still has its binding. + if (!mountedLeafIds.has(leafId) && priorPtyId) { + preservedPtyIdsByLeafId[leafId] = priorPtyId + } + } for (const pane of panes) { const transport = paneTransports.get(pane.id) const livePtyId = transport?.getPtyId() ?? null @@ -155,7 +146,7 @@ export function captureTerminalShutdownLayout({ livePtyIdsByLeafId[pane.leafId] = livePtyId continue } - const priorPtyId = existingLayout?.ptyIdsByLeafId?.[pane.leafId] + const priorPtyId = existingLayout.ptyIdsByLeafId?.[pane.leafId] if (transport && priorPtyId) { // Why: shutdown can capture during the post-remount attach gap where // each pane has a transport but the deferred PTY ID is still null. @@ -171,35 +162,31 @@ export function captureTerminalShutdownLayout({ }) : {} const mergedScrollbackRefs = mergeCapturedLeafState({ - prior: omitClearedLeafState(existingLayout?.scrollbackRefsByLeafId, clearedScrollbackLeafIds), + prior: omitClearedLeafState(existingLayout.scrollbackRefsByLeafId, clearedScrollbackLeafIds), fresh: {}, currentLeafIds }) const ptyIdsByLeafId = { ...preservedPtyIdsByLeafId, ...livePtyIdsByLeafId } - // Why: shutdown snapshots can otherwise persist focus on a mounted pane whose - // transport was already cleared during PTY exit/reconnect cleanup. Unlike - // scrollback, PTY bindings only preserve prior ids during a live attach gap. - layout.activeLeafId = resolveTerminalLayoutActiveLeafId({ - root: layout.root, - activeLeafId: layout.activeLeafId, - ptyIdsByLeafId - }) - if (Object.keys(mergedBuffers).length > 0) { - layout.buffersByLeafId = mergedBuffers + const { + buffersByLeafId: _buffers, + scrollbackRefsByLeafId: _scrollbackRefs, + ptyIdsByLeafId: _ptyIds, + ...structure + } = existingLayout + return { + ...structure, + // Why: shutdown snapshots can otherwise persist focus on a mounted pane whose + // transport was already cleared during PTY exit/reconnect cleanup. Unlike + // scrollback, PTY bindings only preserve prior ids during a live attach gap. + activeLeafId: resolveTerminalLayoutActiveLeafId({ + root: structure.root, + activeLeafId: structure.activeLeafId, + ptyIdsByLeafId + }), + ...(Object.keys(mergedBuffers).length > 0 ? { buffersByLeafId: mergedBuffers } : {}), + ...(Object.keys(mergedScrollbackRefs).length > 0 + ? { scrollbackRefsByLeafId: mergedScrollbackRefs } + : {}), + ...(Object.keys(ptyIdsByLeafId).length > 0 ? { ptyIdsByLeafId } : {}) } - if (Object.keys(mergedScrollbackRefs).length > 0) { - layout.scrollbackRefsByLeafId = mergedScrollbackRefs - } - if (Object.keys(ptyIdsByLeafId).length > 0) { - layout.ptyIdsByLeafId = ptyIdsByLeafId - } - - const titleEntries = panes - .filter((p) => paneTitlesByPaneId[p.id]) - .map((p) => [p.leafId, paneTitlesByPaneId[p.id]] as const) - if (titleEntries.length > 0) { - layout.titlesByLeafId = Object.fromEntries(titleEntries) - } - - return layout } diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-title-effects.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-title-effects.ts index 68a3f7a7e95..9ac1bdf29c5 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-title-effects.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-title-effects.ts @@ -25,19 +25,15 @@ import type { TerminalPaneCloseController } from './use-terminal-pane-close-acti export function useTerminalPaneTitleEffects(controller: TerminalPaneCloseController): void { const { - chatLeafId, clearedScrollbackLeafIdsRef, containerRef, expandedPaneId, - expandedPaneIdRef, - isChatViewMode, isolatedPaneKey, isVisible, managerRef, paneCount, paneLayoutRevision, paneTitles, - paneTitlesRef, paneTransportsRef, renamingPaneId, renameInputRef, @@ -172,28 +168,24 @@ export function useTerminalPaneTitleEffects(controller: TerminalPaneCloseControl useEffect(() => { const captureBuffers = (options?: ShutdownBufferCaptureOptions): void => { const manager = managerRef.current - const container = containerRef.current - if (!manager || !container) { + const state = useAppStore.getState() + const existing = state.terminalLayoutsByTabId[tabId] + // Mounting stores the layout; only closing the tab or its workspace removes it. + if (!manager || !existing) { return } const panes = manager.getPanes() if (panes.length === 0) { return } - const state = useAppStore.getState() - const existing = state.terminalLayoutsByTabId[tabId] const includeLocalBuffers = options?.includeLocalBuffers ?? true const shouldCaptureScrollbackBuffers = includeLocalBuffers ? true : shouldPreserveTerminalScrollbackBuffers(worktreeId, state.repos) const layout = captureTerminalShutdownLayout({ manager, - container, - expandedPaneId: expandedPaneIdRef.current, paneTransports: paneTransportsRef.current, - paneTitlesByPaneId: paneTitlesRef.current, existingLayout: existing, - chatLeafId: isChatViewMode ? chatLeafId : null, // Why resolved: a pane that serializes empty mid-replay must fall back to whichever home // holds its last good copy, not only the shared one. priorBuffersByLeafId: resolveLeafScrollbackBuffers({ @@ -226,7 +218,7 @@ export function useTerminalPaneTitleEffects(controller: TerminalPaneCloseControl } } // oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract. - }, [chatLeafId, isChatViewMode, tabId, worktreeId, setTabLayout, setTabLocalOnlyScrollback]) + }, [tabId, worktreeId, setTabLayout, setTabLocalOnlyScrollback]) useEffect(() => { if (renamingPaneId === null) { From 2036c595099f84bbd2dfe6c14ced62895f0fbaec Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:36:54 -0400 Subject: [PATCH 32/53] refactor(store): route worktree, repo and SSH layout writes through tabs-slice transitions (#28892) Worktree removal, purge, rename, activation wake and SSH target cleanup now build their tab/group/pane layout changes with named transitions in tabs/worktree-layout-lifecycle.ts, folded into the same single store update. Project removal drops its layout writes: purgeWorktreeTerminalState, run just before in the same call, already cleared every one of them. Co-authored-by: Claude Opus 5.5 --- config/renderer-layout-writer-baseline.txt | 8 +- src/renderer/src/store/repos/repo-removal.ts | 39 +--- .../src/store/slices/ssh-target-cleanup.ts | 30 +-- .../slices/tabs/worktree-layout-lifecycle.ts | 195 ++++++++++++++++++ .../worktrees/session/set-active-worktree.ts | 40 +--- .../session/worktree-identity-rename-state.ts | 29 +-- .../teardown/remove-worktree-store-cleanup.ts | 32 +-- .../teardown/worktree-purge-state.ts | 32 +-- 8 files changed, 229 insertions(+), 176 deletions(-) create mode 100644 src/renderer/src/store/slices/tabs/worktree-layout-lifecycle.ts diff --git a/config/renderer-layout-writer-baseline.txt b/config/renderer-layout-writer-baseline.txt index 50775b2cb2b..7203ba31ba6 100644 --- a/config/renderer-layout-writer-baseline.txt +++ b/config/renderer-layout-writer-baseline.txt @@ -51,7 +51,6 @@ 1 src/renderer/src/runtime/web-session-terminal-orphan-recovery-surface.ts 2 src/renderer/src/runtime/web-session-terminal-orphan-topology.ts 3 src/renderer/src/runtime/web-session-terminal-recovery-snapshot-validation.ts -9 src/renderer/src/store/repos/repo-removal.ts 10 src/renderer/src/store/slices/browser/browser-close-actions.ts 6 src/renderer/src/store/slices/browser/browser-hydration-actions.ts 5 src/renderer/src/store/slices/browser/browser-page-focus-actions.ts @@ -81,7 +80,6 @@ 2 src/renderer/src/store/slices/editor/file-ids/hydrated-editor-file-selection.ts 1 src/renderer/src/store/slices/editor/file-ids/open-file-path-rekey.ts 4 src/renderer/src/store/slices/editor/tabs/editor-open-target-group.ts -2 src/renderer/src/store/slices/ssh-target-cleanup.ts 2 src/renderer/src/store/slices/tab-group-reference-repair.ts 4 src/renderer/src/store/slices/tab-group-state.ts 2 src/renderer/src/store/slices/tabs-hydration.ts @@ -98,14 +96,12 @@ 4 src/renderer/src/store/slices/tabs/tabs-session-actions.ts 1 src/renderer/src/store/slices/tabs/tabs-split-ratio-actions.ts 5 src/renderer/src/store/slices/tabs/tabs-surface.ts +33 src/renderer/src/store/slices/tabs/worktree-layout-lifecycle.ts 6 src/renderer/src/store/slices/terminal-orphan-helpers.ts 4 src/renderer/src/store/slices/terminal-tab-title-batch.ts 6 src/renderer/src/store/slices/terminals.ts -7 src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts -15 src/renderer/src/store/slices/worktrees/session/worktree-identity-rename-state.ts +5 src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts 2 src/renderer/src/store/slices/worktrees/session/worktree-slice-lookups.ts -18 src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts -18 src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts 5 src/renderer/src/store/terminals/terminal-layout-state.ts 2 src/renderer/src/store/terminals/terminal-pane-hibernation.ts 3 src/renderer/src/store/terminals/terminal-pty-bindings.ts diff --git a/src/renderer/src/store/repos/repo-removal.ts b/src/renderer/src/store/repos/repo-removal.ts index 9f39a8f4db6..13ad0478827 100644 --- a/src/renderer/src/store/repos/repo-removal.ts +++ b/src/renderer/src/store/repos/repo-removal.ts @@ -165,11 +165,9 @@ export function createRepoRemovalActions( .flatMap((worktree) => (worktree.projectId ? [worktree.projectId] : [])) ] : [] - const killedTabIds = new Set() for (const wId of worktreeIds) { const tabs = get().tabsByWorktree[wId] ?? [] for (const tab of tabs) { - killedTabIds.add(tab.id) for (const ptyId of get().ptyIdsByTabId[tab.id] ?? []) { if (!isRemoteRuntimePtyId(ptyId)) { window.api.pty.kill(ptyId) @@ -205,35 +203,11 @@ export function createRepoRemovalActions( delete nextDetectedWorktrees[projectId] } } - const nextTabs = { ...s.tabsByWorktree } - const nextLayouts = { ...s.terminalLayoutsByTabId } - const nextLocalOnlyScrollback = { ...s.localOnlyScrollbackByTabId } - const nextPtyIdsByTabId = { ...s.ptyIdsByTabId } - const nextRuntimePaneTitlesByTabId = { ...s.runtimePaneTitlesByTabId } - for (const wId of worktreeIds) { - delete nextTabs[wId] - } - for (const tabId of killedTabIds) { - delete nextLayouts[tabId] - delete nextLocalOnlyScrollback[tabId] - delete nextPtyIdsByTabId[tabId] - delete nextRuntimePaneTitlesByTabId[tabId] - } - // Why: editor state is worktree-scoped; clear the repo's open files + active-file tracking so orphans don't linger in the session save. + // Why no tab/layout/editor writes here: purgeWorktreeTerminalState above already dropped them. const worktreeIdSet = new Set(worktreeIds) const removedVisitKeys = new Set( worktreeIds.map((worktreeId) => getWorktreeVisitKey(worktreeId, ownerHostId)) ) - const nextOpenFiles = s.openFiles.filter((f) => !worktreeIdSet.has(f.worktreeId)) - const nextActiveFileIdByWorktree = { ...s.activeFileIdByWorktree } - const nextActiveTabTypeByWorktree = { ...s.activeTabTypeByWorktree } - for (const wId of worktreeIds) { - delete nextActiveFileIdByWorktree[wId] - delete nextActiveTabTypeByWorktree[wId] - } - const activeFileCleared = s.activeFileId - ? s.openFiles.some((f) => f.id === s.activeFileId && worktreeIdSet.has(f.worktreeId)) - : false const nextRepos = s.repos.filter( (r) => !repoMatchesHostIdentity(r, projectId, ownerHostId) ) @@ -268,17 +242,6 @@ export function createRepoRemovalActions( filterRepoIds: s.filterRepoIds.filter((id) => id !== projectId), worktreesByRepo: nextWorktrees, detectedWorktreesByRepo: nextDetectedWorktrees, - tabsByWorktree: nextTabs, - ptyIdsByTabId: nextPtyIdsByTabId, - runtimePaneTitlesByTabId: nextRuntimePaneTitlesByTabId, - terminalLayoutsByTabId: nextLayouts, - localOnlyScrollbackByTabId: nextLocalOnlyScrollback, - activeTabId: s.activeTabId && killedTabIds.has(s.activeTabId) ? null : s.activeTabId, - openFiles: nextOpenFiles, - activeFileIdByWorktree: nextActiveFileIdByWorktree, - activeTabTypeByWorktree: nextActiveTabTypeByWorktree, - activeFileId: activeFileCleared ? null : s.activeFileId, - activeTabType: activeFileCleared ? 'terminal' : s.activeTabType, lastVisitedAtByWorktreeId: nextLastVisitedAtByWorktreeId, folderWorkspacePathStatuses: {}, sortEpoch: s.sortEpoch + 1, diff --git a/src/renderer/src/store/slices/ssh-target-cleanup.ts b/src/renderer/src/store/slices/ssh-target-cleanup.ts index ed620cf1e31..57823f6c6f1 100644 --- a/src/renderer/src/store/slices/ssh-target-cleanup.ts +++ b/src/renderer/src/store/slices/ssh-target-cleanup.ts @@ -3,6 +3,7 @@ import type { SshTarget, SshTargetSummary } from '../../../../shared/ssh-types' import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id' import { sanitizeSshTargetGeneration } from '../../../../shared/ssh-target-generation' import { resolveDirectSshTargetScope } from '../../lib/direct-ssh-target-scope' +import { unbindTerminalTabPtys } from './tabs/worktree-layout-lifecycle' export function sshTargetLabelsEqual( labels: Map, @@ -134,8 +135,7 @@ function clearSshTargetTabPtyState( | 'pendingCodexPaneRestartIds' | 'codexRestartNoticeByPtyId' > & { changed: boolean } { - let nextTabsByWorktree = state.tabsByWorktree - const nextPtyIdsByTabId = { ...state.ptyIdsByTabId } + const unboundTabIds = new Set() const nextLastKnownRelayPtyIdByTabId = { ...state.lastKnownRelayPtyIdByTabId } @@ -143,11 +143,9 @@ function clearSshTargetTabPtyState( ...state.pendingCodexPaneRestartIds } const nextCodexRestartNoticeByPtyId = { ...state.codexRestartNoticeByPtyId } - let changed = false - for (const [worktreeId, tabs] of Object.entries(state.tabsByWorktree)) { - let nextTabs = tabs - for (const [index, tab] of tabs.entries()) { + for (const tabs of Object.values(state.tabsByWorktree)) { + for (const tab of tabs) { const lastKnownPtyId = state.lastKnownRelayPtyIdByTabId[tab.id] const ptyIds = [ ...new Set([ @@ -164,32 +162,18 @@ function clearSshTargetTabPtyState( if (!tab.ptyId && ptyIds.length === 0 && nextLastKnownRelayPtyIdByTabId[tab.id] == null) { continue } - changed = true - if (nextTabs === tabs) { - nextTabs = [...tabs] - } - const { pendingActivationSpawn: _pendingActivationSpawn, ...tabWithoutActivationSpawn } = tab - void _pendingActivationSpawn - nextTabs[index] = { ...tabWithoutActivationSpawn, ptyId: null } - nextPtyIdsByTabId[tab.id] = [] + unboundTabIds.add(tab.id) delete nextLastKnownRelayPtyIdByTabId[tab.id] for (const ptyId of ptyIds) { delete nextPendingCodexPaneRestartIds[ptyId] delete nextCodexRestartNoticeByPtyId[ptyId] } } - if (nextTabs !== tabs) { - if (nextTabsByWorktree === state.tabsByWorktree) { - nextTabsByWorktree = { ...nextTabsByWorktree } - } - nextTabsByWorktree[worktreeId] = nextTabs - } } return { - changed, - tabsByWorktree: nextTabsByWorktree, - ptyIdsByTabId: nextPtyIdsByTabId, + changed: unboundTabIds.size > 0, + ...unbindTerminalTabPtys(state, unboundTabIds), lastKnownRelayPtyIdByTabId: nextLastKnownRelayPtyIdByTabId, pendingCodexPaneRestartIds: nextPendingCodexPaneRestartIds, codexRestartNoticeByPtyId: nextCodexRestartNoticeByPtyId diff --git a/src/renderer/src/store/slices/tabs/worktree-layout-lifecycle.ts b/src/renderer/src/store/slices/tabs/worktree-layout-lifecycle.ts new file mode 100644 index 00000000000..6e47fddb79c --- /dev/null +++ b/src/renderer/src/store/slices/tabs/worktree-layout-lifecycle.ts @@ -0,0 +1,195 @@ +import type { AppState } from '../../types' +import type { BrowserWorkspace } from '../../../../../shared/browser-workspace-types' +import type { TerminalTab } from '../../../../../shared/terminal-tab-types' +import { omitRecordKeys } from '../worktrees/teardown/record-key-omission' +import { getTerminalActivationSpawnSuppression } from '../terminal-activation-spawn-suppression' + +// Layout transitions that worktree, repo and SSH lifecycle code folds into its own store update. +// Pure patches, not actions: each caller's update is one atomic set that also clears non-layout state. + +type WorktreeLayoutPatch = Pick< + AppState, + | 'tabsByWorktree' + | 'terminalLayoutsByTabId' + | 'ptyIdsByTabId' + | 'unifiedTabsByWorktree' + | 'groupsByWorktree' + | 'layoutByWorktree' + | 'activeGroupIdByWorktree' + | 'tabBarOrderByWorktree' + | 'browserTabsByWorktree' + | 'openFiles' + | 'activeTabIdByWorktree' + | 'activeFileIdByWorktree' + | 'activeBrowserTabIdByWorktree' + | 'activeTabTypeByWorktree' + | 'activeTabId' + | 'activeFileId' + | 'activeBrowserTabId' + | 'activeTabType' +> + +/** Drops removed worktrees' tabs, groups and pane layouts, and clears selection that pointed into them. */ +export function removeWorktreeLayout( + s: AppState, + removed: { + worktreeIds: ReadonlySet + tabIds: ReadonlySet + activeFileCleared: boolean + } +): WorktreeLayoutPatch { + const { worktreeIds, tabIds, activeFileCleared } = removed + const omitByWorktree = (m: Record | undefined) => omitRecordKeys(m, worktreeIds) + const omitByTabId = (m: Record | undefined) => omitRecordKeys(m, tabIds) + const removedActiveWorktree = s.activeWorktreeId != null && worktreeIds.has(s.activeWorktreeId) + return { + tabsByWorktree: omitByWorktree(s.tabsByWorktree), + terminalLayoutsByTabId: omitByTabId(s.terminalLayoutsByTabId), + ptyIdsByTabId: omitByTabId(s.ptyIdsByTabId), + // Why: split-tab layout/group state is worktree-owned; leaving it makes a deleted worktree look restorable. + unifiedTabsByWorktree: omitByWorktree(s.unifiedTabsByWorktree), + groupsByWorktree: omitByWorktree(s.groupsByWorktree), + layoutByWorktree: omitByWorktree(s.layoutByWorktree), + activeGroupIdByWorktree: omitByWorktree(s.activeGroupIdByWorktree), + // Why: the tab strip persists visual order per worktree; drop the entry so stale tab IDs aren't retained. + tabBarOrderByWorktree: omitByWorktree(s.tabBarOrderByWorktree), + browserTabsByWorktree: omitByWorktree(s.browserTabsByWorktree), + // Why guarded: a removed worktree usually has no open file, and an unconditional + // filter would hand openFiles a new identity anyway. + openFiles: s.openFiles.some((f) => worktreeIds.has(f.worktreeId)) + ? s.openFiles.filter((f) => !worktreeIds.has(f.worktreeId)) + : s.openFiles, + activeTabIdByWorktree: omitByWorktree(s.activeTabIdByWorktree), + activeFileIdByWorktree: omitByWorktree(s.activeFileIdByWorktree), + activeBrowserTabIdByWorktree: omitByWorktree(s.activeBrowserTabIdByWorktree), + activeTabTypeByWorktree: omitByWorktree(s.activeTabTypeByWorktree), + activeTabId: s.activeTabId && tabIds.has(s.activeTabId) ? null : s.activeTabId, + activeFileId: activeFileCleared ? null : s.activeFileId, + activeBrowserTabId: removedActiveWorktree ? null : s.activeBrowserTabId, + activeTabType: removedActiveWorktree || activeFileCleared ? 'terminal' : s.activeTabType + } +} + +function rekeyWorktreeEntry( + map: Record | undefined, + oldWorktreeId: string, + newWorktreeId: string, + mapValue: (value: T) => T = (value) => value +): Record | undefined { + if (!map || !(oldWorktreeId in map)) { + return undefined + } + const next = { ...map } + next[newWorktreeId] = mapValue(map[oldWorktreeId]) + delete next[oldWorktreeId] + return next +} + +/** Moves a renamed worktree's tabs, groups, pane layouts and selection to its new id. */ +export function renameWorktreeLayoutKeys( + s: AppState, + oldWorktreeId: string, + newWorktreeId: string, + renameBrowserWorkspace: (workspace: BrowserWorkspace) => BrowserWorkspace +): Partial { + const withNewWorktreeId = (value: T): T => + value.worktreeId === oldWorktreeId ? { ...value, worktreeId: newWorktreeId } : value + const rekey = (map: Record | undefined, mapValue?: (value: T) => T) => + rekeyWorktreeEntry(map, oldWorktreeId, newWorktreeId, mapValue) + const patch: Partial = {} + const assign = ( + key: K, + value: WorktreeLayoutPatch[K] | undefined + ): void => { + if (value !== undefined) { + patch[key] = value + } + } + assign( + 'tabsByWorktree', + rekey(s.tabsByWorktree, (tabs) => tabs.map(withNewWorktreeId)) + ) + assign( + 'browserTabsByWorktree', + rekey(s.browserTabsByWorktree, (workspaces) => workspaces.map(renameBrowserWorkspace)) + ) + assign('activeBrowserTabIdByWorktree', rekey(s.activeBrowserTabIdByWorktree)) + assign('activeFileIdByWorktree', rekey(s.activeFileIdByWorktree)) + assign('activeTabTypeByWorktree', rekey(s.activeTabTypeByWorktree)) + assign('activeTabIdByWorktree', rekey(s.activeTabIdByWorktree)) + assign('tabBarOrderByWorktree', rekey(s.tabBarOrderByWorktree)) + assign( + 'unifiedTabsByWorktree', + rekey(s.unifiedTabsByWorktree, (tabs) => tabs.map(withNewWorktreeId)) + ) + assign( + 'groupsByWorktree', + rekey(s.groupsByWorktree, (groups) => groups.map(withNewWorktreeId)) + ) + assign('layoutByWorktree', rekey(s.layoutByWorktree)) + assign('activeGroupIdByWorktree', rekey(s.activeGroupIdByWorktree)) + if (s.openFiles?.some((f) => f.worktreeId === oldWorktreeId)) { + assign('openFiles', s.openFiles.map(withNewWorktreeId)) + } + return patch +} + +/** + * Tags a worktree's wakeable terminal tabs for an activation spawn, optionally bumping their + * generation so dead panes remount. + */ +export function markWorktreeTabsForActivationSpawn( + s: AppState, + worktreeId: string, + options: { isWakeable: (tab: TerminalTab) => boolean; bumpGeneration: boolean } +): Pick { + const { isWakeable, bumpGeneration } = options + return { + tabsByWorktree: { + ...s.tabsByWorktree, + [worktreeId]: (s.tabsByWorktree[worktreeId] ?? []).map((tab) => + !isWakeable(tab) + ? tab + : { + ...tab, + ...(bumpGeneration ? { generation: (tab.generation ?? 0) + 1 } : {}), + // Why: slept terminal remount/spawn is click-driven wake work; tag its PTY updates so they don't reshuffle Recent. + pendingActivationSpawn: getTerminalActivationSpawnSuppression( + s.terminalLayoutsByTabId[tab.id] + ) + } + ) + } + } +} + +/** Detaches terminal tabs from their PTYs, leaving the tabs in place to respawn. */ +export function unbindTerminalTabPtys( + s: AppState, + tabIds: ReadonlySet +): Pick { + let nextTabsByWorktree = s.tabsByWorktree + const nextPtyIdsByTabId = { ...s.ptyIdsByTabId } + for (const [worktreeId, tabs] of Object.entries(s.tabsByWorktree)) { + let nextTabs = tabs + for (const [index, tab] of tabs.entries()) { + if (!tabIds.has(tab.id)) { + continue + } + if (nextTabs === tabs) { + nextTabs = [...tabs] + } + const { pendingActivationSpawn: _pendingActivationSpawn, ...tabWithoutActivationSpawn } = tab + void _pendingActivationSpawn + nextTabs[index] = { ...tabWithoutActivationSpawn, ptyId: null } + nextPtyIdsByTabId[tab.id] = [] + } + if (nextTabs !== tabs) { + if (nextTabsByWorktree === s.tabsByWorktree) { + nextTabsByWorktree = { ...nextTabsByWorktree } + } + nextTabsByWorktree[worktreeId] = nextTabs + } + } + return { tabsByWorktree: nextTabsByWorktree, ptyIdsByTabId: nextPtyIdsByTabId } +} diff --git a/src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts b/src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts index 913620ba43a..eafdd0e40fe 100644 --- a/src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts +++ b/src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts @@ -9,7 +9,7 @@ import { ACTIVE_WORKTREE_TERMINAL_PREP_IDLE_TIMEOUT_MS, ACTIVE_WORKTREE_TERMINAL_PREP_INPUT_QUIET_MS } from '../listing/worktree-slice-constants' -import { getTerminalActivationSpawnSuppression } from '../../terminal-activation-spawn-suppression' +import { markWorktreeTabsForActivationSpawn } from '../../tabs/worktree-layout-lifecycle' import { isWorkspaceKey, parseWorkspaceKey, @@ -114,22 +114,7 @@ export function createSetActiveWorktree( : s.activeRepoId const tabsByWorktreeUpdate = allDead && worktreeId != null - ? { - tabsByWorktree: { - ...s.tabsByWorktree, - [worktreeId]: tabs.map((tab) => - !isWakeable(tab) - ? tab - : { - ...tab, - generation: (tab.generation ?? 0) + 1, - pendingActivationSpawn: getTerminalActivationSpawnSuppression( - s.terminalLayoutsByTabId[tab.id] - ) - } - ) - } - } + ? markWorktreeTabsForActivationSpawn(s, worktreeId, { isWakeable, bumpGeneration: true }) : {} const nextActiveTabTypeByWorktree = @@ -199,23 +184,10 @@ export function createSetActiveWorktree( if (!allDead && !shouldTagTerminalTabs) { return s } - return { - tabsByWorktree: { - ...s.tabsByWorktree, - [worktreeId]: tabs.map((tab) => - !isWakeable(tab) - ? tab - : { - ...tab, - ...(allDead ? { generation: (tab.generation ?? 0) + 1 } : {}), - // Why: slept terminal remount/spawn is click-driven wake work; tag its PTY updates so they don't reshuffle Recent. - pendingActivationSpawn: getTerminalActivationSpawnSuppression( - s.terminalLayoutsByTabId[tab.id] - ) - } - ) - } - } + return markWorktreeTabsForActivationSpawn(s, worktreeId, { + isWakeable, + bumpGeneration: allDead + }) }) } diff --git a/src/renderer/src/store/slices/worktrees/session/worktree-identity-rename-state.ts b/src/renderer/src/store/slices/worktrees/session/worktree-identity-rename-state.ts index 7cfbd6f674c..0fa6af89996 100644 --- a/src/renderer/src/store/slices/worktrees/session/worktree-identity-rename-state.ts +++ b/src/renderer/src/store/slices/worktrees/session/worktree-identity-rename-state.ts @@ -11,30 +11,21 @@ import { remapClosedTerminalTabSnapshotCwds, type ClosedTerminalTabSnapshot } from '../../recently-closed-tabs' +import { renameWorktreeLayoutKeys } from '../../tabs/worktree-layout-lifecycle' // Every worktree-id-keyed store map the rename path re-keys, so a new `*ByWorktree` map isn't silently missed. -// Tab-id/file-id-keyed maps are deliberately excluded: tabs and files keep their ids across a rename. +// Layout maps are re-keyed by renameWorktreeLayoutKeys. Tab-id/file-id-keyed maps are deliberately +// excluded: tabs and files keep their ids across a rename. const WORKTREE_ID_KEYED_MAP_KEYS = [ 'worktreeLineageById', - 'tabsByWorktree', 'deleteStateByWorktreeId', 'baseStatusByWorktreeId', 'remoteBranchConflictByWorktreeId', 'fileSearchStateByWorktree', - 'browserTabsByWorktree', 'recentlyClosedBrowserTabsByWorktree', - 'activeBrowserTabIdByWorktree', - 'activeFileIdByWorktree', - 'activeTabTypeByWorktree', - 'activeTabIdByWorktree', - 'tabBarOrderByWorktree', 'pendingReconnectTabByWorktree', 'rightSidebarTabByWorktree', 'rightSidebarExplorerViewByWorktree', - 'unifiedTabsByWorktree', - 'groupsByWorktree', - 'layoutByWorktree', - 'activeGroupIdByWorktree', 'gitStatusByWorktree', 'gitStatusHeadByWorktree', 'gitBranchLineTotalByWorktree', @@ -100,9 +91,6 @@ export function buildWorktreeRenameState( : renamedValue } const renameValueByKey: Partial> = { - tabsByWorktree: (tabs: { worktreeId: string }[]) => tabs.map(withNewWorktreeId), - browserTabsByWorktree: (workspaces: BrowserWorkspace[]) => - workspaces.map(withNewBrowserWorktreeId), recentlyClosedBrowserTabsByWorktree: ( snapshots: { workspace: BrowserWorkspace; pages: BrowserPage[] }[] ) => @@ -114,9 +102,7 @@ export function buildWorktreeRenameState( fileSearchStateByWorktree: (searchState: AppState['fileSearchStateByWorktree'][string]) => ({ ...searchState, resultOwner: searchState.resultOwner ? withNewWorktreeId(searchState.resultOwner) : null - }), - unifiedTabsByWorktree: (tabs: { worktreeId: string }[]) => tabs.map(withNewWorktreeId), - groupsByWorktree: (groups: { worktreeId: string }[]) => groups.map(withNewWorktreeId) + }) } for (const key of WORKTREE_ID_KEYED_MAP_KEYS) { renameKey(key, renameValueByKey[key] as ((value: unknown) => unknown) | undefined) @@ -151,11 +137,6 @@ export function buildWorktreeRenameState( ) renameKey('remoteStatusesByWorktree') - const openFiles = s.openFiles?.some((f) => f.worktreeId === oldWorktreeId) - ? s.openFiles.map((f) => - f.worktreeId === oldWorktreeId ? { ...f, worktreeId: newWorktreeId } : f - ) - : s.openFiles const currentBrowserPagesByWorkspace = s.browserPagesByWorkspace ?? {} const browserPagesByWorkspace = Object.values(currentBrowserPagesByWorkspace).some((pages) => pages.some( @@ -243,8 +224,8 @@ export function buildWorktreeRenameState( : s.sleepingAgentSessionsByPaneKey return { + ...renameWorktreeLayoutKeys(s, oldWorktreeId, newWorktreeId, withNewBrowserWorktreeId), ...(renamed as Partial), - ...(openFiles !== s.openFiles ? { openFiles } : {}), ...(browserPagesByWorkspace !== s.browserPagesByWorkspace ? { browserPagesByWorkspace } : {}), ...(recentlyClosedBrowserPagesByWorkspace !== s.recentlyClosedBrowserPagesByWorkspace ? { recentlyClosedBrowserPagesByWorkspace } diff --git a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts index 202ad814a45..4355a889e2d 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts @@ -5,6 +5,7 @@ import { removeDeleteStatesForWorktreeIds } from './worktree-delete-state' import { removeWorktreeVisitEntries } from '@/lib/worktree-visit-recency' import { forgetAmbiguousOwnerWarnings } from '../listing/worktree-owner-settings' import { omitRecordKeys } from './record-key-omission' +import { removeWorktreeLayout } from '../../tabs/worktree-layout-lifecycle' /** Clears worktree-owned renderer state after successful removal so a reused identity cannot inherit stale preferences. */ export function applyRemoveWorktreeSuccessState( @@ -36,31 +37,27 @@ export function applyRemoveWorktreeSuccessState( } } const omitByFileId = (m: Record | undefined) => omitRecordKeys(m, removedFileIds) - // Why guarded: a removed worktree usually has no open file, and an unconditional - // filter would hand openFiles a new identity anyway — the sibling purge path - // already does this. - const nextOpenFiles = s.openFiles.some((f) => f.worktreeId === worktreeId) - ? s.openFiles.filter((f) => f.worktreeId !== worktreeId) - : s.openFiles // If the active file belonged to the removed worktree, clear it const activeFileCleared = s.activeFileId ? s.openFiles.some((f) => f.id === s.activeFileId && f.worktreeId === worktreeId) : false const removedActiveWorktree = s.activeWorktreeId === worktreeId return { + ...removeWorktreeLayout(s, { + worktreeIds: new Set(worktreeIds), + tabIds, + activeFileCleared + }), worktreesByRepo: nextWorktreesByRepo, worktreeLineageById: omitByWorktree(s.worktreeLineageById), workspaceLineageByChildKey: omitRecordKeys(s.workspaceLineageByChildKey, [ worktreeWorkspaceKey(worktreeId) ]), - tabsByWorktree: omitByWorktree(s.tabsByWorktree), - ptyIdsByTabId: omitByTabId(s.ptyIdsByTabId), runtimePaneTitlesByTabId: omitByTabId(s.runtimePaneTitlesByTabId), automaticAgentResumeClaimsByTabId: omitByTabId(s.automaticAgentResumeClaimsByTabId), nativeChatLaunchPromptByTabId: omitByTabId(s.nativeChatLaunchPromptByTabId), nativeChatLaunchDraftByTabId: omitByTabId(s.nativeChatLaunchDraftByTabId), unverifiedPtyLossTabIds: omitByTabId(s.unverifiedPtyLossTabIds), - terminalLayoutsByTabId: omitByTabId(s.terminalLayoutsByTabId), localOnlyScrollbackByTabId: omitByTabId(s.localOnlyScrollbackByTabId), // Why: closeTab deletes these per-tab maps but removeWorktree missed them, leaking a split pane's expand flags. expandedPaneByTabId: omitByTabId(s.expandedPaneByTabId), @@ -86,24 +83,10 @@ export function applyRemoveWorktreeSuccessState( activeWorkspaceExecutionHostId: removedActiveWorktree ? null : s.activeWorkspaceExecutionHostId, - activeTabId: s.activeTabId && tabIds.has(s.activeTabId) ? null : s.activeTabId, - openFiles: nextOpenFiles, - browserTabsByWorktree: omitByWorktree(s.browserTabsByWorktree), // Why: closeBrowserTab records a Cmd+Shift+T undo snapshot, but a deleted worktree's tabs can't be restored; purge it. recentlyClosedBrowserTabsByWorktree: omitByWorktree(s.recentlyClosedBrowserTabsByWorktree), - activeFileIdByWorktree: omitByWorktree(s.activeFileIdByWorktree), - activeBrowserTabIdByWorktree: omitByWorktree(s.activeBrowserTabIdByWorktree), - activeTabTypeByWorktree: omitByWorktree(s.activeTabTypeByWorktree), rightSidebarExplorerViewByWorktree: omitByWorktree(s.rightSidebarExplorerViewByWorktree), - activeTabIdByWorktree: omitByWorktree(s.activeTabIdByWorktree), - // Why: the tab strip persists visual order per worktree; drop the entry so stale tab IDs aren't retained. - tabBarOrderByWorktree: omitByWorktree(s.tabBarOrderByWorktree), pendingReconnectTabByWorktree: omitByWorktree(s.pendingReconnectTabByWorktree), - // Why: split-tab layout/group state is worktree-owned; leaving it makes a deleted worktree look restorable. - unifiedTabsByWorktree: omitByWorktree(s.unifiedTabsByWorktree), - groupsByWorktree: omitByWorktree(s.groupsByWorktree), - layoutByWorktree: omitByWorktree(s.layoutByWorktree), - activeGroupIdByWorktree: omitByWorktree(s.activeGroupIdByWorktree), editorDrafts: omitByFileId(s.editorDrafts), markdownViewMode: omitByFileId(s.markdownViewMode), markdownRichModeSizeOverride: omitByFileId(s.markdownRichModeSizeOverride), @@ -129,9 +112,6 @@ export function applyRemoveWorktreeSuccessState( gitBranchCompareRequestStatusHeadByWorktree: omitByWorktree( s.gitBranchCompareRequestStatusHeadByWorktree ), - activeFileId: activeFileCleared ? null : s.activeFileId, - activeBrowserTabId: removedActiveWorktree ? null : s.activeBrowserTabId, - activeTabType: removedActiveWorktree || activeFileCleared ? 'terminal' : s.activeTabType, everActivatedWorktreeIds: s.everActivatedWorktreeIds.has(worktreeId) ? new Set([...s.everActivatedWorktreeIds].filter((id) => id !== worktreeId)) : s.everActivatedWorktreeIds, diff --git a/src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts b/src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts index 22077d75162..6470ffcb550 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts @@ -25,6 +25,7 @@ import { structuredAgentSessionTargetForHost } from '@/runtime/structured-agent-session-owner' import { removeTabsAttention } from '../../workspace-attention/subject-attention-transitions' +import { removeWorktreeLayout } from '../../tabs/worktree-layout-lifecycle' /** Builds a bulk cleanup patch and clears auxiliary warning records without requiring individual terminal teardown. */ export function buildWorktreePurgeState( @@ -115,13 +116,8 @@ export function buildWorktreePurgeState( omitByFileId } = createWorktreePurgeOmitters(s, worktreeIdSet, doomed) - const nextOpenFiles = s.openFiles.some((f) => worktreeIdSet.has(f.worktreeId)) - ? s.openFiles.filter((f) => !worktreeIdSet.has(f.worktreeId)) - : s.openFiles - const removedActive = s.activeWorktreeId != null && worktreeIdSet.has(s.activeWorktreeId) const activeFileCleared = s.activeFileId != null && doomed.removedFileIds.has(s.activeFileId) - const activeTabCleared = s.activeTabId != null && doomed.doomedTabIds.has(s.activeTabId) const nextEverActivatedWorktreeIds = (() => { let hit = false @@ -143,13 +139,15 @@ export function buildWorktreePurgeState( const nextAgentStatusByPaneKey = omitByPaneKeyTabPrefix(s.agentStatusByPaneKey) return { + ...removeWorktreeLayout(s, { + worktreeIds: worktreeIdSet, + tabIds: doomed.doomedTabIds, + activeFileCleared + }), // Worktree-scoped terminal/tab state worktreeLineageById: omitByWorktree(s.worktreeLineageById), workspaceLineageByChildKey: omitWorkspaceLineageByWorktree(s.workspaceLineageByChildKey), - tabsByWorktree: omitByWorktree(s.tabsByWorktree), - terminalLayoutsByTabId: omitByTabId(s.terminalLayoutsByTabId), localOnlyScrollbackByTabId: omitByTabId(s.localOnlyScrollbackByTabId), - ptyIdsByTabId: omitByTabId(s.ptyIdsByTabId), runtimePaneTitlesByTabId: omitByTabId(s.runtimePaneTitlesByTabId), automaticAgentResumeClaimsByTabId: omitByTabId(s.automaticAgentResumeClaimsByTabId), nativeChatLaunchPromptByTabId: omitByTabId(s.nativeChatLaunchPromptByTabId), @@ -195,10 +193,8 @@ export function buildWorktreePurgeState( // File search fileSearchStateByWorktree: omitByWorktree(s.fileSearchStateByWorktree), // Browser state - browserTabsByWorktree: omitByWorktree(s.browserTabsByWorktree), browserPagesByWorkspace: omitByBrowserWorkspaceId(s.browserPagesByWorkspace), recentlyClosedBrowserTabsByWorktree: omitByWorktree(s.recentlyClosedBrowserTabsByWorktree), - activeBrowserTabIdByWorktree: omitByWorktree(s.activeBrowserTabIdByWorktree), // Why: keyed by page/workspace id, only cleaned by closeBrowserTab on the single-removal path; the bulk reconcile missed them, orphaning an entry per page of externally-removed worktrees. browserAnnotationsByPageId: omitByPageId(s.browserAnnotationsByPageId), browserAnnotationMarkerIdsByPageId: omitByPageId(s.browserAnnotationMarkerIdsByPageId), @@ -212,18 +208,9 @@ export function buildWorktreePurgeState( s.recentlyClosedBrowserPagesByWorkspace ), // Editor state - activeFileIdByWorktree: omitByWorktree(s.activeFileIdByWorktree), - activeTabTypeByWorktree: omitByWorktree(s.activeTabTypeByWorktree), - activeTabIdByWorktree: omitByWorktree(s.activeTabIdByWorktree), - tabBarOrderByWorktree: omitByWorktree(s.tabBarOrderByWorktree), pendingReconnectTabByWorktree: omitByWorktree(s.pendingReconnectTabByWorktree), rightSidebarTabByWorktree: pruneRightSidebarTabByWorktree(), rightSidebarExplorerViewByWorktree: omitByWorktree(s.rightSidebarExplorerViewByWorktree ?? {}), - // Split-tab / unified tab state - unifiedTabsByWorktree: omitByWorktree(s.unifiedTabsByWorktree), - groupsByWorktree: omitByWorktree(s.groupsByWorktree), - layoutByWorktree: omitByWorktree(s.layoutByWorktree), - activeGroupIdByWorktree: omitByWorktree(s.activeGroupIdByWorktree), // Git status caches gitStatusByWorktree: omitByWorktree(s.gitStatusByWorktree), // Why: keyed by worktreeId; re-keyed on rename but missed by both removal paths (upstream-status entry). @@ -257,7 +244,6 @@ export function buildWorktreePurgeState( recentlyClosedTerminalTabsByWorktree: omitByWorktree(s.recentlyClosedTerminalTabsByWorktree), recentlyClosedTabKindsByWorktree: omitByWorktree(s.recentlyClosedTabKindsByWorktree), // Top-level actives - openFiles: nextOpenFiles, everActivatedWorktreeIds: nextEverActivatedWorktreeIds, lastVisitedAtByWorktreeId: removeWorktreeVisitEntriesForTargets( s.lastVisitedAtByWorktreeId, @@ -277,10 +263,6 @@ export function buildWorktreePurgeState( return activeScope?.type === 'worktree' && worktreeIdSet.has(activeScope.worktreeId) ? null : s.activeWorkspaceKey - })(), - activeFileId: activeFileCleared ? null : s.activeFileId, - activeBrowserTabId: removedActive ? null : s.activeBrowserTabId, - activeTabId: activeTabCleared ? null : s.activeTabId, - activeTabType: removedActive || activeFileCleared ? 'terminal' : s.activeTabType + })() } } From 48c1d4d86e52eed837e3cee553c02673060da04a Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:37:30 -0400 Subject: [PATCH 33/53] refactor(tabs): move tab drag preview selection writes into tabs-slice actions (#28893) The drag preview's set-and-restore of the active tab per group now goes through previewDragActiveTab, restoreDragActivationSnapshot and restoreSourceGroupActiveTabAfterDrop instead of setState from a component. Co-authored-by: Claude Opus 5.5 --- config/renderer-layout-writer-baseline.txt | 2 +- .../tab-group/tab-drag-hover-preview.ts | 4 +- .../tab-drag-preview-activation.test.ts | 17 +- .../tab-group/tab-drag-preview-activation.ts | 226 +----------------- .../components/tab-group/useTabDragSplit.ts | 12 +- .../store/slices/tabs/create-tabs-slice.ts | 2 + .../slices/tabs/tabs-drag-preview-actions.ts | 207 ++++++++++++++++ .../store/slices/tabs/tabs-slice-contract.ts | 19 ++ 8 files changed, 242 insertions(+), 247 deletions(-) create mode 100644 src/renderer/src/store/slices/tabs/tabs-drag-preview-actions.ts diff --git a/config/renderer-layout-writer-baseline.txt b/config/renderer-layout-writer-baseline.txt index 7203ba31ba6..ccb3f75cbfd 100644 --- a/config/renderer-layout-writer-baseline.txt +++ b/config/renderer-layout-writer-baseline.txt @@ -15,7 +15,6 @@ 1 src/renderer/src/components/status-bar/workspace-space-decision-inputs.ts 2 src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts 3 src/renderer/src/components/tab-bar/use-tab-bar-runtime-model-worktree-write-probe.ts -23 src/renderer/src/components/tab-group/tab-drag-preview-activation.ts 1 src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts 1 src/renderer/src/components/terminal-pane/pty-connection-test-deps.ts 4 src/renderer/src/components/terminal-pane/terminal-recovery-ledger-test-store.ts @@ -86,6 +85,7 @@ 4 src/renderer/src/store/slices/tabs/create-tabs-slice.ts 17 src/renderer/src/store/slices/tabs/tabs-close-actions.ts 14 src/renderer/src/store/slices/tabs/tabs-create-actions.ts +23 src/renderer/src/store/slices/tabs/tabs-drag-preview-actions.ts 11 src/renderer/src/store/slices/tabs/tabs-drop-actions.ts 4 src/renderer/src/store/slices/tabs/tabs-focus-actions.ts 13 src/renderer/src/store/slices/tabs/tabs-group-actions.ts diff --git a/src/renderer/src/components/tab-group/tab-drag-hover-preview.ts b/src/renderer/src/components/tab-group/tab-drag-hover-preview.ts index 75017595999..55667be5d9e 100644 --- a/src/renderer/src/components/tab-group/tab-drag-hover-preview.ts +++ b/src/renderer/src/components/tab-group/tab-drag-hover-preview.ts @@ -2,7 +2,7 @@ import { useCallback, useRef, useState, type RefObject } from 'react' import type { DragMoveEvent, DragOverEvent } from '@dnd-kit/core' import { useAppStore } from '../../store' import type { useHoveredTabInsertion } from './tab-insertion' -import { applyDragPreviewTab, type TabDragActivationSnapshot } from './tab-drag-preview-activation' +import type { TabDragActivationSnapshot } from '../../store/slices/tabs/tabs-slice-contract' import { resolveDragPreviewTabId } from './tab-drag-preview-target' import { getDragPointer } from './tab-drag-pointer' import { @@ -66,7 +66,7 @@ export function useTabDragHoverPreview({ return } lastPreviewRef.current = preview - applyDragPreviewTab({ + useAppStore.getState().previewDragActiveTab({ worktreeId, groupId: preview.groupId, tabId: preview.tabId, diff --git a/src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts b/src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts index 1269ade41a4..42754b25879 100644 --- a/src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts +++ b/src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts @@ -1,12 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Tab } from '../../../../shared/tab-types' import { useAppStore } from '../../store' -import { - applyDragPreviewTab, - captureTabDragActivationSnapshot, - restoreTabDragActivationSnapshot, - restoreSourceGroupActiveTabAfterCrossGroupDrop -} from './tab-drag-preview-activation' +import { captureTabDragActivationSnapshot } from './tab-drag-preview-activation' const WT = 'wt-preview-restore' @@ -65,14 +60,14 @@ describe('restoreTabDragActivationSnapshot', () => { const subscriber = vi.fn() const unsubscribe = useAppStore.subscribe(subscriber) try { - applyDragPreviewTab({ + useAppStore.getState().previewDragActiveTab({ worktreeId: WT, groupId: 'group-1', tabId: 'tab-1', activeGroupId: 'group-1' }) - restoreTabDragActivationSnapshot(WT, snapshot) - restoreSourceGroupActiveTabAfterCrossGroupDrop({ + useAppStore.getState().restoreDragActivationSnapshot(WT, snapshot) + useAppStore.getState().restoreSourceGroupActiveTabAfterDrop({ worktreeId: WT, snapshot, sourceGroupId: 'group-1', @@ -87,7 +82,7 @@ describe('restoreTabDragActivationSnapshot', () => { it('restores active-surface fields after a drag preview is cancelled', () => { const snapshot = captureTabDragActivationSnapshot(WT) - applyDragPreviewTab({ + useAppStore.getState().previewDragActiveTab({ worktreeId: WT, groupId: 'group-1', tabId: 'tab-2', @@ -97,7 +92,7 @@ describe('restoreTabDragActivationSnapshot', () => { expect(useAppStore.getState().activeTabType).toBe('browser') expect(useAppStore.getState().activeBrowserTabId).toBe('browser-1') - restoreTabDragActivationSnapshot(WT, snapshot) + useAppStore.getState().restoreDragActivationSnapshot(WT, snapshot) const state = useAppStore.getState() expect(state.groupsByWorktree[WT]?.[0]?.activeTabId).toBe('tab-1') diff --git a/src/renderer/src/components/tab-group/tab-drag-preview-activation.ts b/src/renderer/src/components/tab-group/tab-drag-preview-activation.ts index 31726672db2..22792ae59e9 100644 --- a/src/renderer/src/components/tab-group/tab-drag-preview-activation.ts +++ b/src/renderer/src/components/tab-group/tab-drag-preview-activation.ts @@ -1,102 +1,5 @@ import { useAppStore } from '../../store' -import type { AppState } from '../../store/types' - -export type TabDragActivationSnapshot = { - activeGroupId: string | null - activeTabIdByGroup: Record -} - -function previewActiveSurfacePatch( - state: AppState, - worktreeId: string, - groupId: string, - tabId: string | null -): Partial { - if (state.activeWorktreeId !== worktreeId || !tabId) { - return {} - } - const unifiedTab = (state.unifiedTabsByWorktree[worktreeId] ?? []).find( - (tab) => tab.id === tabId && tab.groupId === groupId - ) - if (!unifiedTab) { - return {} - } - - const nextActiveTabTypeByWorktree = ( - tabType: AppState['activeTabType'] - ): AppState['activeTabTypeByWorktree'] => ({ - ...state.activeTabTypeByWorktree, - [worktreeId]: tabType - }) - - if (unifiedTab.contentType === 'terminal') { - if ( - state.activeTabType === 'terminal' && - state.activeTabTypeByWorktree[worktreeId] === 'terminal' && - state.activeTabId === unifiedTab.entityId && - state.activeTabIdByWorktree[worktreeId] === unifiedTab.entityId - ) { - return {} - } - return { - activeTabId: unifiedTab.entityId, - activeTabType: 'terminal', - activeTabIdByWorktree: { - ...state.activeTabIdByWorktree, - [worktreeId]: unifiedTab.entityId - }, - activeTabTypeByWorktree: nextActiveTabTypeByWorktree('terminal') - } - } - if (unifiedTab.contentType === 'browser') { - if ( - state.activeTabType === 'browser' && - state.activeTabTypeByWorktree[worktreeId] === 'browser' && - state.activeBrowserTabId === unifiedTab.entityId && - state.activeBrowserTabIdByWorktree[worktreeId] === unifiedTab.entityId - ) { - return {} - } - return { - activeBrowserTabId: unifiedTab.entityId, - activeTabType: 'browser', - activeBrowserTabIdByWorktree: { - ...state.activeBrowserTabIdByWorktree, - [worktreeId]: unifiedTab.entityId - }, - activeTabTypeByWorktree: nextActiveTabTypeByWorktree('browser') - } - } - if (unifiedTab.contentType === 'simulator') { - if ( - state.activeTabType === 'simulator' && - state.activeTabTypeByWorktree[worktreeId] === 'simulator' - ) { - return {} - } - return { - activeTabType: 'simulator', - activeTabTypeByWorktree: nextActiveTabTypeByWorktree('simulator') - } - } - if ( - state.activeTabType === 'editor' && - state.activeTabTypeByWorktree[worktreeId] === 'editor' && - state.activeFileId === unifiedTab.entityId && - state.activeFileIdByWorktree[worktreeId] === unifiedTab.entityId - ) { - return {} - } - return { - activeFileId: unifiedTab.entityId, - activeTabType: 'editor', - activeFileIdByWorktree: { - ...state.activeFileIdByWorktree, - [worktreeId]: unifiedTab.entityId - }, - activeTabTypeByWorktree: nextActiveTabTypeByWorktree('editor') - } -} +import type { TabDragActivationSnapshot } from '../../store/slices/tabs/tabs-slice-contract' export function captureTabDragActivationSnapshot(worktreeId: string): TabDragActivationSnapshot { const state = useAppStore.getState() @@ -106,130 +9,3 @@ export function captureTabDragActivationSnapshot(worktreeId: string): TabDragAct activeTabIdByGroup: Object.fromEntries(groups.map((group) => [group.id, group.activeTabId])) } } - -export function applyDragPreviewTab({ - worktreeId, - groupId, - tabId, - activeGroupId -}: { - worktreeId: string - groupId: string - tabId: string | null - activeGroupId: string -}): void { - useAppStore.setState((state): Partial => { - const groups = state.groupsByWorktree[worktreeId] ?? [] - const targetGroup = groups.find((group) => group.id === groupId) - const groupUnchanged = targetGroup?.activeTabId === tabId - const focusUnchanged = (state.activeGroupIdByWorktree[worktreeId] ?? null) === activeGroupId - const surfacePatch = previewActiveSurfacePatch(state, worktreeId, groupId, tabId) - if (groupUnchanged && focusUnchanged) { - return Object.keys(surfacePatch).length > 0 ? surfacePatch : state - } - - const next: Partial = { ...surfacePatch } - if (!groupUnchanged) { - next.groupsByWorktree = { - ...state.groupsByWorktree, - [worktreeId]: groups.map((group) => - group.id === groupId ? { ...group, activeTabId: tabId } : group - ) - } - } - if (!focusUnchanged) { - next.activeGroupIdByWorktree = { - ...state.activeGroupIdByWorktree, - [worktreeId]: activeGroupId - } - } - return next - }) -} - -export function restoreTabDragActivationSnapshot( - worktreeId: string, - snapshot: TabDragActivationSnapshot -): void { - useAppStore.setState((state): Partial => { - const groups = state.groupsByWorktree[worktreeId] ?? [] - const groupsUnchanged = groups.every( - (group) => (snapshot.activeTabIdByGroup[group.id] ?? null) === group.activeTabId - ) - const focusUnchanged = - (state.activeGroupIdByWorktree[worktreeId] ?? null) === snapshot.activeGroupId - - const next: Partial = {} - if (!groupsUnchanged) { - next.groupsByWorktree = { - ...state.groupsByWorktree, - [worktreeId]: groups.map((group) => ({ - ...group, - activeTabId: snapshot.activeTabIdByGroup[group.id] ?? null - })) - } - } - if (!focusUnchanged) { - if (snapshot.activeGroupId === null) { - const nextActiveGroupIdByWorktree = { ...state.activeGroupIdByWorktree } - delete nextActiveGroupIdByWorktree[worktreeId] - next.activeGroupIdByWorktree = nextActiveGroupIdByWorktree - } else { - next.activeGroupIdByWorktree = { - ...state.activeGroupIdByWorktree, - [worktreeId]: snapshot.activeGroupId - } - } - } - - const restoredGroupId = snapshot.activeGroupId - if (restoredGroupId) { - const restoredTabId = snapshot.activeTabIdByGroup[restoredGroupId] ?? null - Object.assign( - next, - previewActiveSurfacePatch(state, worktreeId, restoredGroupId, restoredTabId) - ) - } - - if (Object.keys(next).length === 0) { - return state - } - - return next - }) -} - -export function restoreSourceGroupActiveTabAfterCrossGroupDrop({ - worktreeId, - snapshot, - sourceGroupId, - movedTabId -}: { - worktreeId: string - snapshot: TabDragActivationSnapshot - sourceGroupId: string - movedTabId: string -}): void { - const preDragActiveTabId = snapshot.activeTabIdByGroup[sourceGroupId] ?? null - // Why: dropUnifiedTab already picks the next active tab when the moved tab - // was the source group's selection; only preview contamination needs undo. - if (preDragActiveTabId === movedTabId) { - return - } - - useAppStore.setState((state): Partial => { - const groups = state.groupsByWorktree[worktreeId] ?? [] - const sourceGroup = groups.find((group) => group.id === sourceGroupId) - if (!sourceGroup || sourceGroup.activeTabId === preDragActiveTabId) { - return state - } - return { - groupsByWorktree: { - ...state.groupsByWorktree, - [worktreeId]: groups.map((group) => - group.id === sourceGroupId ? { ...group, activeTabId: preDragActiveTabId } : group - ) - } - } - }) -} diff --git a/src/renderer/src/components/tab-group/useTabDragSplit.ts b/src/renderer/src/components/tab-group/useTabDragSplit.ts index f67712622ab..f8a74ba9bf5 100644 --- a/src/renderer/src/components/tab-group/useTabDragSplit.ts +++ b/src/renderer/src/components/tab-group/useTabDragSplit.ts @@ -13,13 +13,9 @@ import { } from '@dnd-kit/core' import type { TabGroup } from '../../../../shared/tab-types' import { useAppStore } from '../../store' +import type { TabDragActivationSnapshot } from '../../store/slices/tabs/tabs-slice-contract' import { useHoveredTabInsertion, type HoveredTabInsertion } from './tab-insertion' -import { - captureTabDragActivationSnapshot, - restoreSourceGroupActiveTabAfterCrossGroupDrop, - restoreTabDragActivationSnapshot, - type TabDragActivationSnapshot -} from './tab-drag-preview-activation' +import { captureTabDragActivationSnapshot } from './tab-drag-preview-activation' import { getDragPointer } from './tab-drag-pointer' import { TabDragPointerSensor } from './tab-drag-pointer-sensor' import { @@ -162,7 +158,7 @@ export function useTabDragSplit({ if (!snapshot) { return } - restoreTabDragActivationSnapshot(worktreeId, snapshot) + useAppStore.getState().restoreDragActivationSnapshot(worktreeId, snapshot) }, [worktreeId]) const restoreSourceGroupAfterCrossGroupDrop = useCallback( @@ -171,7 +167,7 @@ export function useTabDragSplit({ if (!snapshot) { return } - restoreSourceGroupActiveTabAfterCrossGroupDrop({ + useAppStore.getState().restoreSourceGroupActiveTabAfterDrop({ worktreeId, snapshot, sourceGroupId: activeData.groupId, diff --git a/src/renderer/src/store/slices/tabs/create-tabs-slice.ts b/src/renderer/src/store/slices/tabs/create-tabs-slice.ts index 52bf7cff18d..06aa66e3dfb 100644 --- a/src/renderer/src/store/slices/tabs/create-tabs-slice.ts +++ b/src/renderer/src/store/slices/tabs/create-tabs-slice.ts @@ -10,6 +10,7 @@ import { createTabsGroupActions } from './tabs-group-actions' import { createTabsMoveActions } from './tabs-move-actions' import { createTabsDropActions } from './tabs-drop-actions' import { createTabsSplitRatioActions } from './tabs-split-ratio-actions' +import { createTabsDragPreviewActions } from './tabs-drag-preview-actions' import { createTabsSessionActions } from './tabs-session-actions' export const createTabsSlice: StateCreator = (set, get) => ({ @@ -26,5 +27,6 @@ export const createTabsSlice: StateCreator = (set, ...createTabsMoveActions(set, get), ...createTabsDropActions(set, get), ...createTabsSplitRatioActions(set), + ...createTabsDragPreviewActions(set), ...createTabsSessionActions(set, get) }) diff --git a/src/renderer/src/store/slices/tabs/tabs-drag-preview-actions.ts b/src/renderer/src/store/slices/tabs/tabs-drag-preview-actions.ts new file mode 100644 index 00000000000..7fe1f379887 --- /dev/null +++ b/src/renderer/src/store/slices/tabs/tabs-drag-preview-actions.ts @@ -0,0 +1,207 @@ +import type { AppState } from '../../types' +import type { TabsSlice, TabsSliceSet } from './tabs-slice-contract' + +function previewActiveSurfacePatch( + state: AppState, + worktreeId: string, + groupId: string, + tabId: string | null +): Partial { + if (state.activeWorktreeId !== worktreeId || !tabId) { + return {} + } + const unifiedTab = (state.unifiedTabsByWorktree[worktreeId] ?? []).find( + (tab) => tab.id === tabId && tab.groupId === groupId + ) + if (!unifiedTab) { + return {} + } + + const nextActiveTabTypeByWorktree = ( + tabType: AppState['activeTabType'] + ): AppState['activeTabTypeByWorktree'] => ({ + ...state.activeTabTypeByWorktree, + [worktreeId]: tabType + }) + + if (unifiedTab.contentType === 'terminal') { + if ( + state.activeTabType === 'terminal' && + state.activeTabTypeByWorktree[worktreeId] === 'terminal' && + state.activeTabId === unifiedTab.entityId && + state.activeTabIdByWorktree[worktreeId] === unifiedTab.entityId + ) { + return {} + } + return { + activeTabId: unifiedTab.entityId, + activeTabType: 'terminal', + activeTabIdByWorktree: { + ...state.activeTabIdByWorktree, + [worktreeId]: unifiedTab.entityId + }, + activeTabTypeByWorktree: nextActiveTabTypeByWorktree('terminal') + } + } + if (unifiedTab.contentType === 'browser') { + if ( + state.activeTabType === 'browser' && + state.activeTabTypeByWorktree[worktreeId] === 'browser' && + state.activeBrowserTabId === unifiedTab.entityId && + state.activeBrowserTabIdByWorktree[worktreeId] === unifiedTab.entityId + ) { + return {} + } + return { + activeBrowserTabId: unifiedTab.entityId, + activeTabType: 'browser', + activeBrowserTabIdByWorktree: { + ...state.activeBrowserTabIdByWorktree, + [worktreeId]: unifiedTab.entityId + }, + activeTabTypeByWorktree: nextActiveTabTypeByWorktree('browser') + } + } + if (unifiedTab.contentType === 'simulator') { + if ( + state.activeTabType === 'simulator' && + state.activeTabTypeByWorktree[worktreeId] === 'simulator' + ) { + return {} + } + return { + activeTabType: 'simulator', + activeTabTypeByWorktree: nextActiveTabTypeByWorktree('simulator') + } + } + if ( + state.activeTabType === 'editor' && + state.activeTabTypeByWorktree[worktreeId] === 'editor' && + state.activeFileId === unifiedTab.entityId && + state.activeFileIdByWorktree[worktreeId] === unifiedTab.entityId + ) { + return {} + } + return { + activeFileId: unifiedTab.entityId, + activeTabType: 'editor', + activeFileIdByWorktree: { + ...state.activeFileIdByWorktree, + [worktreeId]: unifiedTab.entityId + }, + activeTabTypeByWorktree: nextActiveTabTypeByWorktree('editor') + } +} + +export function createTabsDragPreviewActions( + set: TabsSliceSet +): Pick< + TabsSlice, + 'previewDragActiveTab' | 'restoreDragActivationSnapshot' | 'restoreSourceGroupActiveTabAfterDrop' +> { + return { + previewDragActiveTab: ({ worktreeId, groupId, tabId, activeGroupId }) => { + set((state): Partial => { + const groups = state.groupsByWorktree[worktreeId] ?? [] + const targetGroup = groups.find((group) => group.id === groupId) + const groupUnchanged = targetGroup?.activeTabId === tabId + const focusUnchanged = (state.activeGroupIdByWorktree[worktreeId] ?? null) === activeGroupId + const surfacePatch = previewActiveSurfacePatch(state, worktreeId, groupId, tabId) + if (groupUnchanged && focusUnchanged) { + return Object.keys(surfacePatch).length > 0 ? surfacePatch : state + } + + const next: Partial = { ...surfacePatch } + if (!groupUnchanged) { + next.groupsByWorktree = { + ...state.groupsByWorktree, + [worktreeId]: groups.map((group) => + group.id === groupId ? { ...group, activeTabId: tabId } : group + ) + } + } + if (!focusUnchanged) { + next.activeGroupIdByWorktree = { + ...state.activeGroupIdByWorktree, + [worktreeId]: activeGroupId + } + } + return next + }) + }, + + restoreDragActivationSnapshot: (worktreeId, snapshot) => { + set((state): Partial => { + const groups = state.groupsByWorktree[worktreeId] ?? [] + const groupsUnchanged = groups.every( + (group) => (snapshot.activeTabIdByGroup[group.id] ?? null) === group.activeTabId + ) + const focusUnchanged = + (state.activeGroupIdByWorktree[worktreeId] ?? null) === snapshot.activeGroupId + + const next: Partial = {} + if (!groupsUnchanged) { + next.groupsByWorktree = { + ...state.groupsByWorktree, + [worktreeId]: groups.map((group) => ({ + ...group, + activeTabId: snapshot.activeTabIdByGroup[group.id] ?? null + })) + } + } + if (!focusUnchanged) { + if (snapshot.activeGroupId === null) { + const nextActiveGroupIdByWorktree = { ...state.activeGroupIdByWorktree } + delete nextActiveGroupIdByWorktree[worktreeId] + next.activeGroupIdByWorktree = nextActiveGroupIdByWorktree + } else { + next.activeGroupIdByWorktree = { + ...state.activeGroupIdByWorktree, + [worktreeId]: snapshot.activeGroupId + } + } + } + + const restoredGroupId = snapshot.activeGroupId + if (restoredGroupId) { + const restoredTabId = snapshot.activeTabIdByGroup[restoredGroupId] ?? null + Object.assign( + next, + previewActiveSurfacePatch(state, worktreeId, restoredGroupId, restoredTabId) + ) + } + + if (Object.keys(next).length === 0) { + return state + } + + return next + }) + }, + + restoreSourceGroupActiveTabAfterDrop: ({ worktreeId, snapshot, sourceGroupId, movedTabId }) => { + const preDragActiveTabId = snapshot.activeTabIdByGroup[sourceGroupId] ?? null + // Why: dropUnifiedTab already picks the next active tab when the moved tab + // was the source group's selection; only preview contamination needs undo. + if (preDragActiveTabId === movedTabId) { + return + } + + set((state): Partial => { + const groups = state.groupsByWorktree[worktreeId] ?? [] + const sourceGroup = groups.find((group) => group.id === sourceGroupId) + if (!sourceGroup || sourceGroup.activeTabId === preDragActiveTabId) { + return state + } + return { + groupsByWorktree: { + ...state.groupsByWorktree, + [worktreeId]: groups.map((group) => + group.id === sourceGroupId ? { ...group, activeTabId: preDragActiveTabId } : group + ) + } + } + }) + } + } +} diff --git a/src/renderer/src/store/slices/tabs/tabs-slice-contract.ts b/src/renderer/src/store/slices/tabs/tabs-slice-contract.ts index 444fe9a5882..17bcb34f551 100644 --- a/src/renderer/src/store/slices/tabs/tabs-slice-contract.ts +++ b/src/renderer/src/store/slices/tabs/tabs-slice-contract.ts @@ -11,6 +11,11 @@ import type { WorkspaceSessionHydrationOptions } from '@/lib/workspace-session-h export type TabSplitDirection = 'left' | 'right' | 'up' | 'down' +export type TabDragActivationSnapshot = { + activeGroupId: string | null + activeTabIdByGroup: Record +} + export type TabsSlice = { unifiedTabsByWorktree: Record groupsByWorktree: Record @@ -137,6 +142,20 @@ export type TabsSlice = { } ) => boolean setTabGroupSplitRatio: (worktreeId: string, nodePath: string, ratio: number) => void + /** Selects the hovered tab while a drag is in flight; restored from the drag-start snapshot. */ + previewDragActiveTab: (args: { + worktreeId: string + groupId: string + tabId: string | null + activeGroupId: string + }) => void + restoreDragActivationSnapshot: (worktreeId: string, snapshot: TabDragActivationSnapshot) => void + restoreSourceGroupActiveTabAfterDrop: (args: { + worktreeId: string + snapshot: TabDragActivationSnapshot + sourceGroupId: string + movedTabId: string + }) => void reconcileWorktreeTabModel: (worktreeId: string) => { renderableTabCount: number activeRenderableTabId: string | null From 6e46d5ef339f661d67dd5ededf45fe928ace8945 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:42:27 -0400 Subject: [PATCH 34/53] feat(relay): watch a reserve cell and flip it back on the first step 5 stop rule (#28698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(relay): watch a reserve cell and flip it back on the first step 5 stop rule A manual, inert-until-dispatched workflow samples one reserve cell every minute for up to 300 minutes against the step 5 §9 stop rules, logs one JSON line per sample, and on the first trip (or N unreadable samples in a row) runs the flag tool's own admitMode=db write under the flag workflow's concurrency group. dry-run logs the flag tool's plan instead. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): keep the reserve watch's flip back out of the flag group, and baseline 503s before the flip - The flip back holds no concurrency group: GitHub keeps one pending run per group and a newer one cancels it. The flag tool's CAS orders it instead; a generation conflict is re-read and retried (5 tries, backoff) while the switch file still says reserve. - The 503 background comes from the 10 minutes before a required flipped-at, or an explicit value; neither is refused. - A failed or cancelled write-mode watch flips back too. An unreachable cell fails with the exact break-glass inputs. - Missing 503 data (empty series, unpublished minute, unparseable time) is unverifiable. - Seat-behind-row key adds the director instance and cell; six segments share one command. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): read the reserve watch's 503 publishing front from every response code A quiet fleet has no 503 series, so the watch read it as unverifiable and flipped a healthy cell back within minutes. The 503 read now groups request_count by response code in one query; the newest minute any code has a point for is how far the series is published, so a 503 minute at or before it with no point is zero and minutes past it are left out. Also: the flip-back job's steps run on always(), flipped-at ages against the watch's start in later segments, a pending pre-flip background no longer adds to the unverifiable streak, the flip back retries runtime-status before calling for break glass, and the concurrency rationale lives once in the docs. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * docs(relay): state what a cancel during the flip-back lease wait can leave, and the 503 publish-skew assumption Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): fail a segment closed on a corrupt watch state file, and keep no checkout token on disk Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): alarm on a failed flip back, refuse a watch under a rollout lease, wait for reserve to apply, and control the duplicate read - A flip back whose write or plan never finished (no ID token, a failed login, a silent cell) raises an ::error:: annotation and a FLIP BACK FAILED summary, as the clock-skew monitor does. - The first segment reads the Cloud SQL rollout lease through the lease action's own reader and refuses to start while any run holds it, or when it cannot be read. - A cell not yet reporting reserve is waited on for up to 10 minutes, judging nothing, before the watch ends refused without a write. - The duplicate-seat read takes the per-minute ledger summary in the same query and window; with no fresh summary in it the sample is unverifiable, not zero duplicates. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): page on a refused reserve watch or a failed flip back A refused watch can leave a cell in reserve and unwatched, the same as a flip back that did not land. Both now raise an escaped ::error:: annotation through one `alarm` command, and in write mode write orca_relay_reserve_watch_alarm with gcloud logging write. A new log-based metric and alert policy page on any such line, per cell, through relay_alert_notification_channels, and the deploy identity gets roles/logging.logWriter in production to write it. Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * fix(relay): name the log-writer risk, check the alarm's project, and say a failed login cannot page Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f * docs(relay): note a force-cancel can skip the reserve-watch flip-back write --- ...operate-relay-production-reserve-watch.yml | 327 ++++++ .../terraform-root-partition/families.json | 6 + .../dev/scripts/operate-relay-cell-flags.mjs | 19 +- ...ay-production-identity-boundaries.test.mjs | 1 + .../relay-public-workflow-contract.test.mjs | 2 +- .../relay-reserve-dead-man-alert.test.mjs | 23 + .../dev/scripts/relay-reserve-watch-rules.mjs | 258 +++++ .../scripts/relay-same-cap-shadow-gate.mjs | 50 +- .../dev/scripts/watch-relay-reserve-cell.mjs | 581 +++++++++++ .../scripts/watch-relay-reserve-cell.test.mjs | 958 ++++++++++++++++++ ...oad-identity-attribute-conditions.test.mjs | 2 +- cloud/docs/relay-workflows.md | 72 +- cloud/infra/terraform/relay-github-actions.tf | 11 + cloud/infra/terraform/relay-observability.tf | 68 ++ cloud/package.json | 2 +- 15 files changed, 2358 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/cloud-operate-relay-production-reserve-watch.yml create mode 100644 cloud/dev/scripts/relay-reserve-watch-rules.mjs create mode 100644 cloud/dev/scripts/watch-relay-reserve-cell.mjs create mode 100644 cloud/dev/scripts/watch-relay-reserve-cell.test.mjs diff --git a/.github/workflows/cloud-operate-relay-production-reserve-watch.yml b/.github/workflows/cloud-operate-relay-production-reserve-watch.yml new file mode 100644 index 00000000000..e045d10928e --- /dev/null +++ b/.github/workflows/cloud-operate-relay-production-reserve-watch.yml @@ -0,0 +1,327 @@ +name: Operate Relay Production Reserve Watch + +# Watches one cell already switched to reserve mode and flips it back to db on the first stop +# rule it trips (step 5 design §9). Manual only. dry-run evaluates and logs, writes nothing. +# An operator watches the run. A refused watch or a flip back that did not land raises an ::error:: +# annotation, as the clock-skew monitor does, and in write mode logs the line the reserve-watch +# alert pages on. Refuses to start while a rollout holds the Cloud SQL rollout lease. +on: + workflow_dispatch: + inputs: + mode: + description: dry-run logs what a trip would write; write flips the cell back on a trip + required: true + default: dry-run + type: choice + options: [dry-run, write] + cell-id: + description: Exact cell id already in reserve mode, e.g. production-gce-c26 + required: true + type: string + duration-minutes: + description: How long to watch, 5 to 300 + required: true + default: '120' + type: string + flipped-at: + description: >- + When the reserve write applied, ISO with a zone, e.g. 2026-10-14T12:00:00Z. The 503 + background is the median of the 10 minutes before it + required: false + default: '' + type: string + background-503-per-minute: + description: >- + Director 503s per minute before the flip; overrides the one measured before flipped-at. + One of the two is required + required: false + default: '' + type: string + unverifiable-limit: + description: Consecutive samples with any unreadable signal that count as a trip + required: true + default: '3' + type: string + confirmation: + description: Required for write, exactly "WRITE_RELAY_CELL_FLAGS " + required: false + type: string + +permissions: + contents: read + id-token: write + +# One watch per cell; why not the flag workflow's group: cloud/docs/relay-workflows.md. +concurrency: + group: relay-production-reserve-watch-${{ inputs.cell-id }} + cancel-in-progress: false + +defaults: + run: + # Explicit bash runs with pipefail, so the script's exit code survives the tee. + shell: bash + working-directory: cloud + +env: + MODE: ${{ inputs.mode }} + CELL_ID: ${{ inputs.cell-id }} + CONFIRMATION: ${{ inputs.confirmation }} + GCP_PROJECT_ID: onorca-cloud + +jobs: + watch: + if: ${{ vars.ORCA_CLOUD_OPERATIONS_ENABLED == 'true' }} + runs-on: ubuntu-latest + # Six 55-minute segments cover the longest watch; under the 360-minute job limit. + timeout-minutes: 350 + environment: production + outputs: + state: ${{ steps.outcome.outputs.state }} + verdict: ${{ steps.outcome.outputs.verdict }} + env: + DURATION_MINUTES: ${{ inputs.duration-minutes }} + FLIPPED_AT: ${{ inputs.flipped-at }} + BACKGROUND_503_PER_MINUTE: ${{ inputs.background-503-per-minute }} + UNVERIFIABLE_LIMIT: ${{ inputs.unverifiable-limit }} + # One segment, after the auth step before it: fixed text, the inputs arrive as env vars. + WATCH_SEGMENT: | + set -euo pipefail + [[ "${CELL_ID}" =~ ^production-gce-c[0-9]{1,3}$ ]] + node -e "if (!/^[^.]+[.][^.]+[.][^.]+$/.test(process.env.ORCA_RELAY_ADMIN_ID_TOKEN ?? '')) process.exit(1)" + node dev/scripts/watch-relay-reserve-cell.mjs watch \ + --mode "${MODE}" --cell-id "${CELL_ID}" \ + --cell-origin "https://${CELL_ID#production-gce-}.relay.onorca.dev" \ + --project-id "${GCP_PROJECT_ID}" --director-origin "https://relay.onorca.dev" \ + --director-service orca-cloud-relay --confirmation "${CONFIRMATION}" \ + --duration-minutes "${DURATION_MINUTES}" --segment-minutes 55 \ + --flipped-at "${FLIPPED_AT}" --background-503-per-minute "${BACKGROUND_503_PER_MINUTE}" \ + --unverifiable-limit "${UNVERIFIABLE_LIMIT}" \ + --state-file "${RUNNER_TEMP}/relay-reserve-watch.json" --github-output "${GITHUB_OUTPUT}" \ + | tee -a "${RUNNER_TEMP}/relay-reserve-watch.jsonl" + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/setup-node@v4 + with: + node-version: 24 + + - id: google-auth-1 + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - uses: google-github-actions/setup-gcloud@v2 + with: + skip_install: true + + - id: watch-1 + name: Watch segment 1 of 6 + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth-1.outputs.id_token }} + run: bash -c "${WATCH_SEGMENT}" + + - id: google-auth-2 + if: ${{ steps.watch-1.outputs.state == 'watching' }} + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - id: watch-2 + name: Watch segment 2 of 6 + if: ${{ steps.watch-1.outputs.state == 'watching' }} + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth-2.outputs.id_token }} + run: bash -c "${WATCH_SEGMENT}" + + - id: google-auth-3 + if: ${{ steps.watch-2.outputs.state == 'watching' }} + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - id: watch-3 + name: Watch segment 3 of 6 + if: ${{ steps.watch-2.outputs.state == 'watching' }} + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth-3.outputs.id_token }} + run: bash -c "${WATCH_SEGMENT}" + + - id: google-auth-4 + if: ${{ steps.watch-3.outputs.state == 'watching' }} + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - id: watch-4 + name: Watch segment 4 of 6 + if: ${{ steps.watch-3.outputs.state == 'watching' }} + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth-4.outputs.id_token }} + run: bash -c "${WATCH_SEGMENT}" + + - id: google-auth-5 + if: ${{ steps.watch-4.outputs.state == 'watching' }} + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - id: watch-5 + name: Watch segment 5 of 6 + if: ${{ steps.watch-4.outputs.state == 'watching' }} + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth-5.outputs.id_token }} + run: bash -c "${WATCH_SEGMENT}" + + - id: google-auth-6 + if: ${{ steps.watch-5.outputs.state == 'watching' }} + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - id: watch-6 + name: Watch segment 6 of 6 + if: ${{ steps.watch-5.outputs.state == 'watching' }} + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth-6.outputs.id_token }} + run: bash -c "${WATCH_SEGMENT}" + + - id: outcome + name: Record the watch outcome + if: ${{ always() }} + run: | + node dev/scripts/watch-relay-reserve-cell.mjs outcome \ + --state-file "${RUNNER_TEMP}/relay-reserve-watch.json" --github-output "${GITHUB_OUTPUT}" + + # The documented flow writes reserve before dispatching, so a refusal can leave it unwatched. + - name: Alarm on a refused watch + if: ${{ always() && steps.outcome.outputs.state == 'refused' }} + env: + WATCH_VERDICT: ${{ steps.outcome.outputs.verdict }} + run: | + node dev/scripts/watch-relay-reserve-cell.mjs alarm --kind refused \ + --mode "${MODE}" --cell-id "${CELL_ID}" --project-id "${GCP_PROJECT_ID}" --verdict "${WATCH_VERDICT}" + + - name: Audit summary + if: ${{ always() }} + env: + WATCH_STATE: ${{ steps.outcome.outputs.state }} + WATCH_VERDICT: ${{ steps.outcome.outputs.verdict }} + run: | + { + echo "### Relay reserve watch: ${MODE} ${CELL_ID}" + echo "Triggered by: ${GITHUB_TRIGGERING_ACTOR}, run ${GITHUB_RUN_ID} attempt ${GITHUB_RUN_ATTEMPT}" + echo "Outcome: ${WATCH_STATE}: ${WATCH_VERDICT}" + echo '```json' + tail -n 20 "${RUNNER_TEMP}/relay-reserve-watch.jsonl" 2>/dev/null || echo '(no output)' + echo '```' + } >> "${GITHUB_STEP_SUMMARY}" + + flip-back: + needs: watch + # Missing data is never healthy: a watch that crashed, timed out or was cancelled (by an + # operator too) flips back like a trip. In dry-run this only plans the write. + if: ${{ always() && vars.ORCA_CLOUD_OPERATIONS_ENABLED == 'true' && needs.watch.result != 'skipped' && !contains(fromJSON('["completed","stopped","refused"]'), needs.watch.outputs.state) }} + runs-on: ubuntu-latest + # A flip back waits for the cell to lease every control: 8-50 minutes, scaled to its controls. + timeout-minutes: 60 + environment: production + # Only the run's per-cell group above, which a running run never yields (cancel-in-progress off). + # Every step runs on always() too, so an ordinary cancel cannot skip the switch-file write; a force-cancel + # can, and leaves the cell in reserve for the operator to flip back by hand. A cancel during the + # lease wait can still kill the process before Postgres records db: the file says db, Postgres reserve. + env: + VERDICT: ${{ needs.watch.outputs.verdict || format('the watch ended {0} without a verdict', needs.watch.result) }} + steps: + - uses: actions/checkout@v4 + if: ${{ always() }} + with: + persist-credentials: false + + - uses: actions/setup-node@v4 + if: ${{ always() }} + with: + node-version: 24 + + - id: google-auth + if: ${{ always() }} + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - uses: google-github-actions/setup-gcloud@v2 + if: ${{ always() }} + with: + skip_install: true + + - name: Flip the cell back to db, or preview it + if: ${{ always() }} + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth.outputs.id_token }} + run: | + [[ "${CELL_ID}" =~ ^production-gce-c[0-9]{1,3}$ ]] + ORCA_RELAY_CONTROL_ACCESS_TOKEN="$(gcloud auth print-access-token)" + echo "::add-mask::${ORCA_RELAY_CONTROL_ACCESS_TOKEN}" + export ORCA_RELAY_CONTROL_ACCESS_TOKEN + node dev/scripts/watch-relay-reserve-cell.mjs flip-back \ + --mode "${MODE}" --cell-id "${CELL_ID}" \ + --cell-origin "https://${CELL_ID#production-gce-}.relay.onorca.dev" \ + --project-id "${GCP_PROJECT_ID}" --director-origin "https://relay.onorca.dev" \ + --confirmation "${CONFIRMATION}" --verdict "${VERDICT}" \ + | tee "${RUNNER_TEMP}/relay-reserve-flip-back.jsonl" + + # The flip-back line is logged only once the write (or its dry-run plan) finished; without + # it a missing token, a failed login or a silent cell left the cell in reserve, unwatched. + - name: Alarm when the flip back did not land + if: ${{ always() }} + run: | + if grep -qs '"event":"orca_relay_reserve_watch_flip_back"' "${RUNNER_TEMP}/relay-reserve-flip-back.jsonl"; then + exit 0 + fi + { + echo "## FLIP BACK FAILED: ${CELL_ID} may still be in reserve mode" + echo "Flip it to db by hand with Operate Relay Production Cell Flags." + } >> "${GITHUB_STEP_SUMMARY}" + node dev/scripts/watch-relay-reserve-cell.mjs alarm --kind flip-back-failed \ + --mode "${MODE}" --cell-id "${CELL_ID}" --project-id "${GCP_PROJECT_ID}" --verdict "${VERDICT}" + + - name: Audit summary + if: ${{ always() }} + run: | + { + echo "### Relay reserve watch flip back: ${MODE} ${CELL_ID}" + echo "Triggered by: ${GITHUB_TRIGGERING_ACTOR}, run ${GITHUB_RUN_ID} attempt ${GITHUB_RUN_ATTEMPT}" + echo "Verdict: ${VERDICT}" + echo '```json' + cat "${RUNNER_TEMP}/relay-reserve-flip-back.jsonl" 2>/dev/null || echo '(no output)' + echo '```' + } >> "${GITHUB_STEP_SUMMARY}" diff --git a/cloud/dev/fixtures/terraform-root-partition/families.json b/cloud/dev/fixtures/terraform-root-partition/families.json index 87bd04316c9..89d97154b0a 100644 --- a/cloud/dev/fixtures/terraform-root-partition/families.json +++ b/cloud/dev/fixtures/terraform-root-partition/families.json @@ -146,6 +146,7 @@ "google_logging_metric.relay_event_loop_stall", "google_logging_metric.relay_long_cell_inventory_hold", "google_logging_metric.relay_reserve_placement_off_with_reserve_cells", + "google_logging_metric.relay_reserve_watch_alarm", "google_logging_metric.relay_snapshot", "google_monitoring_alert_policy.relay_assignment_5xx", "google_monitoring_alert_policy.relay_assignment_edge_429", @@ -176,6 +177,7 @@ "google_monitoring_alert_policy.relay_process_fatal", "google_monitoring_alert_policy.relay_region_hint_skew", "google_monitoring_alert_policy.relay_reserve_placement_off_with_reserve_cells", + "google_monitoring_alert_policy.relay_reserve_watch_alarm", "google_monitoring_dashboard.relay_incident", "google_project_iam_custom_role.github_production_relay_capacity_mutation", "google_project_iam_custom_role.github_relay_asia_topology_mutation", @@ -304,6 +306,10 @@ "production": "relay", "staging": "apps" }, + "google_project_iam_member.github_log_writer": { + "production": "relay", + "staging": "apps" + }, "google_project_iam_member.github_logging_viewer": { "production": "relay", "staging": "apps" diff --git a/cloud/dev/scripts/operate-relay-cell-flags.mjs b/cloud/dev/scripts/operate-relay-cell-flags.mjs index 9d02572849f..6eebd7cd2ed 100644 --- a/cloud/dev/scripts/operate-relay-cell-flags.mjs +++ b/cloud/dev/scripts/operate-relay-cell-flags.mjs @@ -244,7 +244,7 @@ function objectUrl(request, suffix = '') { } // One transient 5xx or network failure is retried once, as every relay admin step does. -async function readRuntime(fetchImpl, request, idToken, wait) { +export async function readRuntime(fetchImpl, request, idToken, wait) { const response = await fetchAdminOnceMore( fetchImpl, `${request.cellOrigin}/v1/admin/runtime-status`, @@ -262,7 +262,7 @@ async function readRuntime(fetchImpl, request, idToken, wait) { return await response.json() } -async function readCurrentObject(fetchImpl, request, accessToken) { +export async function readCurrentObject(fetchImpl, request, accessToken) { const headers = { authorization: `Bearer ${accessToken}` } const metadata = await fetchImpl(objectUrl(request), { headers }) if (metadata.status === 404) { @@ -278,6 +278,12 @@ async function readCurrentObject(fetchImpl, request, accessToken) { return { generation: String(generation), object: await media.json().catch(() => null) } } +function generationConflict(current, expected) { + return Object.assign(new Error(`current generation is ${current}, not expected ${expected}`), { + generationConflict: true + }) +} + async function writeObject(fetchImpl, request, object, accessToken, audit) { const boundary = `relay-cell-flags-${Date.now()}` const metadata = { @@ -315,7 +321,8 @@ async function writeObject(fetchImpl, request, object, accessToken, audit) { if (response.status === 412) { await response.body?.cancel().catch(() => undefined) throw Object.assign(new Error('the object changed since expected-generation; re-read and retry'), { - notWritten: true + notWritten: true, + generationConflict: true }) } if (!response.ok) { @@ -409,7 +416,7 @@ async function breakGlassToDatabase(request, dependencies, { fetchImpl, log, sle } const current = await readCurrentObject(fetchImpl, request, accessToken) if (current.generation !== request.expectedGeneration) { - throw new Error(`current generation is ${current.generation}, not expected ${request.expectedGeneration}`) + throw generationConflict(current.generation, request.expectedGeneration) } const object = desiredObject(request, current.object) const generation = await writeObject(fetchImpl, request, object, accessToken, audit) @@ -459,9 +466,7 @@ export async function operateCellFlags(request, dependencies) { // Before the generation check, so a dry run always shows the generation to pass next. log(JSON.stringify(plan)) if (current.generation !== request.expectedGeneration) { - throw new Error( - `current generation is ${current.generation}, not expected ${request.expectedGeneration}` - ) + throw generationConflict(current.generation, request.expectedGeneration) } // The cell applies every readable write; a generation it did not apply was voided there. A // write of an object this image accepts whole (checked above) repairs it. diff --git a/cloud/dev/scripts/relay-production-identity-boundaries.test.mjs b/cloud/dev/scripts/relay-production-identity-boundaries.test.mjs index 5e7671523b7..92c8825fcc9 100644 --- a/cloud/dev/scripts/relay-production-identity-boundaries.test.mjs +++ b/cloud/dev/scripts/relay-production-identity-boundaries.test.mjs @@ -33,6 +33,7 @@ test('no workflow names the retired generic production deploy identity', async ( 'operate-relay-asia-admission.yml', 'operate-relay-production-cell-flags.yml', 'operate-relay-production-rehome-job.yml', + 'operate-relay-production-reserve-watch.yml', 'publish-relay-production.yml' ].map((name) => relayWorkflowFile(name)).sort()) }) diff --git a/cloud/dev/scripts/relay-public-workflow-contract.test.mjs b/cloud/dev/scripts/relay-public-workflow-contract.test.mjs index 3f34e96a107..1d6befc616e 100644 --- a/cloud/dev/scripts/relay-public-workflow-contract.test.mjs +++ b/cloud/dev/scripts/relay-public-workflow-contract.test.mjs @@ -20,7 +20,7 @@ const UNGATED = relayWorkflowFile('verify.yml') const relayWorkflows = () => workflowFiles().filter((file) => file !== UNGATED) test('the copy carries every relay workflow', () => { - assert.equal(relayWorkflows().length, 27) + assert.equal(relayWorkflows().length, 28) }) // Why: workflow_run chains match by display name, not filename. Renaming a file is safe; renaming diff --git a/cloud/dev/scripts/relay-reserve-dead-man-alert.test.mjs b/cloud/dev/scripts/relay-reserve-dead-man-alert.test.mjs index b35d2767eb1..71533ea61e8 100644 --- a/cloud/dev/scripts/relay-reserve-dead-man-alert.test.mjs +++ b/cloud/dev/scripts/relay-reserve-dead-man-alert.test.mjs @@ -12,6 +12,8 @@ const deadMan = read('../../apps/relay/src/cell-reserve-dead-man.ts') const registry = read('../../apps/relay/src/host-session-registry.ts') const server = read('../../apps/relay/src/relay-server.ts') const poller = read('../../apps/relay/src/shadow-seat-directory.ts') +const watch = read('./watch-relay-reserve-cell.mjs') +const githubActions = read('../../infra/terraform/relay-github-actions.tf') const block = (kind, name) => { const body = new RegExp(`resource "${kind}" "${name}" \\{([\\s\\S]*?)\\n\\}`).exec(terraform)?.[1] @@ -84,3 +86,24 @@ test('the withheld dead-cell verdict alert counts every director withheld line, assert.match(read('../../infra/terraform/variables.tf'), /variable "relay_dead_cell_verdict_alert" \{[\s\S]*?default = false/) assert.match(read('../../infra/terraform/environments/staging.tfvars'), /relay_dead_cell_verdict_alert\s+= true/) }) + +test('the reserve watch alarm pages on any line the watch runner writes, per cell', () => { + // The runner writes a global-resource entry under its own log name. + assert.match(watch, /export const ALARM_EVENT = 'orca_relay_reserve_watch_alarm'/) + assert.match(watch, /export const ALARM_LOG_NAME = 'orca-relay-reserve-watch'/) + assert.match(watch, /JSON\.stringify\(\{ event: ALARM_EVENT, cellId, kind, reason \}\)/) + const metric = block('google_logging_metric', 'relay_reserve_watch_alarm') + assert.ok( + metric.includes( + 'resource.type=\\"global\\" AND logName=\\"projects/${var.project_id}/logs/orca-relay-reserve-watch\\" AND jsonPayload.event=\\"orca_relay_reserve_watch_alarm\\"' + ) + ) + assert.ok(metric.includes('cell_id = "EXTRACT(jsonPayload.cellId)"')) + notifyingOnAnyLine('relay_reserve_watch_alarm', 'orca_relay_reserve_watch_alarm', 'global', 'cell_id') + // The deploy identity the workflow runs as can write that line, and only in production. + const grant = /resource "google_project_iam_member" "github_log_writer" \{([\s\S]*?)\n\}/.exec(githubActions)?.[1] + assert.ok(grant, 'github_log_writer grant not found') + assert.match(grant, /count = local\.relay_create_production_ops_identity \? 1 : 0/) + assert.match(grant, /role = "roles\/logging\.logWriter"/) + assert.match(grant, /member = google_service_account\.github_deploy\[0\]\.member/) +}) diff --git a/cloud/dev/scripts/relay-reserve-watch-rules.mjs b/cloud/dev/scripts/relay-reserve-watch-rules.mjs new file mode 100644 index 00000000000..7fd9aae89f2 --- /dev/null +++ b/cloud/dev/scripts/relay-reserve-watch-rules.mjs @@ -0,0 +1,258 @@ +// Stop rules for a cell watched in reserve mode (step 5 design §9, "Auto-abort per flip"). Pure: +// it takes one already-read sample and the watch state, and returns the next state and whether +// to flip the cell back. The reader lives in watch-relay-reserve-cell.mjs. +import { backgroundOf, SHADOW_GATE_THRESHOLDS } from './relay-same-cap-shadow-gate-verdict.mjs' + +const MINUTE_MS = 60_000 + +export const RESERVE_WATCH_RULES = { + // The roll ladder's warn line: max(1.5x, background + 20), so a zero background is not tripped + // by one 503. Three complete minutes in a row, not two. The read spans more minutes than that so + // a late publishing front still leaves three published minutes to judge. + director503: { + multiple: SHADOW_GATE_THRESHOLDS.nonDrain503Budget.warnMultiple, + marginPerMinute: SHADOW_GATE_THRESHOLDS.nonDrain503Budget.warnMarginPerMinute, + consecutiveMinutes: 3, + readMinutes: 10, + // Cloud Run publishes a minute's request count up to 3 minutes late. + publishDelayMs: 3 * MINUTE_MS + }, + // The cell closes a demoted seat after 60 s (DEMOTION_CLOSE_MS); 30 s more covers a director's + // map still holding the closed seat. + duplicateSeat: { graceMs: 60_000 + 30_000, lookbackMs: 15 * MINUTE_MS }, + // The floor keeps an almost empty cell's ordinary churn from reading as a 5% drop. + hostDrop: { share: 0.05, windowMs: 5 * MINUTE_MS, minHosts: 10 }, + ledgerDrift: { share: 0.005, sustainMs: 2 * MINUTE_MS }, + // A placing director logs its ledger summary every 60 s; none in this long is no evidence. + ledgerSummaryMaxAgeMs: 150_000, + // A watch dispatched before the reserve write applies waits this long for the cell to report it. + reserveApplyWaitMs: 10 * MINUTE_MS +} + +// Precedence: the first tripped rule names the verdict, so exact signals come before proxies. The +// sample line's `rules` keys follow this order too. +export const RULE_NAMES = ['deadMan', 'overrun', 'director503', 'duplicateSeat', 'hostDrop', 'ledgerDrift'] + +export function initialWatchState({ cellId, startedAt, durationMinutes, background503PerMinute, flippedAt }) { + return { + v: 1, + cellId, + startedAt, + deadlineAt: startedAt + durationMinutes * MINUTE_MS, + state: 'watching', + verdict: null, + samples: 0, + unverifiableStreak: 0, + // The background is measured in the minutes before the flip, unless the operator supplied it. + flippedAt: flippedAt ?? null, + background: + background503PerMinute === null ? null : { medianPerMinute: background503PerMinute, source: 'input' }, + hosts: [], + reserveSeenAt: null, + ledgerAboveSince: null, + seatBehindRowFirstSeen: {} + } +} + +// The background from the per-minute 503 counts before the flip. `publishedThrough` is the newest +// minute any response code has a point for: a 503 minute at or before it with no point had none, +// and the background waits (null) until it covers the last minute. +export function measuredBackground(perMinute, minutes, publishedThrough) { + if (minutes.length === 0 || publishedThrough === null || publishedThrough < minutes.at(-1)) return null + const background = backgroundOf({ minutes, series: minutes.map((minute) => perMinute[minute] ?? 0), unverified: false }) + return { medianPerMinute: background.medianPerMinute, minutes: background.minutes, source: 'measured' } +} + +const pass = (detail = {}) => ({ status: 'pass', ...detail }) +const trip = (reason, detail = {}) => ({ status: 'trip', reason, ...detail }) +const unverifiable = (reason) => ({ status: 'unverifiable', reason }) +// Not readable yet by design, so it does not count toward the unverifiable streak. +const pending = (reason) => ({ status: 'pending', reason }) + +function judgeDeadMan(cell) { + if (!cell.ok) return unverifiable(cell.error) + if (cell.admitModeEffective !== 'db' && cell.admitModeEffective !== 'reserve') { + return unverifiable('runtime-status reports no admitModeEffective') + } + if (cell.admitModeEffective === 'db') { + return trip('the cell tripped its dead-man (reserve configured, db effective); Postgres still records reserve') + } + return pass() +} + +// The cell ledger's total (seats, bookings, in-flight and phone units) against its hard cap. The +// ledger refuses past the cap, so this is an invariant check: it trips only if that refusal broke. +function judgeOverrun(cell) { + if (!cell.ok) return unverifiable(cell.error) + if (!Number.isInteger(cell.hardCap) || !Number.isInteger(cell.enforcedUnits)) { + return unverifiable('runtime-status reports no hard cap or ledger units') + } + if (cell.enforcedUnits > cell.hardCap) { + return trip(`ledger units ${cell.enforcedUnits} over hard cap ${cell.hardCap}`, { + enforcedUnits: cell.enforcedUnits, + hardCap: cell.hardCap + }) + } + return pass({ enforcedUnits: cell.enforcedUnits, hardCap: cell.hardCap }) +} + +// Fleet-wide director 503s: no existing signal attributes a 503 to the cell a host was bound for, +// so a problem local to one cell can stay under the fleet's line. This proxy can miss. +function judgeDirector503(read, state, at) { + const { multiple, marginPerMinute, consecutiveMinutes, publishDelayMs } = RESERVE_WATCH_RULES.director503 + const { background } = state + if (!background) { + const publishesAt = state.flippedAt === null ? null : Math.floor(state.flippedAt / MINUTE_MS) * MINUTE_MS + publishDelayMs + if (publishesAt !== null && at < publishesAt) return pending('the 503 background before the flip is not published yet') + return unverifiable('no 503 background yet') + } + if (!read.ok) return unverifiable(read.error) + const perMinute = background.medianPerMinute + const above = Math.max(perMinute * multiple, perMinute + marginPerMinute) + // Minute keys sort as time. Only minutes the publishing front has reached are judged: a 503 + // minute there with no point had none, one past it is not published yet and is left out. + if (read.publishedThrough === null) return unverifiable('the director request series is empty') + const recent = read.minutes.filter((minute) => minute <= read.publishedThrough).slice(-consecutiveMinutes) + if (recent.length < consecutiveMinutes) { + return unverifiable(`director requests are published only through ${read.publishedThrough}`) + } + const counts = recent.map((minute) => read.perMinute[minute] ?? 0) + const detail = { backgroundPerMinute: perMinute, above, recent: Object.fromEntries(recent.map((m, i) => [m, counts[i]])) } + if (counts.every((count) => count > above)) { + return trip(`director 503s above ${above}/min for ${consecutiveMinutes} min (${counts.join(', ')})`, detail) + } + return pass(detail) +} + +// A seat the ledger reconcile found behind its row and found again after the demotion should +// have closed it. The line names no host, so (director instance, cell, epoch, rowEpoch) stands in +// for one: a proxy that can only over-count, when two hosts share that key. +function judgeDuplicateSeat(read, firstSeen, at) { + if (!read.ok) return { verdict: unverifiable(read.error), firstSeen } + const { graceMs, lookbackMs } = RESERVE_WATCH_RULES.duplicateSeat + const next = Object.fromEntries( + Object.entries(firstSeen).filter(([, seenAt]) => seenAt >= at - 2 * lookbackMs) + ) + for (const report of [...read.reports].sort((left, right) => left.at - right.at)) { + next[report.key] = Math.min(next[report.key] ?? report.at, report.at) + } + for (const report of read.reports) { + const ageMs = report.at - next[report.key] + if (ageMs > graceMs) { + return { + verdict: trip(`a seat behind its ledger row (${report.key}) is still seated ${Math.round(ageMs / 1000)} s after first report`), + firstSeen: next + } + } + } + return { verdict: pass({ reportsInLookback: read.reports.length }), firstSeen: next } +} + +function judgeHostDrop(hosts) { + const { share, minHosts, windowMs } = RESERVE_WATCH_RULES.hostDrop + const current = hosts.at(-1) + if (!current) return unverifiable('no host count') + if (hosts.some((entry) => entry.draining)) return pass({ controls: current.controls, draining: true }) + const peak = Math.max(...hosts.map((entry) => entry.controls)) + const drop = peak - current.controls + const detail = { controls: current.controls, peakInWindow: peak } + if (peak > 0 && drop >= minHosts && drop / peak > share) { + return trip(`connected hosts fell ${peak} -> ${current.controls} within ${windowMs / MINUTE_MS} min outside a drain`, detail) + } + return pass(detail) +} + +// Rows the placing directors have not written (queued) or gave up on (dropped), per seat. The +// summaries are fleet-wide: exact while this is the only reserve cell, an over-count otherwise. +function judgeLedgerDrift(cell, ledger, aboveSince, at) { + if (!cell.ok) return { verdict: unverifiable(cell.error), aboveSince } + if (!ledger.ok) return { verdict: unverifiable(ledger.error), aboveSince } + const { share, sustainMs } = RESERVE_WATCH_RULES.ledgerDrift + const ratio = ledger.unwrittenRows / Math.max(cell.controls, 1) + const detail = { unwrittenRows: ledger.unwrittenRows, directors: ledger.directors, ratio } + if (ratio <= share) return { verdict: pass(detail), aboveSince: null } + const since = aboveSince ?? at + if (at - since >= sustainMs) { + return { + verdict: trip(`ledger drift ${(ratio * 100).toFixed(2)}% for ${Math.round((at - since) / 1000)} s`, detail), + aboveSince: since + } + } + return { verdict: pass({ ...detail, aboveForMs: at - since }), aboveSince: since } +} + +/** + * One sample. `sample.cell` is the cell's runtime-status, `director503` the complete minutes of + * director 503s with the request series' publishing front (`publishedThrough`), `ledger` the placing directors' newest ledger summaries, `seatBehindRow` this + * cell's reconcile reports in the lookback. Each is `{ ok: false, error }` when it could not be read. + */ +export function evaluateWatchSample(state, sample, { unverifiableLimit }) { + const at = sample.at + const next = { ...state, samples: state.samples + 1 } + const { cell } = sample + if (cell.ok && cell.admitModeConfigured !== 'reserve') { + const seen = state.reserveSeenAt !== null + if (!seen && at < state.startedAt + RESERVE_WATCH_RULES.reserveApplyWaitMs) { + // Not judged, and the streak holds: the reserve write may not have applied yet. + const rules = { reserve: pending(`switch file says ${cell.admitModeConfigured}; waiting for reserve to apply`) } + return { state: next, rules, line: sampleLine(next, sample, rules) } + } + // Someone else flipped it back (or it never became reserve): nothing left to watch. + next.state = seen ? 'stopped' : 'refused' + next.verdict = seen + ? `${state.cellId} is not in reserve mode (switch file says ${cell.admitModeConfigured})` + : `${state.cellId} did not report reserve mode within ${RESERVE_WATCH_RULES.reserveApplyWaitMs / MINUTE_MS} min (switch file says ${cell.admitModeConfigured})` + return { state: next, rules: {}, line: sampleLine(next, sample, {}) } + } + if (cell.ok) { + next.reserveSeenAt = state.reserveSeenAt ?? at + next.hosts = [...state.hosts, { at, controls: cell.controls, draining: cell.draining }].filter( + (entry) => entry.at > at - RESERVE_WATCH_RULES.hostDrop.windowMs + ) + } + const duplicate = judgeDuplicateSeat(sample.seatBehindRow, state.seatBehindRowFirstSeen, at) + next.seatBehindRowFirstSeen = duplicate.firstSeen + const ledger = judgeLedgerDrift(cell, sample.ledger, state.ledgerAboveSince, at) + next.ledgerAboveSince = ledger.aboveSince + const rules = { + deadMan: judgeDeadMan(cell), + overrun: judgeOverrun(cell), + director503: judgeDirector503(sample.director503, state, at), + duplicateSeat: duplicate.verdict, + hostDrop: cell.ok ? judgeHostDrop(next.hosts) : unverifiable(cell.error), + ledgerDrift: ledger.verdict + } + const tripped = RULE_NAMES.find((name) => rules[name].status === 'trip') + if (tripped) { + next.state = 'tripped' + next.verdict = `flip back ${state.cellId}: ${tripped}: ${rules[tripped].reason}` + return { state: next, rules, line: sampleLine(next, sample, rules) } + } + const missing = RULE_NAMES.filter((name) => rules[name].status === 'unverifiable') + const waiting = RULE_NAMES.some((name) => rules[name].status === 'pending') + // A pending rule holds the streak where it was: neither evidence of health nor a gap. + next.unverifiableStreak = missing.length > 0 ? state.unverifiableStreak + 1 : waiting ? state.unverifiableStreak : 0 + if (next.unverifiableStreak >= unverifiableLimit) { + next.state = 'tripped' + next.verdict = + `flip back ${state.cellId}: unverifiable: ${next.unverifiableStreak} samples in a row could not ` + + `read ${missing.join(', ')}` + } else if (at >= state.deadlineAt) { + next.state = 'completed' + next.verdict = `${state.cellId} stayed healthy in reserve mode for the whole watch` + } + return { state: next, rules, line: sampleLine(next, sample, rules) } +} + +function sampleLine(state, sample, rules) { + return JSON.stringify({ + event: 'orca_relay_reserve_watch_sample', + cellId: state.cellId, + at: new Date(sample.at).toISOString(), + sample: state.samples, + state: state.state, + unverifiableStreak: state.unverifiableStreak, + rules, + ...(state.verdict ? { verdict: state.verdict } : {}) + }) +} diff --git a/cloud/dev/scripts/relay-same-cap-shadow-gate.mjs b/cloud/dev/scripts/relay-same-cap-shadow-gate.mjs index cacfd7b7bda..2914b282095 100644 --- a/cloud/dev/scripts/relay-same-cap-shadow-gate.mjs +++ b/cloud/dev/scripts/relay-same-cap-shadow-gate.mjs @@ -122,7 +122,7 @@ async function retryingRead(reader, read) { return { failed: true, error: String(lastError?.message ?? lastError) } } -async function readLogEntries(reader, { filter, projection, limit = ENTRY_LIMIT }) { +export async function readLogEntries(reader, { filter, projection, limit = ENTRY_LIMIT }) { const args = [ 'logging', 'read', filter, '--project', reader.projectId, @@ -154,21 +154,22 @@ async function readTimestampsOverWindow(reader, { filter, window }) { // Cloud Run's own request counter, aligned per minute server-side: a brownout's tens of thousands // of 503s are counted, where a log read of them stops at its entry limit (10-01 c29). -async function readDirector503PerMinute(reader, { config, span }) { +async function readDirectorRequestCountSeries(reader, { config, span, filters = [], groupByFields }) { const url = new URL(`https://monitoring.googleapis.com/v3/projects/${config.projectId}/timeSeries`) url.searchParams.set('filter', [ 'metric.type="run.googleapis.com/request_count"', 'resource.type="cloud_run_revision"', `resource.label."service_name"="${config.directorService}"`, - 'metric.label."response_code"="503"' + ...filters ].join(' AND ')) url.searchParams.set('interval.startTime', span.startedAt.toISOString()) url.searchParams.set('interval.endTime', span.endedAt.toISOString()) url.searchParams.set('aggregation.alignmentPeriod', '60s') url.searchParams.set('aggregation.perSeriesAligner', 'ALIGN_DELTA') url.searchParams.set('aggregation.crossSeriesReducer', 'REDUCE_SUM') + if (groupByFields) url.searchParams.set('aggregation.groupByFields', groupByFields) url.searchParams.set('pageSize', '1000') - const read = await retryingRead(reader, async (timeoutMs) => { + return await retryingRead(reader, async (timeoutMs) => { const { stdout } = await reader.runGcloud(['auth', 'print-access-token'], { timeoutMs }) const response = await reader.fetch(url, { headers: { authorization: `Bearer ${stdout.trim()}` }, @@ -179,17 +180,44 @@ async function readDirector503PerMinute(reader, { config, span }) { if (body.nextPageToken) throw new Error('Cloud Monitoring pagination is incomplete') return { timeSeries: body.timeSeries ?? [] } }) - const perMinute = {} - for (const series of read.timeSeries ?? []) { - for (const point of series.points ?? []) { - // A delta point covers the minute that ends at its end time; a minute with no point had none. - const minute = minuteKey(Date.parse(point.interval.endTime) - 60_000) - perMinute[minute] = (perMinute[minute] ?? 0) + Number(point.value.int64Value ?? 0) - } +} + +function addPointsPerMinute(perMinute, series) { + for (const point of series.points ?? []) { + // A delta point covers the minute that ends at its end time; a minute with no point had none. + const minute = minuteKey(Date.parse(point.interval.endTime) - 60_000) + perMinute[minute] = (perMinute[minute] ?? 0) + Number(point.value.int64Value ?? 0) } + return perMinute +} + +export async function readDirector503PerMinute(reader, { config, span }) { + const read = await readDirectorRequestCountSeries(reader, { + config, + span, + filters: ['metric.label."response_code"="503"'] + }) + const perMinute = {} + for (const series of read.timeSeries ?? []) addPointsPerMinute(perMinute, series) return { perMinute, failed: read.failed } } +// Every response code in one read, so a minute missing from the 503 series can be told apart from +// one not published yet: the other codes' series carry the same publishing front. +export async function readDirectorRequestsByCodePerMinute(reader, { config, span }) { + const read = await readDirectorRequestCountSeries(reader, { + config, + span, + groupByFields: 'metric.label."response_code"' + }) + const byCode = {} + for (const series of read.timeSeries ?? []) { + const code = series.metric?.labels?.response_code ?? 'unknown' + byCode[code] = addPointsPerMinute(byCode[code] ?? {}, series) + } + return { byCode, failed: read.failed } +} + // Cells log through the COS container agent, so the text lives in jsonPayload.message; a // textPayload filter matches nothing here and returns zero without saying so. const CELL_LOG_SCOPE = 'resource.type="gce_instance" AND logName:"cos_containers"' diff --git a/cloud/dev/scripts/watch-relay-reserve-cell.mjs b/cloud/dev/scripts/watch-relay-reserve-cell.mjs new file mode 100644 index 00000000000..6f5cc38e279 --- /dev/null +++ b/cloud/dev/scripts/watch-relay-reserve-cell.mjs @@ -0,0 +1,581 @@ +#!/usr/bin/env node +// Watches one cell already in reserve mode and, on the first stop rule it trips, flips it back to +// db through the flag tool's own write (CAS, read-back, re-registration wait). Step 5 design §9. +// +// watch: one segment of the watch (the workflow re-authenticates between segments, since an +// ID token lasts an hour). State carries over in --state-file. Writes nothing. +// outcome: the job's state and verdict, from the state file. +// flip-back: the flag tool's admitMode=db write (or its dry run), then exits non-zero with the +// watch's verdict. Why it holds no flag-workflow group: docs/relay-workflows.md. +// alarm: a refused watch or a flip back that did not land: an error annotation, and in write +// mode a log line the reserve-watch alert pages on. +import { execFile } from 'node:child_process' +import { appendFile, readFile, writeFile } from 'node:fs/promises' +import { parseArgs, promisify } from 'node:util' +import { pathToFileURL } from 'node:url' +import { + BREAK_GLASS_CONFIRMATION, + operateCellFlags, + parseCellFlagsRequest, + readCurrentObject, + readRuntime, + WRITE_CONFIRMATION +} from './operate-relay-cell-flags.mjs' +import { readDirectorRequestsByCodePerMinute, readLogEntries } from './relay-same-cap-shadow-gate.mjs' +import { minutesOf } from './relay-same-cap-shadow-gate-verdict.mjs' +import { PRODUCTION_LEASE } from './cloud-sql-rollout-lock-census.mjs' +import { RELAY_REPOSITORY_ROOT } from './relay-repository.mjs' +import { + evaluateWatchSample, + initialWatchState, + measuredBackground, + RESERVE_WATCH_RULES +} from './relay-reserve-watch-rules.mjs' + +const execFileAsync = promisify(execFile) +const MINUTE_MS = 60_000 +export const SAMPLE_INTERVAL_MS = MINUTE_MS +export const MAX_WATCH_MINUTES = 300 +// Below the hour an ID token lasts, with room for the segment's last sample. +export const MAX_SEGMENT_MINUTES = 55 +const METRIC_DELAY_MS = RESERVE_WATCH_RULES.director503.publishDelayMs +const BACKGROUND_MINUTES = 10 +// A flipped-at older than this is a typo or a stale dispatch, not this flip. +const MAX_FLIPPED_AT_AGE_MS = 24 * 60 * MINUTE_MS +export const FLIP_BACK_ATTEMPTS = 5 +const FLIP_BACK_BACKOFF_MS = 2_000 +// One slow runtime-status reply is not a dead cell: retried this often before break glass. +export const FLIP_BACK_RUNTIME_ATTEMPTS = 4 +const SAMPLE_READ_BUDGET_MS = 40_000 +const LOG_ENTRY_LIMIT = 1_000 + +function integerIn(name, raw, min, max) { + const value = Number(raw) + if (!/^[0-9]+$/.test(raw ?? '') || value < min || value > max) { + throw new Error(`--${name} must be an integer from ${min} to ${max}`) + } + return value +} + +// The flip back's request. Its generation is a placeholder: the flip back re-reads the current one. +export function dbFlipRequest(values) { + return parseCellFlagsRequest({ ...values, set: 'admitMode=db', 'expected-generation': '0' }) +} + +// `now` is the watch's start: a later segment of a long watch must not age flipped-at out. +function parseFlippedAt(raw, now) { + if (raw === '') return null + const at = Date.parse(raw) + if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})$/.test(raw) || !Number.isFinite(at)) { + throw new Error('--flipped-at must be an ISO time with a zone, e.g. 2026-10-14T12:00:00Z') + } + if (at > now) throw new Error('--flipped-at is in the future') + if (at < now - MAX_FLIPPED_AT_AGE_MS) throw new Error('--flipped-at is more than 24 hours ago') + return at +} + +export function parseWatchArguments(values, now = Date.now()) { + // The flag tool's own checks, so the watch is of exactly the cell the flip back would write; + // a write-mode watch refuses a missing confirmation now, not hours later. + const request = dbFlipRequest(values) + if (!/^[a-z][a-z0-9-]{0,62}$/.test(values['director-service'] ?? '')) { + throw new Error('--director-service is not a Cloud Run service name') + } + const background = values['background-503-per-minute'] ?? '' + if (background !== '' && !/^(0|[1-9][0-9]{0,6})(\.[0-9]+)?$/.test(background)) { + throw new Error('--background-503-per-minute must be empty (measure it) or a non-negative number') + } + // The background must predate the flip: a watch starts after it, so its own past is not one. + const flippedAt = parseFlippedAt(values['flipped-at'] ?? '', now) + if (background === '' && flippedAt === null) { + throw new Error('--flipped-at (the background is the 10 minutes before it) or --background-503-per-minute is required') + } + if (!values['state-file']) throw new Error('--state-file is required') + return { + request, + directorService: values['director-service'], + durationMinutes: integerIn('duration-minutes', values['duration-minutes'], 5, MAX_WATCH_MINUTES), + segmentMinutes: integerIn('segment-minutes', values['segment-minutes'], 1, MAX_SEGMENT_MINUTES), + unverifiableLimit: integerIn('unverifiable-limit', values['unverifiable-limit'], 1, 30), + background503PerMinute: background === '' ? null : Number(background), + flippedAt, + stateFile: values['state-file'], + githubOutput: values['github-output'] || null + } +} + +const failedRead = (error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) }) + +export async function readCellSample(fetchImpl, request, idToken, wait) { + try { + const runtime = await readRuntime(fetchImpl, request, idToken, wait) + if (runtime.role !== 'cell' || runtime.cellId !== request.cellId) { + return failedRead(`origin answers as ${runtime.role}/${runtime.cellId}`) + } + if (!runtime.flagsApplied) return failedRead('runtime-status reports no flagsApplied') + const controls = runtime.runtime?.controls + if (!Number.isInteger(controls)) return failedRead('runtime-status reports no control count') + return { + ok: true, + admitModeConfigured: runtime.flagsApplied.flags?.admitMode ?? 'db', + admitModeEffective: runtime.admitModeEffective, + draining: runtime.draining === true, + controls, + enforcedUnits: runtime.runtime?.enforcedConnectionUnits, + hardCap: runtime.connectionCapacity?.hardCap + } + } catch (error) { + return failedRead(error) + } +} + +const minuteFloor = (at) => Math.floor(at / MINUTE_MS) * MINUTE_MS +const failed503Read = (error) => ({ ok: false, error, perMinute: {}, minutes: [], publishedThrough: null }) + +// The `minutes` whole minutes that end at the minute boundary `endedAt`: the 503 counts, and the +// newest minute any response code has a point for. The director always serves something, so that +// front is how far the 503 series is published; no front at all is no evidence. A code publishing +// late only delays a trip: a later read sees its points. +export async function readDirector503Minutes(reader, { projectId, directorService, endedAt, minutes }) { + const span = { startedAt: new Date(endedAt - minutes * MINUTE_MS), endedAt: new Date(endedAt) } + let read + try { + read = await readDirectorRequestsByCodePerMinute(reader, { config: { projectId, directorService }, span }) + } catch (error) { + // A point with an unparseable end time has no minute. + return failed503Read(`director 503 metric unreadable: ${error instanceof Error ? error.message : error}`) + } + if (read.failed) return failed503Read('director 503 metric read failed') + const published = Object.values(read.byCode).flatMap((perMinute) => Object.keys(perMinute)).sort() + return { + ok: true, + perMinute: read.byCode['503'] ?? {}, + minutes: minutesOf(span), + publishedThrough: published.at(-1) ?? null + } +} + +const LEDGER_SUMMARY_EVENT = 'orca_relay_reserve_ledger_summary' +const SEAT_BEHIND_ROW_EVENT = 'orca_relay_reserve_ledger_seat_behind_row' + +function directorLogFilter(directorService, eventClause, sinceAt) { + return [ + 'resource.type="cloud_run_revision"', + `resource.labels.service_name="${directorService}"`, + eventClause, + `timestamp>="${new Date(sinceAt).toISOString()}"` + ].join(' AND ') +} + +// Newest summary per director instance; queued plus dropped rows are the ones not in Postgres. +export async function readLedgerSample(reader, { directorService, at }) { + const read = await readLogEntries(reader, { + filter: directorLogFilter(directorService, `jsonPayload.event="${LEDGER_SUMMARY_EVENT}"`, at - RESERVE_WATCH_RULES.ledgerSummaryMaxAgeMs), + projection: 'json(timestamp,labels.instanceId,jsonPayload.pending,jsonPayload.dropped)', + limit: LOG_ENTRY_LIMIT + }) + if (read.failed) return failedRead(read.error) + if (read.entries.length >= LOG_ENTRY_LIMIT) return failedRead('ledger summary read truncated') + if (read.entries.length === 0) return failedRead('no placing director logged a ledger summary') + if (read.entries.some((entry) => !Number.isFinite(Date.parse(entry.timestamp)))) { + return failedRead('ledger summary with an unparseable timestamp') + } + const newest = new Map() + for (const entry of read.entries) { + const instance = entry.labels?.instanceId ?? 'unknown' + const previous = newest.get(instance) + if (!previous || Date.parse(entry.timestamp) > Date.parse(previous.timestamp)) newest.set(instance, entry) + } + let unwrittenRows = 0 + for (const entry of newest.values()) { + const pending = Number(entry.jsonPayload?.pending) + const dropped = Number(entry.jsonPayload?.dropped) + if (!Number.isFinite(pending) || !Number.isFinite(dropped)) return failedRead('ledger summary without counts') + unwrittenRows += pending + dropped + } + return { ok: true, unwrittenRows, directors: newest.size } +} + +// The same read also takes the ledger summary every placing director logs each minute, as a +// positive control: with no fresh summary in it, no seat-behind-row line is blindness, not zero. +export async function readSeatBehindRowSample(reader, { directorService, cellId, at }) { + const read = await readLogEntries(reader, { + filter: directorLogFilter( + directorService, + `((jsonPayload.event="${SEAT_BEHIND_ROW_EVENT}" AND jsonPayload.cellId="${cellId}") OR jsonPayload.event="${LEDGER_SUMMARY_EVENT}")`, + at - RESERVE_WATCH_RULES.duplicateSeat.lookbackMs + ), + projection: 'json(timestamp,labels.instanceId,jsonPayload.event,jsonPayload.epoch,jsonPayload.rowEpoch)', + limit: LOG_ENTRY_LIMIT + }) + if (read.failed) return failedRead(read.error) + if (read.entries.length >= LOG_ENTRY_LIMIT) return failedRead('seat-behind-row read truncated') + const reports = [] + let controlSeen = false + for (const entry of read.entries) { + const reportedAt = Date.parse(entry.timestamp) + if (!Number.isFinite(reportedAt)) return failedRead('seat-behind-row line with an unparseable timestamp') + if (entry.jsonPayload?.event === LEDGER_SUMMARY_EVENT) { + controlSeen ||= reportedAt >= at - RESERVE_WATCH_RULES.ledgerSummaryMaxAgeMs + continue + } + const { epoch, rowEpoch } = entry.jsonPayload ?? {} + if (epoch === undefined || rowEpoch === undefined) return failedRead('seat-behind-row line without epochs') + // Everything the line carries but its time, which differs on every report of one seat. A + // proxy for the host: two hosts sharing it read as one seat, so it can only over-count. + reports.push({ at: reportedAt, key: `${entry.labels?.instanceId ?? 'unknown'}/${cellId}/${epoch}:${rowEpoch}` }) + } + if (!controlSeen) return failedRead('seat-behind-row read holds no fresh ledger summary (positive control)') + return { ok: true, reports } +} + +function createReader(dependencies, projectId) { + const now = dependencies.now + return { + runGcloud: dependencies.runGcloud, + fetch: dependencies.fetchImpl, + retryDelayMs: dependencies.retryDelayMs ?? 5_000, + readTimeoutMs: 20_000, + now, + // Every sample gets its own budget, so a slow read costs one sample, not the watch. + deadlineAt: now() + SAMPLE_READ_BUDGET_MS, + projectId + } +} + +/** + * Same-cap and capacity rolls hold the Cloud SQL rollout lease for the whole run; it names no + * cell, so any live holder refuses the watch's start. Read only: holding it for hours would block + * every rollout. An unreadable lease refuses too (fail closed, as the lease action does). + */ +export async function rolloutLeaseRefusal(readRolloutLease, now) { + try { + const existing = await readRolloutLease() + if (!existing || existing.record.expires_at <= now) return null + return ( + `a rollout holds the Cloud SQL rollout lease until ${new Date(existing.record.expires_at).toISOString()} ` + + `(${existing.holder}); start the watch after it ends` + ) + } catch (error) { + return `the Cloud SQL rollout lease is unreadable (${error instanceof Error ? error.message : error}); refusing to start` + } +} + +// The lease action's own reader, resolved from the repository root because this tree's depth +// differs between the two repositories' copies. +async function readProductionRolloutLease() { + const action = new URL('.github/actions/cloud-sql-rollout-lease/', RELAY_REPOSITORY_ROOT) + const [{ CloudSqlRolloutLease, describeHolder }, { createAccessTokenSource }] = await Promise.all([ + import(new URL('storage-lease.mjs', action).href), + import(new URL('gcloud-access-token.mjs', action).href) + ]) + const lease = new CloudSqlRolloutLease({ + bucket: PRODUCTION_LEASE.bucket, + objectName: PRODUCTION_LEASE.object, + accessToken: createAccessTokenSource() + }) + const existing = await lease.read() + return existing && { record: existing.record, holder: describeHolder(existing.record) } +} + +// Only a missing file is a fresh watch. Anything else that is not a state fails the segment, which +// the outcome step reads as failed and flips back: a corrupt file must not restart or refuse the watch. +export async function loadState(path) { + const text = await readFile(path, 'utf8').catch((error) => (error?.code === 'ENOENT' ? null : Promise.reject(error))) + if (text === null) return null + const state = JSON.parse(text) + const isObject = state !== null && typeof state === 'object' && !Array.isArray(state) + if (!isObject || typeof state.state !== 'string') throw new Error(`state file ${path} holds no watch state`) + if (state.state === 'watching' && (typeof state.cellId !== 'string' || !Number.isFinite(state.startedAt))) { + throw new Error(`state file ${path} has no cell or start`) + } + return state +} + +// A segment's config. Later segments check flipped-at against the watch's start; bad inputs before +// the first sample are refused, so no flip back follows a typo. +export async function parseWatchSegment(values, now = Date.now()) { + const existing = values['state-file'] ? await loadState(values['state-file']) : null + try { + return parseWatchArguments(values, Number.isFinite(existing?.startedAt) ? existing.startedAt : now) + } catch (error) { + if (values['state-file'] && existing === null) { + await writeFile(values['state-file'], JSON.stringify({ state: 'refused', verdict: error.message })) + } + throw error + } +} + +export async function watchSegment(config, dependencies) { + const now = dependencies.now ?? Date.now + const sleep = dependencies.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))) + const log = dependencies.log ?? console.log + const deps = { ...dependencies, now, sleep, fetchImpl: dependencies.fetchImpl ?? fetch } + const { request, directorService } = config + let state = await loadState(config.stateFile) + if (state === null) { + const refusal = await rolloutLeaseRefusal(dependencies.readRolloutLease, now()) + if (refusal) { + state = { state: 'refused', verdict: refusal } + await writeFile(config.stateFile, JSON.stringify(state)) + if (config.githubOutput) await writeOutcome(config.githubOutput, state) + return state + } + state = initialWatchState({ + cellId: request.cellId, + startedAt: now(), + durationMinutes: config.durationMinutes, + background503PerMinute: config.background503PerMinute, + flippedAt: config.flippedAt + }) + } + if (state.cellId !== request.cellId) throw new Error(`state file is for ${state.cellId}, not ${request.cellId}`) + const segmentEndsAt = now() + config.segmentMinutes * MINUTE_MS + while (state.state === 'watching' && now() < segmentEndsAt) { + const at = now() + const baselineEndsAt = state.flippedAt === null ? null : minuteFloor(state.flippedAt) + if (!state.background && baselineEndsAt !== null && at >= baselineEndsAt + METRIC_DELAY_MS) { + // The minutes just before the flip, once published; retried every sample until one reads. + const read = await readDirector503Minutes(createReader(deps, request.projectId), { + projectId: request.projectId, + directorService, + endedAt: baselineEndsAt, + minutes: BACKGROUND_MINUTES + }) + state = { ...state, background: read.ok ? measuredBackground(read.perMinute, read.minutes, read.publishedThrough) : null } + } + const reader = createReader(deps, request.projectId) + // Serialised: concurrent Logging reads are what earn a 429. + const sample = { + at, + cell: await readCellSample(deps.fetchImpl, request, dependencies.idToken, sleep), + director503: await readDirector503Minutes(reader, { + projectId: request.projectId, + directorService, + // Complete minutes only, held back past the publishing delay. + endedAt: minuteFloor(at - METRIC_DELAY_MS), + minutes: RESERVE_WATCH_RULES.director503.readMinutes + }), + ledger: await readLedgerSample(reader, { directorService, at }), + seatBehindRow: await readSeatBehindRowSample(reader, { directorService, cellId: request.cellId, at }) + } + const result = evaluateWatchSample(state, sample, { unverifiableLimit: config.unverifiableLimit }) + state = result.state + log(result.line) + await writeFile(config.stateFile, JSON.stringify(state)) + if (state.state !== 'watching') break + await sleep(Math.max(0, at + SAMPLE_INTERVAL_MS - now())) + } + if (config.githubOutput) await writeOutcome(config.githubOutput, state) + return state +} + +function writeOutcome(githubOutput, { state, verdict }) { + return appendFile(githubOutput, `state=${state}\nverdict=${(verdict ?? '').replace(/[\r\n]+/g, ' ')}\n`) +} + +// The job's outcome. A watch that ended mid-segment (a crash, a timeout) records failed, which the +// workflow flips back in write mode: missing data is never healthy. +export async function recordOutcome(stateFile, githubOutput) { + const state = await loadState(stateFile).catch(() => null) + const outcome = + state && state.state !== 'watching' ? state : { state: 'failed', verdict: 'the watch stopped before a verdict' } + await writeOutcome(githubOutput, outcome) + return outcome +} + +function breakGlassInputs(request, generation) { + return { + mode: 'write', + 'cell-id': request.cellId, + set: 'admitMode=default', + 'break-glass': `${BREAK_GLASS_CONFIRMATION} ${request.cellId}`, + 'expected-generation': generation, + confirmation: `${WRITE_CONFIRMATION} ${request.cellId}` + } +} + +async function readRuntimeRetrying(fetchImpl, request, idToken, sleep, log) { + for (let attempt = 1; ; attempt += 1) { + try { + return await readRuntime(fetchImpl, request, idToken, sleep) + } catch (error) { + if (attempt >= FLIP_BACK_RUNTIME_ATTEMPTS) throw error + log(JSON.stringify({ event: 'orca_relay_reserve_watch_runtime_retry', cellId: request.cellId, attempt, message: error instanceof Error ? error.message : String(error) })) + await sleep(FLIP_BACK_BACKOFF_MS * 2 ** (attempt - 1)) + } + } +} + +/** + * The flag tool's admitMode=db write at the generation read just now. A generation conflict (some + * other write landed first) is re-read and retried while the switch file still says reserve; a + * file that already says db is done. A cell that still does not answer after the runtime retries + * needs break glass, which only an operator may type. + */ +export async function flipBack(values, dependencies) { + const log = dependencies.log ?? console.log + const fetchImpl = dependencies.fetchImpl ?? fetch + const sleep = dependencies.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))) + const base = dbFlipRequest(values) + const record = (result) => + log( + JSON.stringify({ + event: 'orca_relay_reserve_watch_flip_back', + mode: base.mode, + cellId: base.cellId, + written: result.written, + generation: result.generation ?? null, + ...(result.alreadyDb ? { alreadyDb: true } : {}) + }) + ) + for (let attempt = 1; ; attempt += 1) { + const current = await readCurrentObject(fetchImpl, base, dependencies.accessToken) + if ((current.object?.flags?.admitMode ?? 'db') !== 'reserve') { + const result = { written: false, alreadyDb: true, generation: current.generation } + record(result) + return result + } + try { + await readRuntimeRetrying(fetchImpl, base, dependencies.idToken, sleep, log) + } catch (error) { + const inputs = breakGlassInputs(base, current.generation) + log(JSON.stringify({ event: 'orca_relay_reserve_watch_break_glass_required', cellId: base.cellId, inputs })) + throw new Error( + `${base.cellId} does not answer runtime-status (${error instanceof Error ? error.message : error}), so ` + + 'the flag tool cannot flip it back. Run Operate Relay Production Cell Flags with break glass: ' + + Object.entries(inputs).map(([key, value]) => `${key}=${value}`).join(', ') + ) + } + try { + const result = await operateCellFlags( + { ...base, expectedGeneration: current.generation }, + { ...dependencies, fetchImpl, log, sleep } + ) + record(result) + return result + } catch (error) { + if (!error?.generationConflict || attempt >= FLIP_BACK_ATTEMPTS) throw error + log(JSON.stringify({ event: 'orca_relay_reserve_watch_flip_back_conflict', cellId: base.cellId, attempt, message: error.message })) + await sleep(FLIP_BACK_BACKOFF_MS * 2 ** (attempt - 1)) + } + } +} + +export const ALARM_EVENT = 'orca_relay_reserve_watch_alarm' +export const ALARM_LOG_NAME = 'orca-relay-reserve-watch' +export const ALARM_KINDS = ['refused', 'flip-back-failed'] + +// GitHub's workflow-command escaping, so a verdict cannot end the annotation or start another. +export const escapeWorkflowCommand = (text) => text.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A') +const escapeWorkflowProperty = (text) => escapeWorkflowCommand(text).replace(/:/g, '%3A').replace(/,/g, '%2C') + +/** + * Both kinds can leave the cell in reserve with nothing watching it. Only write mode pages: a + * dry-run watch never flips back, so its refusal or failure changes nothing for the cell. + */ +export async function raiseAlarm({ mode, cellId, projectId, kind, reason }, { runGcloud, log = console.log }) { + if (!ALARM_KINDS.includes(kind)) throw new Error(`--kind must be one of ${ALARM_KINDS.join(', ')}`) + const message = + kind === 'refused' + ? `${cellId} may be in reserve mode and unwatched: the watch was refused (${reason}). Rerun it, or flip the cell to db with Operate Relay Production Cell Flags.` + : `${cellId} may still be in reserve mode with no watch: the flip back did not land (${reason}). Flip it to db by hand with Operate Relay Production Cell Flags (set=admitMode=db), or with the break-glass inputs logged above if the cell does not answer.` + log(`::error title=${escapeWorkflowProperty(`Reserve watch ${kind} for ${cellId}`)}::${escapeWorkflowCommand(message)}`) + if (mode !== 'write') return { paged: false } + const entry = JSON.stringify({ event: ALARM_EVENT, cellId, kind, reason }) + try { + // The flag tool's project check: a missing --project-id must not reach gcloud as "undefined". + if (!/^[a-z][a-z0-9-]{4,62}$/.test(projectId ?? '')) throw new Error('--project-id is not a GCP project id') + await runGcloud( + ['logging', 'write', ALARM_LOG_NAME, entry, '--payload-type=json', '--severity=ERROR', '--project', projectId], + { timeoutMs: 30_000 } + ) + } catch (error) { + log(`::error title=Reserve watch page failed::${escapeWorkflowCommand(`the alarm log line was not written, so nothing pages: ${error instanceof Error ? error.message : error}`)}`) + return { paged: false } + } + return { paged: true } +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { + const [command, ...rest] = process.argv.slice(2) + const { values } = parseArgs({ + args: rest, + options: Object.fromEntries( + [ + 'mode', + 'cell-id', + 'cell-origin', + 'project-id', + 'director-origin', + 'director-service', + 'confirmation', + 'duration-minutes', + 'segment-minutes', + 'unverifiable-limit', + 'background-503-per-minute', + 'flipped-at', + 'state-file', + 'github-output', + 'verdict', + 'kind' + ].map((name) => [name, { type: 'string' }]) + ) + }) + const idToken = process.env.ORCA_RELAY_ADMIN_ID_TOKEN + const runGcloud = (args, { timeoutMs }) => + execFileAsync('gcloud', args, { maxBuffer: 64 * 1024 * 1024, timeout: timeoutMs, killSignal: 'SIGKILL' }) + try { + if (command === 'alarm') { + await raiseAlarm( + { mode: values.mode, cellId: values['cell-id'] ?? '', projectId: values['project-id'], kind: values.kind, reason: values.verdict ?? '' }, + { runGcloud } + ) + // The alarm step is red, whatever the page did. + process.exitCode = 1 + } else if (command === 'outcome') { + if (!values['state-file'] || !values['github-output']) throw new Error('--state-file and --github-output are required') + console.log(JSON.stringify({ event: 'orca_relay_reserve_watch_outcome', ...(await recordOutcome(values['state-file'], values['github-output'])) })) + } else if (!idToken) { + throw new Error('ORCA_RELAY_ADMIN_ID_TOKEN is required') + } else if (command === 'watch') { + const config = await parseWatchSegment(values) + const state = await watchSegment(config, { + idToken, + readRolloutLease: readProductionRolloutLease, + runGcloud + }) + // A cell that was never reserve is the operator's mistake; one flipped back by hand is not. + if (state.state === 'refused') { + console.log(JSON.stringify({ event: 'orca_relay_reserve_watch_refused', verdict: state.verdict })) + process.exitCode = 1 + } + } else if (command === 'flip-back') { + const accessToken = process.env.ORCA_RELAY_CONTROL_ACCESS_TOKEN + if (!accessToken) throw new Error('ORCA_RELAY_CONTROL_ACCESS_TOKEN is required') + if (!values.verdict) throw new Error('--verdict is required') + await flipBack(values, { + accessToken, + idToken, + audit: { + writtenBy: `reserve-watch/${process.env.GITHUB_TRIGGERING_ACTOR ?? process.env.GITHUB_ACTOR ?? 'unknown'}`, + runId: process.env.GITHUB_RUN_ID ?? 'local', + runAttempt: process.env.GITHUB_RUN_ATTEMPT ?? '0' + } + }) + // A trip is a failed watch even once the cell is safely back on db. + console.log(JSON.stringify({ event: 'orca_relay_reserve_watch_verdict', mode: values.mode, verdict: values.verdict })) + process.exitCode = 1 + } else { + throw new Error('command must be watch, outcome, flip-back or alarm') + } + } catch (error) { + console.log( + JSON.stringify({ + event: 'orca_relay_reserve_watch_failed', + message: error instanceof Error ? error.message : String(error) + }) + ) + process.exitCode = 1 + } +} diff --git a/cloud/dev/scripts/watch-relay-reserve-cell.test.mjs b/cloud/dev/scripts/watch-relay-reserve-cell.test.mjs new file mode 100644 index 00000000000..28d34ab4723 --- /dev/null +++ b/cloud/dev/scripts/watch-relay-reserve-cell.test.mjs @@ -0,0 +1,958 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { test } from 'node:test' +import { + evaluateWatchSample, + initialWatchState, + measuredBackground +} from './relay-reserve-watch-rules.mjs' +import { + ALARM_EVENT, + ALARM_LOG_NAME, + FLIP_BACK_ATTEMPTS, + FLIP_BACK_RUNTIME_ATTEMPTS, + flipBack, + parseWatchArguments, + parseWatchSegment, + raiseAlarm, + readDirector503Minutes, + readSeatBehindRowSample, + recordOutcome, + rolloutLeaseRefusal, + watchSegment +} from './watch-relay-reserve-cell.mjs' +import { readRelayWorkflow } from './relay-repository.mjs' + +const CELL = 'production-gce-c26' +const START = Date.parse('2026-10-14T12:00:00Z') +const MINUTE = 60_000 +const LIMIT = { unverifiableLimit: 3 } + +const healthyCell = (overrides = {}) => ({ + ok: true, + admitModeConfigured: 'reserve', + admitModeEffective: 'reserve', + draining: false, + controls: 1_000, + enforcedUnits: 1_100, + hardCap: 2_000, + ...overrides +}) +const minutesBefore = (at, count) => + Array.from({ length: count }, (_, index) => + new Date(Math.floor(at / MINUTE) * MINUTE - (count + 3 - index) * MINUTE).toISOString().slice(0, 16) + ) +const quiet503 = (at, perMinute = 10) => { + const minutes = minutesBefore(at, 3) + return { ok: true, minutes, perMinute: Object.fromEntries(minutes.map((minute) => [minute, perMinute])), publishedThrough: minutes.at(-1) } +} +const sampleAt = (at, overrides = {}) => ({ + at, + cell: healthyCell(), + director503: quiet503(at), + ledger: { ok: true, unwrittenRows: 0, directors: 5 }, + seatBehindRow: { ok: true, reports: [] }, + ...overrides +}) +const freshState = (overrides = {}) => ({ + ...initialWatchState({ cellId: CELL, startedAt: START, durationMinutes: 60, background503PerMinute: 10 }), + ...overrides +}) + +// Runs samples one minute apart; returns every result. +function run(samples, state = freshState()) { + const results = [] + for (const sample of samples) { + const result = evaluateWatchSample(state, sample, LIMIT) + results.push(result) + state = result.state + if (state.state !== 'watching') break + } + return results +} + +test('a healthy cell keeps watching and completes at the deadline', () => { + const results = run(Array.from({ length: 61 }, (_, minute) => sampleAt(START + minute * MINUTE))) + assert.equal(results.length, 61) + assert.ok(results.slice(0, 60).every((result) => result.state.state === 'watching')) + assert.equal(results.at(-1).state.state, 'completed') + const line = JSON.parse(results[0].line) + assert.equal(line.event, 'orca_relay_reserve_watch_sample') + assert.deepEqual(Object.keys(line.rules), ['deadMan', 'overrun', 'director503', 'duplicateSeat', 'hostDrop', 'ledgerDrift']) +}) + +test('503s trip only after three complete minutes over max(1.5x background, background + 20)', () => { + // Background 100: the line is 150. Two minutes over and one under is not a trip. + const state = freshState({ background: { medianPerMinute: 100, source: 'input' } }) + const series = (counts) => { + const minutes = minutesBefore(START, 3) + return { + ok: true, + minutes, + perMinute: Object.fromEntries(minutes.map((minute, index) => [minute, counts[index]])), + publishedThrough: minutes.at(-1) + } + } + const twoOver = evaluateWatchSample(state, sampleAt(START, { director503: series([151, 140, 200]) }), LIMIT) + assert.equal(twoOver.state.state, 'watching') + const atLine = evaluateWatchSample(state, sampleAt(START, { director503: series([150, 150, 150]) }), LIMIT) + assert.equal(atLine.state.state, 'watching') + const threeOver = evaluateWatchSample(state, sampleAt(START, { director503: series([151, 160, 170]) }), LIMIT) + assert.equal(threeOver.state.state, 'tripped') + assert.match(threeOver.state.verdict, /^flip back production-gce-c26: director503: /) + // A zero background still needs 20 a minute, not one 503. + const zero = freshState({ background: { medianPerMinute: 0, source: 'input' } }) + assert.equal(evaluateWatchSample(zero, sampleAt(START, { director503: series([5, 5, 5]) }), LIMIT).state.state, 'watching') + assert.equal(evaluateWatchSample(zero, sampleAt(START, { director503: series([21, 21, 21]) }), LIMIT).state.state, 'tripped') + // A published minute with no 503 point had none. + const gap = series([151, 160, 170]) + delete gap.perMinute[gap.minutes[1]] + assert.equal(evaluateWatchSample(state, sampleAt(START, { director503: gap }), LIMIT).state.state, 'watching') +}) + +test('a quiet fleet with no 503 series at all reads zero, not unverifiable', () => { + const minutes = minutesBefore(START, 10) + const quiet = { ok: true, minutes, perMinute: {}, publishedThrough: minutes.at(-1) } + const [result] = run([sampleAt(START, { director503: quiet })], freshState({ background: { medianPerMinute: 0, source: 'measured' } })) + assert.equal(result.rules.director503.status, 'pass') + assert.deepEqual(Object.values(result.rules.director503.recent), [0, 0, 0]) + assert.equal(result.state.unverifiableStreak, 0) + assert.deepEqual(measuredBackground({}, minutes, minutes.at(-1)), { medianPerMinute: 0, minutes: 10, source: 'measured' }) +}) + +test('503 minutes past the publishing front are left out, and only no front at all is unverifiable', () => { + const state = freshState({ background: { medianPerMinute: 100, source: 'input' } }) + const minutes = minutesBefore(START, 10) + const judge = (director503) => evaluateWatchSample(state, sampleAt(START, { director503 }), LIMIT) + // The front is two minutes behind: the three minutes before it are judged, the two after are not. + const over = Object.fromEntries(minutes.map((minute) => [minute, 500])) + const behind = judge({ ok: true, minutes, perMinute: over, publishedThrough: minutes[7] }) + assert.equal(behind.rules.director503.status, 'trip') + assert.deepEqual(Object.keys(behind.rules.director503.recent), minutes.slice(5, 8)) + // A sparse 503 series: minutes after its last point but within the front are zero, not late. + const sparse = judge({ ok: true, minutes, perMinute: { [minutes[2]]: 500 }, publishedThrough: minutes.at(-1) }) + assert.equal(sparse.rules.director503.status, 'pass') + assert.equal(sparse.state.unverifiableStreak, 0) + // No request series at all. + const empty = judge({ ok: true, minutes, perMinute: {}, publishedThrough: null }) + assert.equal(empty.rules.director503.status, 'unverifiable') + assert.match(empty.rules.director503.reason, /request series is empty/) + assert.equal(empty.state.unverifiableStreak, 1) + // A front too far behind to leave three published minutes in the read. + const stale = judge({ ok: true, minutes, perMinute: {}, publishedThrough: minutes[1] }) + assert.equal(stale.rules.director503.status, 'unverifiable') + assert.match(stale.rules.director503.reason, new RegExp(`published only through ${minutes[1]}`)) +}) + +const SEAT_EVENT = 'orca_relay_reserve_ledger_seat_behind_row' +const SUMMARY_EVENT = 'orca_relay_reserve_ledger_summary' +const summaryLine = (at, instanceId = 'a') => ({ + timestamp: new Date(at).toISOString(), + labels: { instanceId }, + jsonPayload: { event: SUMMARY_EVENT, pending: 0, dropped: 0 } +}) + +// A Cloud Monitoring or Cloud Logging reader over fakes, as the watch builds one. +const fakeReader = ({ fetch: fetchImpl = async () => new Response('{}'), runGcloud = async () => ({ stdout: 'token' }) } = {}) => ({ + fetch: fetchImpl, + runGcloud, + retryDelayMs: 0, + readTimeoutMs: 1_000, + now: () => START, + deadlineAt: START + 10_000, + projectId: 'onorca-cloud' +}) + +test('an unparseable timestamp makes the read unverifiable', async () => { + const badPoint = { + timeSeries: [{ points: [{ interval: { endTime: 'not a time' }, value: { int64Value: '3' } }] }] + } + const read503 = await readDirector503Minutes( + fakeReader({ fetch: async () => new Response(JSON.stringify(badPoint)) }), + { projectId: 'onorca-cloud', directorService: 'orca-cloud-relay', endedAt: START, minutes: 3 } + ) + assert.equal(read503.ok, false) + assert.match(read503.error, /unreadable/) + const seats = await readSeatBehindRowSample( + fakeReader({ runGcloud: async () => ({ stdout: JSON.stringify([summaryLine(START), { timestamp: 'garbage', jsonPayload: { epoch: 1, rowEpoch: 2 } }]) }) }), + { directorService: 'orca-cloud-relay', cellId: CELL, at: START } + ) + assert.equal(seats.ok, false) + assert.match(seats.error, /unparseable timestamp/) +}) + +test('a seat-behind-row key carries the director instance and cell beside the epochs', async () => { + const entry = (instanceId) => ({ timestamp: new Date(START).toISOString(), labels: { instanceId }, jsonPayload: { event: SEAT_EVENT, epoch: 7, rowEpoch: 8 } }) + const read = await readSeatBehindRowSample( + fakeReader({ runGcloud: async () => ({ stdout: JSON.stringify([entry('a'), summaryLine(START), entry('b')]) }) }), + { directorService: 'orca-cloud-relay', cellId: CELL, at: START } + ) + assert.deepEqual(read.reports.map((report) => report.key), [`a/${CELL}/7:8`, `b/${CELL}/7:8`]) +}) + +test('the duplicate read carries a positive control: no fresh ledger summary in it is unverifiable, not zero', async () => { + const filters = [] + const readWith = (entries) => + readSeatBehindRowSample( + fakeReader({ runGcloud: async (args) => (filters.push(args[2]), { stdout: JSON.stringify(entries) }) }), + { directorService: 'orca-cloud-relay', cellId: CELL, at: START } + ) + // One read, one window: the control rides the same filter as the seat lines. + assert.deepEqual(await readWith([summaryLine(START - 30_000)]), { ok: true, reports: [] }) + assert.match(filters[0], new RegExp(`\\(\\(jsonPayload\\.event="${SEAT_EVENT}" AND jsonPayload\\.cellId="${CELL}"\\) OR jsonPayload\\.event="${SUMMARY_EVENT}"\\)`)) + assert.match(filters[0], /resource\.labels\.service_name="orca-cloud-relay"/) + for (const entries of [[], [summaryLine(START - 10 * MINUTE)]]) { + const blind = await readWith(entries) + assert.equal(blind.ok, false) + assert.match(blind.error, /no fresh ledger summary \(positive control\)/) + } + // Blind is a gap in the sample, which counts toward the unverifiable streak. + const [gap] = run([sampleAt(START, { seatBehindRow: await readWith([]) })]) + assert.equal(gap.rules.duplicateSeat.status, 'unverifiable') + assert.equal(gap.state.unverifiableStreak, 1) +}) + +test('the measured background is the median of the minutes before the flip, published gaps as zero', () => { + const minutes = ['2026-10-14T11:47', '2026-10-14T11:48', '2026-10-14T11:49'] + const front = minutes[2] + assert.equal(measuredBackground({ '2026-10-14T11:48': 30 }, minutes, front).medianPerMinute, 0) + assert.equal(measuredBackground({ [minutes[0]]: 10, [minutes[1]]: 30, [minutes[2]]: 20 }, minutes, front).medianPerMinute, 20) + assert.equal(measuredBackground({}, minutes, '2026-10-14T11:52').medianPerMinute, 0) + // Not published through the last minute yet, or no request series: no background yet. + assert.equal(measuredBackground({ [minutes[0]]: 10 }, minutes, minutes[1]), null) + assert.equal(measuredBackground({}, minutes, null), null) + assert.equal(measuredBackground({}, [], front), null) +}) + +test('any overrun trips at once', () => { + const [result] = run([sampleAt(START, { cell: healthyCell({ enforcedUnits: 2_001 }) })]) + assert.equal(result.state.state, 'tripped') + assert.match(result.state.verdict, /overrun: ledger units 2001 over hard cap 2000/) + const [atCap] = run([sampleAt(START, { cell: healthyCell({ enforcedUnits: 2_000 }) })]) + assert.equal(atCap.state.state, 'watching') +}) + +test('a seat reported behind its row again past the grace trips; inside the grace it does not', () => { + const report = (at) => ({ at, key: '7:8' }) + const within = run([ + sampleAt(START, { seatBehindRow: { ok: true, reports: [report(START - 30_000)] } }), + sampleAt(START + MINUTE, { seatBehindRow: { ok: true, reports: [report(START - 30_000), report(START + 50_000)] } }) + ]) + assert.equal(within.at(-1).state.state, 'watching') + // Carried across samples: the second read no longer holds the first report. + const past = run([ + sampleAt(START, { seatBehindRow: { ok: true, reports: [report(START - 30_000)] } }), + sampleAt(START + 2 * MINUTE, { seatBehindRow: { ok: true, reports: [report(START + 70_000)] } }) + ]) + assert.equal(past.at(-1).state.state, 'tripped') + assert.match(past.at(-1).state.verdict, /duplicateSeat: a seat behind its ledger row \(7:8\)/) + // Another seat's report is not the same seat. + const other = run([ + sampleAt(START, { seatBehindRow: { ok: true, reports: [report(START - 30_000)] } }), + sampleAt(START + 2 * MINUTE, { seatBehindRow: { ok: true, reports: [{ at: START + 70_000, key: '3:4' }] } }) + ]) + assert.equal(other.at(-1).state.state, 'watching') +}) + +test('a host drop over 5% in 5 minutes trips outside a drain only', () => { + const controls = [1_000, 990, 980, 960, 949] + const samples = (draining) => + controls.map((count, minute) => + sampleAt(START + minute * MINUTE, { cell: healthyCell({ controls: count, draining: draining && minute === 2 }) }) + ) + assert.equal(run(samples(false)).at(-1).state.state, 'tripped') + assert.match(run(samples(false)).at(-1).state.verdict, /hostDrop: connected hosts fell 1000 -> 949/) + assert.equal(run(samples(true)).at(-1).state.state, 'watching') + // A drop spread over more than 5 minutes is not one. + const slow = [1_000, 990, 980, 970, 960, 955, 950].map((count, minute) => + sampleAt(START + minute * MINUTE, { cell: healthyCell({ controls: count }) }) + ) + assert.equal(run(slow).at(-1).state.state, 'watching') + // Under the floor on a nearly empty cell: 100 -> 92 is 8%, but only 8 hosts. + const small = [100, 92].map((count, minute) => sampleAt(START + minute * MINUTE, { cell: healthyCell({ controls: count }) })) + assert.equal(run(small).at(-1).state.state, 'watching') +}) + +test('ledger drift over 0.5% trips once it has lasted 2 minutes', () => { + const drift = (rows) => ({ ok: true, unwrittenRows: rows, directors: 5 }) + const brief = run([6, 6, 0, 6].map((rows, minute) => sampleAt(START + minute * MINUTE, { ledger: drift(rows) }))) + assert.equal(brief.at(-1).state.state, 'watching') + const sustained = run([6, 6, 6].map((rows, minute) => sampleAt(START + minute * MINUTE, { ledger: drift(rows) }))) + assert.equal(sustained.length, 3) + assert.equal(sustained.at(-1).state.state, 'tripped') + assert.match(sustained.at(-1).state.verdict, /ledgerDrift: ledger drift 0\.60% for 120 s/) + const atLine = run([5, 5, 5, 5].map((rows, minute) => sampleAt(START + minute * MINUTE, { ledger: drift(rows) }))) + assert.equal(atLine.at(-1).state.state, 'watching') +}) + +test('a tripped dead-man is a trip: Postgres still records reserve until a db write', () => { + const [result] = run([sampleAt(START, { cell: healthyCell({ admitModeEffective: 'db' }) })]) + assert.equal(result.state.state, 'tripped') + assert.match(result.state.verdict, /deadMan: the cell tripped its dead-man/) +}) + +test('unreadable data is never healthy: N samples in a row with any gap trip', () => { + const unreadable = { ok: false, error: 'cell runtime status returned 502' } + const gaps = [ + { cell: unreadable }, + { ledger: { ok: false, error: 'no placing director logged a ledger summary' } }, + { director503: { ok: false, error: 'director 503 metric read failed', perMinute: {}, minutes: [], publishedThrough: null } } + ] + const results = run(gaps.map((gap, minute) => sampleAt(START + minute * MINUTE, gap))) + assert.deepEqual(results.map((result) => result.state.unverifiableStreak), [1, 2, 3]) + assert.equal(results.at(-1).state.state, 'tripped') + assert.match(results.at(-1).state.verdict, /unverifiable: 3 samples in a row could not read director503/) + // One whole sample resets the streak. + const reset = run( + [gaps[0], {}, gaps[0], gaps[0]].map((gap, minute) => sampleAt(START + minute * MINUTE, gap)) + ) + assert.deepEqual(reset.map((result) => result.state.unverifiableStreak), [1, 0, 1, 2]) + assert.equal(reset.at(-1).state.state, 'watching') + // No background yet is a gap too. + const noBackground = freshState({ background: null }) + assert.equal(evaluateWatchSample(noBackground, sampleAt(START), LIMIT).state.unverifiableStreak, 1) + // A measured trip still wins over a gap elsewhere. + const [both] = run([sampleAt(START, { cell: healthyCell({ enforcedUnits: 2_500 }), ledger: gaps[1].ledger })]) + assert.equal(both.state.state, 'tripped') + assert.match(both.state.verdict, /overrun/) +}) + +test('a cell no longer in reserve stops the watch without a write; one never in reserve is refused', () => { + const dbCell = healthyCell({ admitModeConfigured: 'db', admitModeEffective: 'reserve' }) + const later = run([sampleAt(START), sampleAt(START + MINUTE, { cell: dbCell })]) + assert.equal(later.at(-1).state.state, 'stopped') + assert.match(later.at(-1).state.verdict, /is not in reserve mode \(switch file says db\)/) + // Never reserve within the wait: refused, which the workflow does not flip back. + const never = run(Array.from({ length: 12 }, (_, minute) => sampleAt(START + minute * MINUTE, { cell: dbCell }))) + assert.equal(never.at(-1).state.state, 'refused') + assert.equal(never.length, 11) + assert.match(never.at(-1).state.verdict, /did not report reserve mode within 10 min \(switch file says db\)/) +}) + +test('a watch dispatched before the reserve write applied waits for it, then judges as usual', () => { + const dbCell = healthyCell({ admitModeConfigured: 'db', admitModeEffective: 'db' }) + const results = run([ + sampleAt(START, { cell: dbCell, ledger: { ok: false, error: 'not yet' } }), + sampleAt(START + MINUTE, { cell: dbCell }), + sampleAt(START + 2 * MINUTE), + sampleAt(START + 3 * MINUTE, { cell: healthyCell({ enforcedUnits: 2_500 }) }) + ]) + // Waiting samples judge nothing: not a dead-man trip on a db cell, not a gap. + assert.deepEqual(results.slice(0, 2).map((result) => result.state.state), ['watching', 'watching']) + assert.equal(results[0].rules.reserve.status, 'pending') + assert.deepEqual(Object.keys(results[0].rules), ['reserve']) + assert.deepEqual(results.map((result) => result.state.unverifiableStreak), [0, 0, 0, 0]) + assert.equal(results[2].state.reserveSeenAt, START + 2 * MINUTE) + assert.equal(results[2].rules.overrun.status, 'pass') + assert.equal(results[3].state.state, 'tripped') + // Once seen in reserve, a later db reads as a hand flip back, not a refusal. + const after = run([sampleAt(START + 2 * MINUTE), sampleAt(START + 3 * MINUTE, { cell: dbCell })], results[0].state) + assert.equal(after.at(-1).state.state, 'stopped') +}) + +const watchValues = (overrides = {}) => ({ + mode: 'dry-run', + 'cell-id': CELL, + 'cell-origin': 'https://c26.relay.onorca.dev', + 'project-id': 'onorca-cloud', + 'director-origin': 'https://relay.onorca.dev', + 'director-service': 'orca-cloud-relay', + 'duration-minutes': '120', + 'segment-minutes': '55', + 'unverifiable-limit': '3', + 'background-503-per-minute': '', + 'flipped-at': new Date(START - 5 * MINUTE).toISOString(), + 'state-file': '/unused', + ...overrides +}) +const parseAt = (values) => parseWatchArguments(values, START) + +test('watch arguments reuse the flag tool checks, so a write watch needs its confirmation up front', () => { + assert.equal(parseAt(watchValues()).background503PerMinute, null) + assert.equal(parseAt(watchValues()).flippedAt, START - 5 * MINUTE) + assert.equal(parseAt(watchValues({ 'background-503-per-minute': '12.5', 'flipped-at': '' })).background503PerMinute, 12.5) + assert.throws(() => parseAt(watchValues({ mode: 'write' })), /WRITE_RELAY_CELL_FLAGS production-gce-c26/) + assert.doesNotThrow(() => parseAt(watchValues({ mode: 'write', confirmation: `WRITE_RELAY_CELL_FLAGS ${CELL}` }))) + assert.throws(() => parseAt(watchValues({ 'cell-origin': 'https://c27.relay.onorca.dev' })), /names another cell/) + assert.throws(() => parseAt(watchValues({ 'duration-minutes': '301' })), /duration-minutes/) + assert.throws(() => parseAt(watchValues({ 'segment-minutes': '56' })), /segment-minutes/) + assert.throws(() => parseAt(watchValues({ 'background-503-per-minute': '-1' })), /background/) +}) + +test('the 503 background needs flipped-at or an explicit value; flipped-at must be a recent zoned time', () => { + assert.throws(() => parseAt(watchValues({ 'flipped-at': '' })), /--flipped-at .* or --background-503-per-minute is required/) + assert.throws(() => parseAt(watchValues({ 'flipped-at': '2026-10-14T11:55:00' })), /ISO time with a zone/) + assert.throws(() => parseAt(watchValues({ 'flipped-at': '2026-10-14T12:01:00Z' })), /in the future/) + assert.throws(() => parseAt(watchValues({ 'flipped-at': '2026-10-13T11:00:00Z' })), /more than 24 hours/) + assert.equal(parseAt(watchValues({ 'flipped-at': '2026-10-14T07:55:00-04:00' })).flippedAt, START - 5 * MINUTE) +}) + +// A cell, Cloud Monitoring and Cloud Logging, answering from `script` per sample. +function fakeSources({ + cell = () => ({}), + // Logged at each sample's minute, so it stays fresh for the positive control. + ledger = (sample) => [{ timestamp: new Date(START + (sample - 1) * MINUTE).toISOString(), labels: { instanceId: 'a' }, jsonPayload: { pending: 0, dropped: 0 } }], + per503 = () => 10, + // Requests of every other code per minute; zero is a minute with no traffic at all. + perOther = () => 1_000, + seatControl = true, + rolloutLease = null +} = {}) { + let samples = 0 + const monitoringSpans = [] + const fetchImpl = async (url) => { + const text = String(url) + if (text.endsWith('/v1/admin/runtime-status')) { + samples += 1 + const overrides = cell(samples) + return new Response( + JSON.stringify({ + role: 'cell', + cellId: CELL, + draining: false, + flagsApplied: { generation: 4, flags: { admitMode: 'reserve', readinessLocal: true } }, + admitModeEffective: 'reserve', + connectionCapacity: { hardCap: 2_000 }, + runtime: { controls: 1_000, enforcedConnectionUnits: 1_100 }, + ...overrides + }) + ) + } + if (text.startsWith('https://monitoring.googleapis.com/')) { + const params = new URL(text).searchParams + const span = { startedAt: Date.parse(params.get('interval.startTime')), endedAt: Date.parse(params.get('interval.endTime')) } + monitoringSpans.push(span) + assert.equal(params.get('aggregation.groupByFields'), 'metric.label."response_code"') + assert.doesNotMatch(params.get('filter'), /response_code/) + // Cloud Monitoring returns no point for a minute with no requests of that code. + const seriesOf = (code, count) => { + const points = [] + for (let minute = span.startedAt; minute < span.endedAt; minute += MINUTE) { + if (count(minute) > 0) { + points.push({ interval: { endTime: new Date(minute + MINUTE).toISOString() }, value: { int64Value: String(count(minute)) } }) + } + } + return points.length === 0 ? [] : [{ metric: { labels: { response_code: code } }, points }] + } + return new Response(JSON.stringify({ timeSeries: [...seriesOf('200', perOther), ...seriesOf('503', per503)] })) + } + throw new Error(`unexpected fetch ${text}`) + } + const runGcloud = async (args) => { + if (args[0] === 'auth') return { stdout: 'token' } + const filter = args[2] + const summaries = ledger(samples).map((entry) => ({ ...entry, jsonPayload: { ...entry.jsonPayload, event: SUMMARY_EVENT } })) + if (filter.includes(SEAT_EVENT)) return { stdout: JSON.stringify(seatControl ? summaries : []) } + if (filter.includes(SUMMARY_EVENT)) return { stdout: JSON.stringify(ledger(samples)) } + return { stdout: '[]' } + } + return { fetchImpl, runGcloud, monitoringSpans, readRolloutLease: async () => rolloutLease } +} + +test('a segment samples once a minute, carries state to the next segment, and stops on a trip', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + let clock = START + const lines = [] + const config = parseAt( + watchValues({ 'state-file': join(directory, 'state.json'), 'github-output': join(directory, 'out'), 'segment-minutes': '3' }) + ) + const sources = fakeSources({ cell: (sample) => (sample === 5 ? { runtime: { controls: 1_000, enforcedConnectionUnits: 2_001 } } : {}) }) + const dependencies = { + ...sources, + idToken: 'id', + retryDelayMs: 0, + now: () => clock, + sleep: async (ms) => { clock += ms }, + log: (line) => lines.push(JSON.parse(line)) + } + const first = await watchSegment(config, dependencies) + assert.equal(first.state, 'watching') + assert.equal(lines.length, 3) + assert.equal(first.background.source, 'measured') + assert.equal(await readFile(join(directory, 'out'), 'utf8'), 'state=watching\nverdict=\n') + const second = await watchSegment(config, dependencies) + assert.equal(second.state, 'tripped') + assert.equal(second.samples, 5) + assert.deepEqual(lines.map((line) => line.sample), [1, 2, 3, 4, 5]) + assert.equal(lines.at(-1).rules.overrun.status, 'trip') + assert.match(await readFile(join(directory, 'out'), 'utf8'), /state=tripped\nverdict=flip back production-gce-c26: overrun: /) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('a sample that cannot read its sources counts as unverifiable rather than crashing the watch', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + let clock = START + const config = parseAt(watchValues({ 'state-file': join(directory, 'state.json'), 'segment-minutes': '10' })) + const sources = fakeSources({ ledger: () => [] }) + const state = await watchSegment(config, { + ...sources, + idToken: 'id', + retryDelayMs: 0, + now: () => clock, + sleep: async (ms) => { clock += ms }, + log: () => {} + }) + assert.equal(state.state, 'tripped') + assert.equal(state.samples, 3) + // No summary also blinds the duplicate rule, whose positive control it is. + assert.match(state.verdict, /unverifiable: 3 samples in a row could not read duplicateSeat, ledgerDrift/) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('a watch that ended without a verdict records failed, which the workflow flips back', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + const output = join(directory, 'out') + assert.equal((await recordOutcome(join(directory, 'missing.json'), output)).state, 'failed') + await writeFile(join(directory, 'watching.json'), JSON.stringify(freshState())) + assert.equal((await recordOutcome(join(directory, 'watching.json'), output)).state, 'failed') + await writeFile(join(directory, 'tripped.json'), JSON.stringify(freshState({ state: 'tripped', verdict: 'flip back x:\ny' }))) + assert.equal((await recordOutcome(join(directory, 'tripped.json'), output)).state, 'tripped') + assert.match(await readFile(output, 'utf8'), /state=tripped\nverdict=flip back x: y\n$/) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +// Google Storage and the director, as far as the flag tool's flip back reaches them. `onUpload` +// returns 'conflict' to play another writer landing first, optionally with its own admitMode. +function fakeFlipBack({ generation = '7', onUpload = () => 'ok', cellAnswers = true } = {}) { + const calls = [] + let uploads = 0 + let object = { generation: Number(generation), flags: { admitMode: 'reserve', readinessLocal: true } } + let applied = object + const fetchImpl = async (url, init = {}) => { + const text = String(url) + calls.push({ url: text, method: init.method ?? 'GET', body: init.body }) + if (text.includes('/upload/storage/v1/')) { + uploads += 1 + assert.match(text, new RegExp(`ifGenerationMatch=${object.generation}$`)) + const outcome = onUpload(uploads) + if (outcome !== 'ok') { + object = { generation: object.generation + 1, flags: { ...object.flags, admitMode: outcome.admitMode ?? 'reserve' } } + applied = object + return new Response('', { status: 412 }) + } + object = { generation: object.generation + 1, flags: JSON.parse(init.body.split('\r\n')[7]).flags } + applied = object + return new Response(JSON.stringify({ generation: String(object.generation) })) + } + if (text.includes('?alt=media')) { + return new Response(JSON.stringify({ v: 1, cellId: CELL, flags: object.flags })) + } + if (text.includes('/storage/v1/b/')) return new Response(JSON.stringify({ generation: String(object.generation) })) + if (text.endsWith('/v1/admin/runtime-status')) { + if (!cellAnswers) return new Response('', { status: 502 }) + return new Response( + JSON.stringify({ + role: 'cell', + cellId: CELL, + flagsApplied: applied, + supportedFlags: { + admitMode: { type: 'enum', values: ['db', 'reserve'] }, + readinessLocal: { type: 'boolean' } + }, + admitModeEffective: applied.flags.admitMode, + runtime: { controls: 10 } + }) + ) + } + if (text.endsWith('/v1/admin/cell-admit-mode')) { + const admitMode = JSON.parse(init.body).admitMode + return new Response(JSON.stringify({ v: 1, cellId: CELL, admitMode: admitMode ?? 'reserve', heartbeatFresh: true })) + } + throw new Error(`unexpected fetch ${text}`) + } + return { fetchImpl, calls, uploads: () => uploads } +} + +const flipDependencies = (fetchImpl, lines, sleeps = []) => { + let clock = START + return { + fetchImpl, + accessToken: 'access', + idToken: 'id', + audit: { writtenBy: 'reserve-watch/test', runId: '1', runAttempt: '1' }, + now: () => clock, + sleep: async (ms) => { sleeps.push(ms); clock += ms }, + log: (line) => lines.push(JSON.parse(line)) + } +} + +const flipValues = (overrides = {}) => ({ + mode: 'write', + 'cell-id': CELL, + 'cell-origin': 'https://c26.relay.onorca.dev', + 'project-id': 'onorca-cloud', + 'director-origin': 'https://relay.onorca.dev', + confirmation: `WRITE_RELAY_CELL_FLAGS ${CELL}`, + ...overrides +}) + +test('the flip back is the flag tool write: admitMode=db at the generation just read, then Postgres db', async () => { + const { fetchImpl, calls } = fakeFlipBack() + const lines = [] + const result = await flipBack(flipValues(), flipDependencies(fetchImpl, lines)) + assert.equal(result.written, true) + const upload = calls.find((call) => call.url.includes('/upload/storage/v1/')) + assert.match(upload.body, /"flags":\{"admitMode":"db","readinessLocal":true\}/) + assert.ok(calls.some((call) => call.url.endsWith('/v1/admin/cell-admit-mode') && JSON.parse(call.body).admitMode === 'db')) + assert.deepEqual(lines.at(-1), { + event: 'orca_relay_reserve_watch_flip_back', + mode: 'write', + cellId: CELL, + written: true, + generation: '8' + }) +}) + +test('a dry-run flip back plans the write and writes nothing', async () => { + const { fetchImpl, calls } = fakeFlipBack() + const lines = [] + const result = await flipBack(flipValues({ mode: 'dry-run', confirmation: '' }), { + fetchImpl, + accessToken: 'access', + idToken: 'id', + audit: {}, + sleep: async () => {}, + log: (line) => lines.push(JSON.parse(line)) + }) + assert.equal(result.written, false) + assert.ok(!calls.some((call) => call.url.includes('/upload/'))) + assert.ok(!calls.some((call) => call.url.endsWith('/v1/admin/cell-admit-mode') && JSON.parse(call.body).admitMode)) + assert.equal(lines.find((line) => line.event === 'orca_relay_cell_flags_plan').desired.flags.admitMode, 'db') +}) + +test('a write flip back without the typed confirmation is refused before any read', async () => { + const { fetchImpl, calls } = fakeFlipBack() + await assert.rejects( + flipBack(flipValues({ confirmation: '' }), { fetchImpl, accessToken: 'a', idToken: 'i', audit: {}, log: () => {} }), + /WRITE_RELAY_CELL_FLAGS production-gce-c26/ + ) + assert.equal(calls.length, 0) +}) + +test('a generation conflict is re-read and retried while the switch file still says reserve', async () => { + const { fetchImpl, uploads } = fakeFlipBack({ onUpload: (n) => (n < 3 ? { admitMode: 'reserve' } : 'ok') }) + const lines = [] + const sleeps = [] + const result = await flipBack(flipValues(), flipDependencies(fetchImpl, lines, sleeps)) + assert.equal(result.written, true) + assert.equal(uploads(), 3) + assert.deepEqual(lines.filter((line) => line.event === 'orca_relay_reserve_watch_flip_back_conflict').map((line) => line.attempt), [1, 2]) + assert.deepEqual(sleeps.slice(0, 1), [2_000]) + assert.equal(lines.at(-1).generation, '10') +}) + +test('a conflict whose writer already flipped the cell to db ends the flip back without a write', async () => { + const { fetchImpl, uploads } = fakeFlipBack({ onUpload: () => ({ admitMode: 'db' }) }) + const lines = [] + const result = await flipBack(flipValues(), flipDependencies(fetchImpl, lines)) + assert.equal(uploads(), 1) + assert.equal(result.written, false) + assert.equal(result.alreadyDb, true) + assert.deepEqual(lines.at(-1), { event: 'orca_relay_reserve_watch_flip_back', mode: 'write', cellId: CELL, written: false, generation: '8', alreadyDb: true }) +}) + +test('conflicts past the retry bound fail the flip back', async () => { + const { fetchImpl, uploads } = fakeFlipBack({ onUpload: () => ({ admitMode: 'reserve' }) }) + await assert.rejects(flipBack(flipValues(), flipDependencies(fetchImpl, [])), /object changed since expected-generation/) + assert.equal(uploads(), FLIP_BACK_ATTEMPTS) +}) + +test('an unreachable cell fails with the exact break-glass inputs and writes nothing', async () => { + const { fetchImpl, uploads } = fakeFlipBack({ cellAnswers: false }) + const lines = [] + await assert.rejects( + flipBack(flipValues(), flipDependencies(fetchImpl, lines)), + (error) => { + assert.match(error.message, /production-gce-c26 does not answer runtime-status \(cell runtime status returned 502\)/) + assert.match(error.message, /set=admitMode=default, break-glass=BREAK GLASS production-gce-c26, expected-generation=7/) + return true + } + ) + assert.equal(uploads(), 0) + assert.deepEqual(lines.at(-1).inputs, { + mode: 'write', + 'cell-id': CELL, + set: 'admitMode=default', + 'break-glass': `BREAK GLASS ${CELL}`, + 'expected-generation': '7', + confirmation: `WRITE_RELAY_CELL_FLAGS ${CELL}` + }) +}) + +test('the 503 background is the 10 minutes before flipped-at, read once they are published', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + let clock = START + // Flipped one minute ago: the minutes before it are not published until 3 minutes after it. + const flippedAt = START - MINUTE + 20_000 + const config = parseAt( + watchValues({ 'state-file': join(directory, 'state.json'), 'segment-minutes': '3', 'flipped-at': new Date(flippedAt).toISOString() }) + ) + const sources = fakeSources({ per503: (minute) => (minute < START - MINUTE ? 4 : 40) }) + const lines = [] + const state = await watchSegment(config, { + ...sources, + idToken: 'id', + retryDelayMs: 0, + now: () => clock, + sleep: async (ms) => { clock += ms }, + log: (line) => lines.push(JSON.parse(line)) + }) + // Pending until then, and pending is not a gap: the streak stays at zero. + assert.deepEqual(lines.map((line) => line.rules.director503.status), ['pending', 'pending', 'pass']) + assert.match(lines[0].rules.director503.reason, /background before the flip is not published yet/) + assert.deepEqual(lines.map((line) => line.unverifiableStreak), [0, 0, 0]) + const baseline = sources.monitoringSpans.find((span) => span.endedAt === START - MINUTE) + assert.equal(baseline.startedAt, START - 11 * MINUTE) + assert.deepEqual(state.background, { medianPerMinute: 4, minutes: 10, source: 'measured' }) + // An explicit value overrides the measurement. + const explicit = initialWatchState({ cellId: CELL, startedAt: START, durationMinutes: 60, background503PerMinute: 9, flippedAt }) + assert.deepEqual(explicit.background, { medianPerMinute: 9, source: 'input' }) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('a quiet fleet with zero director 503s watches on and never trips', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + let clock = START + const config = parseAt( + watchValues({ + 'state-file': join(directory, 'state.json'), + 'segment-minutes': '20', + 'flipped-at': new Date(START - 2 * MINUTE).toISOString() + }) + ) + const lines = [] + const state = await watchSegment(config, { + ...fakeSources({ per503: () => 0 }), + idToken: 'id', + retryDelayMs: 0, + now: () => clock, + sleep: async (ms) => { clock += ms }, + log: (line) => lines.push(JSON.parse(line)) + }) + assert.equal(state.state, 'watching') + assert.equal(lines.length, 20) + assert.deepEqual(state.background, { medianPerMinute: 0, minutes: 10, source: 'measured' }) + assert.ok(lines.slice(1).every((line) => line.rules.director503.status === 'pass')) + assert.ok(lines.every((line) => line.unverifiableStreak === 0)) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('no request series at all is unverifiable and trips at the limit', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + let clock = START + const config = parseAt(watchValues({ 'state-file': join(directory, 'state.json'), 'segment-minutes': '10', 'background-503-per-minute': '0', 'flipped-at': '' })) + const state = await watchSegment(config, { + ...fakeSources({ per503: () => 0, perOther: () => 0 }), + idToken: 'id', + retryDelayMs: 0, + now: () => clock, + sleep: async (ms) => { clock += ms }, + log: () => {} + }) + assert.equal(state.state, 'tripped') + assert.match(state.verdict, /unverifiable: 3 samples in a row could not read director503/) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('a later segment judges flipped-at against the watch start, so a long watch does not age it out', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + const stateFile = join(directory, 'state.json') + const values = watchValues({ 'state-file': stateFile, 'flipped-at': new Date(START - 23 * 60 * MINUTE).toISOString() }) + const later = START + 4 * 60 * MINUTE + await writeFile(stateFile, JSON.stringify(freshState())) + assert.equal((await parseWatchSegment(values, later)).flippedAt, START - 23 * 60 * MINUTE) + // The first segment (no state file yet) still refuses it, and records refused. + await rm(stateFile) + await assert.rejects(parseWatchSegment(values, later), /more than 24 hours/) + assert.equal(JSON.parse(await readFile(stateFile, 'utf8')).state, 'refused') + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('a corrupt state file fails the segment, never refuses or restarts it, and the outcome reads failed', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + const stateFile = join(directory, 'state.json') + const output = join(directory, 'out') + const values = watchValues({ 'state-file': stateFile }) + for (const corrupt of ['null', '[]', '7', JSON.stringify({ state: 'watching', cellId: 'c7' })]) { + await writeFile(stateFile, corrupt) + await assert.rejects(parseWatchSegment(values, START), /state file/) + assert.equal(await readFile(stateFile, 'utf8'), corrupt) + assert.equal((await recordOutcome(stateFile, output)).state, 'failed') + } + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('a slow runtime-status reply is retried before the flip back calls for break glass', async () => { + const { fetchImpl: answering, uploads } = fakeFlipBack() + let runtimeCalls = 0 + // fetchAdminOnceMore makes two tries per read; the first read's two both fail. + const fetchImpl = async (url, init) => { + if (String(url).endsWith('/v1/admin/runtime-status') && (runtimeCalls += 1) <= 2) return new Response('', { status: 503 }) + return answering(url, init) + } + const lines = [] + const sleeps = [] + const result = await flipBack(flipValues(), flipDependencies(fetchImpl, lines, sleeps)) + assert.equal(result.written, true) + assert.equal(uploads(), 1) + assert.deepEqual(lines.filter((line) => line.event === 'orca_relay_reserve_watch_runtime_retry').map((line) => line.attempt), [1]) + assert.ok(!lines.some((line) => line.event === 'orca_relay_reserve_watch_break_glass_required')) + assert.ok(sleeps.includes(2_000)) + assert.ok(FLIP_BACK_RUNTIME_ATTEMPTS > 1) +}) + +const watchWorkflow = readRelayWorkflow('operate-relay-production-reserve-watch.yml') +const jobOf = (name) => { + const start = watchWorkflow.indexOf(`\n ${name}:\n`) + assert.notEqual(start, -1, `job ${name} is missing`) + const next = watchWorkflow.slice(start + 1).search(/\n [a-z-]+:\n/) + return watchWorkflow.slice(start, next === -1 ? undefined : start + 1 + next) +} + +test('workflow: neither job takes the flag workflow group, which would let a newer run cancel a pending flip back', () => { + assert.doesNotMatch(watchWorkflow, /group: relay-production-cell-flags/) + assert.doesNotMatch(jobOf('flip-back'), /^\s+concurrency:/m) + assert.doesNotMatch(jobOf('watch'), /^\s+concurrency:/m) +}) + +test('workflow: a watch that failed or was cancelled without a verdict flips back too', () => { + const flip = jobOf('flip-back') + // A cancel re-evaluates each step: every flip-back step must run on always() too. + const steps = flip.slice(flip.indexOf('\n steps:\n')).split(/\n - /).slice(1) + assert.equal(steps.length, 7) + for (const step of steps) assert.match(step, /\n\s+if: \$\{\{ always\(\) \}\}/, step.split('\n')[0]) + assert.match(flip, /if: \$\{\{ always\(\) && vars\.ORCA_CLOUD_OPERATIONS_ENABLED == 'true' && needs\.watch\.result != 'skipped'/) + assert.match(flip, /!contains\(fromJSON\('\["completed","stopped","refused"\]'\), needs\.watch\.outputs\.state\)/) + // Every segment checks its ID token's shape, and runs the one shared command. + const segments = watchWorkflow.match(/run: bash -c "\$\{WATCH_SEGMENT\}"/g) ?? [] + assert.equal(segments.length, 6) + assert.match(jobOf('watch'), /process\.env\.ORCA_RELAY_ADMIN_ID_TOKEN/) +}) + +test('a live rollout lease refuses the watch before its first sample; an expired one does not', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + const stateFile = join(directory, 'state.json') + const output = join(directory, 'out') + const config = parseAt(watchValues({ 'state-file': stateFile, 'github-output': output, 'segment-minutes': '1' })) + const lease = (expiresAt) => ({ record: { expires_at: expiresAt }, holder: 'Deploy Relay Production Same-Cap (run 9)' }) + let clock = START + const dependencies = (rolloutLease) => ({ + ...fakeSources({ rolloutLease }), + idToken: 'id', + retryDelayMs: 0, + now: () => clock, + sleep: async (ms) => { clock += ms }, + log: () => {} + }) + const held = await watchSegment(config, dependencies(lease(START + MINUTE))) + assert.equal(held.state, 'refused') + assert.match(held.verdict, /a rollout holds the Cloud SQL rollout lease until .*Same-Cap \(run 9\)/) + assert.equal(JSON.parse(await readFile(stateFile, 'utf8')).state, 'refused') + assert.match(await readFile(output, 'utf8'), /^state=refused\n/) + await rm(stateFile) + assert.equal((await watchSegment(config, dependencies(lease(START - 1)))).state, 'watching') + // Only the first segment checks: a later one carries on from its state file. + assert.equal((await watchSegment(config, dependencies(lease(clock + MINUTE)))).state, 'watching') + // A lease it cannot read refuses too, as the lease action fails closed. + assert.match(await rolloutLeaseRefusal(async () => { throw new Error('403') }, START), /lease is unreadable \(403\)/) + assert.equal(await rolloutLeaseRefusal(async () => null, START), null) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +const stepOf = (job, name) => { + const text = jobOf(job) + const start = text.indexOf(`- name: ${name}\n`) + assert.notEqual(start, -1, `step ${name} is missing`) + const end = text.indexOf('\n - ', start + 1) + return text.slice(start, end === -1 ? undefined : end) +} +const ALARM_COMMAND = /node dev\/scripts\/watch-relay-reserve-cell\.mjs alarm --kind KIND \\\n\s+--mode "\$\{MODE\}" --cell-id "\$\{CELL_ID\}" --project-id "\$\{GCP_PROJECT_ID\}" --verdict "\$\{VERDICT_VAR\}"/ +const alarmCommand = (kind, verdict) => new RegExp(ALARM_COMMAND.source.replace('KIND', kind).replace('VERDICT_VAR', verdict)) + +test('workflow: a flip back that did not land and a refused watch both raise the alarm', () => { + const alarm = stepOf('flip-back', 'Alarm when the flip back did not land') + assert.match(alarm, /if: \$\{\{ always\(\) \}\}/) + assert.match(alarm, /grep -qs '"event":"orca_relay_reserve_watch_flip_back"' "\$\{RUNNER_TEMP\}\/relay-reserve-flip-back\.jsonl"/) + assert.match(alarm, alarmCommand('flip-back-failed', 'VERDICT')) + const refused = stepOf('watch', 'Alarm on a refused watch') + assert.match(refused, /if: \$\{\{ always\(\) && steps\.outcome\.outputs\.state == 'refused' \}\}/) + assert.match(refused, /WATCH_VERDICT: \$\{\{ steps\.outcome\.outputs\.verdict \}\}/) + assert.match(refused, alarmCommand('refused', 'WATCH_VERDICT')) + // After the outcome step, so it reads the recorded state. + assert.ok(jobOf('watch').indexOf('- name: Alarm on a refused watch') > jobOf('watch').indexOf('- id: outcome')) + // The flip-back line names only the write's own event, never its conflict retries. + assert.match(readFileSync(new URL('./watch-relay-reserve-cell.mjs', import.meta.url), 'utf8'), /event: 'orca_relay_reserve_watch_flip_back',\n/) +}) + +test('an alarm is an escaped error annotation everywhere, and in write mode a paging log line', async () => { + const calls = [] + const lines = [] + const runGcloud = async (args, options) => (calls.push({ args, options }), { stdout: '' }) + const alarm = (overrides) => + raiseAlarm( + { mode: 'write', cellId: CELL, projectId: 'onorca-cloud', kind: 'refused', reason: '50%\r\nlease held', ...overrides }, + { runGcloud, log: (line) => lines.push(line) } + ) + assert.deepEqual(await alarm(), { paged: true }) + assert.equal( + lines[0], + `::error title=Reserve watch refused for ${CELL}::${CELL} may be in reserve mode and unwatched: the watch was refused (50%25%0D%0Alease held). ` + + 'Rerun it, or flip the cell to db with Operate Relay Production Cell Flags.' + ) + assert.deepEqual(calls[0].args, [ + 'logging', 'write', ALARM_LOG_NAME, + JSON.stringify({ event: ALARM_EVENT, cellId: CELL, kind: 'refused', reason: '50%\r\nlease held' }), + '--payload-type=json', '--severity=ERROR', '--project', 'onorca-cloud' + ]) + // A dry run never flips back, so its alarm annotates but does not page. + assert.deepEqual(await alarm({ mode: 'dry-run', kind: 'flip-back-failed' }), { paged: false }) + assert.equal(calls.length, 1) + assert.match(lines[1], /^::error title=Reserve watch flip-back-failed for production-gce-c26::.*did not land/) + // A title property escapes its own separators too. + await alarm({ mode: 'dry-run', cellId: 'a:b,c' }) + assert.match(lines[2], /^::error title=Reserve watch refused for a%3Ab%2Cc::/) + // A failed log write is named, never swallowed silently. + const failed = await raiseAlarm( + { mode: 'write', cellId: CELL, projectId: 'onorca-cloud', kind: 'flip-back-failed', reason: 'x' }, + { runGcloud: async () => { throw new Error('PERMISSION_DENIED') }, log: (line) => lines.push(line) } + ) + assert.deepEqual(failed, { paged: false }) + assert.match(lines.at(-1), /^::error title=Reserve watch page failed::.*nothing pages: PERMISSION_DENIED/) + await assert.rejects(alarm({ kind: 'other' }), /--kind must be one of/) + // A missing project still annotates, then names the page it could not send. + const before = calls.length + assert.deepEqual(await alarm({ projectId: undefined }), { paged: false }) + assert.equal(calls.length, before) + assert.match(lines.at(-2), /^::error title=Reserve watch refused for /) + assert.match(lines.at(-1), /nothing pages: --project-id is not a GCP project id/) +}) diff --git a/cloud/dev/scripts/workload-identity-attribute-conditions.test.mjs b/cloud/dev/scripts/workload-identity-attribute-conditions.test.mjs index 490bfde3520..cdf3b51c565 100644 --- a/cloud/dev/scripts/workload-identity-attribute-conditions.test.mjs +++ b/cloud/dev/scripts/workload-identity-attribute-conditions.test.mjs @@ -34,7 +34,7 @@ const EXPECTED_CONDITIONS = { "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && assertion.event_name == 'workflow_dispatch' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-push-deploy.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-push-deploy.yml@refs/heads/main'", github: - "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && ((assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-fence-broker.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-director.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-multi-target.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-asia-admission.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-cell-flags.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-publish-relay-production.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-rehome.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-rehome-job.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main' && (assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml@refs/heads/main' || assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main')))", + "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && ((assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-fence-broker.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-director.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-multi-target.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-asia-admission.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-cell-flags.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-reserve-watch.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-publish-relay-production.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-rehome.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-rehome-job.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main' && (assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml@refs/heads/main' || assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main')))", github_monitor: "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-monitor-relay-production.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-monitor-relay-production-job.yml@refs/heads/main'", github_fence: diff --git a/cloud/docs/relay-workflows.md b/cloud/docs/relay-workflows.md index 28ea41dc79e..edab96a4277 100644 --- a/cloud/docs/relay-workflows.md +++ b/cloud/docs/relay-workflows.md @@ -109,7 +109,7 @@ identity bootstrap apply. The providers require their exact workflows on `refs/heads/main` with the `production` environment. Missing values fail closed; no dedicated operations identity falls back to the shared deploy identity. -The shared production identity is restricted to seven named direct Relay callers plus the exact +The shared production identity is restricted to nine named direct Relay callers plus the exact regional-rehome and same-cap reusable wrapper/job pairs on `main` in the `production` environment. Its Artifact Registry and Cloud Run mutation permissions are scoped to the Orca repository, Relay director, and Relay fence broker; it cannot mutate the API or auth services. @@ -801,6 +801,76 @@ It ends with the single command that finishes from where it stopped. That comman state. Each run writes a timestamped log under `~/.orca/relay-director-deploy/` (`--log-directory` overrides it). +## Reserve watch + +`Operate Relay Production Reserve Watch` watches one cell already switched to reserve mode and, on +the first step 5 stop rule it trips, flips it back to db through the flag tool's own write +(`operate-relay-cell-flags.mjs`: CAS, read-back, re-registration wait, Postgres record). It is +manual only and inert until dispatched; `dry-run` logs the flag tool's plan instead of writing. +`write` takes the flag workflow's confirmation, `WRITE_RELAY_CELL_FLAGS `. + +It samples once a minute for up to 300 minutes, re-authenticating every 55-minute segment because +an ID token lasts an hour, and logs one JSON line per sample. The 503 background must predate the +flip: give `flipped-at` (when the reserve write applied; the background is the median of the 10 +minutes before it) or `background-503-per-minute`, which overrides it. With neither, the watch is +refused. A watch dispatched before the reserve write applied waits up to 10 minutes for the cell to +report reserve, judging nothing meanwhile; a cell that never does ends the watch refused, without a +write. It also refuses to start while any run holds the Cloud SQL rollout lease (same-cap and +capacity rolls hold it for their whole run, and it names no cell), or when that lease cannot be +read. It only reads the lease: holding it for hours would block every rollout. Pausing on a roll +that starts mid-watch was not built, since a pause blinds the stop rules for the roll's length and +a flip back mid-roll races its drain, so do not start a roll on a watched cell. It trips on, in +this order: + +| Rule | Signal | Exact or proxy | +|---|---|---| +| Dead-man tripped | runtime-status `admitModeEffective=db` under a reserve switch file | Exact | +| Overrun | runtime-status `enforcedConnectionUnits` over `connectionCapacity.hardCap` | Invariant check: the ledger already refuses past its cap, so this trips only if that refusal broke. It counts seats, bookings, in-flight and phone units | +| 503s | Director `request_count` 503s above max(1.5x background, background + 20) for 3 complete minutes | Proxy that can miss: fleet-wide, since no signal attributes a 503 to a cell, so a problem on one cell can stay under the fleet's line | +| Duplicate past grace | The ledger reconcile's `orca_relay_reserve_ledger_seat_behind_row` for the cell, reported again more than 90 s (60 s demotion close + 30 s map lag) after it was first seen, keyed by director instance, cell, epoch and row epoch | Proxy that can only over-count: the line names no host, so two hosts sharing that key read as one seat | +| Host drop | runtime-status `controls` fell more than 5% (and at least 10 hosts) within 5 minutes with no `draining` sample | Exact | +| Ledger drift | Queued plus dropped rows in each director's newest `orca_relay_reserve_ledger_summary`, over the cell's controls, above 0.5% for 2 minutes | Proxy that can only over-count: the summaries are fleet-wide, so exact only while this is the only reserve cell | + +Any rule it cannot read in a sample makes that sample unverifiable; `unverifiable-limit` such +samples in a row is a trip of its own. The 503 read takes every response code in one query, and the +newest minute any code has a point for is how far the series is published: a 503 minute at or +before it with no point had no 503s, so a quiet fleet reads zero. Minutes past it are left out, not +counted; fewer than 3 published minutes in the read, no request series at all, or an unparseable +timestamp is unverifiable. This assumes the codes publish together; one code lagging the others +delays a trip by its lag but cannot hide one, because a later read sees its points. Until the +minutes before `flipped-at` are published (3 minutes past it) the 503 rule is pending, which does +not add to the streak. The duplicate read also takes `orca_relay_reserve_ledger_summary`, which +every placing director logs each minute, in the same query and window: with no summary from the +last 150 s in it, the read is blind and the sample unverifiable, not zero duplicates. The duplicate +rule keys a seat by director instance, so a director restart (a new instance id) starts its +first-seen clock over. A cell whose switch file no longer says reserve ends the watch without a +write. + +In `write` mode a watch that fails, times out or is cancelled before a verdict flips the cell back +as a trip would; the flip-back job and each of its steps run on `always()`, so a cancel during the +lease wait does not skip the switch-file write. That cancel can still kill the process before +Postgres records db, leaving the file at db and Postgres at reserve; a force-cancel can stop it +outright. The workflow's only group is +its own per-cell one, which a running run never yields. It stays out of the flag workflow's group +because GitHub keeps one pending run per group and a newer one cancels it, so a flip back queued +there could be dropped by any later flag dispatch, and a watch held there would block manual writes +for hours. The flag tool's generation check (CAS) orders it against other writes instead; on a +conflict it re-reads and retries, up to 5 times, while the switch file still says reserve, and stops +if it already says db. A cell that still does not answer runtime-status after 4 tries with backoff +cannot take the ordinary flip back: the run fails and prints the exact break-glass inputs for +`Operate Relay Production Cell Flags` (`set=admitMode=default`, `break-glass=BREAK GLASS `). + +An operator watches the run. A refused watch and a flip back that did not land (no ID token, a +failed login, a cell that does not answer) can each leave the cell in reserve with nothing watching +it, so both raise an `::error::` annotation naming the cell, as the clock-skew monitor does. In +`write` mode both also log `orca_relay_reserve_watch_alarm` (log `orca-relay-reserve-watch`, +resource `global`) with the deploy identity, and the `Orca Relay: reserve cell may be unwatched` +alert pages on any such line. A failed login cannot page: the runner then has no credentials for +`gcloud logging write`, so only the `::error::` and the operator watching the run report it. Apply +the root Terraform that adds the workflow to the shared deploy identity's provider condition and +grants that identity `roles/logging.logWriter` before the first `write` run; until then the +workflow cannot log in and no alarm line can be written. + ## Mobile push gateway `Deploy Push Gateway Production` (`.github/workflows/cloud-push-deploy.yml`) is the deploy path diff --git a/cloud/infra/terraform/relay-github-actions.tf b/cloud/infra/terraform/relay-github-actions.tf index fe4108312e4..f7f130263db 100644 --- a/cloud/infra/terraform/relay-github-actions.tf +++ b/cloud/infra/terraform/relay-github-actions.tf @@ -20,6 +20,7 @@ locals { "deploy-relay-production.yml", "operate-relay-asia-admission.yml", "operate-relay-production-cell-flags.yml", + "operate-relay-production-reserve-watch.yml", "publish-relay-production.yml" ] @@ -380,6 +381,16 @@ resource "google_project_iam_member" "github_logging_viewer" { member = google_service_account.github_deploy[0].member } +# The reserve watch logs its paging alarm line (orca_relay_reserve_watch_alarm) from the runner. +# Accepted risk: Cloud Logging cannot narrow writes to one log, so this identity can write any line. +resource "google_project_iam_member" "github_log_writer" { + count = local.relay_create_production_ops_identity ? 1 : 0 + + project = var.project_id + role = "roles/logging.logWriter" + member = google_service_account.github_deploy[0].member +} + resource "google_project_iam_member" "github_cloudsql_viewer" { count = local.relay_create_production_ops_identity ? 1 : 0 diff --git a/cloud/infra/terraform/relay-observability.tf b/cloud/infra/terraform/relay-observability.tf index 0e867366613..5149d355040 100644 --- a/cloud/infra/terraform/relay-observability.tf +++ b/cloud/infra/terraform/relay-observability.tf @@ -1748,6 +1748,74 @@ resource "google_monitoring_alert_policy" "relay_cell_reserve_dead_man_tripped" depends_on = [google_logging_metric.relay_cell_reserve_dead_man_tripped] } +# The reserve watch workflow logs this from its runner (resource global) when a watch is refused or +# a flip back did not land in write mode: either can leave a cell in reserve unwatched. +resource "google_logging_metric" "relay_reserve_watch_alarm" { + project = var.project_id + name = "orca_relay_reserve_watch_alarm" + description = "Reserve watch runs that were refused or whose flip back to db did not land, so the cell may be in reserve mode with nothing watching it." + filter = "resource.type=\"global\" AND logName=\"projects/${var.project_id}/logs/orca-relay-reserve-watch\" AND jsonPayload.event=\"orca_relay_reserve_watch_alarm\"" + label_extractors = { + cell_id = "EXTRACT(jsonPayload.cellId)" + kind = "EXTRACT(jsonPayload.kind)" + } + + metric_descriptor { + metric_kind = "DELTA" + value_type = "INT64" + unit = "1" + + labels { + key = "cell_id" + value_type = "STRING" + description = "Durable relay cell identifier." + } + + labels { + key = "kind" + value_type = "STRING" + description = "refused or flip-back-failed." + } + } +} + +resource "google_monitoring_alert_policy" "relay_reserve_watch_alarm" { + project = var.project_id + display_name = "Orca Relay: reserve cell may be unwatched" + combiner = "OR" + enabled = true + notification_channels = var.relay_alert_notification_channels + + conditions { + display_name = "A reserve watch was refused or its flip back did not land" + + condition_threshold { + filter = "resource.type=\"global\" AND metric.type=\"logging.googleapis.com/user/orca_relay_reserve_watch_alarm\"" + comparison = "COMPARISON_GT" + threshold_value = 0 + duration = "0s" + + aggregations { + alignment_period = "300s" + per_series_aligner = "ALIGN_SUM" + cross_series_reducer = "REDUCE_SUM" + group_by_fields = ["metric.label.\"cell_id\""] + } + + trigger { + count = 1 + } + } + } + + documentation { + content = "A write-mode `Operate Relay Production Reserve Watch` run was refused (a rollout held the Cloud SQL rollout lease, the lease was unreadable, bad inputs, or the cell never reported reserve within 10 minutes) or its flip back to db did not land (no ID token, a failed login, or a cell that does not answer). The cell may be in reserve mode with nothing watching it. Read the cell's `runtime-status`; if its switch file says reserve, either rerun the watch or run `cloud-operate-relay-production-cell-flags.yml` with `admitMode=db` for that cell, using the break-glass inputs the failed run printed if the cell does not answer." + mime_type = "text/markdown" + } + + depends_on = [google_logging_metric.relay_reserve_watch_alarm] +} + # host-session-registry.ts: a flip back closed a control it could not re-register (4409), after 20 refusals. resource "google_logging_metric" "relay_cell_reregistration_gave_up" { project = var.project_id diff --git a/cloud/package.json b/cloud/package.json index c49f6ca9c66..8f7a8046b22 100644 --- a/cloud/package.json +++ b/cloud/package.json @@ -17,7 +17,7 @@ "load:relay:recovery-gate": "node dev/scripts/run-relay-recovery-wave-gate.mjs", "ops:relay": "pnpm --filter @orca-cloud/relay-ops dev", "pretest": "node --test dev/scripts/capture-terraform-plan-baseline.test.mjs dev/scripts/operate-relay-asia-admission.test.mjs dev/scripts/prepare-relay-asia-director-cells.test.mjs dev/scripts/prepare-relay-asia-topology-input.test.mjs dev/scripts/production-cloud-sql-rollout-lock.test.mjs dev/scripts/read-relay-serving-regional-placement-version.test.mjs dev/scripts/relay-asia-rollout-evidence.test.mjs dev/scripts/relay-asia-topology-workflow.test.mjs dev/scripts/relay-cloud-sql-connection-budget.test.mjs dev/scripts/relay-load-reader-evidence.test.mjs dev/scripts/relay-control-flag-alerts.test.mjs dev/scripts/relay-db-fault-alerts.test.mjs dev/scripts/relay-lock-contention-alerts.test.mjs dev/scripts/relay-reconnect-herd-alerts.test.mjs dev/scripts/relay-reserve-dead-man-alert.test.mjs dev/scripts/relay-region-hint-metrics.test.mjs dev/scripts/relay-staging-deploy-identity.test.mjs dev/scripts/sanitize-relay-asia-admission-result.test.mjs dev/scripts/terraform-root-partition.test.mjs dev/scripts/validate-relay-asia-topology-plan.test.mjs ../.github/actions/cloud-sql-rollout-lease/action-contract.test.mjs ../.github/actions/cloud-sql-rollout-lease/storage-lease.test.mjs && pnpm test:runner", - "test": "pnpm -r --filter \"!@orca-cloud/relay\" test && pnpm --filter @orca-cloud/relay test && node --test dev/scripts/check-relay-same-cap-headroom.test.mjs dev/scripts/classify-relay-production-capacity-director.test.mjs dev/scripts/classify-relay-staging-bootstrap.test.mjs dev/scripts/deploy-relay-blue-green.test.mjs dev/scripts/deploy-relay-gce-candidate.test.mjs dev/scripts/deploy-relay-gce-multi-target.test.mjs dev/scripts/drive-relay-director-deploy.test.mjs dev/scripts/github-smoke-token.test.mjs dev/scripts/infra.test.mjs dev/scripts/measure-relay-drain-disconnect-gap.test.mjs dev/scripts/operate-relay-cell-flags.test.mjs dev/scripts/operate-relay-regional-rehome.test.mjs dev/scripts/power-staging-relay.test.mjs dev/scripts/prepare-relay-capacity-canary.test.mjs dev/scripts/prepare-relay-production-capacity-canary.test.mjs dev/scripts/probe-relay-legacy-admission.test.mjs dev/scripts/probe-relay-rehome-trust.test.mjs dev/scripts/production-cell-image-digest-consistency.test.mjs dev/scripts/push-gateway-workflow.test.mjs dev/scripts/push-gateway-recovery.test.mjs dev/scripts/read-relay-production-capacity-identity.test.mjs dev/scripts/relay-admin-endpoint-retry-workflow.test.mjs dev/scripts/relay-admin-transient-retry.test.mjs dev/scripts/relay-admission-selector.test.mjs dev/scripts/relay-gce-terraform-fence.test.mjs dev/scripts/relay-load-connection-failure.test.mjs dev/scripts/relay-load-control-peer.test.mjs dev/scripts/relay-load-director-capacity-gate.test.mjs dev/scripts/relay-load-duplicate-assign.test.mjs dev/scripts/relay-load-model.test.mjs dev/scripts/relay-load-phase-barrier.test.mjs dev/scripts/relay-load-placement-boundary.test.mjs dev/scripts/relay-load-profile.test.mjs dev/scripts/relay-load-rebind-boundary.test.mjs dev/scripts/relay-load-region-behavior.test.mjs dev/scripts/relay-load-request-unit-boundary.test.mjs dev/scripts/relay-load-run-lifecycle.test.mjs dev/scripts/relay-load-splice-retry.test.mjs dev/scripts/relay-monitor-evidence.test.mjs dev/scripts/relay-production-capacity-wave.test.mjs dev/scripts/relay-production-capacity-workflow.test.mjs dev/scripts/relay-production-identity-boundaries.test.mjs dev/scripts/relay-production-same-cap-wave.test.mjs dev/scripts/relay-public-workflow-contract.test.mjs dev/scripts/relay-recovery-wave-gate.test.mjs dev/scripts/relay-region-observation-evidence.test.mjs dev/scripts/relay-rehome-aggregate-evidence.test.mjs dev/scripts/relay-repository.test.mjs dev/scripts/relay-same-cap-job-mode-conditions.test.mjs dev/scripts/relay-same-cap-script-census.test.mjs dev/scripts/relay-same-cap-shadow-gate.test.mjs dev/scripts/relay-staging-c4-refresh-workflow.test.mjs dev/scripts/relay-staging-capacity-identity.test.mjs dev/scripts/staging-relay-apply-guard.test.mjs dev/scripts/validate-relay-capacity-plan.test.mjs dev/scripts/verify-relay-capacity-transition.test.mjs dev/scripts/verify-relay-legacy-bootstrap.test.mjs dev/scripts/workload-identity-attribute-conditions.test.mjs", + "test": "pnpm -r --filter \"!@orca-cloud/relay\" test && pnpm --filter @orca-cloud/relay test && node --test dev/scripts/check-relay-same-cap-headroom.test.mjs dev/scripts/classify-relay-production-capacity-director.test.mjs dev/scripts/classify-relay-staging-bootstrap.test.mjs dev/scripts/deploy-relay-blue-green.test.mjs dev/scripts/deploy-relay-gce-candidate.test.mjs dev/scripts/deploy-relay-gce-multi-target.test.mjs dev/scripts/drive-relay-director-deploy.test.mjs dev/scripts/github-smoke-token.test.mjs dev/scripts/infra.test.mjs dev/scripts/measure-relay-drain-disconnect-gap.test.mjs dev/scripts/operate-relay-cell-flags.test.mjs dev/scripts/operate-relay-regional-rehome.test.mjs dev/scripts/power-staging-relay.test.mjs dev/scripts/prepare-relay-capacity-canary.test.mjs dev/scripts/prepare-relay-production-capacity-canary.test.mjs dev/scripts/probe-relay-legacy-admission.test.mjs dev/scripts/probe-relay-rehome-trust.test.mjs dev/scripts/production-cell-image-digest-consistency.test.mjs dev/scripts/push-gateway-workflow.test.mjs dev/scripts/push-gateway-recovery.test.mjs dev/scripts/read-relay-production-capacity-identity.test.mjs dev/scripts/relay-admin-endpoint-retry-workflow.test.mjs dev/scripts/relay-admin-transient-retry.test.mjs dev/scripts/relay-admission-selector.test.mjs dev/scripts/relay-gce-terraform-fence.test.mjs dev/scripts/relay-load-connection-failure.test.mjs dev/scripts/relay-load-control-peer.test.mjs dev/scripts/relay-load-director-capacity-gate.test.mjs dev/scripts/relay-load-duplicate-assign.test.mjs dev/scripts/relay-load-model.test.mjs dev/scripts/relay-load-phase-barrier.test.mjs dev/scripts/relay-load-placement-boundary.test.mjs dev/scripts/relay-load-profile.test.mjs dev/scripts/relay-load-rebind-boundary.test.mjs dev/scripts/relay-load-region-behavior.test.mjs dev/scripts/relay-load-request-unit-boundary.test.mjs dev/scripts/relay-load-run-lifecycle.test.mjs dev/scripts/relay-load-splice-retry.test.mjs dev/scripts/relay-monitor-evidence.test.mjs dev/scripts/relay-production-capacity-wave.test.mjs dev/scripts/relay-production-capacity-workflow.test.mjs dev/scripts/relay-production-identity-boundaries.test.mjs dev/scripts/relay-production-same-cap-wave.test.mjs dev/scripts/relay-public-workflow-contract.test.mjs dev/scripts/relay-recovery-wave-gate.test.mjs dev/scripts/relay-region-observation-evidence.test.mjs dev/scripts/relay-rehome-aggregate-evidence.test.mjs dev/scripts/relay-repository.test.mjs dev/scripts/relay-same-cap-job-mode-conditions.test.mjs dev/scripts/relay-same-cap-script-census.test.mjs dev/scripts/relay-same-cap-shadow-gate.test.mjs dev/scripts/relay-staging-c4-refresh-workflow.test.mjs dev/scripts/relay-staging-capacity-identity.test.mjs dev/scripts/staging-relay-apply-guard.test.mjs dev/scripts/validate-relay-capacity-plan.test.mjs dev/scripts/verify-relay-capacity-transition.test.mjs dev/scripts/verify-relay-legacy-bootstrap.test.mjs dev/scripts/watch-relay-reserve-cell.test.mjs dev/scripts/workload-identity-attribute-conditions.test.mjs", "typecheck": "pnpm -r typecheck", "test:runner": "node --import tsx --conditions=orca-source --test dev/scripts/run-relay-tests.test.mjs" }, From 1f50bd305f968a97d1cf938d556ebee6d4f274dc Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:42:56 -0700 Subject: [PATCH 35/53] Reduce unrelated setup in attention and mailbox fixtures (#28897) --- ...n-mailbox-notification-consistency.test.ts | 35 +++++++++---------- ...attention-read-retirement.test-fixture.tsx | 1 + 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/runtime/orchestration-mailbox-notification-consistency.test.ts b/src/main/runtime/orchestration-mailbox-notification-consistency.test.ts index 3475ae70eb0..ee03a149410 100644 --- a/src/main/runtime/orchestration-mailbox-notification-consistency.test.ts +++ b/src/main/runtime/orchestration-mailbox-notification-consistency.test.ts @@ -9,7 +9,6 @@ import { OrchestrationDb } from './orchestration/db' import { checkBoundMailbox, createBoundRun, - createDatabase, createRuntime, driveToLiveIdle, insertDirectRunMessage, @@ -52,7 +51,7 @@ describe('orchestration notification mailbox consistency', () => { it('never points direct Run A mail after the pane is rebound to Run B', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-consistency-') + const db = new OrchestrationDb(':memory:') const first = createRuntime(db) const runA = createBoundRun(db, 'Run A') const staleDirectMessage = insertDirectRunMessage(db, runA.id, 'Run A completion') @@ -78,7 +77,7 @@ describe('orchestration notification mailbox consistency', () => { it('does not point unbound direct mail that a later Run binding would hide', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-unbound-bind-race-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const runA = createBoundRun(db, 'Unbound Run A') const message = insertDirectRunMessage(db, runA.id, 'Run A before later binding') @@ -151,7 +150,7 @@ describe('orchestration notification mailbox consistency', () => { it('pages a large persisted mismatch and coalesces each mailbox wake', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-paged-reconciliation-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const runA = createBoundRun(db, 'Backlog Run A') const messages = Array.from({ length: 151 }, (_, index) => @@ -188,7 +187,7 @@ describe('orchestration notification mailbox consistency', () => { }) it('uses composite indexes for direct ownership reconciliation', () => { - const db = createDatabase('orca-mailbox-routing-indexes-') + const db = new OrchestrationDb(':memory:') const sqlite = sqliteFor(db) const plan = (sql: string, ...params: string[]): string => (sqlite.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(...params) as { detail: string }[]) @@ -239,7 +238,7 @@ describe('orchestration notification mailbox consistency', () => { }) it('finds owned mail without scanning an older unowned direct backlog', () => { - const db = createDatabase('orca-mailbox-owned-routing-plan-') + const db = new OrchestrationDb(':memory:') const detachedHandle = 'term_backlogged_detached' const ownedRun = db.createRun({ objective: 'Owned after backlog', @@ -388,7 +387,7 @@ describe('orchestration notification mailbox consistency', () => { it('fences the pointed mailbox instead of checking a rebound empty Run', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-post-submit-rebind-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const runA = createBoundRun(db, 'Run A') insertDirectRunMessage(db, runA.id, 'Run A completion') @@ -434,7 +433,7 @@ describe('orchestration notification mailbox consistency', () => { it('releases a staged pointer when its Run moves to another live pane', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-live-rebind-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const runA = createBoundRun(db, 'Run A') const message = insertDirectRunMessage(db, runA.id, 'Run A completion') @@ -474,7 +473,7 @@ describe('orchestration notification mailbox consistency', () => { it('serializes equivalent panes while newer mail arrives during Enter delay', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-equivalent-pane-flight-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Equivalent pane Run') const first = insertDirectRunMessage(db, run.id, 'First status') @@ -520,7 +519,7 @@ describe('orchestration notification mailbox consistency', () => { 'submits a staged pointer after %s becomes working without duplicating it', async (_provider, workingTitle, idleTitle) => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-working-before-enter-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Working-before-Enter Run') const message = insertDirectRunMessage(db, run.id, 'Actionable status') @@ -546,7 +545,7 @@ describe('orchestration notification mailbox consistency', () => { 'does not submit a staged pointer after %s enters a permission state', async (_provider, permissionTitle) => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-permission-before-enter-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Permission-before-Enter Run') const message = insertDirectRunMessage(db, run.id, 'Actionable status') @@ -563,7 +562,7 @@ describe('orchestration notification mailbox consistency', () => { ) it('submits a staged pointer once when a live PTY is cold-parked before Enter', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-cold-park-submit-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Cold-park Run') const message = insertDirectRunMessage(db, run.id, 'Submit while parked') @@ -582,7 +581,7 @@ describe('orchestration notification mailbox consistency', () => { it('keeps a staged Enter deferred when a parked watcher republishes the leaf beside a decoy', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-cold-park-published-leaf-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Cold-park published leaf Run') insertDirectRunMessage(db, run.id, 'Keep Enter deferred') @@ -662,7 +661,7 @@ describe('orchestration notification mailbox consistency', () => { 'handles a cold-parked pointer after the agent becomes %s', async (state, title, idleTitle) => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-cold-park-transition-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Cold-park transition Run') const message = insertDirectRunMessage(db, run.id, 'Do not submit while unavailable') @@ -692,7 +691,7 @@ describe('orchestration notification mailbox consistency', () => { it('releases staged pointer state when an explicit check owns the batch', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-explicit-check-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Explicit-check Run') insertDirectRunMessage(db, run.id, 'Checked before Enter') @@ -712,7 +711,7 @@ describe('orchestration notification mailbox consistency', () => { it('routes same-Run direct mail through the bound check and acknowledgment path', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-current-run-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Current Run') const messages = Array.from({ length: 51 }, (_, index) => @@ -804,7 +803,7 @@ describe('orchestration notification mailbox consistency', () => { it('does not replay a visible pointer when the provider rejects Enter', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-provider-refusal-') + const db = new OrchestrationDb(':memory:') const first = createRuntime(db) const recordWrite = first.write as unknown as (id: string, payload: string) => unknown const write = vi.fn((ptyId: string, data: string) => { @@ -835,7 +834,7 @@ describe('orchestration notification mailbox consistency', () => { it('does not point newer Run mail behind an outstanding Delivery', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-outstanding-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Outstanding Delivery Run') const firstMessage = db.insertMessage({ diff --git a/tests/e2e/structured-attention-read-retirement.test-fixture.tsx b/tests/e2e/structured-attention-read-retirement.test-fixture.tsx index 2b76b192f5a..023e601401a 100644 --- a/tests/e2e/structured-attention-read-retirement.test-fixture.tsx +++ b/tests/e2e/structured-attention-read-retirement.test-fixture.tsx @@ -1,4 +1,5 @@ // @vitest-environment happy-dom +import '../../src/main/runtime/rpc/unused-default-rpc-methods.test-fixture' import { transport } from './__mocks__/structured-attention-read-surfaces' import { cleanup } from '@testing-library/react' import { beforeEach, afterEach, vi } from 'vitest' From 11aad143f8d0519c8ff389dfa2d37a220a8debd7 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:48:08 -0700 Subject: [PATCH 36/53] fix(app-shell): let users close the profile storage delay warning (#28900) The warning sticks until profile saving catches up, but it turned off the close button every other Orca toast gets from the app toaster. --- ...profile-state-save-delay-notice.sonner.test.tsx | 14 ++++++++++++-- .../use-profile-state-save-delay-notice.test.tsx | 4 +--- .../use-profile-state-save-delay-notice.ts | 4 +--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx b/src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx index 471dfaf3ed5..44242495404 100644 --- a/src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx +++ b/src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx @@ -11,9 +11,9 @@ const title = 'Profile storage is taking longer than usual' const frames: FrameRequestCallback[] = [] const listeners = new Set<(delayed: boolean) => void>() -function Notice() { +function Notice({ closeButton = false }: { closeButton?: boolean }) { useProfileStateSaveDelayNotice() - return + return } beforeEach(() => { @@ -97,6 +97,16 @@ it('does not let StrictMode cleanup dismiss the initial delayed snapshot', async expect(view.queryAllByText(title)).toHaveLength(1) }) +it('lets the user close the warning with the app toaster close button', async () => { + const view = render() + await advanceTimers() + expect(view.queryByText(title)).not.toBeNull() + + act(() => view.getByRole('button', { name: 'Close toast' }).click()) + await finishDismissal() + expect(view.queryByText(title)).toBeNull() +}) + it('keeps a new warning if recovery and another delay arrive before a dismissal frame', async () => { const view = render() await advanceTimers() diff --git a/src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx b/src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx index d673aba02c1..05018d5aef7 100644 --- a/src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx +++ b/src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx @@ -62,9 +62,7 @@ it('restores a persistent warning from main and clears it on completion', async { id: undefined, description: 'Further saves may be delayed while this operation finishes.', - duration: Infinity, - dismissible: false, - closeButton: false + duration: Infinity } ) publish(false) diff --git a/src/renderer/src/app-shell/use-profile-state-save-delay-notice.ts b/src/renderer/src/app-shell/use-profile-state-save-delay-notice.ts index 99815c1c3f4..7b35bba58eb 100644 --- a/src/renderer/src/app-shell/use-profile-state-save-delay-notice.ts +++ b/src/renderer/src/app-shell/use-profile-state-save-delay-notice.ts @@ -34,9 +34,7 @@ export function useProfileStateSaveDelayNotice(): void { 'app.saving.delayedDescription', 'Further saves may be delayed while this operation finishes.' ), - duration: Infinity, - dismissible: false, - closeButton: false + duration: Infinity } ) } From d78363ab36338d32e7b624f89103734d95127b67 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:52:41 -0700 Subject: [PATCH 37/53] fix(agent-launch): find a repo's project the way the window does (#28237) * fix(agent-launch): find a repo's project the way the window does The host now also takes a project named after the repo as that repo's project, as the window does, so both pick the same project row and runtime even when a repo was moved out of the project named after it. * refactor(agent-launch): share one repo-to-project match with the window The host's agent-launch lookup and the window's launch preflight now call one projectMatchesRepo instead of each writing the match out. No behaviour change on either side. --- src/main/local-project-runtime-resolution.ts | 4 ++-- src/main/runtime/agent-launch-scope-runtime.test.ts | 6 +++--- src/renderer/src/lib/local-preflight-context.ts | 4 ++-- src/shared/project-repo-match.ts | 7 +++++++ 4 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 src/shared/project-repo-match.ts diff --git a/src/main/local-project-runtime-resolution.ts b/src/main/local-project-runtime-resolution.ts index 961a7148408..09dbc5754a0 100644 --- a/src/main/local-project-runtime-resolution.ts +++ b/src/main/local-project-runtime-resolution.ts @@ -21,6 +21,7 @@ import { hasCachedWslDistros } from './wsl' import { getRepoIdFromWorktreeId } from '../shared/worktree/id' +import { projectMatchesRepo } from '../shared/project-repo-match' import { resolveLocalWindowsTerminalRuntimeOptions, type LocalWindowsTerminalRuntimeOptions @@ -111,8 +112,7 @@ export function resolveLocalWorkspaceRuntime( ) { return undefined } - // TEMPORARY parity: main finds the project by its source repos only; the window also by its id. - const project = store.getProjects().find((entry) => entry.sourceRepoIds.includes(repo.id)) + const project = store.getProjects().find((entry) => projectMatchesRepo(entry, repo.id)) const preference = getWorkspaceRuntimePreference( project?.localWindowsRuntimePreference, workspacePath diff --git a/src/main/runtime/agent-launch-scope-runtime.test.ts b/src/main/runtime/agent-launch-scope-runtime.test.ts index 3070f36c460..1a9eb1495ad 100644 --- a/src/main/runtime/agent-launch-scope-runtime.test.ts +++ b/src/main/runtime/agent-launch-scope-runtime.test.ts @@ -97,15 +97,15 @@ it("answers from main's WSL cache, which the window may not have read yet", () = expect(claudeRuntime(store({}), scope(SHARE, { repo: repo(SHARE) })).platform).toBe('linux') }) -// TEMPORARY parity: the host finds a repo's project by its source repos only, as main does. -it("finds a repo's project by its source repos, not a project named after the repo", () => { +// The window's lookup: a project named after the repo counts as its project, as does one listing it. +it("finds a repo's project the way the window does, by its id too", () => { const projects = [ { id: 'r', sourceRepoIds: [], localWindowsRuntimePreference: UBUNTU }, { id: 'p', sourceRepoIds: ['r'], localWindowsRuntimePreference: { kind: 'windows-host' } } ] const fake = { getProjects: () => projects, getSettings: () => ({}), getRepo: () => repo(WIN) } // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads only these members. - expect(agentLaunchPlatformForScope(fake as never, scope(WIN, { repo: repo(WIN) }))).toBe('win32') + expect(agentLaunchPlatformForScope(fake as never, scope(WIN, { repo: repo(WIN) }))).toBe('linux') }) // Relied on by C2: the pane's pty:spawn takes the fact as is, for quoting and shell alike. diff --git a/src/renderer/src/lib/local-preflight-context.ts b/src/renderer/src/lib/local-preflight-context.ts index 1d72186d991..b51446f522b 100644 --- a/src/renderer/src/lib/local-preflight-context.ts +++ b/src/renderer/src/lib/local-preflight-context.ts @@ -11,6 +11,7 @@ import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../shared import type { Repo } from '../../../shared/repo-types' import type { Worktree } from '../../../shared/worktree/types' import { parseWorkspaceKey } from '../../../shared/workspace-scope' +import { projectMatchesRepo } from '../../../shared/project-repo-match' import { getIndexedRepoMap } from '@/store/worktree-repo-index' import { defaultScopeHost } from './default-creation-host' import { getLocalProjectRuntimeWorkspace } from './local-project-runtime-workspace' @@ -318,8 +319,7 @@ function getLocalRuntimeProject( repoId: string ) { return state.projects?.find( - (entry) => - entry.id === projectId || entry.id === repoId || entry.sourceRepoIds?.includes(repoId) + (entry) => entry.id === projectId || projectMatchesRepo(entry, repoId) ) } diff --git a/src/shared/project-repo-match.ts b/src/shared/project-repo-match.ts new file mode 100644 index 00000000000..9b6f7d74973 --- /dev/null +++ b/src/shared/project-repo-match.ts @@ -0,0 +1,7 @@ +/** A repo's project is the one named after the repo, or one that lists it as a source repo. */ +export function projectMatchesRepo( + project: { id: string; sourceRepoIds?: readonly string[] }, + repoId: string +): boolean { + return project.id === repoId || (project.sourceRepoIds?.includes(repoId) ?? false) +} From c056cfc140e30a99d6046543238c47c64aabec5e Mon Sep 17 00:00:00 2001 From: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:55:06 +0000 Subject: [PATCH 38/53] feat(native-chat): add a "New" line above unseen messages (#28333) --- docs/STYLEGUIDE.md | 2 +- ...ession-history-forward-read-budget.test.ts | 5 + .../agent-session-subscriber-catch-up.ts | 1 + src/renderer/src/assets/main.css | 3 + .../native-chat/NativeChatDockedTaskList.tsx | 23 ++ ...veChatMessageList.new-rows-marker.test.tsx | 151 +++++++++ .../native-chat/NativeChatMessageList.tsx | 28 +- .../NativeChatNewRowsMarker.test.tsx | 109 ++++++ .../native-chat/NativeChatNewRowsMarker.tsx | 87 +++++ ...tiveChatStructuredSession.test-harness.tsx | 3 + .../NativeChatStructuredSession.test.tsx | 21 ++ .../native-chat/NativeChatTranscriptItems.tsx | 13 +- .../native-chat/NativeChatTranscriptRow.tsx | 12 +- .../native-chat-appearance-palette.test.ts | 4 + .../native-chat-live-session-contract.ts | 3 + .../native-chat-new-rows-marker.test.tsx | 313 ++++++++++++++++++ .../native-chat-new-rows-marker.ts | 188 +++++++++++ .../native-chat-pinned-rows.test.ts | 6 + .../native-chat/native-chat-pinned-rows.ts | 12 +- .../structured-agent-session-read-owner.ts | 23 +- ...tured-agent-session-read-transport.test.ts | 4 + ...structured-agent-session-read-transport.ts | 21 +- .../use-native-chat-reveal-latest.ts | 23 +- .../use-native-chat-transcript-window.ts | 7 +- ...use-structured-agent-session-read.test.tsx | 69 +++- .../use-structured-agent-session-read.ts | 1 + .../use-structured-agent-session.ts | 3 + .../use-structured-chat-live-session.ts | 4 +- .../use-orca-window-foreground-focused.ts | 25 ++ src/renderer/src/i18n/locales/en.json | 2 + src/renderer/src/i18n/locales/es.json | 2 + src/renderer/src/i18n/locales/fr.json | 2 + src/renderer/src/i18n/locales/ja.json | 2 + src/renderer/src/i18n/locales/ko.json | 2 + src/renderer/src/i18n/locales/zh.json | 2 + src/shared/agent-session-wire.ts | 7 +- ...structured-agent-session-coalescer.test.ts | 21 ++ .../structured-agent-session-coalescer.ts | 4 +- 38 files changed, 1159 insertions(+), 49 deletions(-) create mode 100644 src/renderer/src/components/native-chat/NativeChatDockedTaskList.tsx create mode 100644 src/renderer/src/components/native-chat/NativeChatMessageList.new-rows-marker.test.tsx create mode 100644 src/renderer/src/components/native-chat/NativeChatNewRowsMarker.test.tsx create mode 100644 src/renderer/src/components/native-chat/NativeChatNewRowsMarker.tsx create mode 100644 src/renderer/src/components/native-chat/native-chat-new-rows-marker.test.tsx create mode 100644 src/renderer/src/components/native-chat/native-chat-new-rows-marker.ts create mode 100644 src/renderer/src/hooks/use-orca-window-foreground-focused.ts diff --git a/docs/STYLEGUIDE.md b/docs/STYLEGUIDE.md index 733ef99402e..e236389c7d8 100644 --- a/docs/STYLEGUIDE.md +++ b/docs/STYLEGUIDE.md @@ -113,7 +113,7 @@ The chat root (`.native-chat-appearance`) redefines these steps as ratios of the - **Secondary text on the chat canvas** (transcript rows, notices, labels between messages) uses `text-chat-foreground-faint`. `text-muted-foreground` is for text on a lifted surface inside the chat: cards, menus, popovers and the docks above the composer. - **Row hover** in the transcript is `hover:bg-accent/20`. Buttons, menu items and card controls use `hover:bg-accent`. - **Focus rings:** full-width transcript rows use the inset ring (`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring`) so it isn't clipped by the scroll container; standalone controls use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring`. Inside the chat `--ring` is `--chat-focus-ring`, which clears 3:1 on the canvas; don't lower its opacity. -- **State colours in the chat:** warnings use `text-chat-warning`; diff lines use `text-chat-diff-added` / `text-chat-diff-removed`. They are mixed toward the foreground so they pass text contrast in light and dark. Don't use raw palette colours (`text-yellow-500`) or the git decoration tokens for text on a tinted row. +- **State colours in the chat:** warnings use `text-chat-warning`; diff lines use `text-chat-diff-added` / `text-chat-diff-removed`; what the reader has not seen yet uses `text-chat-unread`. They are mixed toward the foreground so they pass text contrast in light and dark. Don't use raw palette colours (`text-yellow-500`) or the git decoration tokens for text on a tinted row. ## Radius diff --git a/src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts b/src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts index f94624c26c6..7cce60a3bdd 100644 --- a/src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts +++ b/src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts @@ -119,6 +119,11 @@ describe('forward history SQL read budget', () => { Array.from({ length: count }, (_, index) => `item-${index}`) ) expect(batches.at(-1)?.batch.cursor).toEqual(journal.cursor()) + // Only the last page says the reader is caught up. + expect(batches.map((event) => event.hasNewer === true)).toEqual([ + ...Array(9).fill(true), + false + ]) expect(returnedRows).toEqual([...Array(9).fill(201), 200]) expect(parse).toHaveBeenCalledTimes(2_009) }) diff --git a/src/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.ts b/src/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.ts index 3546a7a6b8f..f54be54da13 100644 --- a/src/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.ts +++ b/src/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.ts @@ -115,6 +115,7 @@ export function deliverToSubscriber( }, fence: subscriber.fence, ...(page.latestTurn !== undefined ? { latestTurn: page.latestTurn } : {}), + ...(page.hasNewer ? { hasNewer: true as const } : {}), ...shared }, // On a multi-page catch-up the draft list rides only the final page, or a diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 840733db0e4..9a539b555fe 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -58,6 +58,7 @@ --color-chat-foreground: var(--chat-foreground); --color-chat-foreground-strong: var(--chat-foreground-strong); --color-chat-foreground-faint: var(--chat-foreground-faint); + --color-chat-unread: var(--chat-unread); --color-chat-user-surface: var(--chat-user-surface); --color-chat-user-border: var(--chat-user-border); --color-chat-code-foreground: var(--chat-code-foreground); @@ -637,6 +638,8 @@ body:has(> [data-slot='dialog-overlay'], > [data-slot='sheet-overlay']) { /* Mixed toward the foreground so each clears text contrast on the canvas in either scheme. */ --chat-focus-ring: color-mix(in srgb, var(--chat-source-foreground) 50%, var(--chat-canvas)); --chat-warning: color-mix(in srgb, var(--status-warning) 70%, var(--chat-source-foreground)); + /* What the reader has not seen yet: the transcript's "New" marker, in the note hue. */ + --chat-unread: color-mix(in srgb, var(--callout-note) 70%, var(--chat-source-foreground)); --chat-diff-added: color-mix( in srgb, var(--git-decoration-added) 70%, diff --git a/src/renderer/src/components/native-chat/NativeChatDockedTaskList.tsx b/src/renderer/src/components/native-chat/NativeChatDockedTaskList.tsx new file mode 100644 index 00000000000..3ed6376e95a --- /dev/null +++ b/src/renderer/src/components/native-chat/NativeChatDockedTaskList.tsx @@ -0,0 +1,23 @@ +import { useMemo } from 'react' +import type { NativeChatMessage } from '../../../../shared/native-chat-types' +import { NativeChatTaskList } from './NativeChatTaskList' +import { nativeChatTaskListState } from './native-chat-task-list-state' + +/** The conversation's current task list, docked under the transcript. */ +export function NativeChatDockedTaskList({ + messages +}: { + messages: readonly NativeChatMessage[] +}): React.JSX.Element | null { + const { list } = useMemo(() => nativeChatTaskListState(messages), [messages]) + if (!list || list.tasks.length === 0) { + return null + } + return ( +
+
+ +
+
+ ) +} diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.new-rows-marker.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.new-rows-marker.test.tsx new file mode 100644 index 00000000000..536d4a35e8f --- /dev/null +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.new-rows-marker.test.tsx @@ -0,0 +1,151 @@ +// @vitest-environment happy-dom + +import '@testing-library/jest-dom/vitest' + +import { act, cleanup, render, screen } from '@testing-library/react' +import { createRef } from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { NativeChatMessage } from '../../../../shared/native-chat-types' +import { NativeChatMessageList } from './NativeChatMessageList' +import type { NativeChatMessageListHandle } from './use-native-chat-reveal-latest' +import { + deliverResizes, + layout, + marker, + ROW_PITCH_PX, + scrollTranscript, + session, + stubLayout, + stubResizeObserver, + TRANSCRIPT_LENGTH +} from './native-chat-windowing-test-harness' + +// Pacing is not this test's subject. +vi.mock('./use-native-chat-paced-text', async (importOriginal) => + (await import('./native-chat-unpaced-text-fixture')).unpacedTextModule(importOriginal) +) + +afterEach(cleanup) + +/** A transcript of rows the journal placed, as a structured chat's are. */ +function chat( + length: number, + isVisible: boolean, + { + ref, + readCurrent = true + }: { ref?: React.Ref; readCurrent?: boolean } = {} +): React.JSX.Element { + const messages: NativeChatMessage[] = Array.from({ length }, (_, index) => ({ + ...marker(index), + journalPosition: { sequence: index + 1, index: 0 } + })) + return ( + + ) +} + +describe('the "New" marker in the transcript', () => { + let paneShown = true + let restore: (() => void)[] = [] + beforeEach(() => { + paneShown = true + vi.spyOn(document, 'hasFocus').mockReturnValue(true) + restore = [ + stubLayout({ scrollGeometry: true, offsetChain: true, isVisible: () => paneShown }), + stubResizeObserver() + ] + }) + afterEach(() => { + for (const undo of restore.toReversed()) { + undo() + } + layout.measuredRowHeights = [] + vi.restoreAllMocks() + }) + + // The window mounts only rows near the view; the marker's row is far from the end it opens at. + it('is drawn above the first row that came while the chat was hidden, however far back', () => { + const { rerender } = render(chat(TRANSCRIPT_LENGTH, true)) + rerender(chat(TRANSCRIPT_LENGTH, false)) + rerender(chat(TRANSCRIPT_LENGTH + 60, true)) + + const row = screen.getByRole('separator', { name: 'New' }).closest('[data-index]') + expect(row?.dataset.index).toBe(String(TRANSCRIPT_LENGTH)) + }) + + it('tells a screen reader that new messages came, in a region that was already there', () => { + const { container, rerender } = render(chat(TRANSCRIPT_LENGTH, true)) + const region = container.querySelector('[data-native-chat-scroll] ~ [aria-live="polite"]') + expect(region).toBeEmptyDOMElement() + + rerender(chat(TRANSCRIPT_LENGTH, false)) + rerender(chat(TRANSCRIPT_LENGTH + 60, true)) + expect(region).toHaveTextContent('New messages arrived while you were away') + }) + + it('goes once the reader sends something', () => { + const ref = createRef() + const { rerender } = render(chat(TRANSCRIPT_LENGTH, true, { ref })) + rerender(chat(TRANSCRIPT_LENGTH, false, { ref })) + rerender(chat(TRANSCRIPT_LENGTH + 60, true, { ref })) + expect(screen.getByRole('separator', { name: 'New' })).toBeInTheDocument() + + act(() => ref.current?.revealLatest()) + + expect(screen.queryByRole('separator', { name: 'New' })).toBeNull() + }) + + describe('when the reader comes back to new rows', () => { + /** The pane is hidden and shown again; the read catches up after the reveal has put the + * reader's place back. */ + function returnToNewRows(rerender: (ui: React.JSX.Element) => void, count = 60): void { + paneShown = false + rerender(chat(TRANSCRIPT_LENGTH, false, { readCurrent: false })) + paneShown = true + rerender(chat(TRANSCRIPT_LENGTH, true, { readCurrent: false })) + rerender(chat(TRANSCRIPT_LENGTH + count, true)) + } + const scrollTop = (container: HTMLElement): number | undefined => + container.querySelector('[data-native-chat-scroll]')?.scrollTop + + it('brings a reader who was at the end to where the new rows begin', () => { + const { container, rerender } = render(chat(TRANSCRIPT_LENGTH, true)) + returnToNewRows(rerender) + + expect(scrollTop(container)).toBe(TRANSCRIPT_LENGTH * ROW_PITCH_PX) + }) + + // A reply is drawn short and revealed over the frames after it arrives. + it('keeps the reader where the new rows begin while a reply is still being drawn', async () => { + const { container, rerender } = render(chat(TRANSCRIPT_LENGTH, true)) + returnToNewRows(rerender, 1) + + const drawn: number[] = [] + drawn[TRANSCRIPT_LENGTH] = 2_000 + layout.measuredRowHeights = drawn + act(() => void deliverResizes()) + await act(async () => { + await new Promise((resolve) => requestAnimationFrame(() => resolve())) + }) + + expect(scrollTop(container)).toBe(TRANSCRIPT_LENGTH * ROW_PITCH_PX) + }) + + it('leaves a reader who was parked further up where they were', () => { + const { container, rerender } = render(chat(TRANSCRIPT_LENGTH, true)) + const readingAt = 2_000 + scrollTranscript(container, readingAt) + returnToNewRows(rerender) + + expect(screen.getByRole('separator', { name: 'New' })).toBeInTheDocument() + expect(scrollTop(container)).toBe(readingAt) + }) + }) +}) diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx index b537a0195e3..af3a96b2c4d 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx @@ -3,6 +3,7 @@ import { NATIVE_CHAT_TRANSCRIPT_COLUMN_CLASS } from './native-chat-appearance-style' import { NativeChatJumpToLatest } from './NativeChatJumpToLatest' +import { NativeChatNewRowsAnnouncement } from './NativeChatNewRowsMarker' import { useNativeChatRowTypography } from './use-native-chat-row-typography' import { useCallback, useMemo, useRef, useState } from 'react' import type { CommentMarkdownLinkClickHandler } from '@/components/sidebar/CommentMarkdown' @@ -10,9 +11,8 @@ import { translate } from '@/i18n/i18n' import type { NativeChatLiveSession } from './use-native-chat-live-session' import { useNativeChatTranscriptProjection } from './use-native-chat-transcript-projection' import { structuredQuestionTranscript } from './structured-agent-question-projection' -import { nativeChatTaskListState } from './native-chat-task-list-state' import { nativeChatTaskListPredecessors } from './native-chat-task-list-history' -import { NativeChatTaskList } from './NativeChatTaskList' +import { NativeChatDockedTaskList } from './NativeChatDockedTaskList' import { useNativeChatTurnStatus } from './use-native-chat-turn-status' import { NativeChatAwaitingInputRow } from './NativeChatAwaitingInputRow' import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client-types' @@ -38,6 +38,7 @@ import { useNativeChatSubagentSections } from './use-native-chat-subagent-sectio import { toggleNativeChatExpandedKey } from './native-chat-expanded-keys' import { useNativeChatTurnMembership } from './use-native-chat-turn-membership' import { useNativeChatTranscriptScroll } from './use-native-chat-transcript-scroll' +import { useNativeChatNewRowsMarker } from './native-chat-new-rows-marker' import { useNativeChatOlderHistoryAutoload } from './use-native-chat-older-history-autoload' import { NativeChatOlderHistoryRow } from './NativeChatOlderHistoryRow' import { useNativeChatMessageRail } from './use-native-chat-message-rail' @@ -163,7 +164,6 @@ export function NativeChatMessageList({ [messages, subagentSections] ) const taskListPredecessors = useMemo(() => nativeChatTaskListPredecessors(messages), [messages]) - const taskListState = useMemo(() => nativeChatTaskListState(messages), [messages]) // Each row's turn, which turn is live, and the order the rows draw in, resolved once. const turnRows = useNativeChatTurnMembership( messages, @@ -227,6 +227,11 @@ export function NativeChatMessageList({ ), [allSlots, journalItems, stopping, journalSubmissions] ) + const { marker: newRowsMarker, dismiss } = useNativeChatNewRowsMarker( + slots, + waitingSlots, + isVisible && session.readCurrent === true + ) const transcriptWindow = useNativeChatTranscriptWindow({ scrollRef, slots, @@ -234,6 +239,7 @@ export function NativeChatMessageList({ // One pin serves both: revealing a diff and jumping from the rail are // mutually exclusive things to be doing. revealIndex: nativeChatSlotIndexOf(slots, railJump?.messageId ?? revealedDiff?.messageId), + newRowsMarkerIndex: nativeChatSlotIndexOf(slots, newRowsMarker?.key), typography }) const { showJump, onScroll, scrollToBottom, scrollMessageToTop, readerLeavesEnd } = @@ -321,7 +327,7 @@ export function NativeChatMessageList({ onTakeScroll: transcriptWindow.cancelAlign, onLeaveEnd: readerLeavesEnd }) - useNativeChatMessageListHandle(ref, jumpToLatest, readerScrollInput.revealFindMatch) + useNativeChatMessageListHandle(ref, jumpToLatest, readerScrollInput.revealFindMatch, dismiss) useNativeChatRailJumpLanding({ railJump, slots, @@ -398,6 +404,7 @@ export function NativeChatMessageList({ loadedMessageIds={loadedMessageIds} context={rowContext} window={transcriptWindow} + newRowsMarker={newRowsMarker} /> {liveLine ? ( + - {taskListState.list && taskListState.list.tasks.length > 0 ? ( -
-
- -
-
- ) : null} + ) diff --git a/src/renderer/src/components/native-chat/NativeChatNewRowsMarker.test.tsx b/src/renderer/src/components/native-chat/NativeChatNewRowsMarker.test.tsx new file mode 100644 index 00000000000..772c5a973c4 --- /dev/null +++ b/src/renderer/src/components/native-chat/NativeChatNewRowsMarker.test.tsx @@ -0,0 +1,109 @@ +// @vitest-environment happy-dom + +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { NativeChatNewRowsMarker } from './NativeChatNewRowsMarker' +import type { NativeChatNewRowsMarkerState as Marker } from './native-chat-new-rows-marker' +import { overrideLayoutProperty } from './native-chat-windowing-test-harness' + +afterEach(cleanup) + +function marker(overrides: Partial = {}): Marker { + return { + key: 'answer', + place: 'above-row', + arriving: true, + onArrived: vi.fn(), + onDismiss: vi.fn(), + ...overrides + } +} + +const VIEWPORT_PX = 600 +const ROW_TOP_PX = 1_000 + +let restoreLayout: (() => void)[] = [] + +/** A chat pane whose marker sits `intoRowPx` down its row. The view's bottom edge rests + * `aboveMarkerPx` short of the marker: 0 for a reader who was at the end of what there was. */ +function pane( + value: Marker, + { intoRowPx = 0, aboveMarkerPx = 0 }: { intoRowPx?: number; aboveMarkerPx?: number } = {} +) { + const isRow = (element: HTMLElement): boolean => element.hasAttribute('data-index') + const isMarker = (element: HTMLElement): boolean => element.getAttribute('role') === 'separator' + const layout = (name: string, get: (this: HTMLElement) => unknown) => + overrideLayoutProperty(name, { get }) + restoreLayout = [ + layout('clientHeight', () => VIEWPORT_PX), + layout('scrollTop', () => ROW_TOP_PX + intoRowPx - VIEWPORT_PX - aboveMarkerPx), + layout('offsetTop', function () { + return isRow(this) ? ROW_TOP_PX : isMarker(this) ? intoRowPx : 0 + }), + layout('offsetParent', function () { + return isRow(this) || isMarker(this) ? this.parentElement : null + }) + ] + const onReveal = vi.fn() + const view = render( +
+
+ +
+
+
+ +
+
+