test: remove 145 low-value tests and nine suites (#37982)

* test: remove 145 low-value test declarations

* test: clean up remaining type-only relay imports

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-03 16:59:13 -08:00
committed by GitHub
co-authored by OrcaWin
parent c332002953
commit 249faa46c3
48 changed files with 185 additions and 2761 deletions
@@ -10,7 +10,7 @@ import {
assignmentLeaseKeyId,
classifyAssignmentLease
} from './assignment-lease.js'
import { ASSIGNMENT_LEASE_SHADOW_EVENT, AssignmentLeaseShadow } from './assignment-lease-shadow.js'
import { AssignmentLeaseShadow } from './assignment-lease-shadow.js'
import type { RelayAssignmentStore } from './assignment-store.js'
import type { RelayConfig } from '../config.js'
import type { RelayCredentialStore } from '../credential-store.js'
@@ -61,14 +61,6 @@ const classify = async (value: string | undefined, helloEpoch = 4) =>
})
describe('assignment lease classification', () => {
it('names the key by a short hash of it', () => {
expect(assignmentLeaseKeyId(KEY)).toMatch(/^k1-[0-9a-f]{8}$/)
expect(assignmentLeaseKeyId(KEY)).toBe(
`k1-${createHash('sha256').update(KEY).digest('hex').slice(0, 8)}`
)
expect(assignmentLeaseKeyId(OTHER_KEY)).not.toBe(assignmentLeaseKeyId(KEY))
})
it('sorts every lease into one class', async () => {
expect(await classify(undefined)).toBe('absent')
expect(await classify(await lease())).toBe('valid')
@@ -90,58 +82,6 @@ describe('assignment lease classification', () => {
})
})
describe('assignment lease shadow', () => {
it('does nothing while the switch is off', () => {
const shadow = new AssignmentLeaseShadow({ enabled: () => false, key: KEY, cellId: CELL_ID })
expect(shadow.check({ lease: undefined, ...HOST, helloEpoch: 4 })).toBeNull()
})
it('counts each class against the database answer and logs one line per window', async () => {
let now = 0
const log = vi.fn()
const shadow = new AssignmentLeaseShadow({
enabled: () => true,
key: KEY,
cellId: CELL_ID,
now: () => now,
log
})
const valid = await lease()
const record = async (value: string | undefined, dbValid: boolean, dbReadMs: number) => {
const check = shadow.check({ lease: value, ...HOST, helloEpoch: 4 })
expect(check).not.toBeNull()
if (check) shadow.record(check, dbValid, dbReadMs)
await check
await Promise.resolve()
}
await record(valid, true, 2)
await record(valid, false, 4)
await record(undefined, true, 6)
expect(log).not.toHaveBeenCalled()
now = 60_000
await record(valid, true, 8)
expect(log).toHaveBeenCalledTimes(1)
expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toEqual({
event: ASSIGNMENT_LEASE_SHADOW_EVENT,
cellId: CELL_ID,
windowMs: 60_000,
classes: {
agree: 2,
disagree: 1,
absent: 1,
expired: 0,
badSignature: 0,
wrongHost: 0,
wrongCell: 0,
epochBehind: 0,
epochAhead: 0
},
dbRefused: 1,
dbReadMs: { samples: 4, p50: 4, p99: 8, max: 8 }
})
})
})
class FakeSocket extends EventEmitter {
readonly OPEN = 1
readonly CLOSED = 3
@@ -181,7 +121,13 @@ describe('host hello with the lease shadow on', () => {
{} as RelayCredentialStore,
{ verifyCellAssignment } as unknown as RelayAssignmentStore,
new ProcessQueuedByteBudget(),
{ recordAuth: vi.fn(), recordForwardedBytes: vi.fn(), recordHttp: vi.fn(), recordReconnect: vi.fn(), recordSql: vi.fn() },
{
recordAuth: vi.fn(),
recordForwardedBytes: vi.fn(),
recordHttp: vi.fn(),
recordReconnect: vi.fn(),
recordSql: vi.fn()
},
Date.now,
Math.random,
'incarnation-1',
@@ -189,7 +135,13 @@ describe('host hello with the lease shadow on', () => {
)
const socket = new FakeSocket()
const hostLease = input.withLease ? await lease({ relayHostId }) : undefined
registry.acceptControl(socket as unknown as WebSocket, identity, undefined, undefined, hostLease)
registry.acceptControl(
socket as unknown as WebSocket,
identity,
undefined,
undefined,
hostLease
)
socket.emit(
'message',
Buffer.from(
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { RelayHomeCellUnavailableError, type RelayAssignment } from './assignment-store.js'
import type { RelayAssignment } from './assignment-store.js'
import type { RelayConfig } from '../config.js'
const fakes = vi.hoisted(() => ({
@@ -42,9 +42,9 @@ describe('assignment rejection logging', () => {
expect(hinted.status).toBe(503)
expect(await hinted.json()).toEqual({ error: 'relay_capacity_exhausted' })
const line = warn.mock.calls.map((call) => String(call[0])).find((entry) =>
entry.includes('assignment rejected')
)
const line = warn.mock.calls
.map((call) => String(call[0]))
.find((entry) => entry.includes('assignment rejected'))
expect(line).toContain('route=assign')
expect(line).toContain('lane=sticky')
expect(line).toContain('hinted=true')
@@ -53,67 +53,6 @@ describe('assignment rejection logging', () => {
expect(line).not.toContain(host)
})
it('separates an unavailable home cell from capacity and names its cause', async () => {
const host = 'cccccccccccccccc'
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const app = createRelayApp(config(), {
store: {} as never,
assignments: {
assign: vi.fn(async () => {
throw new RelayHomeCellUnavailableError('cell-asia-1', 'not_ready')
}),
// The sticky lane refuses a host whose home cell is not live, so this
// arrives hinted on the placement lane.
resolve: vi.fn(async () => null)
} as never,
drain: vi.fn(),
ready: vi.fn(async () => true)
})
const response = await app.request('/v1/assign', assignmentRequest(host, { reconnect: true }))
expect(response.status).toBe(503)
expect(await response.json()).toEqual({ error: 'relay_home_cell_unavailable' })
const line = warn.mock.calls.map((call) => String(call[0])).find((entry) =>
entry.includes('assignment rejected')
)
expect(line).toContain('lane=placement')
expect(line).toContain('hinted=true')
expect(line).toContain('reason=relay_home_cell_unavailable')
expect(line).toContain('cause=not_ready')
expect(line).toContain('cell=cell-asia-1')
expect(line).not.toContain('relay_capacity_exhausted')
expect(line).not.toContain(host)
})
it('logs an unhinted placement rejection without the raw host id', async () => {
const host = 'gggggggggggggggg'
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const app = createRelayApp(config(), {
store: {} as never,
assignments: {
assign: vi.fn(async () => {
throw new Error('relay_connection_headroom_exhausted')
}),
resolve: vi.fn(async () => null)
} as never,
drain: vi.fn(),
ready: vi.fn(async () => true)
})
const response = await app.request('/v1/assign', assignmentRequest(host))
expect(response.status).toBe(503)
const line = warn.mock.calls.map((call) => String(call[0])).find((entry) =>
entry.includes('assignment rejected')
)
expect(line).toContain('route=assign')
expect(line).toContain('lane=placement')
expect(line).toContain('hinted=false')
expect(line).toContain('reason=relay_connection_headroom_exhausted')
expect(line).not.toContain(host)
})
it('names the throttled host once per window and closes it with the suppressed count', async () => {
vi.useFakeTimers()
const host = 'mmmmmmmmmmmmmmmm'
@@ -148,27 +87,6 @@ describe('assignment rejection logging', () => {
expect(throttled()[1]).toContain('suppressed=1')
expect(throttled()[1]).toContain(`host=${relayHostLogDigest(host)}`)
})
it('stays silent for successful assignments', async () => {
const host = 'ssssssssssssssss'
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const app = createRelayApp(config(), {
store: {} as never,
assignments: {
assign: vi.fn(async () => assignment('cell-r', host)),
resolve: vi.fn(async () => null)
} as never,
drain: vi.fn(),
ready: vi.fn(async () => true)
})
expect((await app.request('/v1/assign', assignmentRequest(host))).status).toBe(200)
expect(
warn.mock.calls.map((call) => String(call[0])).filter((entry) =>
entry.includes('assignment rejected')
)
).toEqual([])
})
})
function assignmentRequest(
@@ -224,80 +142,3 @@ function config(overrides: Partial<RelayConfig> = {}): RelayConfig {
...overrides
}
}
describe('assignment grant logging', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('logs sticky grants with cell id and host digest only', async () => {
const host = 'gggggggggggggggg'
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const app = createRelayApp(config(), {
store: {} as never,
assignments: {
assign: vi.fn(async () => assignment('cell-r', host)),
resolve: vi.fn(async () => assignment('cell-r', host))
} as never,
drain: vi.fn(),
ready: vi.fn(async () => true)
})
const response = await app.request('/v1/assign', assignmentRequest(host, { reconnect: true }))
expect(response.status).toBe(200)
const line = warn.mock.calls.map((call) => String(call[0])).find((entry) =>
entry.includes('assignment granted')
)
expect(line).toContain('lane=sticky')
expect(line).toContain('cell=cell-r')
expect(line).toContain(`host=${relayHostLogDigest(host)}`)
expect(line).not.toContain(host)
})
it('logs a hinted grant served by the placement lane', async () => {
const host = 'rrrrrrrrrrrrrrrr'
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const app = createRelayApp(config(), {
store: {} as never,
assignments: {
assign: vi.fn(async () => assignment('cell-new', host)),
resolve: vi.fn(async () => null)
} as never,
drain: vi.fn(),
ready: vi.fn(async () => true)
})
const response = await app.request('/v1/assign', assignmentRequest(host, { reconnect: true }))
expect(response.status).toBe(200)
const line = warn.mock.calls.map((call) => String(call[0])).find((entry) =>
entry.includes('assignment granted')
)
expect(line).toContain('lane=placement')
expect(line).toContain('hinted=true')
expect(line).toContain('cell=cell-new')
expect(line).not.toContain(host)
})
it('does not log unhinted placement grants', async () => {
const host = 'pppppppppppppppp'
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const app = createRelayApp(config(), {
store: {} as never,
assignments: {
assign: vi.fn(async () => assignment('cell-r', host)),
resolve: vi.fn(async () => null)
} as never,
drain: vi.fn(),
ready: vi.fn(async () => true)
})
expect((await app.request('/v1/assign', assignmentRequest(host))).status).toBe(200)
expect(
warn.mock.calls.map((call) => String(call[0])).filter((entry) =>
entry.includes('assignment granted')
)
).toEqual([])
})
})
@@ -34,11 +34,15 @@ describe('assignment reserve-mode behavior', () => {
// A store that has never read the set: the table is unreadable.
async function unknownSet(now: () => number) {
const unknown = new RelayAssignmentStore(database!, now)
await database!.query('ALTER TABLE relay_cell_admit_modes RENAME TO relay_cell_admit_modes_gone')
await database!.query(
'ALTER TABLE relay_cell_admit_modes RENAME TO relay_cell_admit_modes_gone'
)
return {
unknown,
restore: async () =>
await database!.query('ALTER TABLE relay_cell_admit_modes_gone RENAME TO relay_cell_admit_modes')
await database!.query(
'ALTER TABLE relay_cell_admit_modes_gone RENAME TO relay_cell_admit_modes'
)
}
}
@@ -61,55 +65,6 @@ describe('assignment reserve-mode behavior', () => {
expect(await store.releaseExpiredActivityLeases()).toBeGreaterThan(0)
})
it('logs an unreadable reserve-mode set at most once a minute', async () => {
let now = 1_000
await setup(() => now)
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const clock = vi.spyOn(performance, 'now')
let at = 1_000_000
clock.mockImplementation(() => at)
const { unknown, restore } = await unknownSet(() => now)
try {
for (let read = 0; read < 3; read += 1) {
expect(await unknown.reserveModeCells()).toBeNull()
at += 10_000
}
at += 60_000
expect(await unknown.reserveModeCells()).toBeNull()
const lines = warn.mock.calls.filter(([line]) => String(line).includes('orca_relay_reserve_mode_read_failed'))
expect(lines).toHaveLength(2)
} finally {
await restore()
clock.mockRestore()
warn.mockRestore()
}
})
it('tells break glass whether a cell is still heartbeating', async () => {
let now = 1_000
const store = await setup(() => now)
expect(await store.cellHeartbeatFresh('cell-a')).toBe(false)
await store.reconcileCells([{ ...CELLS[0]!, connectionHardCap: 600, connectionUnobservedBound: 50 }, CELLS[1]!])
await store.recordCellHeartbeat({
cellId: 'cell-a',
cellUrl: CELLS[0]!.url,
cellIncarnation: '11111111-1111-4111-8111-111111111111',
startedAt: 50,
ready: false,
observedRequests: 0,
totalConnections: 0,
inFlightConnections: 0,
reservedConnectionUnits: 0,
enforcedConnectionUnits: 0,
connectionHardCap: 600,
connectionUnobservedBound: 50
})
// Not ready is still alive: break glass is for a cell that has gone silent.
expect(await store.cellHeartbeatFresh('cell-a')).toBe(true)
now += 45_001
expect(await store.cellHeartbeatFresh('cell-a')).toBe(false)
})
it('keeps an idle host sticky on a reserve-mode cell where today it would be re-placed', async () => {
let now = 1_000
const store = await setup(() => now)
@@ -136,8 +91,13 @@ describe('assignment reserve-mode behavior', () => {
await setReserve(store, [first.cellId])
// The reserve ledger stamps last_activity_at and the cell keeps no activity rows.
now += 5 * 60_000
await database!.query('DELETE FROM relay_assignment_activity_leases WHERE user_id = ?', [identity.userId])
await database!.query('UPDATE relay_assignments SET last_activity_at = ? WHERE user_id = ?', [now, identity.userId])
await database!.query('DELETE FROM relay_assignment_activity_leases WHERE user_id = ?', [
identity.userId
])
await database!.query('UPDATE relay_assignments SET last_activity_at = ? WHERE user_id = ?', [
now,
identity.userId
])
await store.setCellAdmissionState(first.cellId, 'existing-only')
now += 2 * 60_000
expect(await store.assign(identity)).toMatchObject({
@@ -221,12 +181,16 @@ describe('assignment reserve-mode behavior', () => {
await store.changeActivity(identity, 'control', -1)
now += ASSIGNMENT_LIMITS.activityLeaseMs + ASSIGNMENT_LIMITS.dormantTtlMs + 1
expect([...((await store.reserveModeCells()) ?? [])]).toEqual([])
await database!.query('ALTER TABLE relay_cell_admit_modes RENAME TO relay_cell_admit_modes_gone')
await database!.query(
'ALTER TABLE relay_cell_admit_modes RENAME TO relay_cell_admit_modes_gone'
)
at += 61_000
expect(await store.reserveModeCells()).toBeNull()
expect((await store.assign(identity)).assignmentEpoch).toBeGreaterThan(first.assignmentEpoch)
} finally {
await database!.query('ALTER TABLE relay_cell_admit_modes_gone RENAME TO relay_cell_admit_modes').catch(() => undefined)
await database!
.query('ALTER TABLE relay_cell_admit_modes_gone RENAME TO relay_cell_admit_modes')
.catch(() => undefined)
clock.mockRestore()
}
})
@@ -277,11 +241,16 @@ describe('assignment reserve-mode behavior', () => {
source_request_units, target_reserved_units, expires_at, created_at, updated_at)
VALUES ('user-a', 'host000000000001', 'cell-b', 'cell-a', 1, 2, 0, 1, 9999, 1000, 1000)`
)
await expect(store.setCellAdmitMode('cell-a', 'reserve')).rejects.toThrow('cell_has_open_migration')
await expect(store.setCellAdmitMode('cell-a', 'reserve')).rejects.toThrow(
'cell_has_open_migration'
)
expect(await store.cellAdmitMode('cell-a')).toMatchObject({ admitMode: 'db' })
await store.setCellAdmitMode('cell-a', 'db')
await database!.query(`UPDATE relay_assignment_migrations SET aborted_at = 1001`)
await store.setCellAdmitMode('cell-a', 'reserve')
expect(await store.cellAdmitMode('cell-a')).toMatchObject({ admitMode: 'reserve', updatedAt: 1_000 })
expect(await store.cellAdmitMode('cell-a')).toMatchObject({
admitMode: 'reserve',
updatedAt: 1_000
})
})
})
+3 -67
View File
@@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type WebSocket from 'ws'
import { z } from 'zod'
import type { RelayAssignmentStore } from './assignment/assignment-store.js'
import { CELL_FLAG_DEFAULTS, type CellFlags } from './cell-flags.js'
import type { CellFlags } from './cell-flags.js'
import type { RelayConfig } from './config.js'
import type { RelayCredentialStore } from './credential-store.js'
import { HostSessionRegistry, type HostSession } from './host-session-registry.js'
@@ -227,26 +227,6 @@ describe('cell seat feed', () => {
vi.useRealTimers()
})
it('answers a first poll with every seated host, matching the controls count', async () => {
const { connect, poll, read } = createCell()
await connect(1)
await connect(2, { epoch: 4 })
expect((await poll()).status).toBe(200)
const body = await read()
expect(body).toMatchObject({
v: 1,
cellId: 'production-gce-c7',
incarnation,
seq: 2,
draining: false,
counts: { controls: 2, seats: 2 }
})
expect(body.full).toHaveLength(body.counts.seats)
expect(body.full).toContainEqual(
expect.objectContaining({ userId: 'user-2', epoch: 4, generation: 1, state: 'active' })
)
})
it('keeps a closing socket seated until its close lands, as the change log does', async () => {
const { connect, read } = createCell()
const socket = await connect(1)
@@ -270,10 +250,7 @@ describe('cell seat feed', () => {
const body = await read(`${incarnation}:${seq}`)
expect(body.full).toBeUndefined()
expect(body.draining).toBe(true)
expect(body.changes?.map((change) => change.kind)).toEqual([
'drain-only',
'leave'
])
expect(body.changes?.map((change) => change.kind)).toEqual(['drain-only', 'leave'])
expect(body.changes?.[1]).toMatchObject({
userId: 'user-1',
closeCode: RELAY_CLOSE_CODE.DRAINING,
@@ -322,9 +299,7 @@ describe('cell seat feed', () => {
await connect(1, { generation: 2 })
const body = await read(`${incarnation}:${seq}`)
// Readers drop the leave: its generation is older than the seat's.
expect(
body.changes?.map((change) => [change.kind, change.generation])
).toEqual([
expect(body.changes?.map((change) => [change.kind, change.generation])).toEqual([
['leave', 1],
['join', 2]
])
@@ -338,25 +313,6 @@ describe('cell seat feed', () => {
expect((await poll('not a cursor')).status).toBe(400)
})
it('reports the applied cell flags, so a flip can be read back through the feed and runtime status', async () => {
let applied: AppliedControlFlags<CellFlags> = { generation: 0, flags: CELL_FLAG_DEFAULTS }
const { read, app } = createCell(config(), () => applied)
expect((await read()).flagsApplied).toEqual({ generation: 0, flags: CELL_FLAG_DEFAULTS })
applied = { generation: 12, flags: { ...CELL_FLAG_DEFAULTS, readinessLocal: true, ticketCheck: 'shadow' } }
expect((await read()).flagsApplied).toEqual(applied)
const runtime = await app.request('/v1/admin/runtime-status', {
method: 'POST',
headers: { authorization: 'Bearer deploy-token', 'content-type': 'application/json' },
body: JSON.stringify({ v: 1 })
})
expect(await runtime.json()).toMatchObject({
flagsApplied: applied,
// Not reserve mode, so db whatever the switch says.
phonePathEffective: 'db',
databasePoolMax: expect.any(Number)
})
})
it('accepts only the directors rehome identity, verified once per poll', async () => {
const { poll } = createCell()
adminTokenChecks.count = 0
@@ -377,24 +333,4 @@ describe('cell seat feed', () => {
const director = createCell(config({ role: 'director', cellId: 'director' }))
expect((await director.poll()).status).toBe(404)
})
// Review B1: the feed must not need the pair on more cells, and must not change what a cell
// with or without it reports as its rehome protocol.
it('leaves the reported rehome protocol to the existing env', async () => {
const rehomeProtocol = async (relayConfig: RelayConfig) => {
const response = await createCell(relayConfig).app.request('/v1/admin/runtime-status', {
method: 'POST',
headers: { authorization: 'Bearer deploy-token', 'content-type': 'application/json' },
body: JSON.stringify({ v: 1 })
})
return z.object({ regionalRehomeProtocol: z.number() }).parse(await response.json())
.regionalRehomeProtocol
}
expect(await rehomeProtocol(config())).toBe(3)
expect(
await rehomeProtocol(
config({ rehomeAudience: undefined, rehomeDirectorServiceAccount: undefined })
)
).toBe(0)
})
})
@@ -21,7 +21,11 @@ import type { CellReserveClient } from './cell-reserve-client.js'
import { openInMemoryRelayDatabase, type RelayDatabase } from './database.js'
import { ReserveAssignment, type ReservePlacementMode } from './reserve-assignment.js'
import { ReservePlacer } from './reserve-placement.js'
import { ShadowSeatDirectory, type SeatFeedCell, type SeatFeedResponse } from './shadow-seat-directory.js'
import {
ShadowSeatDirectory,
type SeatFeedCell,
type SeatFeedResponse
} from './shadow-seat-directory.js'
const INCARNATION = '11111111-1111-4111-8111-111111111111'
const CELLS = [
@@ -66,7 +70,9 @@ describe('reserve-aware cell status', () => {
database = undefined
})
async function setup(options: { mode?: ReservePlacementMode; startedAt?: number; placement?: boolean } = {}) {
async function setup(
options: { mode?: ReservePlacementMode; startedAt?: number; placement?: boolean } = {}
) {
const now = { value: 100_000 }
database = await openInMemoryRelayDatabase()
const store = new RelayAssignmentStore(database, () => now.value, { heartbeatTtlMs: 45_000 })
@@ -98,7 +104,10 @@ describe('reserve-aware cell status', () => {
directory,
cells: () => cells,
startedAt: options.startedAt ?? 0,
placer: new ReservePlacer(() => now.value, () => 0.25),
placer: new ReservePlacer(
() => now.value,
() => 0.25
),
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the status route never books or demotes.
client: {} as CellReserveClient,
readRow: async () => null,
@@ -130,12 +139,19 @@ describe('reserve-aware cell status', () => {
const reserve = await store.reserveAwareCellDeploymentStatus('cell-a')
expect(reserve).toMatchObject({
admitMode: 'reserve',
status: { cellId: 'cell-a', runtime: { cellIncarnation: INCARNATION, ready: true, heartbeatFresh: true } }
status: {
cellId: 'cell-a',
runtime: { cellIncarnation: INCARNATION, ready: true, heartbeatFresh: true }
}
})
expect(Object.keys(reserve.status).filter((key) => PER_HOST_FIELDS.includes(key))).toEqual([])
await database!.query('ALTER TABLE relay_cell_admit_modes RENAME TO relay_cell_admit_modes_gone')
await database!.query(
'ALTER TABLE relay_cell_admit_modes RENAME TO relay_cell_admit_modes_gone'
)
const unknown = new RelayAssignmentStore(database!, () => 100_000)
await expect(unknown.reserveAwareCellDeploymentStatus('cell-b')).rejects.toThrow('reserve_mode_unknown')
await expect(unknown.reserveAwareCellDeploymentStatus('cell-b')).rejects.toThrow(
'reserve_mode_unknown'
)
})
it('keeps the 404 for a reserve cell without the opt-in, and 400s an unknown view', async () => {
@@ -146,18 +162,15 @@ describe('reserve-aware cell status', () => {
expect((await status({ cellId: 'cell-a', view: 'all' })).status).toBe(400)
})
it('answers a database cell with the opt-in as today, plus reserve: null', async () => {
const { status } = await setup()
const today = await statusBody(await status({ cellId: 'cell-b' }))
const response = await status({ cellId: 'cell-b', view: 'reserve-aware' })
expect(response.status).toBe(200)
expect(await response.json()).toEqual({ ...today, status: { ...today.status, reserve: null } })
})
it('answers a reserve cell from the map, with perHost null and never zeros', async () => {
const { directory, now, status } = await setup()
directory.apply('cell-a', feed('cell-a', { draining: true }), now.value, now.value - 1_500)
directory.apply('cell-b', feed('cell-b', { admitModeEffective: 'db', admitModeRaw: 'db' }), now.value, now.value)
directory.apply(
'cell-b',
feed('cell-b', { admitModeEffective: 'db', admitModeRaw: 'db' }),
now.value,
now.value
)
now.value += 20_000
const response = await status({ cellId: 'cell-a', view: 'reserve-aware' })
expect(response.status).toBe(200)
@@ -233,19 +246,25 @@ describe('reserve-aware cell status', () => {
// A starting director with one cell unheard.
const starting = await setup({ startedAt: 99_000 })
starting.directory.apply('cell-a', feed('cell-a'), starting.now.value, starting.now.value)
expect(await unavailable(await starting.status({ cellId: 'cell-a', view: 'reserve-aware' }))).toEqual(
expected('map-incomplete')
)
expect(
await unavailable(await starting.status({ cellId: 'cell-a', view: 'reserve-aware' }))
).toEqual(expected('map-incomplete'))
await database?.close()
const { directory, now, status } = await setup()
directory.apply('cell-b', feed('cell-b'), now.value, now.value)
directory.markNoFeed('cell-a', now.value)
expect(await unavailable(await status({ cellId: 'cell-a', view: 'reserve-aware' }))).toEqual(expected('no-feed'))
expect(await unavailable(await status({ cellId: 'cell-a', view: 'reserve-aware' }))).toEqual(
expected('no-feed')
)
// A feed without the drain flag or counts is an older image: no feed, never an empty cell.
directory.apply('cell-a', feed('cell-a', { draining: undefined }), now.value, now.value)
expect(await unavailable(await status({ cellId: 'cell-a', view: 'reserve-aware' }))).toEqual(expected('no-feed'))
expect(await unavailable(await status({ cellId: 'cell-a', view: 'reserve-aware' }))).toEqual(
expected('no-feed')
)
directory.apply('cell-a', feed('cell-a', { counts: undefined }), now.value, now.value)
expect(await unavailable(await status({ cellId: 'cell-a', view: 'reserve-aware' }))).toEqual(expected('no-feed'))
expect(await unavailable(await status({ cellId: 'cell-a', view: 'reserve-aware' }))).toEqual(
expected('no-feed')
)
// The process that answered the feed is not the one heartbeating the runtime row.
directory.apply(
'cell-a',
@@ -257,41 +276,6 @@ describe('reserve-aware cell status', () => {
expected('incarnation-mismatch')
)
})
it('probes rehome trust on a reserve cell from its runtime rows', async () => {
const { store } = await setup()
await database!.query(
`INSERT INTO relay_cell_capabilities (cell_id, cell_incarnation, regional_rehome_protocol, last_heartbeat_at)
VALUES ('cell-a', ?, 1, 100000)`,
[INCARNATION]
)
const sourceFetch = vi.fn(async () =>
Response.json({ v: 1, outcome: 'host-not-connected', sharedRuntimeIdentityRejected: true })
)
const app = createRelayApp(
config({
rehomeDirectorServiceAccount: 'relay-director@example.test',
rehomeAudience: 'https://relay.example.test/v1/admin/host-drain'
}),
{
store: {} as never,
assignments: store,
drain: vi.fn(),
regionalRehomeIdentityToken: vi.fn(async () => 'rehome-token'),
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the probe only awaits a Response.
regionalRehomeFetch: sourceFetch as unknown as typeof fetch,
ready: vi.fn(async () => true)
}
)
const response = await app.request('/v1/admin/regional-rehome-trust-probe', {
method: 'POST',
headers: { authorization: 'Bearer deploy-token', 'content-type': 'application/json' },
body: JSON.stringify({ v: 1, sourceCellId: 'cell-a', sourceCellIncarnation: INCARNATION })
})
expect(response.status).toBe(200)
expect(await response.json()).toMatchObject({ proven: true })
expect(sourceFetch).toHaveBeenCalledTimes(2)
})
})
async function statusBody(response: Response) {
@@ -2,11 +2,7 @@ import { ASSIGNMENT_LIMITS } from '@orca-cloud/relay-contract'
import { describe, expect, it, vi } from 'vitest'
import { RelayAssignmentStore } from './assignment/assignment-store.js'
import type { ControlRenewalOutcome } from './control-renewal-statement.js'
import {
openInMemoryRelayDatabase,
type RelayDatabase,
type SqlRow
} from './database.js'
import { openInMemoryRelayDatabase, type RelayDatabase, type SqlRow } from './database.js'
const now = 1_900_000_000_000
const expiresAt = now + 105_000
@@ -57,53 +53,6 @@ class RenewalStatementProbe implements RelayDatabase {
}
describe('batched control renewals on PostgreSQL', () => {
it('spends one statement on every host that came due', async () => {
const probe = new RenewalStatementProbe(() => 'renewed')
const store = new RelayAssignmentStore(probe, () => now)
const outcomes = await store.renewControlActivities([
renewal('user-a', 'host000000000001'),
renewal('user-a', 'host000000000002'),
renewal('user-b', 'host000000000003')
])
expect(outcomes).toEqual(['renewed', 'renewed', 'renewed'])
expect(probe.statements).toHaveLength(1)
expect(probe.statements[0]!.params[0]).toEqual(['user-a', 'user-a', 'user-b'])
expect(probe.statements[0]!.params[4]).toEqual([expiresAt, expiresAt, expiresAt])
})
it('locks assignment rows in primary-key order and still answers in input order', async () => {
const probe = new RenewalStatementProbe((userId) =>
userId === 'user-b' ? 'control_activity_moved' : 'renewed'
)
const store = new RelayAssignmentStore(probe, () => now)
const outcomes = await store.renewControlActivities([
renewal('user-c', 'host000000000003'),
renewal('user-a', 'host000000000002'),
renewal('user-b', 'host000000000001'),
renewal('user-a', 'host000000000001')
])
// (user_id, relay_host_id) is the primary key of relay_assignments, and the
// statement's ORDER BY repeats it: no batch can queue against another in a
// different sequence.
expect(probe.statements[0]!.params[0]).toEqual(['user-a', 'user-a', 'user-b', 'user-c'])
expect(probe.statements[0]!.params[1]).toEqual([
'host000000000001',
'host000000000002',
'host000000000001',
'host000000000003'
])
expect(outcomes).toEqual([
'renewed',
'renewed',
'control_activity_moved',
'renewed'
])
})
it('keeps a malformed request out of the statement and fails only that row', async () => {
const probe = new RenewalStatementProbe(() => 'renewed')
const store = new RelayAssignmentStore(probe, () => now)
@@ -148,39 +97,6 @@ describe('batched control renewals on PostgreSQL', () => {
}
})
it('runs the batch on the priority lane and the per-host fallback on the general one', async () => {
const probe = new RenewalStatementProbe(() => 'renewed')
probe.failuresRemaining = 1
const lanes: string[] = []
const database: RelayDatabase = {
dialect: 'postgres',
query: async (sql, params) => {
lanes.push('general')
return await probe.query(sql, params)
},
queryPriority: async (sql, params) => {
lanes.push('priority')
return await probe.query(sql, params)
},
queryLocked: async () => await probe.queryLocked(),
transaction: async () => await probe.transaction(),
close: async () => undefined
}
const store = new RelayAssignmentStore(database, () => now)
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
try {
await store.renewControlActivities([
renewal('user-a', 'host000000000001'),
renewal('user-a', 'host000000000002')
])
// A 200-row fallback would otherwise jump the whole general queue.
expect(lanes).toEqual(['priority', 'general', 'general'])
} finally {
warn.mockRestore()
}
})
it('does not fan out per host when the batch never got a connection', async () => {
const probe = new RenewalStatementProbe(() => 'renewed')
probe.failuresRemaining = 1
@@ -216,59 +132,6 @@ describe('batched control renewals on PostgreSQL', () => {
warn.mockRestore()
}
})
it('counts a lone renewal that threw before rethrowing it', async () => {
const probe = new RenewalStatementProbe(() => 'renewed')
probe.failuresRemaining = 1
const recordControlRenewal = vi.fn()
const store = new RelayAssignmentStore(probe, () => now, { recordControlRenewal })
// A one-row flush keeps the pre-batch contract and rethrows, but the metric
// still owes an outcome for the attempt.
await expect(
store.renewControlActivities([renewal('user-a', 'host000000000001')])
).rejects.toThrow('statement timeout')
expect(recordControlRenewal).toHaveBeenCalledTimes(1)
expect(recordControlRenewal.mock.calls[0]![1]).toBe('database_error')
})
it('counts a batch in which every row was rejected before the statement', async () => {
const probe = new RenewalStatementProbe(() => 'renewed')
const recordControlRenewal = vi.fn()
const store = new RelayAssignmentStore(probe, () => now, { recordControlRenewal })
const outcomes = await store.renewControlActivities([
{ ...renewal('user-a', 'host000000000001'), activityId: '' },
renewal('user-a', 'host000000000002', now - 1)
])
expect(outcomes).toEqual(['invalid_activity_id', 'invalid_activity_expiry'])
expect(probe.statements).toHaveLength(0)
expect(recordControlRenewal.mock.calls.map((call) => call[1])).toEqual([
'invalid_activity_id',
'invalid_activity_expiry'
])
})
it('counts one renewal outcome metric per row', async () => {
const probe = new RenewalStatementProbe((userId) =>
userId === 'user-b' ? 'assignment_not_found' : 'renewed'
)
const recordControlRenewal = vi.fn()
const store = new RelayAssignmentStore(probe, () => now, { recordControlRenewal })
await store.renewControlActivities([
renewal('user-a', 'host000000000001'),
renewal('user-b', 'host000000000002')
])
expect(recordControlRenewal).toHaveBeenCalledTimes(2)
expect(recordControlRenewal.mock.calls.map((call) => call[1])).toEqual([
'renewed',
'assignment_not_found'
])
})
})
describe('batched control renewals on SQLite', () => {
@@ -210,20 +210,6 @@ describe('host pre-auth latency', () => {
}
})
it('records a hello the database refuses on the db path', async () => {
const h = setup({ admitMode: 'db', databaseSays: false })
const { keyPair, identity } = hostKey()
const socket = new FakeSocket()
h.registry.acceptControl(socket.asWebSocket(), identity)
h.advance(120)
sendHello(socket, keyPair, identity.relayHostId)
await vi.waitFor(() => expect(socket.close).toHaveBeenCalled())
expect(socket.close).toHaveBeenCalledWith(RELAY_CLOSE_CODE.WRONG_CELL, 'wrong assignment epoch')
expect(h.take).not.toHaveBeenCalled()
expect(h.recordHostPreAuth).toHaveBeenCalledExactlyOnceWith('db', 'refused', 120)
})
it('records a hello timeout as a refusal before any check, once', () => {
vi.useFakeTimers()
const h = setup({ admitMode: 'db', databaseSays: true })
@@ -240,17 +226,6 @@ describe('host pre-auth latency', () => {
expect(h.recordHostPreAuth).toHaveBeenCalledExactlyOnceWith('beforeCheck', 'refused', 2_000)
})
it('records a host that hangs up before its hello as abandoned', () => {
const h = setup({ admitMode: 'db', databaseSays: true })
const { identity } = hostKey()
const socket = new FakeSocket()
h.registry.acceptControl(socket.asWebSocket(), identity)
h.advance(300)
socket.close()
expect(h.recordHostPreAuth).toHaveBeenCalledExactlyOnceWith('beforeCheck', 'abandoned', 300)
})
it.each([true, false])(
'records a database answer (%s) that arrives after the host hung up as abandoned',
async (valid) => {
@@ -270,33 +245,6 @@ describe('host pre-auth latency', () => {
expect(h.recordHostPreAuth).toHaveBeenCalledExactlyOnceWith('db', 'abandoned', 900)
}
)
it('records a host that hangs up during its proof as abandoned on its path', async () => {
const h = setup({ admitMode: 'reserve', databaseSays: false })
const { keyPair, identity } = hostKey()
const socket = new FakeSocket()
h.registry.acceptControl(socket.asWebSocket(), identity)
sendHello(socket, keyPair, identity.relayHostId)
await vi.waitFor(() => expect(socket.send).toHaveBeenCalledTimes(1))
h.advance(700)
socket.close()
expect(h.recordHostPreAuth).toHaveBeenCalledExactlyOnceWith('reserve', 'abandoned', 700)
})
it('records the pool-pressure shed as a refusal before any check', async () => {
const h = setup({ admitMode: 'reserve', databaseSays: true, databaseShedding: true })
h.take.mockReturnValue(null)
const { keyPair, identity } = hostKey()
const socket = new FakeSocket()
h.registry.acceptControl(socket.asWebSocket(), identity)
h.advance(40)
sendHello(socket, keyPair, identity.relayHostId)
await vi.waitFor(() => expect(socket.close).toHaveBeenCalled())
expect(h.verifyCellAssignment).not.toHaveBeenCalled()
expect(h.recordHostPreAuth).toHaveBeenCalledExactlyOnceWith('beforeCheck', 'refused', 40)
})
})
function config(): RelayConfig {
@@ -127,41 +127,6 @@ describe('POST /v1/admin/host-whereabouts', () => {
expect(capacity.status).toBe(401)
})
it('returns the database row, the map seats, the verdict and the named cells', async () => {
const lookup = vi.fn(async () => ROW)
const app = appWith(lookup, { shadowSeats: directory() })
const response = await app.request(
'/v1/admin/host-whereabouts',
request({ v: 1, relayHostId: HOST, userId: 'user-a' })
)
expect(response.status).toBe(200)
expect(lookup).toHaveBeenCalledWith({ userId: 'user-a', relayHostId: HOST })
expect(await response.json()).toMatchObject({
v: 1,
directorIncarnation: 'director-incarnation',
relayHostId: HOST,
shadow: { state: 'on', complete: true },
users: [
{
userId: 'user-a',
database: ROW,
seats: [{ cellId: 'cell-a', epoch: 3, state: 'active' }],
recentlyLeft: [],
verdict: { class: 'agree', explained: true }
}
],
cells: {
'cell-a': {
status: 'live',
lastFailure: null,
flagsApplied: { generation: '7', flags: { readinessLocal: false } }
}
}
})
})
it('names the user from the map when the request has only a host id', async () => {
const lookup = vi.fn(async () => ROW)
const app = appWith(lookup, { shadowSeats: directory() })
@@ -196,44 +161,6 @@ describe('POST /v1/admin/host-whereabouts', () => {
users: [{ database: ROW, verdict: { class: 'map-only-cell-unlive', explained: true } }]
})
})
it('answers an unknown host with an empty result, not a 404', async () => {
const lookup = vi.fn(async () => null)
const shadowOff = appWith(lookup)
const unknown = await shadowOff.request(
'/v1/admin/host-whereabouts',
request({ v: 1, relayHostId: 'uuuuuuuuuuuuuuuu' })
)
expect(unknown.status).toBe(200)
expect(await unknown.json()).toEqual({
v: 1,
directorIncarnation: 'director-incarnation',
relayHostId: 'uuuuuuuuuuuuuuuu',
shadow: { state: 'off' },
users: [],
cells: {}
})
expect(lookup).not.toHaveBeenCalled()
const named = await shadowOff.request(
'/v1/admin/host-whereabouts',
request({ v: 1, relayHostId: 'uuuuuuuuuuuuuuuu', userId: 'user-z' })
)
expect(await named.json()).toMatchObject({
users: [{ userId: 'user-z', database: null, seats: [], recentlyLeft: [], verdict: null }]
})
})
it('answers a briefly unreachable database with a retryable 503', async () => {
const app = appWith(async () => {
throw new Error('Connection terminated due to connection timeout')
})
const response = await app.request(
'/v1/admin/host-whereabouts',
request({ v: 1, relayHostId: HOST, userId: 'user-a' })
)
expect(response.status).toBe(503)
})
})
describe('RelayAssignmentStore.hostWhereabouts', () => {
@@ -1,10 +1,5 @@
import { describe, expect, it } from 'vitest'
import {
PLACEMENT_INTAKE_FLOOR,
PLACEMENT_INTAKE_SHARE,
PLACEMENT_INTAKE_WINDOW_MS,
PlacementLoadBand
} from './placement-load-band.js'
import { PLACEMENT_INTAKE_SHARE, PlacementLoadBand } from './placement-load-band.js'
import { RelayAssignmentStore } from './assignment/assignment-store.js'
import { openInMemoryRelayDatabase } from './database.js'
@@ -36,8 +31,7 @@ function simulate(
.filter((cell) => cell.reserved < cell.capacity)
.map((cell) => ({ cellId: cell.cellId, loadRatio: cell.reserved / cell.capacity, cell }))
.sort(
(left, right) =>
left.loadRatio - right.loadRatio || left.cellId.localeCompare(right.cellId)
(left, right) => left.loadRatio - right.loadRatio || left.cellId.localeCompare(right.cellId)
)
const director = directors[Math.floor(route() * directors.length)]
const picked = director?.pick(ranked, at(index))
@@ -73,26 +67,6 @@ function drainShare(paceMs: number, seed: number) {
}
describe('PlacementLoadBand', () => {
it('spreads a drain over an empty cell and four fuller cells within one window', () => {
const cells: SimulatedCell[] = [
{ cellId: 'cell-empty', reserved: 0, capacity: 6000 },
...['cell-b', 'cell-c', 'cell-d', 'cell-e'].map((cellId) => ({
cellId,
reserved: 2100,
capacity: 6000
}))
]
const director = new PlacementLoadBand(seededRandom(7), quiet)
const counts = simulate([director], cells, 2000, () => 1_000)
expect([...counts.values()].reduce((sum, count) => sum + count, 0)).toBe(2000)
expect(counts.get('cell-empty')).toBeLessThanOrEqual(PLACEMENT_INTAKE_SHARE * 2000 + 1)
for (const cellId of ['cell-b', 'cell-c', 'cell-d', 'cell-e']) {
expect(counts.get(cellId)).toBeGreaterThan(200)
}
for (const cell of cells) expect(cell.reserved).toBeLessThanOrEqual(cell.capacity)
})
it.each([
['1,200 s', 1_200_000],
['300 s', 300_000]
@@ -105,74 +79,6 @@ describe('PlacementLoadBand', () => {
for (const cell of cells) expect(cell.reserved).toBeLessThanOrEqual(cell.capacity)
}
})
it('gives the empty cell about 60% when only one other cell is eligible', () => {
const cells: SimulatedCell[] = [
{ cellId: 'cell-empty', reserved: 0, capacity: 6000 },
{ cellId: 'cell-b', reserved: 2100, capacity: 6000 }
]
const counts = simulate([new PlacementLoadBand(seededRandom(4), quiet)], cells, 1000, () => 1)
const share = (counts.get('cell-empty') ?? 0) / 1000
expect(share).toBeGreaterThan(0.55)
expect(share).toBeLessThan(0.65)
})
it('logs one summary a minute with skips, fallbacks and band size', () => {
const lines: Record<string, unknown>[] = []
const band = new PlacementLoadBand(seededRandom(6), (line) => lines.push(JSON.parse(line)))
const ranked = [
{ cellId: 'cell-a', loadRatio: 0 },
{ cellId: 'cell-b', loadRatio: 0.5 }
]
for (let second = 0; second <= 60; second += 1) band.pick(ranked, second * 1_000)
expect(lines).toHaveLength(1)
expect(lines[0]).toMatchObject({
event: 'orca_relay_placement_load_band',
windowMs: 60_000,
picks: 61,
bandSizeMax: 1
})
expect(lines[0]?.budgetSkips).toBeGreaterThan(0)
expect(lines[0]?.fallbacks).toBeGreaterThan(0)
})
it('falls back to the least-loaded cell when every candidate is over budget', () => {
const band = new PlacementLoadBand(seededRandom(3), quiet)
const only = [{ cellId: 'cell-a', loadRatio: 0.5 }]
for (let index = 0; index < 50; index += 1) {
expect(band.pick(only, 1_000)?.cellId).toBe('cell-a')
}
expect(band.pick([], 1_000)).toBeUndefined()
})
it('keeps quiet placement on the least-loaded cell outside the band', () => {
let now = 0
const band = new PlacementLoadBand(seededRandom(5), quiet)
const ranked = [
{ cellId: 'cell-a', loadRatio: 0.1 },
{ cellId: 'cell-b', loadRatio: 0.3 }
]
for (let index = 0; index < 20; index += 1) {
now += PLACEMENT_INTAKE_WINDOW_MS / PLACEMENT_INTAKE_FLOOR
expect(band.pick(ranked, now)?.cellId).toBe('cell-a')
}
})
it('picks at random among cells inside the band', () => {
const band = new PlacementLoadBand(seededRandom(9), quiet)
const ranked = [
{ cellId: 'cell-a', loadRatio: 0.3 },
{ cellId: 'cell-b', loadRatio: 0.32 },
{ cellId: 'cell-c', loadRatio: 0.4 }
]
const picked = new Set<string>()
let now = 0
for (let index = 0; index < 40; index += 1) {
now += PLACEMENT_INTAKE_WINDOW_MS
picked.add(band.pick(ranked, now)?.cellId ?? '')
}
expect([...picked].sort()).toEqual(['cell-a', 'cell-b'])
})
})
describe('leastLoadedCell with the load band', () => {
@@ -10,8 +10,6 @@ import {
SLOW_CONNECTION_WINDOW,
SlowConnectionRetirement
} from './postgres-connection-latency.js'
import { DIRECTOR_SLOW_CONNECTION_STALLS } from './test-fixtures/director-slow-connection-stalls.js'
import { DIRECTOR_TAIL_CONNECTION_STALLS } from './test-fixtures/director-tail-connection-stalls.js'
let clock = 0
@@ -19,7 +17,10 @@ type Slot = { name: string; statementMs: () => number }
// Hands out its connections in turn; a client released with a truthy value is replaced by a fresh
// 3 ms one, as pg-pool would dial a new connection on the next ask.
function pool(slots: Slot[], options: { retiresSlowConnections?: boolean } = { retiresSlowConnections: true }) {
function pool(
slots: Slot[],
options: { retiresSlowConnections?: boolean } = { retiresSlowConnections: true }
) {
const destroyed: string[] = []
let dialed = 0
let turn = 0
@@ -167,18 +168,6 @@ describe('slow connection retirement', () => {
expect(tx.destroyed).toEqual(['slow'])
})
it('opens no connection itself, so the pool never grows', async () => {
const slow = pool([
{ name: 'a', statementMs: steady(3) },
{ name: 'slow', statementMs: steady(55) }
])
await runStatements(slow.database, SLOW_CONNECTION_WINDOW * 2)
expect(slow.destroyed).toEqual(['slow'])
// Two original connections plus the one redial that replaced the retired client.
expect(slow.dialed()).toBe(3)
})
it('retires at most one connection per pool per interval', async () => {
const two = pool([
{ name: 'a', statementMs: steady(3) },
@@ -221,7 +210,9 @@ describe('slow connection retirement', () => {
expect(retiredAt.length).toBeLessThanOrEqual(3)
expect(retiredAt.length).toBeGreaterThan(0)
expect(Math.max(...retiredAt)).toBeLessThan(SLOW_CONNECTION_PEER_SLACK_MS + SLOW_CONNECTION_RETIRE_INTERVAL_MS)
expect(Math.max(...retiredAt)).toBeLessThan(
SLOW_CONNECTION_PEER_SLACK_MS + SLOW_CONNECTION_RETIRE_INTERVAL_MS
)
})
// Every 50th statement takes `outlierMs`: a 2% tail, like the ~200/400/800 ms retransmit stalls.
@@ -239,7 +230,11 @@ describe('slow connection retirement', () => {
await runStatements(lossy.database, 600)
expect(lossy.destroyed).toEqual(['lossy'])
expect(JSON.parse(warnings[0] ?? '{}')).toMatchObject({ shape: 'tail', tailMs: 415, peerTailMs: 3 })
expect(JSON.parse(warnings[0] ?? '{}')).toMatchObject({
shape: 'tail',
tailMs: 415,
peerTailMs: 3
})
})
it('retires nothing when every connection has the same tail, as a whole-database spike does', async () => {
@@ -256,13 +251,6 @@ describe('slow connection retirement', () => {
expect(far.destroyed).toEqual([])
})
it.each([
['14:58', 320, '14:58:22'],
['15:24', 367, '15:24:22']
] as const)('replays the %s tail stall of 2026-10-26 and retires c%i in the window ending %s', (stall, id, windowEnd) => {
expect(replayTail(stall)).toEqual([{ connectionId: id, windowEnd }])
})
it('lets one peer with a slow statement of its own block a tail retirement', async () => {
const noisy = pool([
{ name: 'a', statementMs: steady(3) },
@@ -307,88 +295,9 @@ describe('slow connection retirement', () => {
retirement.shouldRetire(live, 51)
expect(retirement.trackedClients).toBe(1)
})
it('replays the 09:38 stall of 2026-10-26 and retires only its slow connection', () => {
expect(replay('09:38')).toEqual([{ connectionId: 11, windowEnd: '09:38:21' }])
})
it('replays the 09:51 stall, where a second connection (c31, 38 ms) also turned slow', () => {
expect(replay('09:51')).toEqual([
{ connectionId: 19, windowEnd: '09:51:51' },
{ connectionId: 31, windowEnd: '09:52:51' }
])
})
})
function fakeClient(): pg.PoolClient {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the retirement rule only uses clients as identity keys.
return {} as pg.PoolClient
}
// Spreads each 30 s window's statements evenly at its median and releases after every statement,
// as autocommit queries do. Stops judging a connection once it is retired.
function replay(stall: '09:38' | '09:51'): { connectionId: number; windowEnd: string }[] {
const at = (time: string) => Date.parse(`2026-10-26T${time}Z`)
const samples = DIRECTOR_SLOW_CONNECTION_STALLS.filter(([name]) => name === stall).flatMap(
([, windowEnd, connectionId, statements, medianMs]) =>
Array.from({ length: statements }, (_, index) => ({
at: at(windowEnd) - 30_000 + ((index + 0.5) * 30_000) / statements,
windowEnd,
connectionId,
medianMs
}))
)
samples.sort((a, b) => a.at - b.at)
const clients = new Map<number, pg.PoolClient>()
const retired: { connectionId: number; windowEnd: string }[] = []
const retirement = new SlowConnectionRetirement(() => clock)
for (const sample of samples) {
if (retired.some((entry) => entry.connectionId === sample.connectionId)) {
continue
}
clock = sample.at
const client = clients.get(sample.connectionId) ?? fakeClient()
clients.set(sample.connectionId, client)
retirement.track(client)
noteConnectionStatement(client, sample.medianMs)
if (retirement.shouldRetire(client, sample.connectionId)) {
retired.push({ connectionId: sample.connectionId, windowEnd: sample.windowEnd })
}
}
return retired
}
// Spreads each window's statements evenly at its median, with ceil(1%) of them at its p99 and one at
// its max, and releases after every statement. Stops judging a connection once it is retired.
function replayTail(stall: '14:58' | '15:24'): { connectionId: number; windowEnd: string }[] {
const at = (time: string) => Date.parse(`2026-10-26T${time}Z`)
const samples = DIRECTOR_TAIL_CONNECTION_STALLS.filter(([name]) => name === stall).flatMap(
([, windowEnd, connectionId, statements, medianMs, p99Ms, maxMs]) => {
const tailEvery = Math.floor(statements / Math.ceil(statements * 0.01))
return Array.from({ length: statements }, (_, index) => ({
at: at(windowEnd) - 30_000 + ((index + 0.5) * 30_000) / statements,
windowEnd,
connectionId,
ms: index === statements - 1 ? maxMs : (index + 1) % tailEvery === 0 ? p99Ms : medianMs
}))
}
)
samples.sort((a, b) => a.at - b.at)
const clients = new Map<number, pg.PoolClient>()
const retired: { connectionId: number; windowEnd: string }[] = []
const retirement = new SlowConnectionRetirement(() => clock)
for (const sample of samples) {
if (retired.some((entry) => entry.connectionId === sample.connectionId)) {
continue
}
clock = sample.at
const client = clients.get(sample.connectionId) ?? fakeClient()
clients.set(sample.connectionId, client)
retirement.track(client)
noteConnectionStatement(client, sample.ms)
if (retirement.shouldRetire(client, sample.connectionId)) {
retired.push({ connectionId: sample.connectionId, windowEnd: sample.windowEnd })
}
}
return retired
}
@@ -1,11 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { RelayAssignmentRowBusyError, type RelayAssignment } from './assignment/assignment-store.js'
import type { RelayConfig } from './config.js'
import type { StickySlotSample } from './sticky-slot-statements.js'
import {
ScriptedPoolClient,
scriptedPostgresDatabase
} from './test-fixtures/scripted-postgres-pool.js'
const fakes = vi.hoisted(() => ({
verifyRelayToken: vi.fn(async (token: string) => ({
@@ -105,26 +100,6 @@ describe('public assignment reconnect lane', () => {
expect(assign).toHaveBeenCalledTimes(1)
})
it('sends an unverified reconnect hint through the placement lane unchanged', async () => {
const host = 'nnnnnnnnnnnnnnnn'
const assign = vi.fn(async () => assignment('cell-n', host))
const resolve = vi.fn(async () => null)
const outcomes: string[] = []
const app = createRelayApp(config(), {
store: {} as never,
assignments: { assign, resolve } as never,
drain: vi.fn(),
ready: vi.fn(async () => true),
recordAssignmentAdmission: (outcome) => outcomes.push(outcome)
})
const response = await app.request('/v1/assign', assignmentRequest(host, { reconnect: true }))
expect(response.status).toBe(200)
expect(assign).toHaveBeenCalledTimes(1)
expect(outcomes).toEqual(['placement'])
})
it('rejects on the fast lane when the verification probe fails transiently', async () => {
const host = 'tttttttttttttttt'
const assign = vi.fn()
@@ -208,70 +183,6 @@ describe('public assignment reconnect lane', () => {
expect(await response.json()).toEqual({ error: 'relay_assignment_row_busy' })
warn.mockRestore()
})
it('reports what each sticky slot spent its hold on', async () => {
vi.useFakeTimers({ toFake: ['performance'] })
const first = 'ffffffffffffffff'
const queued = 'qqqqqqqqqqqqqqqq'
const newcomer = 'nnnnnnnnnnnnnnnn'
const firstHost = new ScriptedPoolClient()
const queuedHost = new ScriptedPoolClient()
// first: verify, assign; newcomer: assign; queued: verify, assign.
const database = scriptedPostgresDatabase([
firstHost,
firstHost,
new ScriptedPoolClient(),
queuedHost,
queuedHost
])
const firstAssign = deferred<RelayAssignment>()
const firstReady = deferred<void>()
const resolve = vi.fn(async ({ relayHostId }: { relayHostId: string }) => {
await database.query('SELECT verify')
return assignment('cell-r', relayHostId)
})
const assign = vi.fn(async ({ relayHostId }: { relayHostId: string }) => {
await database.transaction(async (transaction) => {
await transaction.query('SELECT pin')
await transaction.query('UPDATE lease')
})
if (relayHostId === first) {
firstReady.resolve()
return await firstAssign.promise
}
return assignment('cell-r', relayHostId)
})
const samples: StickySlotSample[] = []
const app = createRelayApp(config(), {
store: {} as never,
assignments: { assign, resolve } as never,
drain: vi.fn(),
ready: vi.fn(async () => true),
recordStickySlot: (sample) => samples.push(sample)
})
const holding = app.request('/v1/assign', assignmentRequest(first, { reconnect: true }))
await firstReady.promise
expect(firstHost.statements).toHaveLength(5)
const waiting = app.request('/v1/assign', assignmentRequest(queued, { reconnect: true }))
// A placement request's statements are its own, even while a sticky slot is held.
expect((await app.request('/v1/assign', assignmentRequest(newcomer))).status).toBe(200)
await vi.advanceTimersByTimeAsync(60)
firstAssign.resolve(assignment('cell-r', first))
expect((await holding).status).toBe(200)
expect((await waiting).status).toBe(200)
// Verify, then BEGIN, two statements and COMMIT, each on that host's own connection.
const roundTrips = (sample: StickySlotSample | undefined) =>
[...(sample?.statementMsByConnection ?? [])].map(([id, ms]) => [id, ms.length])
expect(samples).toHaveLength(2)
expect(samples[0]).toMatchObject({ statements: 5 })
expect(roundTrips(samples[0])).toEqual([[1, 5]])
expect(samples[0]?.queueWaitMs).toBe(0)
expect(samples[1]).toMatchObject({ statements: 5 })
expect(roundTrips(samples[1])).toEqual([[3, 5]])
expect(samples[1]?.queueWaitMs).toBe(60)
})
})
function assignmentRequest(relayHostId: string, extra: { reconnect?: boolean } = {}): RequestInit {
@@ -1,8 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { RelayAssignmentStore } from './assignment/assignment-store.js'
import { rehomeSourceRegionAllowed } from './idle-regional-rehome-selection.js'
import { openIdleRehomeTestDatabase } from './idle-regional-rehome-test-database.js'
import { REGIONAL_REHOME_POLL_SUMMARY_INTERVAL_MS } from './regional-rehome-poll-telemetry.js'
type Region = 'us-central1' | 'asia-east2'
const cell = (id: string, region: Region) => ({
@@ -141,12 +139,6 @@ afterEach(() => {
})
describe('rehome source-region filter', () => {
it('keys on the source region, never on the target region', () => {
expect(rehomeSourceRegionAllowed('us-central1', 'us-central1')).toBe(true)
expect(rehomeSourceRegionAllowed('asia-east2', 'us-central1')).toBe(false)
expect(rehomeSourceRegionAllowed('asia-east2', undefined)).toBe(true)
})
it('keeps US to Asia moves and drops Asia to US moves', async () => {
const context = await setup('us-central1')
const candidates = await context.store.selectIdleRegionalRehomeCandidates(context.safety())
@@ -168,23 +160,6 @@ describe('rehome source-region filter', () => {
await context.database.close()
})
it('counts the skipped source cells in the poll summary line', async () => {
const context = await setup('us-central1')
const lines: string[] = []
vi.spyOn(console, 'warn').mockImplementation((line: unknown) => {
lines.push(String(line))
})
await context.store.selectIdleRegionalRehomeCandidates(context.safety())
context.advance(REGIONAL_REHOME_POLL_SUMMARY_INTERVAL_MS)
await context.beatAll()
await context.store.selectIdleRegionalRehomeCandidates(context.safety())
const summary = lines
.map((line) => JSON.parse(line) as Record<string, unknown>)
.find(({ event }) => event === 'orca_relay_regional_rehome_poll_summary')
expect(summary).toMatchObject({ open: 2, skippedOffRegionSourceCells: 2 })
await context.database.close()
})
it('reports the filtered hosts in the preview, agreeing with selection', async () => {
const context = await setup('us-central1')
const preview = await context.store.previewRegionalRehomeEligibility(context.safety())
@@ -1,11 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { RelayAssignmentStore } from './assignment/assignment-store.js'
import type { RelayConfig } from './config.js'
import {
combineRegionalRehomeSafety,
REGIONAL_REHOME_RECONNECTS_PER_CELL_LIMIT,
regionalRehomeSafetyFailure
} from './regional-rehome-safety.js'
import { startRegionalRehomeWorker } from './regional-rehome-worker.js'
describe('regional rehome worker', () => {
@@ -14,12 +9,16 @@ describe('regional rehome worker', () => {
it('bounds empty polling to the six-second cadence and stops its timer', async () => {
vi.useFakeTimers()
const selectIdleRegionalRehomeCandidates = vi.fn().mockResolvedValue([])
const worker = startRegionalRehomeWorker(config(), {
selectIdleRegionalRehomeCandidates
} as unknown as RelayAssignmentStore, {
safetySnapshot: () => safety(Date.now()),
random: () => 0
})!
const worker = startRegionalRehomeWorker(
config(),
{
selectIdleRegionalRehomeCandidates
} as unknown as RelayAssignmentStore,
{
safetySnapshot: () => safety(Date.now()),
random: () => 0
}
)!
try {
await vi.advanceTimersByTimeAsync(0)
expect(selectIdleRegionalRehomeCandidates).toHaveBeenCalledTimes(1)
@@ -36,56 +35,6 @@ describe('regional rehome worker', () => {
}
})
it('passes unsafe process telemetry to the durable claim gate', async () => {
let now = 0
let sqlFailures = 0
const selectIdleRegionalRehomeCandidates = vi.fn().mockResolvedValue([])
const assignments = {
selectIdleRegionalRehomeCandidates
} as unknown as RelayAssignmentStore
const worker = startRegionalRehomeWorker(config(), assignments, {
now: () => now,
safetySnapshot: () => ({ ...safety(now), sqlFailures }),
intervalMs: 60_000
})!
await settleWorker()
selectIdleRegionalRehomeCandidates.mockClear()
now = 100
sqlFailures = 1
await worker.run()
worker.stop()
expect(selectIdleRegionalRehomeCandidates).toHaveBeenCalledWith(
expect.objectContaining({ observedAt: 100, sqlFailures: 1 })
)
})
it('starts on directors with no candidates and stays disabled on cells', async () => {
let now = 0
const selectIdleRegionalRehomeCandidates = vi.fn().mockResolvedValue([])
const assignments = {
selectIdleRegionalRehomeCandidates
} as unknown as RelayAssignmentStore
const worker = startRegionalRehomeWorker(config(), assignments, {
now: () => now,
safetySnapshot: () => safety(now),
intervalMs: 60_000
})
expect(worker).not.toBeNull()
await settleWorker()
selectIdleRegionalRehomeCandidates.mockClear()
now = 100
await worker!.run()
worker!.stop()
expect(selectIdleRegionalRehomeCandidates).toHaveBeenCalledOnce()
expect(
startRegionalRehomeWorker(config({ role: 'cell' }), {} as RelayAssignmentStore, {
safetySnapshot: () => safety(1)
})
).toBeNull()
})
it('stops walking the page when the source names a deferral no candidate can pass', async () => {
const fetchImpl = respondWith([{ outcome: 'deferred', reason: 'concurrency-limit' }])
const summaries = collectSummaries()
@@ -143,39 +92,6 @@ describe('regional rehome worker', () => {
outcomes: { 'deferred:host-unsupported': 1, busy: 1, committed: 1 }
})
})
it('counts a source that answers with an error in the same summary', async () => {
const fetchImpl = vi.fn(async () => new Response('nope', { status: 503 }))
const summaries = collectSummaries()
try {
await runOnePoll(fetchImpl, 2, summaries)
} finally {
summaries.restore()
}
expect(summaries.entries[0]).toMatchObject({ dispatched: 2, outcomes: { failed: 2 } })
})
it('treats the reconnect threshold as per-cell and excludes the director', () => {
const cells = 2
const limit = cells * REGIONAL_REHOME_RECONNECTS_PER_CELL_LIMIT
const processSafety = { ...safety(100), reconnects: limit * 10 }
const fleetSafety = { ...safety(100), reconnects: limit }
expect(
regionalRehomeSafetyFailure(
combineRegionalRehomeSafety(processSafety, fleetSafety),
100,
cells
)
).toBeNull()
expect(
regionalRehomeSafetyFailure(
combineRegionalRehomeSafety(processSafety, { ...fleetSafety, reconnects: limit + 1 }),
100,
cells
)
).toBe('elevated_reconnects')
})
})
function respondWith(bodies: { outcome: string; reason?: string }[]) {
@@ -271,8 +187,3 @@ function safety(observedAt: number) {
databasePoolWaitMsMax: 0
}
}
async function settleWorker(): Promise<void> {
await Promise.resolve()
await Promise.resolve()
}
@@ -45,15 +45,6 @@ describe('Relay regional placement', () => {
afterEach(async () => await database.close())
it('prefers the requested region and keeps unhinted placement US-first', async () => {
await expect(
store.assign({ userId: 'asia-user', relayHostId: 'asiahost00000001' }, 'asia-east2')
).resolves.toMatchObject({ cellId: 'asia-c1', region: 'asia-east2' })
await expect(
store.assign({ userId: 'us-user', relayHostId: 'ushost0000000001' })
).resolves.toMatchObject({ cellId: 'us-c1', region: 'us-central1' })
})
it('falls back globally only when the target region has no safe general cell', async () => {
await store.configureCell(CELLS[1]!, 'migration-only')
await store.configureCell(CELLS[2]!, 'migration-only')
@@ -89,8 +80,9 @@ describe('Relay regional placement', () => {
'us-central1'
)
).resolves.toMatchObject({ cellId: 'us-c1', region: 'us-central1' })
expect(await database.query(`SELECT preferred_region FROM relay_assignment_region_preferences`))
.toEqual([{ preferred_region: 'asia-east2' }])
expect(
await database.query(`SELECT preferred_region FROM relay_assignment_region_preferences`)
).toEqual([{ preferred_region: 'asia-east2' }])
})
it('returns at most two deterministic healthy general probe origins per region', async () => {
@@ -203,14 +195,6 @@ describe('Relay regional placement', () => {
for (const cell of CELLS.slice(1)) await store.configureCell(cell, 'migration-only')
}
it('books the reserve cell rather than spill out of a full region', async () => {
await fillAsia()
const place = vi.fn(async () => onReserve)
await expect(assignAsia(place)).resolves.toEqual({ ...onReserve, placed: true })
expect(place).toHaveBeenCalledOnce()
expect(await database.query(`SELECT cell_id FROM relay_assignments`)).toEqual([])
})
it('answers a throttled reserve cell with its pace, not a cell abroad', async () => {
await fillAsia()
await expect(
@@ -233,15 +217,6 @@ describe('Relay regional placement', () => {
store.assign({ ...identity, relayHostId: 'spillhost0000002' }, 'asia-east2', 'asia-east2')
).resolves.toMatchObject({ cellId: 'us-c1' })
})
it('leaves a region with database room to the database lane', async () => {
const place = vi.fn(async () => onReserve)
await expect(assignAsia(place)).resolves.toMatchObject({
cellId: 'asia-c1',
region: 'asia-east2'
})
expect(place).not.toHaveBeenCalled()
})
})
it('expires identity-linked region preferences after 30 days', async () => {
+43 -34
View File
@@ -2,7 +2,6 @@ import { RELAY_CLOSE_CODE } from '@orca-cloud/relay-contract'
import { describe, expect, it } from 'vitest'
import { RelayAssignmentStore } from './assignment/assignment-store.js'
import { openInMemoryRelayDatabase } from './database.js'
import { hostCohortBucket } from './host-cohort-bucket.js'
import {
RESERVE_REHOME_HOST_COOLDOWN_MS,
RESERVE_REHOME_SWITCH_STALE_MS,
@@ -16,7 +15,10 @@ const ROOM: RehomeTargetRoom = { free: 500, ceiling: 1_000, bookable: true }
// 2,000 controls refill one token a minute.
const CONTROLS = 2_000
function admitted(rehome: ReserveRehome, overrides: Partial<Parameters<ReserveRehome['admit']>[0]> = {}) {
function admitted(
rehome: ReserveRehome,
overrides: Partial<Parameters<ReserveRehome['admit']>[0]> = {}
) {
return rehome.admit({
identity: HOST,
sourceCellId: 'us-r',
@@ -37,25 +39,14 @@ function warmed(percent = 100) {
}
describe('rehome on reconnect rules', () => {
it('is off at 0%, and admits a host whose every rule holds', () => {
expect(new ReserveRehome({ percent: 0, switchOpen: () => true }).enabled).toBe(false)
expect(admitted(warmed())).toBeNull()
})
it('stays off while the operator switch is closed', () => {
expect(admitted(new ReserveRehome({ percent: 100, switchOpen: () => false }))).toBe('switch')
})
it('moves only the cohort below the percent', () => {
const bucket = hostCohortBucket(HOST)
expect(admitted(new ReserveRehome({ percent: bucket, switchOpen: () => true }))).toBe('cohort')
expect(admitted(warmed(bucket + 1))).toBeNull()
})
it('leaves a host that another install dropped in the last 10 minutes', () => {
const peerDropped = { region: 'us-central1' as const, closeCode: RELAY_CLOSE_CODE.PEER_DROPPED }
expect(admitted(warmed(), { leaves: [{ ...peerDropped, at: 60_000 - 10 * 60_000 }] })).toBe('peer-dropped')
expect(admitted(warmed(), { leaves: [{ ...peerDropped, at: 60_000 - 10 * 60_000 - 1 }] })).toBeNull()
expect(admitted(warmed(), { leaves: [{ ...peerDropped, at: 60_000 - 10 * 60_000 }] })).toBe(
'peer-dropped'
)
expect(
admitted(warmed(), { leaves: [{ ...peerDropped, at: 60_000 - 10 * 60_000 - 1 }] })
).toBeNull()
})
it('leaves a host whose remembered leaves span regions, or that it moved within a day', () => {
@@ -64,7 +55,9 @@ describe('rehome on reconnect rules', () => {
{ region: 'asia-east2' as const, at: 2 }
]
expect(admitted(warmed(), { leaves })).toBe('moved-recently')
expect(admitted(warmed(), { leaves: [{ region: undefined, at: 1 }] })).toBe('unknown-leave-region')
expect(admitted(warmed(), { leaves: [{ region: undefined, at: 1 }] })).toBe(
'unknown-leave-region'
)
const rehome = warmed()
rehome.recordMoved(HOST, 0)
expect(admitted(rehome)).toBe('moved-recently')
@@ -72,17 +65,27 @@ describe('rehome on reconnect rules', () => {
})
it('moves no one into a region under 10% free, however full the source is', () => {
expect(admitted(warmed(), { target: { free: 20, ceiling: 1_000, bookable: true } })).toBe('target-room')
expect(admitted(warmed(), { target: { free: 99, ceiling: 1_000, bookable: true } })).toBe('target-room')
expect(admitted(warmed(), { target: { free: 20, ceiling: 1_000, bookable: true } })).toBe(
'target-room'
)
expect(admitted(warmed(), { target: { free: 99, ceiling: 1_000, bookable: true } })).toBe(
'target-room'
)
expect(admitted(warmed(), { target: { free: 100, ceiling: 1_000, bookable: true } })).toBeNull()
expect(admitted(warmed(), { target: { free: 500, ceiling: 1_000, bookable: false } })).toBe('target-room')
expect(admitted(warmed(), { target: { free: 0, ceiling: 0, bookable: false } })).toBe('target-room')
expect(admitted(warmed(), { target: { free: 500, ceiling: 1_000, bookable: false } })).toBe(
'target-room'
)
expect(admitted(warmed(), { target: { free: 0, ceiling: 0, bookable: false } })).toBe(
'target-room'
)
})
it('takes a source cell at 0.05% of its controls a minute, burst one, and refunds an unmade move', () => {
const rehome = warmed()
expect(admitted(rehome)).toBeNull()
expect(admitted(rehome, { identity: { ...HOST, relayHostId: 'qrstuvwxyzabcdef' } })).toBe('source-budget')
expect(admitted(rehome, { identity: { ...HOST, relayHostId: 'qrstuvwxyzabcdef' } })).toBe(
'source-budget'
)
expect(admitted(rehome, { sourceCellId: 'us-r2', now: 0 })).toBe('source-budget')
rehome.refund('us-r')
expect(admitted(rehome, { identity: { ...HOST, relayHostId: 'qrstuvwxyzabcdef' } })).toBeNull()
@@ -100,7 +103,10 @@ describe('rehome switch', () => {
enabled: true,
notBefore: 0
})
const control = new ReserveRehomeSwitch(async () => await answer(), () => now)
const control = new ReserveRehomeSwitch(
async () => await answer(),
() => now
)
expect(control.isOpen(now)).toBe(false)
await control.refresh()
expect(control.isOpen(now)).toBe(true)
@@ -129,14 +135,17 @@ describe('the rehome switch under a stalled read', () => {
let now = 1_000
let release!: (value: { enabled: boolean; notBefore: number }) => void
const reads: ('stalled' | 'off')[] = []
const control = new ReserveRehomeSwitch(async () => {
if (reads.length === 0) {
reads.push('stalled')
return await new Promise((resolve) => (release = resolve))
}
reads.push('off')
return { enabled: false, notBefore: 0 }
}, () => now)
const control = new ReserveRehomeSwitch(
async () => {
if (reads.length === 0) {
reads.push('stalled')
return await new Promise((resolve) => (release = resolve))
}
reads.push('off')
return { enabled: false, notBefore: 0 }
},
() => now
)
const stalled = control.refresh()
await control.refresh()
expect(reads).toEqual(['stalled'])
+1 -62
View File
@@ -5,11 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import type WebSocket from 'ws'
import type { RawData } from 'ws'
import { RELAY_ADMISSION_BUDGETS } from '@orca-cloud/relay-contract'
import {
ProcessQueuedByteBudget,
wireSplice,
type SpliceCloseInfo
} from './splice-forwarder.js'
import { ProcessQueuedByteBudget, wireSplice } from './splice-forwarder.js'
// SAFETY: ws exports Receiver at runtime; its published types omit this Writable constructor.
const { Receiver } = createRequire(import.meta.url)('ws') as {
@@ -39,29 +35,6 @@ class FakeSocket extends EventEmitter {
describe('splice forwarder', () => {
afterEach(() => vi.useRealTimers())
it('preserves text/binary opcodes and propagates peer close', () => {
const client = new FakeSocket()
const host = new FakeSocket()
const onClose = vi.fn()
const onForwardedBytes = vi.fn()
wireSplice({
client: client as unknown as WebSocket,
host: host as unknown as WebSocket,
budget: new ProcessQueuedByteBudget(),
onClose,
onForwardedBytes
})
client.emit('message', Buffer.from('text'), false)
client.emit('message', Buffer.from([1, 2]), true)
expect(host.sent).toEqual([
{ data: Buffer.from('text'), binary: false },
{ data: Buffer.from([1, 2]), binary: true }
])
expect(onForwardedBytes.mock.calls).toEqual([[4], [2]])
client.emit('close', 1000, Buffer.alloc(0))
expect(host.closes[0]?.code).toBe(4408)
expect(onClose).toHaveBeenCalledOnce()
})
it('hard-closes a splice exceeding the queue limit and releases its global queued-byte reservation', () => {
const client = new FakeSocket()
@@ -82,40 +55,6 @@ describe('splice forwarder', () => {
expect(budget.current()).toBe(0)
})
it('reports the close trigger so limit and oversize kills are attributable', () => {
const wire = (): { client: FakeSocket; host: FakeSocket; closes: SpliceCloseInfo[] } => {
const client = new FakeSocket()
const host = new FakeSocket()
const closes: SpliceCloseInfo[] = []
wireSplice({
client: client as unknown as WebSocket,
host: host as unknown as WebSocket,
budget: new ProcessQueuedByteBudget(),
onClose: vi.fn(),
onClosed: (closeInfo) => closes.push(closeInfo)
})
return { client, host, closes }
}
const queueLimit = wire()
queueLimit.host.bufferedAmount = 1024 * 1024
queueLimit.client.emit('message', Buffer.alloc(8 * 1024 * 1024), true)
queueLimit.client.emit('message', Buffer.alloc(512 * 1024), true)
expect(queueLimit.closes).toEqual([
{ code: 4429, reason: 'relay queue limit exceeded', trigger: 'queue-limit' }
])
// The ws receiver error for a frame above maxPayload names the payload cap.
const oversize = wire()
oversize.host.emit('error', new RangeError('Max payload size exceeded'))
expect(oversize.closes[0]?.trigger).toBe('host-oversize-frame')
const peerClose = wire()
peerClose.client.emit('close', 1001, Buffer.alloc(0))
expect(peerClose.closes[0]?.trigger).toBe('client-closed')
expect(peerClose.closes).toHaveLength(1)
})
it('queues one full catalog-sized frame for a backpressured peer without closing', () => {
// Large workspace catalogs exceed 1MiB; one maxFrameBytes frame must survive backpressure.
vi.useFakeTimers()
@@ -1,41 +0,0 @@
// Director instance ...48a393ae on 2026-10-26, from its runtime lines' stickySqlByConnection: per 30 s
// window ending at `windowEnd` (UTC), one pooled connection's statement count and median ms.
// Columns: stall, windowEnd, connectionId, statements, medianMs.
type StallWindow = readonly [
stall: '09:38' | '09:51',
windowEnd: string,
connectionId: number,
statements: number,
medianMs: number
]
export const DIRECTOR_SLOW_CONNECTION_STALLS: readonly StallWindow[] = [
['09:38', '09:37:21', 7, 1216, 3.536],
['09:38', '09:37:21', 10, 10, 3.595],
['09:38', '09:37:21', 11, 322, 3.796],
['09:38', '09:37:51', 7, 184, 3.535],
['09:38', '09:37:51', 11, 733, 3.592],
['09:38', '09:37:51', 12, 616, 3.435],
['09:38', '09:38:21', 11, 488, 52.398],
['09:38', '09:38:21', 13, 237, 3.481],
['09:38', '09:38:51', 11, 495, 58.993],
['09:38', '09:39:21', 11, 533, 56.129],
['09:38', '09:39:21', 12, 10, 4.379],
['09:38', '09:39:51', 11, 1052, 8.312],
['09:38', '09:39:51', 12, 213, 3.549],
['09:38', '09:40:21', 11, 713, 3.62],
['09:38', '09:40:21', 15, 868, 3.405],
['09:51', '09:50:51', 19, 654, 3.388],
['09:51', '09:50:51', 29, 829, 3.121],
['09:51', '09:51:21', 19, 835, 3.536],
['09:51', '09:51:21', 29, 382, 3.205],
['09:51', '09:51:51', 19, 498, 58.791],
['09:51', '09:52:21', 19, 536, 55.549],
['09:51', '09:52:51', 19, 19, 57.906],
['09:51', '09:52:51', 29, 991, 3.375],
['09:51', '09:52:51', 31, 354, 37.754],
['09:51', '09:53:21', 19, 506, 3.272],
['09:51', '09:53:21', 31, 784, 19.04],
['09:51', '09:53:51', 19, 1262, 3.304],
['09:51', '09:53:51', 31, 400, 3.205],
]
@@ -1,52 +0,0 @@
// Director instance ...1dc2e1c3 on 2026-10-26, from its runtime lines' stickySqlByConnection: per 30 s
// window ending at `windowEnd` (UTC), one pooled connection's statement count, median, p99 and max ms.
type TailStallWindow = readonly [
stall: '14:58' | '15:24',
windowEnd: string,
connectionId: number,
statements: number,
medianMs: number,
p99Ms: number,
maxMs: number
]
export const DIRECTOR_TAIL_CONNECTION_STALLS: readonly TailStallWindow[] = [
['14:58', '14:56:52', 315, 1053, 3.481, 10.815, 65.702],
['14:58', '14:56:52', 320, 727, 3.474, 11.571, 18.8],
['14:58', '14:56:52', 321, 10, 3.598, 6.737, 6.737],
['14:58', '14:57:22', 315, 741, 3.594, 11.245, 19.562],
['14:58', '14:57:22', 320, 584, 3.461, 8.032, 9.57],
['14:58', '14:57:52', 315, 1205, 3.462, 7.312, 20.124],
['14:58', '14:57:52', 320, 425, 3.523, 13.859, 18.517],
['14:58', '14:58:22', 315, 418, 3.395, 7.049, 17.142],
['14:58', '14:58:22', 320, 413, 4.854, 416.306, 815.523],
['14:58', '14:58:52', 315, 780, 3.506, 8.739, 51.87],
['14:58', '14:58:52', 320, 914, 4.543, 215.253, 418.243],
['14:58', '14:59:22', 315, 790, 3.453, 7.602, 16.841],
['14:58', '14:59:22', 320, 968, 3.517, 10.595, 214.811],
['14:58', '14:59:22', 324, 10, 3.296, 7.945, 7.945],
['14:58', '14:59:22', 325, 22, 3.277, 7.373, 7.373],
['14:58', '14:59:52', 315, 921, 3.425, 11.826, 52.059],
['14:58', '14:59:52', 325, 958, 3.422, 7.779, 17.631],
['15:24', '15:22:52', 363, 162, 3.531, 6.968, 8.816],
['15:24', '15:22:52', 365, 852, 3.307, 7.833, 13.862],
['15:24', '15:22:52', 366, 417, 3.645, 10.906, 17.662],
['15:24', '15:23:22', 365, 770, 3.341, 8.067, 20.72],
['15:24', '15:23:22', 366, 154, 3.492, 7.03, 7.406],
['15:24', '15:23:22', 367, 513, 3.654, 11.077, 211.46],
['15:24', '15:23:52', 365, 847, 3.438, 9.728, 29.608],
['15:24', '15:23:52', 367, 629, 3.476, 9.464, 17.448],
['15:24', '15:24:22', 365, 522, 3.309, 12.595, 25.105],
['15:24', '15:24:22', 367, 687, 4.551, 414.215, 417.657],
['15:24', '15:24:52', 367, 604, 4.967, 414.827, 418.756],
['15:24', '15:24:52', 368, 48, 3.75, 10.407, 10.407],
['15:24', '15:24:52', 370, 6, 4.097, 9.925, 9.925],
['15:24', '15:25:22', 367, 845, 4.71, 415.764, 818.912],
['15:24', '15:25:22', 370, 30, 3.399, 7.265, 7.265],
['15:24', '15:25:52', 367, 1220, 4.383, 213.454, 216.581],
['15:24', '15:25:52', 370, 196, 3.363, 13.136, 17.227],
['15:24', '15:25:52', 371, 420, 3.445, 18.831, 42.126],
['15:24', '15:26:22', 367, 570, 3.259, 13.859, 39.525],
['15:24', '15:26:22', 371, 792, 3.295, 8.899, 44.169],
['15:24', '15:26:22', 372, 286, 3.255, 6.977, 13.688],
]
@@ -1,110 +1,9 @@
import { describe, expect, it } from 'vitest'
import {
ConnectorDeliverySchema,
ConnectorPollResponseSchema,
ConnectorAcksRequestSchema
} from './delivery-messages.js'
import {
ConnectorEventSchema,
ConnectorEventsRequestSchema,
ConnectorPendingPromptSchema,
ConnectorReplyResponseSchema
} from './event-messages.js'
import { ConnectorHelloRequestSchema, ConnectorHelloResponseSchema } from './hello-messages.js'
import { ConnectorEventSchema } from './event-messages.js'
import { ConnectorHelloRequestSchema } from './hello-messages.js'
import { CONNECTOR_LIMITS } from './connector-limits.js'
const conversation = { source: 'slack', externalId: 'T1:C9:1792.9:U5' } as const
const base = { deliveryId: 'T1:C9:1793.0001', seq: 41, expiresAt: 1_793_000_300_000, conversation }
const start = { ...base, kind: 'start', agent: 'claude', placeId: 'pl_1', prompt: 'fix it' }
const poll = (deliveries: unknown[], extra: object = {}) =>
ConnectorPollResponseSchema.safeParse({
v: 1,
serverNow: 1_793_000_000_000,
holdMs: 50_000,
deliveries,
...extra
})
describe('poll response', () => {
it('parses every known delivery kind and drops unknown fields', () => {
const result = poll([
{ ...start, addedLater: true },
{ ...base, kind: 'send', text: 'more' },
{ ...base, kind: 'answer', promptId: 'p7', optionId: 'allow_once' },
{ ...base, kind: 'cancel' }
])
expect(result.success && result.data.deliveries.map((d) => d.kind)).toEqual([
'start',
'send',
'answer',
'cancel'
])
expect(result.data?.deliveries[0]).not.toHaveProperty('addedLater')
expect(result.data?.control).toBeUndefined()
})
it('rejects a delivery the cloud must never send', () => {
for (const delivery of [
{ ...base, kind: 'rewind' },
{ ...start, prompt: '' },
{ deliveryId: base.deliveryId },
{ ...base, conversation: { source: 'github', externalId: 'G1' }, kind: 'cancel' }
]) {
expect(ConnectorDeliverySchema.safeParse(delivery).success).toBe(false)
}
})
it('carries one control at a time', () => {
expect(poll([], { control: { stop: 'client_too_old' } }).data?.control).toEqual({
stop: 'client_too_old'
})
expect(poll([], { control: { pausedForMs: 0 } }).success).toBe(false)
expect(poll([], { control: { stop: 'client_too_old', reconnect: true } }).success).toBe(false)
expect(poll([], { control: { reconnect: true, addedLater: 1 } }).data?.control).toEqual({
reconnect: true
})
})
it('bounds the batch and the hold', () => {
const tooMany = Array.from({ length: CONNECTOR_LIMITS.maxDeliveriesPerPoll + 1 }, () => start)
expect(poll(tooMany).success).toBe(false)
expect(poll([], { holdMs: CONNECTOR_LIMITS.pollTimeoutMs }).success).toBe(false)
})
})
describe('acks', () => {
it('accepts received and done with each result outcome', () => {
const results = [
{ outcome: 'accepted', reopened: true },
{ outcome: 'refused', reason: 'prompt_not_pending' },
{ outcome: 'unknown', reason: 'no_local_record' },
{ outcome: 'expired' },
{ outcome: 'unsupported' }
]
expect(
ConnectorAcksRequestSchema.safeParse({
v: 1,
helloRev: 7,
acks: [
{ deliveryId: 'd1', step: 'received' },
...results.map((result) => ({ deliveryId: 'd1', step: 'done', result }))
]
}).success
).toBe(true)
expect(
ConnectorAcksRequestSchema.safeParse({
v: 1,
helloRev: 7,
acks: [{ deliveryId: 'd1', step: 'done' }]
}).success
).toBe(false)
})
it('names the hello they belong to', () => {
const acks = [{ deliveryId: 'd1', step: 'received' }]
expect(ConnectorAcksRequestSchema.safeParse({ v: 1, acks }).success).toBe(false)
})
})
describe('events', () => {
const turnEnded = {
@@ -116,58 +15,18 @@ describe('events', () => {
stopReason: null
}
it('names the turn that ended by its start', () => {
const { turnId: _, ...withoutTurnId } = turnEnded
expect(ConnectorEventSchema.safeParse(turnEnded).success).toBe(true)
expect(ConnectorEventSchema.safeParse(withoutTurnId).success).toBe(false)
})
it('caps the last assistant message in UTF-8 bytes', () => {
const limit = CONNECTOR_LIMITS.maxLastAssistantMessageBytes
expect(
ConnectorEventSchema.safeParse({ ...turnEnded, lastAssistantMessage: 'a'.repeat(limit) })
.success
).toBe(true)
expect(
ConnectorEventSchema.safeParse({ ...turnEnded, lastAssistantMessage: 'é'.repeat(limit / 2 + 1) })
.success
).toBe(false)
})
it('carries the pending prompt in snapshots', () => {
expect(
ConnectorEventSchema.safeParse({
kind: 'snapshot',
conversations: [
{
conversation,
paneKey: 'tab:leaf',
agent: 'codex',
working: false,
prompt: { kind: 'approval', promptId: 'p1', tool: 'Bash', options: [] }
}
]
...turnEnded,
lastAssistantMessage: 'é'.repeat(limit / 2 + 1)
}).success
).toBe(true)
})
it('numbers every events batch within the hello it belongs to', () => {
const events = [{ kind: 'turn-started', conversation, paneKey: 'tab:leaf', at: 1 }]
const parse = (body: object) => ConnectorEventsRequestSchema.safeParse({ v: 1, events, ...body })
expect(parse({ helloRev: 7, seq: 0 }).success).toBe(true)
expect(parse({ helloRev: 7 }).success).toBe(false)
expect(parse({ seq: 0 }).success).toBe(false)
expect(parse({ helloRev: 7, seq: -1 }).success).toBe(false)
})
it('carries a question the agent left without text, as the seam reports it', () => {
const prompt = {
kind: 'question',
promptId: 'p2',
question: '',
options: [{ id: '1', label: '' }]
}
expect(ConnectorPendingPromptSchema.safeParse(prompt).success).toBe(true)
).toBe(false)
})
})
@@ -214,37 +73,12 @@ describe('hello and replies', () => {
projects
}).success
const longest = 'x'.repeat(CONNECTOR_LIMITS.maxDisplayTextLength)
expect(hello(Array.from({ length: CONNECTOR_LIMITS.maxHelloProjects }, () => project(longest)))).toBe(true)
expect(
hello(Array.from({ length: CONNECTOR_LIMITS.maxHelloProjects }, () => project(longest)))
).toBe(true)
expect(hello([project(`${longest}x`)])).toBe(false)
expect(
hello(Array.from({ length: CONNECTOR_LIMITS.maxHelloProjects + 1 }, () => project('orca')))
).toBe(false)
})
it('answers a hello with the revision the cloud assigned', () => {
expect(ConnectorHelloResponseSchema.safeParse({ helloRev: 3 }).success).toBe(true)
expect(ConnectorHelloResponseSchema.safeParse({}).success).toBe(false)
})
it('reads a reply as posted or queued', () => {
expect(ConnectorReplyResponseSchema.parse({ status: 'queued' })).toEqual({ status: 'queued' })
expect(
ConnectorReplyResponseSchema.parse({ status: 'posted', messageId: '1793.0001' })
).toEqual({
status: 'posted',
messageId: '1793.0001'
})
})
it('carries a conversation from any connected platform', () => {
for (const source of ['slack', 'discord', 'linear']) {
expect(
ConnectorDeliverySchema.safeParse({
...base,
conversation: { source, externalId: 'x1' },
kind: 'cancel'
}).success
).toBe(true)
}
})
})
+7 -26
View File
@@ -676,10 +676,10 @@
"oracle": "After acknowledged phone replies with stale host status, the exact card disappears and Send is enabled; unknown, refused or unverifiable-settlement replies retain enabled choices, while an older host's accepted reply dismisses like an acknowledged one; a same-occurrence view toggle retains pending acknowledgment and real replacements discard it. A slow 32 KiB SSH answer completes all chunks before Enter; cancellation preserves issued bytes but omits remaining groups. Paired settlement invokes the actual provider and lost callbacks remain unknown; older hosts deliver ordinary messages, launch and selector groups once and their accepted answers dismiss as before. An answered card stays hidden across a view remount; a collapsed card is a strip that frees the composer and expands back; an identical question in a new wait shows again; a composer send caught by a new card keeps its bubble without submitting or clearing the line under it.",
"commands": [
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts src/renderer/src/components/native-chat/NativeChatComposer.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/lib/agent-paste-draft.test.ts src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts src/renderer/src/lib/agent-followup-delivery.test.ts src/main/ipc/pty/ipc/write-input-acceptance.test.ts src/main/ipc/pty/ipc/write-input-user-input.test.ts src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts src/main/ipc/pty/pty-write-ipc-validation.test.ts src/main/daemon/daemon-pty-write-settlement-recovery.test.ts src/main/runtime/runtime-terminal-writer.test.ts src/main/runtime/rpc/terminal-send.test.ts src/main/runtime/terminal-send-stale-leaf-liveness.test.ts src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts tests/e2e/native-chat-ssh-delivery.unit.test.ts --maxWorkers=2",
"ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/native-chat/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/native-chat/use-mobile-native-chat-controller.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/native-chat/MobileNativeChatView.stop.test.ts mobile/src/session/native-chat/MobileNativeChatView.failure-banner.test.ts mobile/src/session/native-chat/MobileNativeChatView.rows.test.ts mobile/src/session/native-chat/MobileNativeChatView.composer-lock.test.ts mobile/src/session/native-chat/MobileNativeChatView.tail-follow.test.ts mobile/src/session/native-chat/MobileNativeChatView.history-paging.test.ts mobile/src/session/mobile-prompt-controller.test.ts mobile/src/session/native-chat/input/mobile-native-chat-permission-send.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-cancel-ask.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-answer-send.test.ts mobile/src/session/native-chat/transcript/use-mobile-native-chat-message-send.test.ts mobile/src/session/native-chat/mobile-native-chat-send.test.ts mobile/src/session/native-chat/MobileNativeChatCardHeaderAction.test.tsx mobile/src/session/native-chat/input/MobileNativeChatPromptCard.test.tsx --maxWorkers=2",
"ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/native-chat/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/native-chat/use-mobile-native-chat-controller.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/native-chat/MobileNativeChatView.stop.test.ts mobile/src/session/native-chat/MobileNativeChatView.failure-banner.test.ts mobile/src/session/native-chat/MobileNativeChatView.composer-lock.test.ts mobile/src/session/native-chat/MobileNativeChatView.tail-follow.test.ts mobile/src/session/native-chat/MobileNativeChatView.history-paging.test.ts mobile/src/session/mobile-prompt-controller.test.ts mobile/src/session/native-chat/input/mobile-native-chat-permission-send.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-cancel-ask.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-answer-send.test.ts mobile/src/session/native-chat/transcript/use-mobile-native-chat-message-send.test.ts mobile/src/session/native-chat/mobile-native-chat-send.test.ts mobile/src/session/native-chat/input/MobileNativeChatPromptCard.test.tsx --maxWorkers=2",
"ORCA_BACKGROUND_LAUNCH=1 pnpm test tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts --maxWorkers=1",
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/lib/agent-paste-draft.test.ts src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts src/renderer/src/lib/agent-followup-delivery.test.ts src/main/ipc/pty/ipc/write-input-acceptance.test.ts src/main/ipc/pty/ipc/write-input-user-input.test.ts src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts src/main/ipc/pty/pty-write-ipc-validation.test.ts src/main/daemon/daemon-pty-write-settlement-recovery.test.ts --maxWorkers=2",
"ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/native-chat/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/native-chat/use-mobile-native-chat-controller.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/native-chat/MobileNativeChatView.stop.test.ts mobile/src/session/native-chat/MobileNativeChatView.failure-banner.test.ts mobile/src/session/native-chat/MobileNativeChatView.rows.test.ts mobile/src/session/native-chat/MobileNativeChatView.composer-lock.test.ts mobile/src/session/native-chat/MobileNativeChatView.tail-follow.test.ts mobile/src/session/native-chat/MobileNativeChatView.history-paging.test.ts --maxWorkers=2"
"ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/native-chat/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/native-chat/use-mobile-native-chat-controller.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/native-chat/MobileNativeChatView.stop.test.ts mobile/src/session/native-chat/MobileNativeChatView.failure-banner.test.ts mobile/src/session/native-chat/MobileNativeChatView.composer-lock.test.ts mobile/src/session/native-chat/MobileNativeChatView.tail-follow.test.ts mobile/src/session/native-chat/MobileNativeChatView.history-paging.test.ts --maxWorkers=2"
],
"testFiles": [
"src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx",
@@ -712,7 +712,6 @@
"mobile/src/session/native-chat/input/use-mobile-native-chat-ask-dismiss.test.ts",
"mobile/src/session/native-chat/MobileNativeChatView.stop.test.ts",
"mobile/src/session/native-chat/MobileNativeChatView.failure-banner.test.ts",
"mobile/src/session/native-chat/MobileNativeChatView.rows.test.ts",
"mobile/src/session/native-chat/MobileNativeChatView.composer-lock.test.ts",
"mobile/src/session/native-chat/MobileNativeChatView.tail-follow.test.ts",
"mobile/src/session/native-chat/MobileNativeChatView.history-paging.test.ts",
@@ -722,7 +721,6 @@
"mobile/src/session/native-chat/input/use-mobile-native-chat-answer-send.test.ts",
"mobile/src/session/native-chat/transcript/use-mobile-native-chat-message-send.test.ts",
"mobile/src/session/native-chat/mobile-native-chat-send.test.ts",
"mobile/src/session/native-chat/MobileNativeChatCardHeaderAction.test.tsx",
"mobile/src/session/native-chat/input/MobileNativeChatPromptCard.test.tsx",
"tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts"
],
@@ -814,16 +812,7 @@
"durationSeconds": 232.36,
"summary": "257 tests passed across 19 explicit files, covering production cards, observed sends, existing launch/paste callers, IPC validation, SSH acknowledgment and daemon settlement recovery."
},
{
"date": "2026-10-05",
"runner": "local",
"platform": "macos",
"command": "ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/use-mobile-native-chat-controller.test.ts mobile/src/session/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/MobileNativeChatView.stop.test.ts mobile/src/session/MobileNativeChatView.failure-banner.test.ts mobile/src/session/MobileNativeChatView.rows.test.ts mobile/src/session/MobileNativeChatView.composer-lock.test.ts mobile/src/session/MobileNativeChatView.tail-follow.test.ts mobile/src/session/MobileNativeChatView.history-paging.test.ts --maxWorkers=2",
"currentCommand": "ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/native-chat/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/native-chat/use-mobile-native-chat-controller.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/native-chat/MobileNativeChatView.stop.test.ts mobile/src/session/native-chat/MobileNativeChatView.failure-banner.test.ts mobile/src/session/native-chat/MobileNativeChatView.rows.test.ts mobile/src/session/native-chat/MobileNativeChatView.composer-lock.test.ts mobile/src/session/native-chat/MobileNativeChatView.tail-follow.test.ts mobile/src/session/native-chat/MobileNativeChatView.history-paging.test.ts --maxWorkers=2",
"result": "passed",
"durationSeconds": 10.32,
"summary": "73 tests passed across four explicit files after the controller extraction, covering the production view/card/composer, occurrence dismissal, controller session scoping, unknown/refused replies and stopping active dictation."
},
{
"date": "2026-10-06",
"runner": "local",
@@ -834,16 +823,7 @@
"durationSeconds": 14.53,
"summary": "391 tests passed across 29 explicit files after the round-4 review fix, including older-host acceptance, the inert hidden composer, collapse that keeps a partial answer and returns focus to the composer, pane-scoped card focus, wait-start occurrence identity and the pane remount signal."
},
{
"date": "2026-10-06",
"runner": "local",
"platform": "macos",
"command": "ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/use-mobile-native-chat-controller.test.ts mobile/src/session/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/MobileNativeChatView.stop.test.ts mobile/src/session/MobileNativeChatView.failure-banner.test.ts mobile/src/session/MobileNativeChatView.rows.test.ts mobile/src/session/MobileNativeChatView.composer-lock.test.ts mobile/src/session/MobileNativeChatView.tail-follow.test.ts mobile/src/session/MobileNativeChatView.history-paging.test.ts mobile/src/session/mobile-prompt-controller.test.ts mobile/src/session/mobile-native-chat-permission-send.test.ts mobile/src/session/use-mobile-native-chat-cancel-ask.test.ts mobile/src/session/use-mobile-native-chat-answer-send.test.ts mobile/src/session/use-mobile-native-chat-message-send.test.ts mobile/src/session/mobile-native-chat-send.test.ts mobile/src/session/MobileNativeChatCardHeaderAction.test.tsx mobile/src/session/MobileNativeChatPromptCard.test.tsx --maxWorkers=2",
"result": "passed",
"durationSeconds": 1.19,
"summary": "186 tests passed across 12 explicit files after the round-3 review fixes, including older-host answers dismissing like acknowledged ones, collapse that keeps the card mounted, one card per occurrence, wait-start question identity and only Send blocked.",
"currentCommand": "ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/native-chat/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/native-chat/use-mobile-native-chat-controller.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/native-chat/MobileNativeChatView.stop.test.ts mobile/src/session/native-chat/MobileNativeChatView.failure-banner.test.ts mobile/src/session/native-chat/MobileNativeChatView.rows.test.ts mobile/src/session/native-chat/MobileNativeChatView.composer-lock.test.ts mobile/src/session/native-chat/MobileNativeChatView.tail-follow.test.ts mobile/src/session/native-chat/MobileNativeChatView.history-paging.test.ts mobile/src/session/mobile-prompt-controller.test.ts mobile/src/session/native-chat/input/mobile-native-chat-permission-send.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-cancel-ask.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-answer-send.test.ts mobile/src/session/native-chat/transcript/use-mobile-native-chat-message-send.test.ts mobile/src/session/native-chat/mobile-native-chat-send.test.ts mobile/src/session/native-chat/MobileNativeChatCardHeaderAction.test.tsx mobile/src/session/native-chat/input/MobileNativeChatPromptCard.test.tsx --maxWorkers=2"
},
{
"date": "2026-10-06",
"runner": "local",
@@ -871,6 +851,8 @@
"evidence": "No polling, subprocess, startup await, new status producer or dismissal timer. Reuses the existing bounded per-tab committed observation and per-PTY queue; waits for each group before its existing pacing timer. Duplicate-cancel and yielding chunk tests bound writes. Optional paired proof reuses provider settlement only for requesting clients; legacy requests keep their raw path."
},
"knownGaps": [
"Historical receipt before suite removal; its original command and results are preserved without assigning them to remaining tests: {\"date\":\"2026-10-05\",\"runner\":\"local\",\"platform\":\"macos\",\"command\":\"ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/use-mobile-native-chat-controller.test.ts mobile/src/session/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/MobileNativeChatView.stop.test.ts mobile/src/session/MobileNativeChatView.failure-banner.test.ts mobile/src/session/MobileNativeChatView.rows.test.ts mobile/src/session/MobileNativeChatView.composer-lock.test.ts mobile/src/session/MobileNativeChatView.tail-follow.test.ts mobile/src/session/MobileNativeChatView.history-paging.test.ts --maxWorkers=2\",\"currentCommand\":\"ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/native-chat/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/native-chat/use-mobile-native-chat-controller.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/native-chat/MobileNativeChatView.stop.test.ts mobile/src/session/native-chat/MobileNativeChatView.failure-banner.test.ts mobile/src/session/native-chat/MobileNativeChatView.rows.test.ts mobile/src/session/native-chat/MobileNativeChatView.composer-lock.test.ts mobile/src/session/native-chat/MobileNativeChatView.tail-follow.test.ts mobile/src/session/native-chat/MobileNativeChatView.history-paging.test.ts --maxWorkers=2\",\"result\":\"passed\",\"durationSeconds\":10.32,\"summary\":\"73 tests passed across four explicit files after the controller extraction, covering the production view/card/composer, occurrence dismissal, controller session scoping, unknown/refused replies and stopping active dictation.\"}",
"Historical receipt before suite removal; its original command and results are preserved without assigning them to remaining tests: {\"date\":\"2026-10-06\",\"runner\":\"local\",\"platform\":\"macos\",\"command\":\"ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/use-mobile-native-chat-controller.test.ts mobile/src/session/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/MobileNativeChatView.stop.test.ts mobile/src/session/MobileNativeChatView.failure-banner.test.ts mobile/src/session/MobileNativeChatView.rows.test.ts mobile/src/session/MobileNativeChatView.composer-lock.test.ts mobile/src/session/MobileNativeChatView.tail-follow.test.ts mobile/src/session/MobileNativeChatView.history-paging.test.ts mobile/src/session/mobile-prompt-controller.test.ts mobile/src/session/mobile-native-chat-permission-send.test.ts mobile/src/session/use-mobile-native-chat-cancel-ask.test.ts mobile/src/session/use-mobile-native-chat-answer-send.test.ts mobile/src/session/use-mobile-native-chat-message-send.test.ts mobile/src/session/mobile-native-chat-send.test.ts mobile/src/session/MobileNativeChatCardHeaderAction.test.tsx mobile/src/session/MobileNativeChatPromptCard.test.tsx --maxWorkers=2\",\"result\":\"passed\",\"durationSeconds\":1.19,\"summary\":\"186 tests passed across 12 explicit files after the round-3 review fixes, including older-host answers dismissing like acknowledged ones, collapse that keeps the card mounted, one card per occurrence, wait-start question identity and only Send blocked.\",\"currentCommand\":\"ORCA_BACKGROUND_LAUNCH=1 mobile/node_modules/.bin/vitest run --config mobile/vitest.config.ts mobile/src/session/native-chat/MobileNativeChatView.prompt-presentation.test.ts mobile/src/session/native-chat/use-mobile-native-chat-controller.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-ask-dismiss.test.ts mobile/src/session/native-chat/MobileNativeChatView.stop.test.ts mobile/src/session/native-chat/MobileNativeChatView.failure-banner.test.ts mobile/src/session/native-chat/MobileNativeChatView.rows.test.ts mobile/src/session/native-chat/MobileNativeChatView.composer-lock.test.ts mobile/src/session/native-chat/MobileNativeChatView.tail-follow.test.ts mobile/src/session/native-chat/MobileNativeChatView.history-paging.test.ts mobile/src/session/mobile-prompt-controller.test.ts mobile/src/session/native-chat/input/mobile-native-chat-permission-send.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-cancel-ask.test.ts mobile/src/session/native-chat/input/use-mobile-native-chat-answer-send.test.ts mobile/src/session/native-chat/transcript/use-mobile-native-chat-message-send.test.ts mobile/src/session/native-chat/mobile-native-chat-send.test.ts mobile/src/session/native-chat/MobileNativeChatCardHeaderAction.test.tsx mobile/src/session/native-chat/input/MobileNativeChatPromptCard.test.tsx --maxWorkers=2\"}",
"Provider acknowledgment proves handoff, not agent consumption or prompt closure; a host prompt gate remains deferred pending captured closing lifecycles.",
"Rendered Electron and physical mobile/microphone behavior require remote QA; native surfaces are mocked in these unit tests.",
"Physical WSL, Linux/Windows, paired mixed-version deployment and network fault campaigns have not been exercised.",
@@ -6078,8 +6060,7 @@
"a trusted press on an external anchor reports it once and stops the navigation, including through what the anchor wraps and an SVG animated href",
"a fragment link scrolls in the document, raw or percent-encoded, and a bare hash goes to the top",
"a sibling preview document is left to the guest's own navigation, and a middle click opens nothing whether or not it is trusted",
"an SVG link is resolved against the document, so its relative, rooted and fragment forms behave exactly like the identical HTML anchor",
"the preload reports on exactly the channel main listens on"
"an SVG link is resolved against the document, so its relative, rooted and fragment forms behave exactly like the identical HTML anchor"
]
},
{
-1
View File
@@ -7108,7 +7108,6 @@
"src/packages/process-host/src/wsl-distro-allowlist.test.ts": 55,
"src/preload/api/notification-sound-playback.test.ts": 64,
"src/preload/api/platform-bridge.test.ts": 48,
"src/preload/api/wsl-hosts-api.test.ts": 45,
"src/preload/app-restart-checkpoint-routing.test.ts": 2028,
"src/preload/browser-client-page-renderer-requests.test.ts": 154,
"src/preload/browser-extension-content-script-relay.test.ts": 65,
@@ -40,11 +40,6 @@ function firstMermaid(tree: ReactTestRenderer) {
}
describe('MobileMarkdown mermaid routing', () => {
it('routes a closed mermaid fence to MermaidDiagram', () => {
const tree = render('```mermaid\ngraph TD; A-->B\n```')
expect(mermaidCount(tree)).toBe(1)
})
it('keeps a streaming (unterminated) mermaid fence as raw code', () => {
const tree = render('```mermaid\ngraph TD; A-->B')
expect(mermaidCount(tree)).toBe(0)
@@ -75,9 +70,4 @@ describe('MobileMarkdown mermaid routing', () => {
)
expect(firstMermaid(tree)).not.toBe(initial)
})
it('does not route other code fences to MermaidDiagram', () => {
const tree = render('```ts\nconst a = 1\n```')
expect(mermaidCount(tree)).toBe(0)
})
})
@@ -34,15 +34,6 @@ afterEach(async () => {
})
describe('troubleshooting paired endpoints', () => {
it('loads endpoint advice before the user runs diagnostics', async () => {
hosts.catalog.mockResolvedValue([{ endpoint: 'ws://desktop.local:6769' }])
await act(async () => {
tree = create(createElement(Harness))
})
expect(state.current?.diagnosticStatus).toBe('idle')
expect(state.current?.hostEndpoints).toEqual(['ws://desktop.local:6769'])
})
it('keeps the newly probed endpoints when the initial catalog read finishes later', async () => {
let finishCatalog: (value: { endpoint: string }[]) => void = () => {}
hosts.catalog
@@ -55,25 +55,6 @@ describe('Markdown preview images', () => {
}
)
it('draws an SVG from its source text, since hosts read SVG as text', async () => {
const markup = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 4"/>'
const client = {
sendRequest: vi.fn(async () => ({
id: '1',
ok: true as const,
_meta: { runtimeId: 'remote' },
result: { content: markup, truncated: false, byteLength: markup.length }
}))
}
await expect(
loadMarkdownPreviewImage(client, 'wt', 'docs/readme.md', 'logo.svg')
).resolves.toBe(`data:image/svg+xml;charset=utf-8,${encodeURIComponent(markup)}`)
expect(client.sendRequest).toHaveBeenCalledWith('files.read', {
worktree: 'id:wt',
relativePath: 'docs/logo.svg'
})
})
it('lets native Image load public URLs without sending them to the workspace host', async () => {
const client = host()
await expect(
@@ -81,10 +62,4 @@ describe('Markdown preview images', () => {
).resolves.toBe('https://example.com/a.png')
expect(client.sendRequest).not.toHaveBeenCalled()
})
it('reports a disconnected host instead of trying to read phone-local files', async () => {
await expect(loadMarkdownPreviewImage(null, 'wt', 'readme.md', 'a.png')).rejects.toThrow(
'Waiting for desktop'
)
})
})
@@ -1,86 +0,0 @@
import { createElement } from 'react'
import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { HostProfile } from '../transport/types'
import { MobileHomeTasksAndAutomations } from './MobileHomeTasksAndAutomations'
vi.mock('react-native', () => ({
Pressable: 'Pressable',
StyleSheet: { create: <T,>(styles: T) => styles, hairlineWidth: 1 },
Text: 'Text',
View: 'View'
}))
vi.mock('lucide-react-native', () => ({
CalendarClock: 'CalendarClock',
ChevronRight: 'ChevronRight',
ListTodo: 'ListTodo'
}))
vi.mock('../components/PickerModal', () => ({ PickerModal: 'PickerModal' }))
function host(id: string): HostProfile {
return {
id,
name: id,
endpoint: `ws://${id}.local:6768`,
deviceToken: `token-${id}`,
publicKeyB64: `key-${id}`,
lastConnected: 1
}
}
describe('Home Tasks and Automations sections', () => {
let tree: ReactTestRenderer | null = null
afterEach(() => {
act(() => tree?.unmount())
tree = null
})
function render(connectedHosts: HostProfile[]) {
const callbacks = { onOpenTasks: vi.fn(), onOpenAutomations: vi.fn() }
act(() => {
tree = create(createElement(MobileHomeTasksAndAutomations, { connectedHosts, ...callbacks }))
})
return callbacks
}
function row(label: string): ReactTestInstance {
return tree!.root
.findAll((node) => String(node.type) === 'Pressable')
.find((node) =>
node
.findAll((child) => String(child.type) === 'Text')
.some((child) => child.props.children === label)
)!
}
function picker(title: string): ReactTestInstance {
return tree!.root
.findAll((node) => String(node.type) === 'PickerModal')
.find((node) => node.props.title === title)!
}
it('opens the only connected host from the Automations row, placed right after Tasks', () => {
const callbacks = render([host('desk')])
const rows = tree!.root.findAll((node) => String(node.type) === 'Pressable')
expect(rows.indexOf(row('Automations'))).toBe(rows.indexOf(row('Tasks')) + 1)
expect(row('Automations').props.disabled).toBe(false)
act(() => row('Automations').props.onPress())
expect(callbacks.onOpenAutomations).toHaveBeenCalledExactlyOnceWith('desk')
expect(callbacks.onOpenTasks).not.toHaveBeenCalled()
})
it('keeps the automation host choice independent and disables both rows without a connection', () => {
const callbacks = render([host('desk'), host('laptop')])
act(() => row('Automations').props.onPress())
expect(picker('Open Automations On').props.visible).toBe(true)
expect(picker('Open Tasks On').props.visible).toBe(false)
act(() => picker('Open Automations On').props.onSelect('laptop'))
act(() => picker('Open Automations On').props.onAfterClose())
expect(callbacks.onOpenAutomations).toHaveBeenCalledExactlyOnceWith('laptop')
expect(callbacks.onOpenTasks).not.toHaveBeenCalled()
act(() => tree?.unmount())
render([])
expect(row('Tasks').props.disabled).toBe(true)
expect(row('Automations').props.disabled).toBe(true)
})
})
@@ -2,9 +2,7 @@ import { beforeEach, expect, it, vi } from 'vitest'
import { AppState } from 'react-native'
import {
DEFAULT_NOTIFICATION_DELIVERY,
loadNotificationDeliveryPreferences,
notificationPreferencesFilter,
saveNotificationDeliveryPreferences
loadNotificationDeliveryPreferences
} from './notification-delivery-preferences'
import {
setNotificationViewingWorkspace,
@@ -14,10 +12,7 @@ import {
const storage = new Map<string, string>()
vi.mock('@react-native-async-storage/async-storage', () => ({
default: {
getItem: vi.fn(async (key: string) => storage.get(key) ?? null),
setItem: vi.fn(async (key: string, value: string) => {
storage.set(key, value)
})
getItem: vi.fn(async (key: string) => storage.get(key) ?? null)
}
}))
vi.mock('react-native', () => ({ AppState: { currentState: 'background' } }))
@@ -27,45 +22,6 @@ beforeEach(() => {
AppState.currentState = 'background'
})
it('persists only phone-specific delivery preferences', async () => {
expect(await loadNotificationDeliveryPreferences()).toEqual(DEFAULT_NOTIFICATION_DELIVERY)
const value = {
...DEFAULT_NOTIFICATION_DELIVERY,
onlyWhenDesktopAway: false,
sound: false
}
await saveNotificationDeliveryPreferences(value)
expect(await loadNotificationDeliveryPreferences()).toEqual(value)
expect(notificationPreferencesFilter(value)).toEqual({
onlyWhenDesktopAway: false,
onlyWhenAgentNeedsInput: false,
sound: false
})
})
it('ignores unrelated stored preferences', async () => {
storage.set(
'orca:notificationDeliveryPreferences',
JSON.stringify({
onlyWhenDesktopAway: false,
sound: false,
suppressWhileViewing: false,
unrelatedSetting: false
})
)
expect(await loadNotificationDeliveryPreferences()).toEqual({
onlyWhenDesktopAway: false,
onlyWhenAgentNeedsInput: false,
sound: false,
suppressWhileViewing: false
})
expect(notificationPreferencesFilter(await loadNotificationDeliveryPreferences())).toEqual({
onlyWhenDesktopAway: false,
onlyWhenAgentNeedsInput: false,
sound: false
})
})
it('suppresses only the workspace being viewed on this phone, and never while backgrounded', async () => {
const event = { source: 'terminal-bell', worktreeId: 'folder-id' }
setNotificationViewingWorkspace({ hostId: 'ssh-host', worktreeId: 'folder-id' })
@@ -1,80 +0,0 @@
import { createElement } from 'react'
import { act, create } from 'react-test-renderer'
import { describe, expect, it, vi } from 'vitest'
import { normalizeExecutionHostId } from '../../../src/shared/execution-host'
import { WorkspaceExecutionHostContext } from '../navigation/workspace-execution-host'
import { mountFixture } from '../test-support/rpc-recording/recorder-fixture-shape'
import type { ActionSheetAction } from '../components/ActionSheetModal'
const sheets = vi.hoisted(() => ({ labelsByTitle: new Map<string, string[]>() }))
vi.mock('react-native', () => ({ Platform: { OS: 'ios' } }))
vi.mock('lucide-react-native', () => ({
Copy: 'Icon',
FileText: 'Icon',
Globe: 'Icon',
RefreshCw: 'Icon',
SquareTerminal: 'Icon'
}))
vi.mock('../platform/clipboard', () => ({ useClipboardWriter: () => ({}) }))
vi.mock('../platform/haptics', () => ({ triggerSuccess: () => {}, triggerError: () => {} }))
vi.mock('../components/ActionSheetModal', () => ({
ActionSheetModal: ({ title, actions }: { title: string; actions: ActionSheetAction[] }) => {
sheets.labelsByTitle.set(
title,
actions.map((action) => action.label)
)
return null
}
}))
vi.mock('../components/TextInputModal', () => ({ TextInputModal: () => null }))
vi.mock('../components/ConfirmModal', () => ({ ConfirmModal: () => null }))
vi.mock('../components/CustomKeyModal', () => ({ CustomKeyModal: () => null }))
vi.mock('../components/MobileDictationSetupSheet', () => ({
MobileDictationSetupSheet: () => null
}))
vi.mock('./MobileSessionTerminalLayoutSheet', () => ({
MobileSessionTerminalLayoutSheet: () => null
}))
vi.mock('./QuickCommandsSheet', () => ({ QuickCommandsSheet: () => null }))
vi.mock('./MobileBrowserTabActionSheet', () => ({ MobileBrowserTabActionSheet: () => null }))
vi.mock('./mobile-terminal-action-sheet-actions', () => ({
getMobileTerminalActionSheetActions: () => []
}))
import { MobileSessionSheets } from './MobileSessionSheets'
async function newTabLabels(executionHost?: string): Promise<string[]> {
sheets.labelsByTitle.clear()
const controller = mountFixture<Parameters<typeof MobileSessionSheets>[0]['controller']>({
worktreeId: 'repo::/wt',
isFolderWorkspaceRoute: false,
isFloatingWorkspaceRoute: false,
sessionTabs: [],
nativeChatController: { isTabChatView: () => false },
createTabAgentActions: [],
sendDiffNotesAgentActions: [],
closeWithBulkActions: () => []
})
await act(async () => {
create(
createElement(
WorkspaceExecutionHostContext.Provider,
{ value: normalizeExecutionHostId(executionHost) ?? undefined },
createElement(MobileSessionSheets, { controller })
)
)
})
return sheets.labelsByTitle.get('New Tab') ?? []
}
describe('the New Tab sheet', () => {
it('offers Markdown Note for a workspace the desktop hosts', async () => {
expect(await newTabLabels()).toContain('Markdown Note')
})
it('leaves out Markdown Note for a server workspace, whose host opens no editor tabs', async () => {
const labels = await newTabLabels('runtime:vm')
expect(labels).toEqual(['Terminal', 'Browser'])
})
})
@@ -1,79 +0,0 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, expect, it, vi } from 'vitest'
import { mountFixture } from '../test-support/rpc-recording/recorder-fixture-shape'
const layout = vi.hoisted(() => ({ sideRail: false, padAvailable: false, os: 'ios' }))
vi.mock('react-native', () => ({ Platform: { OS: 'web' } }))
vi.mock('../platform/use-reduced-motion', () => ({ useReducedMotionEnabled: () => null }))
vi.mock('../platform/host-os', () => ({ hostOs: () => layout.os }))
vi.mock('../layout/use-shell-side-rail', () => ({
useShellSideRail: () => layout.sideRail,
useDirectionPadAvailable: () => layout.padAvailable
}))
import { useMobileSessionPresentation } from './use-mobile-session-presentation'
type Scope = Parameters<typeof useMobileSessionPresentation>[0]
let tree: ReactTestRenderer | undefined
afterEach(() => {
act(() => tree?.unmount())
tree = undefined
Object.assign(layout, { sideRail: false, padAvailable: false, os: 'ios' })
})
it.each(['ios', 'android'])(
'keeps the keyboard boundary aligned across Duo posture changes on %s',
(os) => {
layout.os = os
const scope = mountFixture<Scope>({
client: null,
connState: 'connected',
activeHandle: 'a',
terminalsLoaded: true,
visibleTabs: [{ id: 'a', type: 'terminal', terminal: 'a' }],
insets: { top: 0, left: 0, right: 0, bottom: 34 },
keyboardHeight: 334,
terminalKeyboardMetrics: new Map(),
terminalFrameRef: { current: null },
toastOpacityRef: { current: {} }
})
function Screen({ keyboardHeight, view }: { keyboardHeight: number; view: string }) {
const scene = mountFixture<Scope>({
showNativeChat: view === 'chat',
activeMarkdownTab: view === 'markdown' ? {} : null,
activeFileTab: view === 'file' ? {} : null,
activeBrowserTab: view === 'browser' ? {} : null
})
const value = useMobileSessionPresentation({ ...scope, ...scene, keyboardHeight })
return createElement('KeyboardBoundary', value)
}
const render = (keyboardHeight = 334, view = 'terminal') =>
act(() => {
if (tree) {
tree.update(createElement(Screen, { keyboardHeight, view }))
} else {
tree = create(createElement(Screen, { keyboardHeight, view }))
}
})
const lift = () =>
tree!.root.find((node) => String(node.type) === 'KeyboardBoundary').props.keyboardLift
render()
expect(lift()).toBe(os === 'ios' ? 300 : 334)
layout.sideRail = true
render()
expect(lift()).toBe(334)
layout.sideRail = false
layout.padAvailable = true
render()
expect(lift()).toBe(334)
for (const view of ['chat', 'browser', 'markdown', 'file']) {
render(334, view)
expect(lift()).toBe(os === 'ios' ? 300 : 334)
render()
expect(lift()).toBe(334)
}
render(0)
expect(lift()).toBe(0)
layout.padAvailable = false
render()
expect(lift()).toBe(os === 'ios' ? 300 : 334)
}
)
@@ -1,91 +0,0 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { describe, expect, it, vi } from 'vitest'
import { Keyboard } from 'react-native'
import { MobileNativeChatAsk } from './input/MobileNativeChatAsk'
import { MobileNativeChatPermission } from './input/MobileNativeChatPermission'
vi.mock('react-native', () => ({
Keyboard: { dismiss: vi.fn() },
Pressable: 'Pressable',
ScrollView: 'ScrollView',
StyleSheet: { create: (styles: unknown) => styles, hairlineWidth: 1 },
Text: 'Text',
TextInput: 'TextInput',
View: 'View',
Platform: { OS: 'ios', select: (o: Record<string, unknown>) => o.ios }
}))
vi.mock('lucide-react-native', () => ({
Check: 'Check',
ChevronDown: 'ChevronDown',
ChevronUp: 'ChevronUp',
ShieldQuestion: 'ShieldQuestion',
X: 'X'
}))
vi.mock('../../components/MobileMarkdown', () => ({ MobileMarkdown: 'Markdown' }))
describe('rendered prompt card collapse controls', () => {
it('collapses an Ask from its header chevron without cancelling or answering it', async () => {
const onCollapse = vi.fn()
const onCancel = vi.fn(async () => true)
const onAnswer = vi.fn(async () => true)
let tree: ReactTestRenderer | null = null
await act(async () => {
tree = create(
createElement(MobileNativeChatAsk, {
prompt: {
questions: [{ question: 'Pick?', options: [{ label: 'East' }], multiSelect: false }]
},
onAnswer,
onCancel,
onCollapse
})
)
})
act(() => tree!.root.findByProps({ accessibilityLabel: 'Collapse' }).props.onPress())
expect(onCollapse).toHaveBeenCalledOnce()
expect(Keyboard.dismiss).toHaveBeenCalledOnce()
expect(onCancel).not.toHaveBeenCalled()
expect(onAnswer).not.toHaveBeenCalled()
act(() => tree!.unmount())
})
it('shows Collapse in a terminal permission header and keeps Cancel where the lane can cancel', async () => {
const permission = {
title: 'Allow Bash?',
options: [
{ label: 'Allow', send: '1' },
{ label: 'Deny', send: '\x1b' }
]
}
const onCollapse = vi.fn()
const onCancel = vi.fn(async () => true)
let tree: ReactTestRenderer | null = null
await act(async () => {
tree = create(
createElement(MobileNativeChatPermission, {
permission,
onRespond: async () => true,
onCollapse
})
)
})
expect(tree!.root.findAllByProps({ accessibilityLabel: 'Cancel' })).toHaveLength(0)
act(() => tree!.root.findByProps({ accessibilityLabel: 'Collapse' }).props.onPress())
expect(onCollapse).toHaveBeenCalledOnce()
await act(async () => {
tree!.update(
createElement(MobileNativeChatPermission, {
permission,
onRespond: async () => true,
onCancel,
onCollapse
})
)
})
expect(tree!.root.findAllByProps({ accessibilityLabel: 'Collapse' })).toHaveLength(0)
act(() => tree!.root.findByProps({ accessibilityLabel: 'Cancel' }).props.onPress())
expect(onCancel).toHaveBeenCalledOnce()
act(() => tree!.unmount())
})
})
@@ -1,80 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import {
installChatViewHooks,
assistantTurn,
listIds,
render,
renderedRow,
update
} from './MobileNativeChatView.harness.test-fixture'
vi.mock('react-native', async () => {
const { reactNativeStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return reactNativeStub()
})
vi.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 })
}))
vi.mock('react-native-gesture-handler', async () => {
const { gestureHandlerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return gestureHandlerStub()
})
vi.mock('lucide-react-native', () => ({
ArrowDown: 'ArrowDown',
ChevronsDownUp: 'ChevronsDownUp',
ChevronsUpDown: 'ChevronsUpDown',
Square: 'Square'
}))
vi.mock('./transcript/MobileNativeChatMessage', () => ({
MobileNativeChatMessage: 'ChatMessage'
}))
vi.mock('./transcript/MobileNativeChatLiveLine', () => ({
MobileNativeChatLiveLine: 'LiveStatus'
}))
vi.mock('./input/MobileNativeChatAsk', () => ({ MobileNativeChatAsk: 'ChatAsk' }))
vi.mock('./input/MobileNativeChatPermission', () => ({
MobileNativeChatPermission: 'ChatPermission'
}))
vi.mock('./input/MobileNativeChatQuestion', () => ({
MobileNativeChatQuestion: 'ChatQuestion'
}))
vi.mock('../../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionSheetModal' }))
vi.mock('./composer/MobileNativeChatComposer', async () => {
const { composerStub } = await import('./MobileNativeChatView.render-stubs.test-fixture')
return composerStub()
})
describe('MobileNativeChatView: rows and the streaming bubble', () => {
installChatViewHooks()
// Streaming eligibility is tested in the overlay, which stays mounted across chat-view toggles.
it('appends the gated streaming bubble after the folded transcript', async () => {
const folded = [assistantTurn('a1', 'The tests pass.')]
await render({ folded })
expect(listIds()).toEqual(['a1'])
await update({ folded, streaming: 'The tests' })
expect(listIds()).toEqual(['a1', 'streaming'])
})
it('tells a row whether the host still waits on the prompt it stands for', async () => {
const rows = [assistantTurn('asked', 'Tea or coffee?'), assistantTurn('open', 'Which branch?')]
await render({ messages: rows, folded: rows, actionablePromptIds: ['open'] })
expect(renderedRow('asked').props.awaiting).toBe(false)
expect(renderedRow('open').props.awaiting).toBe(true)
})
it('renders an accepted optimistic image send without a queued state', async () => {
await render({
pending: [{ id: 'pending-1', text: 'look', images: ['file:///phone-photo.jpg'] }]
})
expect(listIds()).toEqual(['pending-1'])
expect(renderedRow('pending-1').props).not.toHaveProperty('queued')
})
})
vi.mock('../MobileQueuedMessageComposer', () => ({
MobileQueuedMessageComposer: 'QueuedMessageComposer'
}))
@@ -77,14 +77,4 @@ describe('useMobileNativeChatSkills', () => {
expect(sendRequest).toHaveBeenCalledExactlyOnceWith('skills.discover', { worktreeId: 'wt-1' })
expect(state?.nativeChatSkills.skills.map((entry) => entry.name)).toEqual(['shared'])
})
it('lists none from a host that needs the folder the phone does not know', async () => {
const sendRequest = vi.fn(async () => reply)
await mount(fakeClient(sendRequest), false)
await act(async () => state?.loadNativeChatSkills())
expect(sendRequest).not.toHaveBeenCalled()
expect(state?.nativeChatSkills.skills).toEqual([])
})
})
@@ -1,92 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
formatQuestionAnswer,
formatQuestionFreeTextAnswer,
mobileChatQuestionKey,
type MobileChatQuestion
} from './mobile-native-chat-question'
describe('formatQuestionAnswer', () => {
const numbered: MobileChatQuestion = {
question: 'Pick',
options: ['Alpha', 'Beta', 'Gamma'],
multiSelect: false,
optionTokens: ['1', '2', '3']
}
it('echoes the leading token for single-select', () => {
expect(formatQuestionAnswer(numbered, ['Beta'])).toBe('2')
})
it('comma-joins tokens for multi-select', () => {
const multi: MobileChatQuestion = { ...numbered, multiSelect: true }
expect(formatQuestionAnswer(multi, ['Alpha', 'Gamma'])).toBe('1, 3')
})
it('sends the label text when the option had no token (bullet list)', () => {
const bullets: MobileChatQuestion = {
question: 'Pick',
options: ['Rebase', 'Merge'],
multiSelect: false,
optionTokens: [null, null]
}
expect(formatQuestionAnswer(bullets, ['Merge'])).toBe('Merge')
})
it('echoes letter tokens', () => {
const lettered: MobileChatQuestion = {
question: 'Pick',
options: ['Alpha', 'Beta'],
multiSelect: false,
optionTokens: ['a', 'b']
}
expect(formatQuestionAnswer(lettered, ['Beta'])).toBe('b')
})
it('passes free-text / unknown entries through verbatim', () => {
expect(formatQuestionAnswer(numbered, ['something custom'])).toBe('something custom')
})
it('returns empty string when nothing is selected', () => {
expect(formatQuestionAnswer(numbered, [])).toBe('')
expect(formatQuestionAnswer(numbered, [' '])).toBe('')
})
it('prefixes free-text answers with an opaque prompt token when provided', () => {
expect(
formatQuestionFreeTextAnswer({ ...numbered, freeTextToken: 'target' }, ' hi there ')
).toBe(`target:${encodeURIComponent('hi there')}`)
})
it('encodes empty and whitespace answers only when allowed', () => {
const withInput: MobileChatQuestion = {
...numbered,
freeTextToken: 'target',
freeTextInput: { allowEmpty: true }
}
expect(formatQuestionFreeTextAnswer(withInput, '')).toBe('target:')
expect(formatQuestionFreeTextAnswer(withInput, ' \n ')).toBe(
`target:${encodeURIComponent(' \n ')}`
)
expect(formatQuestionFreeTextAnswer({ ...withInput, freeTextInput: undefined }, ' \n ')).toBe(
''
)
})
})
describe('mobileChatQuestionKey', () => {
it('changes when a replacement prompt changes any positional content', () => {
const first: MobileChatQuestion = {
question: 'Pick',
options: ['A', 'B'],
multiSelect: true,
optionTokens: ['1', '2']
}
expect(mobileChatQuestionKey({ ...first, options: ['A', 'C'] })).not.toBe(
mobileChatQuestionKey(first)
)
expect(mobileChatQuestionKey({ ...first, freeTextToken: 'target-2' })).not.toBe(
mobileChatQuestionKey(first)
)
})
})
@@ -2,7 +2,6 @@ import { AGENT_TUI_CLEAR_INPUT_MAX } from '../../../../src/shared/agent-launch/s
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { RpcClient } from '../../transport/rpc-client'
import {
clearMobileNativeChatInputStale,
healMobileNativeChatStaleInput,
isMobileNativeChatInputStale,
markMobileNativeChatInputStale,
@@ -27,14 +26,6 @@ describe('mobile native chat stale input markers', () => {
resetMobileNativeChatStaleInputForTests()
})
it('tracks each terminal independently', () => {
markMobileNativeChatInputStale('term-1')
expect(isMobileNativeChatInputStale('term-1')).toBe(true)
expect(isMobileNativeChatInputStale('term-2')).toBe(false)
clearMobileNativeChatInputStale('term-1')
expect(isMobileNativeChatInputStale('term-1')).toBe(false)
})
it('writes nothing when the terminal is not marked', async () => {
const client = makeClient()
await expect(
@@ -1,87 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { RpcClient } from '../transport/rpc-client'
import {
buildWorktreeSetHostedReviewLinkParams,
fetchWorktreeGitHubPRLink,
linkMobilePr
} from './mobile-pr-link'
describe('buildWorktreeSetHostedReviewLinkParams', () => {
it.each([
['github', 'linkedPR'],
['gitlab', 'linkedGitLabMR'],
['bitbucket', 'linkedBitbucketPR'],
['azure-devops', 'linkedAzureDevOpsPR'],
['gitea', 'linkedGiteaPR']
] as const)('maps %s reviews to the matching worktree field', (provider, key) => {
expect(buildWorktreeSetHostedReviewLinkParams('repo42::/p', provider, 12)).toEqual({
worktree: 'id:repo42::/p',
[key]: 12
})
})
it('includes a submitted base ref so mobile diff review refreshes against the review base', () => {
expect(
buildWorktreeSetHostedReviewLinkParams('repo42::/p', 'github', 12, {
baseRef: ' origin/release '
})
).toEqual({
worktree: 'id:repo42::/p',
baseRef: 'origin/release',
linkedPR: 12
})
})
it('does not invent a metadata field for unsupported providers', () => {
expect(buildWorktreeSetHostedReviewLinkParams('repo42::/p', 'unsupported', 12)).toEqual({
worktree: 'id:repo42::/p'
})
})
})
describe('fetchWorktreeGitHubPRLink', () => {
const client = (result: unknown, okFlag = true) =>
({
sendRequest: vi.fn(async () =>
okFlag ? { ok: true, result } : { ok: false, error: { message: 'x' } }
)
}) as unknown as Pick<RpcClient, 'sendRequest'>
const none = { linkedPR: null, suppressedGitHubPR: null, pushTarget: null }
it('returns the linked and suppressed PR numbers when present', async () => {
expect(
await fetchWorktreeGitHubPRLink(
client({ worktree: { linkedPR: null, suppressedGitHubPR: 12 } }),
'w'
)
).toMatchObject({ linkedPR: null, suppressedGitHubPR: 12, pushTarget: null })
})
it('returns nulls when unset, null, or non-numeric', async () => {
expect(await fetchWorktreeGitHubPRLink(client({ worktree: {} }), 'w')).toMatchObject(none)
expect(
await fetchWorktreeGitHubPRLink(client({ worktree: { linkedPR: 'x' } }), 'w')
).toMatchObject(none)
})
it('returns nulls when the request fails or rejects', async () => {
expect(await fetchWorktreeGitHubPRLink(client(null, false), 'w')).toEqual(none)
const rejecting = {
sendRequest: vi.fn(async () => {
throw new Error('transport closed')
})
} as unknown as Pick<RpcClient, 'sendRequest'>
expect(await fetchWorktreeGitHubPRLink(rejecting, 'w')).toEqual(none)
})
})
describe('linkMobilePr transport rejection', () => {
it('normalizes a thrown sendRequest into { ok:false, error }', async () => {
const rejecting = {
sendRequest: vi.fn(async () => {
throw new Error('socket hung up')
})
} as unknown as Pick<RpcClient, 'sendRequest'>
expect(await linkMobilePr(rejecting, 'w', 5)).toEqual({ ok: false, error: 'socket hung up' })
})
})
@@ -1,95 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { HostedReviewDecision } from '../../../src/shared/hosted-review'
import {
getHostedChecksLabel,
getHostedMergeLabel,
getHostedReviewLabel,
getHostedReviewSignalTone
} from './mobile-hosted-check-status'
describe('mobile hosted check status', () => {
it.each(['merged', 'closed'] as const)('shows %s before stale merge enrichment', (state) => {
expect(getHostedMergeLabel({ state, mergeable: 'CONFLICTING' })).toBe(
state === 'merged' ? 'Merged' : 'Closed'
)
expect(getHostedReviewSignalTone({ state, mergeable: 'CONFLICTING' }, 'merge')).toBe('neutral')
})
it('does not call pending checks Behind', () => {
const item = {
mergeable: 'MERGEABLE' as const,
checksSummary: {
state: 'pending' as const,
total: 1,
passed: 0,
failed: 0,
pending: 1,
neutral: 0
}
}
expect(getHostedMergeLabel(item)).toBe('Checks pending')
expect(getHostedReviewSignalTone(item, 'merge')).toBe('warning')
})
it('renders a hydrated neutral GitLab summary as unresolved checks', () => {
const summary = {
state: 'neutral' as const,
total: 2,
passed: 1,
failed: 0,
pending: 0,
neutral: 1
}
expect(getHostedChecksLabel({ checksSummary: summary })).toBe('Unresolved checks')
expect(getHostedReviewSignalTone({ checksSummary: summary }, 'checks')).toBe('neutral')
})
it('accepts provider-neutral GitHub status fields without a shared work-item type', () => {
expect(
getHostedReviewSignalTone(
{ reviewDecision: 'approved', reviewRequests: [], mergeable: 'UNKNOWN' },
'review'
)
).toBe('success')
})
it('renders hydrated GitLab approval, merge, and check states', () => {
expect(getHostedReviewLabel({ reviewDecision: 'review_required', reviewerCount: 2 })).toBe(
'Review required'
)
expect(
getHostedReviewSignalTone(
{
reviewDecision: 'review_required',
reviewerCount: 2,
mergeable: 'MERGEABLE',
checksSummary: {
state: 'success',
total: 1,
passed: 1,
failed: 0,
pending: 0,
neutral: 0
}
},
'review'
)
).toBe('warning')
expect(getHostedReviewSignalTone({ mergeable: 'MERGEABLE' }, 'merge')).toBe('success')
expect(getHostedReviewSignalTone({ reviewDecision: 'approved' }, 'review')).toBe('success')
expect(getHostedReviewSignalTone({ mergeable: 'CONFLICTING' }, 'merge')).toBe('danger')
})
it('renders a shared typed GitLab review decision', () => {
const reviewDecision: HostedReviewDecision = 'changes_requested'
expect(getHostedReviewLabel({ reviewDecision })).toBe('Changes requested')
expect(getHostedReviewSignalTone({ reviewDecision }, 'review')).toBe('danger')
})
it('keeps missing GitLab enrichment neutral', () => {
expect(getHostedReviewLabel({})).toBe('No reviewers')
expect(getHostedReviewSignalTone({}, 'review')).toBe('neutral')
expect(getHostedReviewSignalTone({}, 'merge')).toBe('neutral')
})
})
+1 -51
View File
@@ -1,10 +1,5 @@
import { describe, expect, it } from 'vitest'
import {
isSetupHookTrusted,
normalizeSetupHookTrust,
persistSetupHookTrustApproval,
wasSetupHookPreviouslyApproved
} from './setup-hook-trust'
import { isSetupHookTrusted, persistSetupHookTrustApproval } from './setup-hook-trust'
import type { PersistedTrustedOrcaHooks } from '../../../src/shared/orca-yaml-hook-types'
import type { RpcClient } from '../transport/rpc-client'
@@ -18,35 +13,6 @@ describe('setup hook trust', () => {
expect(isSetupHookTrusted(trust, [{ id: 'repo-1' }], 'repo-1', 'hash-2')).toBe(false)
})
it('treats an always-trusted repo as trusted for changed setup scripts', () => {
const trust: PersistedTrustedOrcaHooks = {
'repo-1': { all: { approvedAt: 1000 } }
}
expect(isSetupHookTrusted(trust, [{ id: 'repo-1' }], 'repo-1', 'new-hash')).toBe(true)
})
it('persists and returns the approved trust state', async () => {
let persisted: unknown
const client = {
sendRequest: async (_method: string, params: unknown) => {
persisted = params
return { ok: true, result: null }
}
} as unknown as RpcClient
const next = await persistSetupHookTrustApproval({
client,
trust: {},
repoId: 'repo-1',
contentHash: 'setup-hash',
alwaysTrust: false
})
expect(persisted).toEqual({ trustedOrcaHooks: next })
expect(isSetupHookTrusted(next, [{ id: 'repo-1' }], 'repo-1', 'setup-hash')).toBe(true)
})
it('keeps the approved hash with Always trust so a repo on two hosts stops prompting', async () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: persisting only calls sendRequest.
const client = {
@@ -65,20 +31,4 @@ describe('setup hook trust', () => {
expect(isSetupHookTrusted(next, repos, 'repo-1', 'setup-hash')).toBe(true)
expect(isSetupHookTrusted(next, repos, 'repo-1', 'edited-hash')).toBe(false)
})
it('detects previous setup approval and ignores incomplete trust payloads', () => {
expect(
wasSetupHookPreviouslyApproved(
{ 'repo-1': { setup: { contentHash: 'hash-1', approvedAt: 1000 } } },
'repo-1'
)
).toBe(true)
expect(normalizeSetupHookTrust({ contentHash: 'hash-1', scriptContent: '' })).toBe(null)
expect(
normalizeSetupHookTrust({ contentHash: 'hash-1', scriptContent: 'pnpm install' })
).toEqual({
contentHash: 'hash-1',
scriptContent: 'pnpm install'
})
})
})
@@ -2,7 +2,6 @@ import { terminalInputSend } from './mobile-terminal-operations'
import { describe, expect, it, vi } from 'vitest'
import { sendTerminalLiveAccessoryRawBytes } from './terminal-live-accessory-raw-send'
import type { RpcClient } from '../transport/rpc-client'
import type { RpcResponse } from '../transport/types'
function captureClient(
result: Promise<unknown> = Promise.resolve({
@@ -27,20 +26,6 @@ const BASE_ARGS = {
} as const
describe('terminal live accessory raw send', () => {
it('sends raw bytes now-or-never with the device presence tag', async () => {
const { client, sendRequest } = captureClient()
await expect(sendTerminalLiveAccessoryRawBytes({ ...BASE_ARGS, client })).resolves.toBe(true)
expect(sendRequest).toHaveBeenCalledWith(
'terminal.send',
{ terminal: 'terminal-a', text: '', enter: false, client: { id: 'tok', type: 'mobile' } },
// Never park accessory keys to fire into a reconnected PTY long after the tap.
{ failWhenDisconnected: true }
)
expect(sendRequest).toHaveBeenCalledTimes(1)
})
it('drops the bytes instead of sending while disconnected', async () => {
const { client, sendRequest } = captureClient()
@@ -60,22 +45,4 @@ describe('terminal live accessory raw send', () => {
expect(sendRequest).not.toHaveBeenCalled()
})
it('reports a rejected send without surfacing transport errors', async () => {
const { client } = captureClient(Promise.reject(new Error('Not connected: terminal.send')))
await expect(sendTerminalLiveAccessoryRawBytes({ ...BASE_ARGS, client })).resolves.toBe(false)
})
it('reports a fulfilled RPC failure as a failed send', async () => {
const response: RpcResponse = {
id: 'send',
ok: false,
error: { code: 'terminal_error', message: 'failed' },
_meta: { runtimeId: 'test-runtime' }
}
const { client } = captureClient(Promise.resolve(response))
await expect(sendTerminalLiveAccessoryRawBytes({ ...BASE_ARGS, client })).resolves.toBe(false)
})
})
@@ -23,12 +23,6 @@ describe('decoding a bundle range', () => {
expect(decodeMobileWebBundleRange(RANGE, GZIP, RAW.byteLength)).toEqual(RAW)
})
it('passes an identity range through', () => {
expect(
decodeMobileWebBundleRange({ ...RANGE, encoding: 'identity' }, RAW, RAW.byteLength)
).toEqual(RAW)
})
it('refuses a truncated gzip body', () => {
expect(
refusalOf(() =>
@@ -66,45 +66,6 @@ describe('spawnDetachedWithoutInheritance', () => {
spawnProcess.mockReset()
})
it('uses a detached, ignoring libuv spawn off Windows', () => {
onPlatform('linux')
const spawner = vi.fn<IsolatedSpawner>()
setIsolatedSpawner(spawner)
spawnProcess.mockReturnValue({ pid: 1 })
spawnDetachedWithoutInheritance({ program: '/bin/true', args: ['x'], cwd: '/' })
expect(spawner).not.toHaveBeenCalled()
expect(spawnProcess).toHaveBeenCalledWith({
program: '/bin/true',
args: ['x'],
cwd: '/',
detached: true,
stdio: 'ignore'
})
})
it('falls back to libuv on Windows when no spawner is installed', () => {
onPlatform('win32')
spawnProcess.mockReturnValue({ pid: 1 })
spawnDetachedWithoutInheritance({ program: 'C:\\Windows\\System32\\wsl.exe' })
expect(spawnProcess).toHaveBeenCalledOnce()
})
it.each(['C:\\tools\\run.cmd', 'wsl.exe'])('falls back to libuv for %s', (program) => {
onPlatform('win32')
const spawner = vi.fn<IsolatedSpawner>()
setIsolatedSpawner(spawner)
spawnProcess.mockReturnValue({ pid: 1 })
spawnDetachedWithoutInheritance({ program, env: { ComSpec: 'cmd.exe' } })
expect(spawner).not.toHaveBeenCalled()
expect(spawnProcess).toHaveBeenCalledOnce()
})
it('hands the native spawner a libuv-shaped command line and environment block', async () => {
onPlatform('win32')
let exit: (code: number) => void = () => {}
@@ -161,24 +122,6 @@ describe('spawnDetachedWithoutInheritance', () => {
expect(killed.kill()).toBe(false)
})
it('logs a child whose breakaway from our job was denied, and still starts it', async () => {
onPlatform('win32')
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
setIsolatedSpawner(() => ({ pid: 7, exitWatchError: null, breakawayDenied: true }))
const child = spawnDetachedWithoutInheritance({ program: 'C:\\wsl.exe' })
await once(child, 'spawn')
expect(child.pid).toBe(7)
expect(warn).toHaveBeenCalledWith(
"[process-host] pid 7 could not break away from this process's job"
)
warn.mockClear()
setIsolatedSpawner(() => ({ pid: 8, exitWatchError: null, breakawayDenied: false }))
spawnDetachedWithoutInheritance({ program: 'C:\\wsl.exe' })
expect(warn).not.toHaveBeenCalled()
})
it('refuses to kill a child whose exit it cannot watch, since its pid may be reused', async () => {
onPlatform('win32')
setIsolatedSpawner(() => ({ pid: 77, exitWatchError: 5, breakawayDenied: false }))
@@ -46,19 +46,6 @@ describe('runProcess termination failure', () => {
vi.clearAllMocks()
})
it('reports ordinary child close exactly once', async () => {
const child = mockChild()
const onChildTerminated = vi.fn()
spawnMock.mockReturnValue(child)
const pending = runProcess({ program: 'git', timeoutMs: null, onChildTerminated })
child.emit('close', 0, null)
child.emit('close', 0, null)
await expect(pending).resolves.toMatchObject({ code: 0 })
expect(onChildTerminated).toHaveBeenCalledOnce()
})
it('does not report a live-child error before its eventual close', async () => {
const child = mockChild()
const onChildTerminated = vi.fn()
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { existsSync, mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import {
@@ -9,7 +9,6 @@ import {
notifySpawnObserver,
setSpawnObserver
} from './spawn-observer'
import { runProcess, runProcessSync, spawnProcess } from './run-process'
afterEach(() => {
setSpawnObserver(null)
@@ -19,87 +18,6 @@ afterEach(() => {
})
describe('spawn observer', () => {
it('reports the resolved program for a spawnProcess child', async () => {
const seen: { command: string; args: readonly string[]; blockMs: number }[] = []
setSpawnObserver((command, args, blockMs) => seen.push({ command, args, blockMs }))
const child = spawnProcess({ program: process.execPath, args: ['-e', 'process.exit(0)'] })
await new Promise((resolve) => child.on('exit', resolve))
expect(seen).toHaveLength(1)
expect(seen[0].command).toBe(process.execPath)
expect(seen[0].args).toEqual(['-e', 'process.exit(0)'])
expect(seen[0].blockMs).toBeGreaterThanOrEqual(0)
})
it('reports exactly once for runProcess, which spawns through spawnProcess', async () => {
let calls = 0
setSpawnObserver(() => {
calls += 1
})
await runProcess({ program: process.execPath, args: ['-e', 'process.exit(0)'] })
expect(calls).toBe(1)
})
it('reports the whole blocking run of runProcessSync', () => {
const seen: { command: string; blockMs: number }[] = []
setSpawnObserver((command, _args, blockMs) => seen.push({ command, blockMs }))
const result = runProcessSync({
program: process.execPath,
args: ['-e', 'setTimeout(() => process.exit(3), 50)']
})
expect(result.code).toBe(3)
expect(seen).toHaveLength(1)
expect(seen[0].command).toBe(process.execPath)
expect(seen[0].blockMs).toBeGreaterThanOrEqual(40)
})
it('reports a runProcessSync launch that throws or times out', () => {
const seen: string[] = []
setSpawnObserver((command) => seen.push(command))
const missing = path.join(tmpdir(), 'orca-spawn-observer-missing-program')
expect(() => runProcessSync({ program: missing, args: [] })).toThrow()
const timedOut = runProcessSync({
program: process.execPath,
args: ['-e', 'setTimeout(() => {}, 10_000)'],
timeoutMs: 50
})
expect(timedOut.timedOut).toBe(true)
expect(seen).toEqual([missing, process.execPath])
})
it('stops reporting after the observer is cleared', async () => {
let calls = 0
setSpawnObserver(() => {
calls += 1
})
setSpawnObserver(null)
await runProcess({ program: process.execPath, args: ['-e', 'process.exit(0)'] })
expect(calls).toBe(0)
})
it('logs every spawn, sync or async, to the e2e spawn log when one is named', async () => {
const dir = mkdtempSync(path.join(tmpdir(), 'orca-spawn-log-'))
const log = path.join(dir, 'spawns.jsonl')
process.env[E2E_SPAWN_LOG_ENV] = log
process.env[E2E_USER_DATA_DIR_ENV] = dir
try {
await runProcess({ program: process.execPath, args: ['-e', 'process.exit(0)'] })
runProcessSync({ program: process.execPath, args: ['-e', 'process.exit(0)'] })
const lines = readFileSync(log, 'utf8')
.trim()
.split('\n')
.map((line) => JSON.parse(line))
const spawned = {
file: process.execPath,
args: ['-e', 'process.exit(0)'],
at: expect.any(Number),
stack: expect.arrayContaining([expect.stringContaining('spawn-observer.test')])
}
expect(lines).toEqual([spawned, spawned])
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
it('outside e2e mode writes nothing and captures no stack, even with the log variable set', () => {
const dir = mkdtempSync(path.join(tmpdir(), 'orca-spawn-log-'))
const log = path.join(dir, 'spawns.jsonl')
-50
View File
@@ -1,50 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('electron', () => ({ ipcRenderer: { invoke: vi.fn(), on: vi.fn() } }))
import { getWslHostsApi } from './wsl-hosts-api'
describe('WSL hosts preload API', () => {
beforeEach(() => {
vi.stubEnv('MODE', 'e2e')
})
afterEach(() => {
vi.unstubAllEnvs()
})
it('ignores the e2e fake outside an e2e build', () => {
vi.stubEnv('MODE', 'production')
const env = { ORCA_INTERNAL_WSL_HOSTS: '1', ORCA_E2E_FAKE_WSL_HOSTS: '{}' }
expect(getWslHostsApi(env, 'linux')).toBeUndefined()
expect(getWslHostsApi(env, 'win32')).toBeDefined()
})
it('is absent on Windows and under the e2e fake while the internal gate is closed', () => {
expect(getWslHostsApi({}, 'win32', [])).toBeUndefined()
expect(getWslHostsApi({ ORCA_INTERNAL_WSL_HOSTS: 'true' }, 'win32', [])).toBeUndefined()
expect(getWslHostsApi({ ORCA_E2E_FAKE_WSL_HOSTS: '{}' }, 'darwin', [])).toBeUndefined()
})
it('is present behind the open gate only where WSL hosts can run', () => {
expect(getWslHostsApi({ ORCA_INTERNAL_WSL_HOSTS: '1' }, 'win32')).toBeDefined()
expect(
getWslHostsApi({ ORCA_INTERNAL_WSL_HOSTS: '1', ORCA_E2E_FAKE_WSL_HOSTS: '{}' }, 'linux')
).toBeDefined()
expect(getWslHostsApi({ ORCA_INTERNAL_WSL_HOSTS: '1' }, 'darwin')).toBeUndefined()
})
it('is present behind a closed gate when main stamped serve-only, still only where hosts run', () => {
const argv = ['--orca-wsl-hosts-serve-only']
expect(getWslHostsApi({}, 'win32', argv)).toBeDefined()
expect(getWslHostsApi({ ORCA_E2E_FAKE_WSL_HOSTS: '{}' }, 'linux', argv)).toBeDefined()
expect(getWslHostsApi({}, 'darwin', argv)).toBeUndefined()
expect(getWslHostsApi({}, 'win32', [])).toBeUndefined()
})
it('is absent behind an open gate a remote policy blocked, unless main stamped serve-only', () => {
const env = { ORCA_INTERNAL_WSL_HOSTS: '1' }
const blocked = '--orca-wsl-hosts-policy-blocked'
expect(getWslHostsApi(env, 'win32', [blocked])).toBeUndefined()
expect(getWslHostsApi(env, 'win32', [blocked, '--orca-wsl-hosts-serve-only'])).toBeDefined()
})
})
@@ -8,7 +8,6 @@ import {
} from './browser-extension-document-targets'
const DISCARDED_TAB = PRELOAD_FIRST_DISCARDED_TAB_ID + 3
const REPLACED_TAB = PRELOAD_FIRST_DISCARDED_TAB_ID + 4
const ORCA_TOP = 'A'.repeat(32)
const ORCA_AD = 'B'.repeat(32)
@@ -354,24 +353,6 @@ describe('Orca documentIds in native scripting targets', () => {
expect(chrome.runtime.lastError).toBeUndefined()
expect(nativeListeners.size).toBe(0)
})
it('keeps native result identities for untargeted executions', async () => {
const { chrome, calls } = install(['webNavigation', 'scripting'])
await expect(chrome.scripting.executeScript({ target: { tabId: 3 } })).resolves.toEqual([
{ frameId: 0, documentId: CHROMIUM_TOP, result: 0 }
])
expect(calls).toEqual([])
})
it('calls the native functions directly when there is nothing to map', () => {
const { chrome, executeScript, sendMessage } = install(['scripting'])
void chrome.scripting.executeScript({ target: { tabId: 3 } })
void chrome.tabs.sendMessage(3, 'ping')
expect(executeScript).toHaveBeenCalledTimes(1)
expect(sendMessage).toHaveBeenCalledTimes(1)
})
})
describe('calls to a discarded tab', () => {
@@ -393,10 +374,4 @@ describe('calls to a discarded tab', () => {
expect(executeScript).not.toHaveBeenCalled()
expect(sendMessage).not.toHaveBeenCalled()
})
it('leave a retired synthetic id to Electron, which reports no such tab', async () => {
const { chrome, executeScript } = install(['scripting', 'webNavigation'])
await chrome.scripting.executeScript({ target: { tabId: REPLACED_TAB } })
expect(executeScript).toHaveBeenCalledTimes(1)
})
})
@@ -1,14 +1,10 @@
// @vitest-environment happy-dom
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
DOC_PREVIEW_LINK_CLICK_CHANNEL,
type DocPreviewLinkModifiers
} from '../shared/doc-preview-scheme'
import type { DocPreviewLinkModifiers } from '../shared/doc-preview-scheme'
import {
handleDocPreviewLinkAuxClick,
handleDocPreviewLinkClick,
installDocPreviewLinkInterception,
PRELOAD_DOC_PREVIEW_LINK_CLICK_CHANNEL
installDocPreviewLinkInterception
} from './doc-preview-link-interception'
const NO_MODIFIERS = { metaKey: false, ctrlKey: false, shiftKey: false }
@@ -70,10 +66,6 @@ beforeEach(() => {
})
describe('doc preview link interception', () => {
it('sends on exactly the channel main listens on', () => {
expect(PRELOAD_DOC_PREVIEW_LINK_CLICK_CHANNEL).toBe(DOC_PREVIEW_LINK_CLICK_CHANNEL)
})
it('listens for both press kinds in the capture phase, so the document cannot consume them first', () => {
loadPreviewDocument('<a id="external" href="https://example.com/docs">go</a>')
const addEventListener = vi.spyOn(document, 'addEventListener')
@@ -127,15 +119,6 @@ describe('doc preview link interception', () => {
expect(event.defaultPrevented).toBe(false)
})
it('reports a trusted press on an external anchor exactly once and stops the navigation', () => {
loadPreviewDocument('<a id="external" href="https://example.com/docs">go</a>')
const event = pressTrusted('#external')
expect(report).toHaveBeenCalledExactlyOnceWith('https://example.com/docs', NO_MODIFIERS)
expect(event.defaultPrevented).toBe(true)
})
it('reports the anchor even when the press landed on what it wraps', () => {
loadPreviewDocument('<a href="https://example.com/docs"><span id="inner">go</span></a>')
@@ -175,16 +158,6 @@ describe('doc preview link interception', () => {
expect(event.defaultPrevented).toBe(true)
})
it('scrolls to the top for a bare # on an SVG anchor', () => {
loadSvgAnchorDocument('#')
const scrollTo = vi.spyOn(window, 'scrollTo').mockImplementation(() => {})
pressTrusted('#svg-hit')
expect(scrollTo).toHaveBeenCalledOnce()
expect(report).not.toHaveBeenCalled()
})
it('keeps a rooted SVG link inside the preview rather than reading it as a filesystem path', () => {
loadSvgAnchorDocument('/assets/a.html')
@@ -249,17 +222,6 @@ describe('doc preview link interception', () => {
expect(report).toHaveBeenCalledExactlyOnceWith('https://example.com/page#section', NO_MODIFIERS)
})
it('opens nothing for a middle click on a link', () => {
loadPreviewDocument('<a id="external" href="https://example.com/docs">go</a>')
const event = new MouseEvent('auxclick', { bubbles: true, button: 1, cancelable: true })
Object.defineProperty(event, 'isTrusted', { configurable: true, value: true })
dispatch('#external', event)
expect(report).not.toHaveBeenCalled()
expect(event.defaultPrevented).toBe(true)
})
it('leaves a middle click the document dispatched itself alone', () => {
loadPreviewDocument('<a id="external" href="https://example.com/docs">go</a>')
const event = new MouseEvent('auxclick', { bubbles: true, button: 1, cancelable: true })
@@ -1,10 +1,5 @@
import { describe, expect, it } from 'vitest'
import { PLUGIN_DECLARATIVE_MESSAGES } from '../shared/plugins/declarative/plugin-declarative-message-names'
import { PLUGIN_DECLARATIVE_RENDER_LANE } from '../shared/plugins/declarative/plugin-declarative-limits'
import {
createPluginFrameSendBudget,
PRELOAD_PLUGIN_FRAME_LANES
} from './plugin-frame-guest-send-budget'
import { createPluginFrameSendBudget } from './plugin-frame-guest-send-budget'
function chain(depth: number): unknown {
let tree: unknown = { type: 'Text', key: '#0', props: {}, children: ['leaf'] }
@@ -15,18 +10,6 @@ function chain(depth: number): unknown {
}
describe('guest preload render lane', () => {
it("pins its written-out caps to the host's render lane", () => {
const { render } = PRELOAD_PLUGIN_FRAME_LANES
expect(render.events).toEqual([
PLUGIN_DECLARATIVE_MESSAGES.render,
PLUGIN_DECLARATIVE_MESSAGES.persist,
PLUGIN_DECLARATIVE_MESSAGES.retain
])
expect(render.maxBytes).toBe(PLUGIN_DECLARATIVE_RENDER_LANE.maxBytes)
expect(render.maxNodes).toBe(PLUGIN_DECLARATIVE_RENDER_LANE.maxObjects)
expect(render.maxDepth).toBe(PLUGIN_DECLARATIVE_RENDER_LANE.maxDepth)
})
it('sends trees the host accepts, past the shared 64 KiB and depth-100 caps', () => {
const send = createPluginFrameSendBudget()
const render = (tree: unknown) => ({
@@ -1,7 +1,7 @@
/**
* Isolated-world budget for what a guest may send to the host. Why written out: the sandboxed
* preload can't import shared chunks; plugin-frame-guest.test.ts pins these numbers to the host's
* lanes. Sizes are measured like the host measures them (UTF-8 strings, buffer bytes), so a
* preload can't import shared chunks. Sizes are measured like the host measures them
* (UTF-8 strings, buffer bytes), so a
* message this admits is one the host can admit too.
*/
export const PRELOAD_PLUGIN_FRAME_LANES = {
+1 -21
View File
@@ -7,19 +7,10 @@ import {
PLUGIN_FRAME_KEY_CHANNEL,
PLUGIN_FRAME_TRANSPORT_GLOBAL
} from '../shared/plugins/frames/plugin-frame-guest'
import { PLUGIN_SAVE_AS_MAX_BYTES } from '../shared/plugins/workers/plugin-host-api'
import { PANEL_MESSAGE_MAX_BYTES } from '../shared/plugins/frames/plugin-frame-bridge'
import { CUSTOM_EDITOR_DOCUMENT_MAX_BYTES } from '../shared/plugins/frames/plugin-custom-editor-bridge'
import {
PRELOAD_PLUGIN_FRAME_IPC_CHANNEL,
PRELOAD_PLUGIN_FRAME_KEY_CHANNEL,
PRELOAD_PLUGIN_FRAME_TRANSPORT_GLOBAL
} from './plugin-frame-guest-transport'
import {
createPluginFrameGestureTracker,
createPluginFrameSendBudget,
measurePluginFrameMessageBytes,
PRELOAD_PLUGIN_FRAME_LANES
measurePluginFrameMessageBytes
} from './plugin-frame-guest-send-budget'
type Transport = { send: (message: unknown) => void; subscribe: (fn: (m: unknown) => void) => void }
@@ -82,17 +73,6 @@ function runPreload(isMainFrame: boolean, contentType = 'text/html') {
}
describe('plugin frame guest preload', () => {
it('pins its written-out constants to the shared ones', () => {
expect(PRELOAD_PLUGIN_FRAME_IPC_CHANNEL).toBe(PLUGIN_FRAME_IPC_CHANNEL)
expect(PRELOAD_PLUGIN_FRAME_KEY_CHANNEL).toBe(PLUGIN_FRAME_KEY_CHANNEL)
expect(PRELOAD_PLUGIN_FRAME_TRANSPORT_GLOBAL).toBe(PLUGIN_FRAME_TRANSPORT_GLOBAL)
const { shared, saveAs, edit } = PRELOAD_PLUGIN_FRAME_LANES
expect(shared.maxBytes).toBeGreaterThanOrEqual(PANEL_MESSAGE_MAX_BYTES)
expect(shared.maxBytes).toBeLessThan(PANEL_MESSAGE_MAX_BYTES * 2)
expect(saveAs.maxBytes).toBe(PLUGIN_SAVE_AS_MAX_BYTES + 4096)
expect(edit.maxBytes).toBe(CUSTOM_EDITOR_DOCUMENT_MAX_BYTES + 4096)
})
it('hands the shell a deletable transport and stamps each send with a gesture serial', () => {
const { sendToHost, ipcListeners, pageWindow, input, transport } = runPreload(true)
expect(
-64
View File
@@ -44,7 +44,6 @@ vi.mock('../main/pty/shell-startup/git-bash', () => ({
vi.mock('@orca/process-host', () => ({ runProcess: runProcessMock }))
import {
buildCommandLookupSpecs,
hasAbsoluteCommandPath,
isCommandOnPathForRelay,
PreflightHandler
@@ -75,29 +74,6 @@ beforeEach(() => {
isGitBashAvailableMock.mockReset()
})
describe('buildCommandLookupSpecs', () => {
it('allows a custom shell path only when the account login shell matches', () => {
expect(
buildCommandLookupSpecs(
'codex',
'darwin',
{ SHELL: '/opt/homebrew/bin/zsh' },
'/opt/homebrew/bin/zsh'
)
).toEqual([
{ file: '/opt/homebrew/bin/zsh', args: lookupArgs('codex', '-ilc') },
{ file: '/bin/sh', args: lookupArgs('codex') }
])
})
it('allows conservative system shell paths when account lookup is unavailable', () => {
expect(buildCommandLookupSpecs('codex', 'linux', { SHELL: '/usr/bin/bash' }, null)[0]).toEqual({
file: '/usr/bin/bash',
args: lookupArgs('codex', '-ilc')
})
})
})
describe('isCommandOnPathForRelay', () => {
it('falls back to inherited PATH when shell startup returns no absolute command path', async () => {
execFileAsyncMock
@@ -280,44 +256,4 @@ describe('PreflightHandler', () => {
})
}
})
it('reports remote Windows shell capabilities through the SSH preflight path', async () => {
const originalPlatform = process.platform
Object.defineProperty(process, 'platform', {
configurable: true,
value: 'win32'
})
isWslAvailableAsyncMock.mockResolvedValue(true)
listWslDistrosAsyncMock.mockResolvedValue(['Ubuntu'])
isPwshAvailableAsyncMock.mockResolvedValue(true)
isGitBashAvailableMock.mockReturnValue(true)
const requestHandlers = new Map<string, (params: Record<string, unknown>) => Promise<unknown>>()
const dispatcher = {
onRequest: vi.fn(
(method: string, handler: (params: Record<string, unknown>) => Promise<unknown>) => {
requestHandlers.set(method, handler)
}
)
}
new PreflightHandler(dispatcher as never)
try {
const handler = requestHandlers.get('preflight.detectWindowsTerminalCapabilities')
expect(handler).toBeDefined()
await expect(handler!({})).resolves.toEqual({
wslAvailable: true,
wslDistros: ['Ubuntu'],
pwshAvailable: true,
gitBashAvailable: true,
hostPlatform: 'win32'
})
} finally {
Object.defineProperty(process, 'platform', {
configurable: true,
value: originalPlatform
})
}
})
})