test: remove 321 low-value main and shared tests (#39429)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-07 01:37:39 -08:00
committed by GitHub
co-authored by OrcaWin
parent fc18a6be45
commit 95fadd01ba
83 changed files with 20 additions and 6679 deletions
-1
View File
@@ -888,7 +888,6 @@
1 test-timeout-needs-reason:src/main/foreign-sqlite-readers/foreign-sqlite-reader-event-loop.test.ts
4 test-timeout-needs-reason:src/main/git/refs/maintenance/repo-ref-maintenance-real-git.test.ts
1 test-timeout-needs-reason:src/main/git/worktrees/removal/worktree-removal-real-git.test.ts
1 test-timeout-needs-reason:src/main/hermes/hook-service.test.ts
1 test-timeout-needs-reason:src/main/ipc/filesystem/filesystem-quick-open-options.integration.test.ts
1 test-timeout-needs-reason:src/main/ipc/preflight-runnable-local-cli.test.ts
1 test-timeout-needs-reason:src/main/ipc/worktrees/git/worktree-git-status-attribution-real-git.test.ts
File diff suppressed because one or more lines are too long
@@ -1945,7 +1945,6 @@
"src/main/emulator/serve-sim-helper-processes.test.ts": 59,
"src/main/emulator/serve-sim-kill-contract.test.ts": 1284,
"src/main/emulator/serve-sim-runtime-materializer.test.ts": 178,
"src/main/emulator/simctl-simulator-devices.test.ts": 59,
"src/main/ephemeral-vm/recipe-runner.test.ts": 460,
"src/main/ephemeral-vm/runtime-service.test.ts": 870,
"src/main/ephemeral-vm/runtime-ssh-cleanup.test.ts": 188,
@@ -2226,7 +2225,6 @@
"src/main/gitlab/client-mr-listing.test.ts": 558,
"src/main/gitlab/client-mr-review-actions.test.ts": 482,
"src/main/gitlab/client-work-items.test.ts": 492,
"src/main/gitlab/client.test.ts": 534,
"src/main/gitlab/gitlab-known-host-backoff.test.ts": 477,
"src/main/gitlab/gitlab-known-host-probe.test.ts": 389,
"src/main/gitlab/gitlab-known-host-retirement.test.ts": 509,
@@ -2529,7 +2527,6 @@
"src/main/ipc/skill-install-management-ipc-handlers.test.ts": 193,
"src/main/ipc/skill-install-progress-ipc.test.ts": 48,
"src/main/ipc/source-control/source-control-ai-linked-issue.test.ts": 60,
"src/main/ipc/speech.test.ts": 60,
"src/main/ipc/ssh/ssh-app-shutdown.test.ts": 4455,
"src/main/ipc/ssh/ssh-connect-decision-cancellation.test.ts": 6414,
"src/main/ipc/ssh/ssh-credential-cancel.test.ts": 4604,
@@ -6293,7 +6290,6 @@
"src/renderer/src/components/native-chat/NativeChatModelCombobox.test.tsx": 692,
"src/renderer/src/components/native-chat/NativeChatNewRowsMarker.test.tsx": 3019,
"src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx": 3399,
"src/renderer/src/components/native-chat/NativeChatPacedMarkdown.file-links.test.tsx": 2576,
"src/renderer/src/components/native-chat/NativeChatPacedMarkdown.mermaid.test.tsx": 2702,
"src/renderer/src/components/native-chat/NativeChatPaneHeader.test.tsx": 2736,
"src/renderer/src/components/native-chat/NativeChatPromptEditor.paragraph-selection.test.tsx": 652,
@@ -6324,7 +6320,6 @@
"src/renderer/src/components/native-chat/NativeChatStructuredSession.wsl-host-stopped.test.tsx": 3657,
"src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.test.tsx": 583,
"src/renderer/src/components/native-chat/NativeChatTaskList.test.tsx": 544,
"src/renderer/src/components/native-chat/NativeChatToolFileTarget.test.tsx": 2047,
"src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx": 3544,
"src/renderer/src/components/native-chat/NativeChatToolOutput.test.tsx": 410,
"src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx": 3128,
@@ -6364,7 +6359,6 @@
"src/renderer/src/components/native-chat/native-chat-composer-draft-store.test.ts": 6688,
"src/renderer/src/components/native-chat/native-chat-composer-drop-scope.test.tsx": 1409,
"src/renderer/src/components/native-chat/native-chat-composer-ime-file-drop.test.tsx": 1326,
"src/renderer/src/components/native-chat/native-chat-composer-mobile-hold.test.tsx": 899,
"src/renderer/src/components/native-chat/native-chat-composer-prompt-recall.test.tsx": 1724,
"src/renderer/src/components/native-chat/native-chat-composer-reveal-focus.test.tsx": 526,
"src/renderer/src/components/native-chat/native-chat-composer-workspace-file-drop.test.tsx": 1166,
@@ -9144,10 +9138,8 @@
"src/shared/ndjson-retained-chunk-memory.test.ts": 327,
"src/shared/nested-repo-telemetry-schema.test.ts": 196,
"src/shared/nested-repo-telemetry.test.ts": 55,
"src/shared/nested-worker-depth.test.ts": 45,
"src/shared/network-proxy.test.ts": 44,
"src/shared/network/manual-address.test.ts": 68,
"src/shared/network/server-share-address.test.ts": 50,
"src/shared/new-workspace-dialog-repo.test.ts": 50,
"src/shared/new-workspace/smart-workspace-linear-intent.test.ts": 64,
"src/shared/new-workspace/smart-workspace-url-source-results.test.ts": 67,
@@ -4,7 +4,6 @@ import { join } from 'node:path'
import { afterEach, expect, it, vi } from 'vitest'
import { selectAcpHostAccountHome } from '../acp/acp-launch-command'
import type { AcpHostBackendSettings } from '../acp/acp-launch-specs'
import { resolveAcpLaunchInvocation } from '../acp/acp-structured-launch-resolution'
import { ANTIGRAVITY_LAUNCH_SPEC } from './antigravity-acp-launch-spec'
// agy's own sign-in, read from the host's keyring or token file.
@@ -127,26 +126,3 @@ it.skipIf(process.platform === 'win32')(
).resolves.toBeNull()
}
)
it('a chat launches on the backend its account home pins', async () => {
const resolve = (path: string) =>
resolveAcpLaunchInvocation(
ANTIGRAVITY_LAUNCH_SPEC,
{ variable: 'GEMINI_HOME', path },
{
resolveEnvironment: async () => ({ PATH: '/usr/bin', HOME: '/home/user' }),
resolveCommand: (command) => `/resolved/${command}`,
dataDirectory: '/orca-data'
}
)
const host = await resolve('/home/user/.gemini')
expect(host.command).toBe('/resolved/agy')
expect(host.spec.args({ fullAccess: false, pluginDir: null })).toEqual(['acp'])
expect(host.env.GEMINI_HOME).toBeUndefined()
expect(host.env.AGY_ACP_FORCE_FILE_STORAGE).toBeUndefined()
const managed = await resolve('/orca-data/antigravity-chat')
expect(managed.spec.managedRuntime).toBeDefined()
expect(managed.env.GEMINI_HOME).toBe('/orca-data/antigravity-chat')
expect(managed.env.AGY_ACP_FORCE_FILE_STORAGE).toBe('1')
})
@@ -46,30 +46,6 @@ function launch(runtime: ReturnType<typeof fakeRuntime>, reuseSession = false) {
}
describe('headless automation agent launch', () => {
it.each([
['is refused', async () => false, "Aider started, but Orca could not send this run's prompt."],
[
'throws',
async () => {
throw new Error('writer broke')
},
'writer broke'
]
])(
'opens the terminal and reports the prompt undelivered when delivery %s',
async (_, deliver, error) => {
const opened = await launch(fakeRuntime(deliver))
expect(opened).toMatchObject({
terminalHandle: 'term-1',
terminalSessionId: 'tab-1',
terminalPaneKey: 'tab-1:leaf-1',
terminalPtyId: 'pty-1'
})
await expect(opened.delivery).resolves.toEqual({ error })
}
)
it.each([
['resolves on a paired server', { hostId: 'runtime:env-1' }, null],
['is gone', null, new Error('selector_not_found')]
@@ -68,44 +68,6 @@ const automation: Automation = {
}
describe('headless automation workspace create args', () => {
it('stamps automation provenance for serve-mode new-per-run workspaces', () => {
const args = buildAutomationWorktreeCreateArgs({
automation,
run: {
id: 'run-1',
title: 'Nightly review run',
scheduledFor: Date.UTC(2026, 0, 2, 3, 4, 5)
},
repo,
createdAt: 123
})
expect(args).toMatchObject({
repoSelector: { selector: 'id:repo-1', hostId: 'ssh:ssh-target-1' },
name: 'auto-nightly-review-run-20260102T0304',
baseBranch: 'origin/main',
setupDecision: 'skip',
activate: false,
createdWithAgent: 'codex',
startupAgent: 'codex',
startupPrompt: 'Review changes',
telemetrySource: 'unknown',
automationProvenance: {
kind: 'created-by-automation',
automationId: 'automation-1',
automationNameSnapshot: 'Nightly review',
automationRunId: 'run-1',
automationRunTitleSnapshot: 'Nightly review run',
createdAt: 123,
executionTargetType: 'ssh',
executionTargetId: 'ssh-target-1',
projectId: 'project-1',
repoId: 'repo-1',
hostId: 'ssh:ssh-target-1'
}
})
})
it('skips setup when the created branch differs from the content approved for the automation', async () => {
const worktreePath = mkdtempSync(path.join(tmpdir(), 'orca-automation-setup-'))
try {
@@ -89,22 +89,7 @@ describe('AutomationService zero-grace tick latency', () => {
expect(await statusAt(lateMs)).toBe('dispatched')
})
it('still skips a zero-grace occurrence that came due during a long sleep', async () => {
expect(await statusAt(4 * 60 * 60 * 1000)).toBe('skipped_missed')
})
it('skips once lateness exceeds the tick-latency tolerance', async () => {
expect(await statusAt(2 * 60_000 + 1)).toBe('skipped_missed')
})
it('dispatches after a restart that crosses the occurrence within tolerance', async () => {
vi.setSystemTime(new Date('2026-05-13T08:00:00'))
const store = await createStore()
store.addRepo(makeRepo())
const automation = makeZeroGrace(store)
await evaluateAt(store, DUE - 30_000)
expect(store.listAutomationRuns(automation.id)).toHaveLength(0)
await evaluateAt(store, DUE + 30_000)
expect(store.listAutomationRuns(automation.id)[0]?.status).toBe('dispatched')
})
})
@@ -158,27 +158,6 @@ describe('reconciling retained runs against a graph that has not published yet',
service.stop()
})
it('does not fail a serve-startup retained run whose host pane has not reattached', async () => {
const store = await createStore()
const automation = createAutomation(store)
const retained = retainDispatchedRun(store, automation)
const surface = createPaneSurface()
const service = new AutomationService(store, {
terminalLauncher: OPEN_LAUNCHER,
terminalObserver: surface.observer
})
service.start()
await vi.advanceTimersByTimeAsync(30_000)
expect(readRun(store, automation.id, retained.id).status).toBe('dispatched')
surface.mountPane()
await vi.advanceTimersByTimeAsync(5_000)
expect(surface.observedHandles).toEqual(['handle-1'])
expect(readRun(store, automation.id, retained.id).status).toBe('dispatched')
service.stop()
})
it('still closes out a run the ready surface cannot find after the settle window', async () => {
const store = await createStore()
const automation = createAutomation(store)
@@ -1,11 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { AutomationRun, AutomationRunStatus } from '../../shared/automations-types'
import { runKeyedSerializedOperation } from '../cli/keyed-promise-queue'
import {
createRunTerminalRetention,
RUN_TERMINAL_GRACE_MS,
RUN_TERMINALS_KEPT_PER_AUTOMATION
} from './run-terminal-retention'
import { createRunTerminalRetention, RUN_TERMINAL_GRACE_MS } from './run-terminal-retention'
function makeRun(
id: string,
@@ -87,19 +83,6 @@ afterEach(() => {
})
describe('headless run terminal retention', () => {
it('closes finished run terminals past the grace period, keeping the newest few viewable', async () => {
const runs = [0, 1, 2, 3, 4, 5].map((hour) => makeRun(`r${hour}`, hour * 3_600_000))
const h = harness(runs)
await h.retention.sweep()
expect(h.closed).toEqual([])
vi.advanceTimersByTime(RUN_TERMINAL_GRACE_MS)
await h.retention.sweep()
expect(h.closed.toSorted()).toEqual(['tab-r0:1', 'tab-r1:1', 'tab-r2:1'])
expect(6 - h.closed.length).toBe(RUN_TERMINALS_KEPT_PER_AUTOMATION)
})
it('never closes a run that has not finished, however old', async () => {
const runs = [
makeRun('working', 0, 'dispatched'),
@@ -142,18 +125,6 @@ describe('headless run terminal retention', () => {
}
)
it('keeps the newest few per automation, not across all of them', async () => {
const runs = [
...[0, 1, 2].map((n) => makeRun(`a${n}`, n, 'completed', 'a')),
...[0, 1, 2].map((n) => makeRun(`b${n}`, n, 'completed', 'b'))
]
const h = harness(runs)
await h.retention.sweep()
vi.advanceTimersByTime(RUN_TERMINAL_GRACE_MS)
await h.retention.sweep()
expect(h.closed).toEqual([])
})
it('drains every completed, unused run terminal for an update, ignoring keep and grace', async () => {
const runs = [
...[0, 1, 2, 3, 4].map((n) => makeRun(`r${n}`, n)),
@@ -190,30 +161,6 @@ describe('headless run terminal retention', () => {
expect(h.forgotten[0]).toMatchObject({ id: 'missing', status: 'dispatch_failed' })
})
it('closes a still-dispatched run whose agent exited, leaving its shell idle', async () => {
const runs = [
makeRun('exited', 0, 'dispatched'),
...[1, 2, 3].map((n) => makeRun(`done${n}`, n))
]
const h = harness(runs)
h.idleShell.add('exited')
await h.retention.sweep()
vi.advanceTimersByTime(RUN_TERMINAL_GRACE_MS)
await h.retention.sweep()
expect(h.closed).toEqual(['tab-exited:1'])
})
it('keeps the newest three live terminals, not counting ones already gone', async () => {
const runs = [0, 1, 2, 3, 4].map((n) => makeRun(`r${n}`, n))
const h = harness(runs)
h.dead.add('r4')
await h.retention.sweep()
vi.advanceTimersByTime(RUN_TERMINAL_GRACE_MS)
await h.retention.sweep()
expect(h.closed).toEqual(['tab-r0:1'])
expect(h.forgotten.map((run) => run.id).toSorted()).toEqual(['r0', 'r4'])
})
it('drains idle failed and exited runs for an update, never a live agent', async () => {
const runs = [
makeRun('missing', 0, 'dispatch_failed'),
@@ -329,27 +276,6 @@ describe('headless run terminal retention', () => {
expect(h.forgotten.map((run) => run.id)).not.toContain('r0')
})
it('closes a failed run in its own workspace, and never sleeps or closes a used one', async () => {
const runs = [
makeRun('failed', 0, 'dispatch_failed'),
makeRun('typed', 1),
...[2, 3, 4].map((n) => makeRun(`done${n}`, n))
]
const h = harness(runs)
h.ownWorkspace.add('failed')
h.ownWorkspace.add('typed')
h.idleShell.add('failed')
h.use.set('typed', 'used')
await h.retention.drain()
expect(h.slept).toEqual([])
expect(h.closed.toSorted()).toEqual([
'tab-done2:1',
'tab-done3:1',
'tab-done4:1',
'tab-failed:1'
])
})
it('keeps a run in its own workspace whose sleep declines or fails, retrying each sweep', async () => {
const runs = [0, 1, 2, 3].map((n) => makeRun(`r${n}`, n))
const h = harness(runs)
@@ -1,16 +1,10 @@
import { EventEmitter } from 'node:events'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createServer, type Server, type Socket } from 'node:net'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
import { DaemonClient } from '../daemon-client'
import type { DaemonPendingRequests } from '../daemon-client-pending-requests'
import {
encodeNdjson,
NDJSON_MAX_LINE_BYTES,
NdjsonLineTooLongError
} from '../../../../../shared/main-process-ndjson-framer'
import { encodeNdjson } from '../../../../../shared/main-process-ndjson-framer'
import { DaemonConnectionLostError } from '../../../protocol/daemon-errors'
import type { HelloMessage } from '../../../protocol/daemon-hello-protocol'
import type { DaemonRequest } from '../../../protocol/daemon-rpc-contract'
@@ -147,23 +141,6 @@ describe('DaemonClient', () => {
}
describe('connect', () => {
it('captures one matching endpoint identity from both authenticated sockets', async () => {
const identity = {
pid: 123,
startedAtMs: 456,
launchNonce: 'launch-a',
entryPath: '/Applications/Orca.app/Contents/Resources/daemon-entry.js',
appVersion: '1.2.3',
spawnerExecPath: '/Applications/Orca.app/Contents/MacOS/Orca'
}
await startMockDaemon({ helloIdentity: () => identity })
client = new DaemonClient({ socketPath, tokenPath })
await client.ensureConnected()
expect(client.getDaemonIdentity()).toEqual(identity)
})
it('rejects a v24 daemon that omits endpoint identity', async () => {
await startMockDaemon({ omitHelloIdentity: true })
client = new DaemonClient({ socketPath, tokenPath })
@@ -196,30 +173,6 @@ describe('DaemonClient', () => {
expect(client.getDaemonIdentity()).toBeNull()
})
it('removes socket startup listeners after connecting', async () => {
await startMockDaemon()
client = new DaemonClient({ socketPath, tokenPath })
await client.ensureConnected()
const connectedClient = client as unknown as {
controlSocket: Socket | null
streamSocket: Socket | null
}
for (const socket of [connectedClient.controlSocket, connectedClient.streamSocket]) {
expect(socket?.listenerCount('connect')).toBe(0)
// The post-handshake disconnect handler still needs one error listener.
expect(socket?.listenerCount('error')).toBe(1)
}
})
it('rejects on version mismatch', async () => {
await startMockDaemon({ rejectVersion: true })
client = new DaemonClient({ socketPath, tokenPath })
await expect(client.ensureConnected()).rejects.toThrow()
})
it('times out when the daemon never answers hello', async () => {
let resolveHello: () => void = () => {}
const helloReceived = new Promise<void>((resolve) => {
@@ -276,127 +229,9 @@ describe('DaemonClient', () => {
expect(disconnected.controlSocket).toBeNull()
expect(disconnected.streamSocket).toBeNull()
})
it('removes hello startup listeners after timeout', async () => {
vi.useFakeTimers()
client = new DaemonClient({ socketPath, tokenPath })
const write = vi.fn(() => true)
const destroy = vi.fn()
const socket = new EventEmitter() as Socket
socket.write = write as unknown as Socket['write']
socket.destroy = destroy as unknown as Socket['destroy']
const sendHello = (
client as unknown as {
sendHello(
socket: Socket,
token: string,
role: 'control' | 'stream',
timeoutMs: number
): Promise<void>
}
).sendHello.bind(client)
const promise = sendHello(socket, 'test-token-123', 'control', 5000)
const rejection = expect(promise).rejects.toThrow('Hello response timed out')
await vi.advanceTimersByTimeAsync(5000)
await rejection
expect(write).toHaveBeenCalledOnce()
expect(destroy).toHaveBeenCalledOnce()
expect(socket.listenerCount('data')).toBe(0)
expect(socket.listenerCount('error')).toBe(0)
expect(socket.listenerCount('close')).toBe(0)
})
it('rejects when the daemon closes before hello completes', async () => {
await startMockDaemon({ closeOnHello: true })
client = new DaemonClient({ socketPath, tokenPath })
await expect(client.ensureConnected()).rejects.toThrow(
'Connection closed before hello response'
)
})
it('rejects when the daemon closes immediately after connect', async () => {
await startMockDaemon({ closeOnConnect: true })
client = new DaemonClient({ socketPath, tokenPath })
await expect(client.ensureConnected()).rejects.toThrow()
})
it('fails a retired endpoint at the connect, not at the token read', async () => {
rmSync(tokenPath)
client = new DaemonClient({ socketPath, tokenPath })
const error = (await client
.ensureConnected()
.catch((err: unknown) => err)) as NodeJS.ErrnoException
expect(error.syscall).toBe('connect')
expect(['ENOENT', 'ECONNREFUSED']).toContain(error.code)
})
it('still attempts the handshake when the token is gone but a daemon is listening', async () => {
const hellos: HelloMessage[] = []
await startMockDaemon({ onHello: (msg) => hellos.push(msg) })
rmSync(tokenPath)
client = new DaemonClient({ socketPath, tokenPath })
await client.ensureConnected()
expect(hellos.length).toBeGreaterThan(0)
expect(hellos[0]?.token).toBe('')
})
})
describe('RPC', () => {
it('rejects an oversized request before installing a timer or writing', async () => {
await startMockDaemon()
client = new DaemonClient({ socketPath, tokenPath })
await client.ensureConnected()
const internals = client as unknown as {
controlSocket: Socket
pendingRequests: DaemonPendingRequests
}
const writeSpy = vi.spyOn(internals.controlSocket, 'write')
const timerSpy = vi.spyOn(globalThis, 'setTimeout')
try {
await expect(
client.request('write', { data: 'x'.repeat(NDJSON_MAX_LINE_BYTES) })
).rejects.toBeInstanceOf(NdjsonLineTooLongError)
expect(timerSpy).not.toHaveBeenCalled()
expect(writeSpy).not.toHaveBeenCalled()
expect(internals.pendingRequests.size).toBe(0)
} finally {
timerSpy.mockRestore()
writeSpy.mockRestore()
}
})
it('sends request and receives response', async () => {
await startMockDaemon({
onControlMessage: (msg) => {
const req = msg as { id: string; type: string }
if (req.type === 'listSessions') {
return encodeNdjson({
id: req.id,
ok: true,
payload: { sessions: [] }
})
}
return null
}
})
client = new DaemonClient({ socketPath, tokenPath })
await client.ensureConnected()
const result = await client.request('listSessions', undefined)
expect(result).toEqual({ sessions: [] })
})
it('survives a cancelCreateOrAttach the daemon refuses', async () => {
// Old daemons reject cancellation; that refusal must not disconnect sibling sessions.
const received: string[] = []
@@ -454,47 +289,6 @@ describe('DaemonClient', () => {
await rejected
})
it('rejects on error response', async () => {
await startMockDaemon({
onControlMessage: (msg) => {
const req = msg as { id: string; type: string }
return encodeNdjson({
id: req.id,
ok: false,
error: 'Something went wrong'
})
}
})
client = new DaemonClient({ socketPath, tokenPath })
await client.ensureConnected()
await expect(client.request('listSessions', undefined)).rejects.toThrow(
'Something went wrong'
)
})
it('adds recovery hints to node-pty daemon diagnostics', async () => {
await startMockDaemon({
onControlMessage: (msg) => {
const req = msg as { id: string; type: string }
return encodeNdjson({
id: req.id,
ok: false,
error:
"node-pty: posix_spawn failed: ENOENT (errno 2, No such file or directory) - helper='/tmp/deleted/spawn-helper'"
})
}
})
client = new DaemonClient({ socketPath, tokenPath })
await client.ensureConnected()
await expect(client.request('listSessions', undefined)).rejects.toThrow(
"Daemon's node-pty install is gone (worktree deleted?). Restart Orca. node-pty: posix_spawn failed: ENOENT"
)
})
})
describe('events', () => {
@@ -541,123 +335,7 @@ describe('DaemonClient', () => {
})
})
describe('disconnect', () => {
it('removes socket listeners when disconnecting', async () => {
await startMockDaemon()
client = new DaemonClient({ socketPath, tokenPath })
await client.ensureConnected()
const connectedClient = client as unknown as {
controlSocket: Socket | null
streamSocket: Socket | null
}
const sockets = [connectedClient.controlSocket, connectedClient.streamSocket]
for (const socket of sockets) {
expect(socket?.listenerCount('data')).toBe(1)
expect(socket?.listenerCount('close')).toBe(1)
expect(socket?.listenerCount('error')).toBe(1)
}
client.disconnect()
for (const socket of sockets) {
expect(socket?.listenerCount('data')).toBe(0)
expect(socket?.listenerCount('close')).toBe(0)
expect(socket?.listenerCount('error')).toBe(0)
}
})
it('emits disconnected when server destroys sockets', async () => {
const serverSockets: Socket[] = []
await startMockDaemon()
server.on('connection', (socket) => serverSockets.push(socket))
client = new DaemonClient({ socketPath, tokenPath })
const disconnected = vi.fn()
client.onDisconnected(disconnected)
await client.ensureConnected()
await waitFor(() => serverSockets.length >= 2)
for (const socket of serverSockets) {
socket.destroy()
}
await waitFor(() => disconnected.mock.calls.length > 0, 3000)
expect(client.isConnected()).toBe(false)
})
it('disconnect() can be called safely when not connected', () => {
client = new DaemonClient({ socketPath, tokenPath })
expect(() => client.disconnect()).not.toThrow()
})
})
describe('notify (fire-and-forget)', () => {
it('sends request with notify_ prefix without expecting response', async () => {
const received: unknown[] = []
await startMockDaemon({
onControlMessage: (msg) => {
received.push(msg)
return null
}
})
client = new DaemonClient({ socketPath, tokenPath })
await client.ensureConnected()
const delivered = client.notify('write', { sessionId: 'session-1', data: 'hello' })
expect(delivered).toBe(true)
await waitFor(() => received.length > 0)
const msg = received[0] as { id: string; type: string }
expect(msg.id).toMatch(/^notify_/)
expect(msg.type).toBe('write')
})
it('reports a dropped delivery when not connected', () => {
client = new DaemonClient({ socketPath, tokenPath })
expect(client.notify('write', { sessionId: 'session-1', data: 'hello' })).toBe(false)
})
it('reports a dropped delivery for an oversized payload without writing', async () => {
await startMockDaemon()
client = new DaemonClient({ socketPath, tokenPath })
await client.ensureConnected()
const internals = client as unknown as { controlSocket: Socket }
const writeSpy = vi.spyOn(internals.controlSocket, 'write')
expect(client.notify('write', { data: 'x'.repeat(NDJSON_MAX_LINE_BYTES) })).toBe(false)
expect(writeSpy).not.toHaveBeenCalled()
})
it('rejects an oversized settled notification without disconnecting', async () => {
await startMockDaemon()
client = new DaemonClient({ socketPath, tokenPath })
await client.ensureConnected()
const internals = client as unknown as { controlSocket: Socket }
const writeSpy = vi.spyOn(internals.controlSocket, 'write')
await expect(
client.notifyWithSettlement('write', { data: 'x'.repeat(NDJSON_MAX_LINE_BYTES) })
).resolves.toEqual({ outcome: 'refused', reason: 'encode_failed' })
expect(writeSpy).not.toHaveBeenCalled()
expect(client.isConnected()).toBe(true)
})
it('reports a dropped delivery when the socket write throws', async () => {
await startMockDaemon()
client = new DaemonClient({ socketPath, tokenPath })
await client.ensureConnected()
const internals = client as unknown as { controlSocket: Socket }
vi.spyOn(internals.controlSocket, 'write').mockImplementation(() => {
throw new Error('EPIPE')
})
expect(client.notify('write', { sessionId: 'session-1', data: 'hello' })).toBe(false)
})
it('reports an asynchronous socket write failure at settlement', async () => {
await startMockDaemon()
client = new DaemonClient({ socketPath, tokenPath })
@@ -1,21 +1,11 @@
import { describe, expect, it, vi } from 'vitest'
import {
createBinaryStreamFrameReader,
encodeBinaryStreamDataFrame,
encodeBinaryStreamEventFrame
encodeBinaryStreamDataFrame
} from '../daemon-stream-binary-framing'
import { createNdjsonParser } from '../../../../../shared/main-process-ndjson-framer'
import { streamFramesFor, type StreamWriteChunk } from '../daemon-stream-data-split'
import { streamFramesFor } from '../daemon-stream-data-split'
import type { DaemonEvent } from '../../../protocol/daemon-stream-events'
function isDaemonEvent(record: unknown): record is DaemonEvent {
return (
typeof record === 'object' && record !== null && 'type' in record && record.type === 'event'
)
}
const TERMINAL_TEXT = '\x1b[38;5;208m┌─ build ─┐\x1b[0m \u{1F680} ok\r\n"quoted" \\ tab\t'
function readAll(chunks: Buffer[], splitEvery?: number): { events: DaemonEvent[]; fatal: Error[] } {
const events: DaemonEvent[] = []
const fatal: Error[] = []
@@ -31,53 +21,7 @@ function readAll(chunks: Buffer[], splitEvery?: number): { events: DaemonEvent[]
return { events, fatal }
}
function binaryEvents(write: (stream: { write(chunk: StreamWriteChunk): void }) => void) {
const frames: Buffer[] = []
write({ write: (chunk) => frames.push(Buffer.from(chunk)) })
return readAll(frames).events
}
function ndjsonEvents(write: (stream: { write(chunk: StreamWriteChunk): void }) => void) {
const events: DaemonEvent[] = []
const parser = createNdjsonParser(
(record) => {
if (isDaemonEvent(record)) {
events.push(record)
}
},
() => {}
)
write({ write: (chunk) => parser.feed(String(chunk)) })
return events
}
describe('binary stream framing', () => {
it('round-trips data and event frames across any socket chunking', () => {
const frames = [
encodeBinaryStreamDataFrame('session-1', TERMINAL_TEXT, { seq: 42, rawLength: 7 }),
encodeBinaryStreamEventFrame({
type: 'event',
event: 'exit',
sessionId: 'session-1',
payload: { code: 0 }
}),
encodeBinaryStreamDataFrame('session-2', '')
]
const expected = [
{
type: 'event',
event: 'data',
sessionId: 'session-1',
payload: { seq: 42, rawLength: 7, data: TERMINAL_TEXT }
},
{ type: 'event', event: 'exit', sessionId: 'session-1', payload: { code: 0 } },
{ type: 'event', event: 'data', sessionId: 'session-2', payload: { data: '' } }
]
for (const splitEvery of [undefined, 1, 3, 7]) {
expect(readAll(frames, splitEvery)).toEqual({ events: expected, fatal: [] })
}
})
it('drops an unparsable record but keeps reading the frames after it', () => {
const broken = Buffer.from([1, 0, 0, 0, 3, 0x7b, 0x7b, 0x7b])
const next = encodeBinaryStreamDataFrame('session-1', 'after')
@@ -102,64 +46,6 @@ describe('binary stream framing', () => {
expect(oversized).toHaveBeenCalledTimes(1)
})
it.each([
['plain', undefined, undefined, false],
['seq and raw length', 12_000, 90_000, false],
['zero raw length', 0, 7, false],
['transformed', 3, 11, true]
] as const)(
'follows the NDJSON per-chunk metadata rules for %s data',
(_label, raw, seq, transformed) => {
const data = TERMINAL_TEXT.repeat(400)
const rawLength = raw ?? data.length
const write =
(framing: 'ndjson' | 'binary-v1') =>
(stream: { write(chunk: StreamWriteChunk): void }): void =>
streamFramesFor(framing).data(
stream,
'session-1',
data,
4096,
rawLength,
seq,
transformed
)
// Chunk boundaries differ by design (byte vs JSON budgets); the metadata rules must not.
const describeChunks = (events: DaemonEvent[]) => {
let consumed = 0
return events.map((event) => {
if (event.event !== 'data') {
throw new Error('expected data events only')
}
const { data: chunk, ...meta } = event.payload
consumed += chunk.length
return {
meta,
expected: transformed
? { seq, rawLength, sequenceChars: rawLength, transformed: true }
: {
...(seq === undefined ? {} : { seq: seq - (data.length - consumed) }),
...(raw === undefined && seq === undefined
? {}
: {
rawLength: raw === 0 ? 0 : chunk.length,
sequenceChars: raw === 0 ? 0 : chunk.length
})
}
}
})
}
for (const events of [binaryEvents(write('binary-v1')), ndjsonEvents(write('ndjson'))]) {
expect(
events.map((event) => (event.event === 'data' ? event.payload.data : '')).join('')
).toBe(data)
for (const { meta, expected } of describeChunks(events)) {
expect(meta).toEqual(expected)
}
}
}
)
it('keeps every frame within the frame budget', () => {
const frames: Buffer[] = []
streamFramesFor('binary-v1').data(
@@ -22,12 +22,6 @@ function write(
}
describe('writeStreamDataEvents wire parity', () => {
it('keeps JSON escaping, Unicode and newline framing byte-for-byte', () => {
expect(write('"\\\n\t\u0000é中🐙', 4096)).toEqual([
'{"type":"event","event":"data","sessionId":"session-1","payload":{"data":"\\\"\\\\\\n\\t\\u0000é中🐙"}}\n'
])
})
it('keeps split frames within the byte cap and preserves code points and sequence spans', () => {
const data = '🐙é中\x1b[0m"\\\n'.repeat(100)
for (const seq of [undefined, 9000]) {
@@ -1,42 +0,0 @@
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, expect, it, vi } from 'vitest'
import { readAndroidDeviceSystemMetadata } from './android-device-system-metadata'
let directory: string | undefined
afterEach(async () => {
vi.unstubAllEnvs()
if (directory) {
await rm(directory, { recursive: true, force: true })
}
})
it('reads the API of a stopped AVD from its configured custom data directory', async () => {
directory = await mkdtemp(join(tmpdir(), 'orca-avd-metadata-'))
const avdHome = join(directory, 'avds')
const data = join(directory, 'custom-data')
await mkdir(avdHome)
await mkdir(data)
await writeFile(join(avdHome, 'Pixel.ini'), `path=${data}\n`)
await writeFile(
join(data, 'config.ini'),
'image.sysdir.1=system-images/android-36/google_apis/arm64-v8a/\n'
)
vi.stubEnv('ANDROID_AVD_HOME', avdHome)
const runner = vi.fn()
expect(
await readAndroidDeviceSystemMetadata(
{
backend: 'android',
id: 'Pixel',
name: 'Pixel',
detail: 'avd',
state: 'shutdown',
isAvailable: true
},
runner,
{ sdkRoot: '/sdk', adb: '/sdk/adb', emulator: '/sdk/emulator' }
)
).toEqual({ apiLevel: '36' })
expect(runner).not.toHaveBeenCalled()
})
@@ -1,68 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
resolveEmulatorOpeningPreference,
rememberEmulatorDevice
} from './emulator-opening-preference'
import type { EmulatorDevice } from './backends/emulator-backend'
const devices: EmulatorDevice[] = [
{ backend: 'ios', id: 'iphone', name: 'iPhone', state: 'booted', isAvailable: true },
{
backend: 'android',
id: 'emulator-5558',
name: 'Pixel9_API36',
detail: 'emulator',
state: 'booted',
isAvailable: true
}
]
describe('emulator opening preferences', () => {
it('asks on first use instead of choosing a running iPhone', () => {
expect(resolveEmulatorOpeningPreference({}, devices)).toEqual({
selectionRequired: 'first-use'
})
})
it('preserves a legacy fixed device, but respects an explicit ask or last-used mode', () => {
const settings = {
mobileEmulatorDefaultDeviceUdid: 'iphone',
mobileEmulatorRecentDevices: rememberEmulatorDevice(devices[1], [])
}
expect(resolveEmulatorOpeningPreference(settings, devices)).toEqual({ device: 'iphone' })
expect(
resolveEmulatorOpeningPreference({ ...settings, mobileEmulatorOpeningMode: 'ask' }, devices)
).toEqual({ selectionRequired: 'ask' })
expect(
resolveEmulatorOpeningPreference(
{ ...settings, mobileEmulatorOpeningMode: 'last-used' },
devices
)
).toEqual({ device: 'emulator-5558' })
})
it('does not replace a missing or unavailable preference with another platform', () => {
expect(
resolveEmulatorOpeningPreference({ mobileEmulatorDefaultDeviceUdid: 'removed' }, devices)
).toEqual({ selectionRequired: 'missing' })
expect(
resolveEmulatorOpeningPreference({ mobileEmulatorDefaultDeviceUdid: 'iphone' }, [
{ ...devices[0], isAvailable: false }
])
).toEqual({ selectionRequired: 'unavailable' })
})
it('keeps physical serials distinct from AVD names and bounds recent preferences', () => {
const physical: EmulatorDevice = {
...devices[1],
id: 'USB-123',
name: 'Pixel9_API36',
detail: 'device'
}
const recent = rememberEmulatorDevice(physical, rememberEmulatorDevice(devices[1], []))
expect(recent.map((item) => item.id)).toEqual(['USB-123', 'Pixel9_API36'])
expect(
resolveEmulatorOpeningPreference({ mobileEmulatorRecentDevices: recent }, devices)
).toEqual({ selectionRequired: 'missing' })
let history = recent
for (let i = 0; i < 25; i++) {
history = rememberEmulatorDevice({ ...physical, id: `USB-${i}` }, history)
}
expect(history).toHaveLength(20)
})
})
@@ -1,188 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as Os from 'node:os'
const { execFileMock, platformMock } = vi.hoisted(() => ({
execFileMock: vi.fn(),
platformMock: vi.fn(() => 'darwin')
}))
vi.mock('child_process', () => ({
execFile: execFileMock
}))
vi.mock('os', async (importOriginal) => {
const actual = await importOriginal<typeof Os>()
return {
...actual,
platform: platformMock
}
})
vi.mock('./serve-sim-execution', () => ({
execServeSimCommand: vi.fn()
}))
import {
listSimulatorDevices,
resolveSimulatorUdid,
shutdownSimulatorDevice
} from './simctl-simulator-devices'
describe('listSimulatorDevices', () => {
beforeEach(() => {
execFileMock.mockReset()
platformMock.mockReset()
platformMock.mockReturnValue('darwin')
})
it('keeps lastBootedAt as usage evidence only when lastUsedAt is absent', async () => {
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callback(
null,
JSON.stringify({
devices: {
'com.apple.CoreSimulator.SimRuntime.iOS-26-0': [
{
udid: 'booted-before',
name: 'iPhone',
state: 'Shutdown',
lastBootedAt: '2026-10-29T10:00:00Z'
},
{
udid: 'used-after',
name: 'iPhone',
state: 'Shutdown',
lastBootedAt: '2026-10-29T10:00:00Z',
lastUsedAt: '2026-10-30T11:00:00Z'
}
]
}
}),
''
)
})
const devices = await listSimulatorDevices()
expect(devices.map((device) => device.lastUsedAt)).toEqual([
'2026-10-29T10:00:00Z',
'2026-10-30T11:00:00Z'
])
})
it('normalizes missing simctl errors for UI and CLI callers', async () => {
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callback(
Object.assign(new Error('Command failed: xcrun simctl list devices -j'), { code: 72 }),
'',
'xcrun: error: unable to find utility "simctl", not a developer tool or in PATH'
)
})
const error = await listSimulatorDevices().catch((caught: unknown) => caught)
expect(error).toMatchObject({
code: 'emulator_simctl_unavailable',
message: expect.stringContaining('Xcode Simulator tools are unavailable')
})
expect(error).toBeInstanceOf(Error)
expect((error as Error).message).not.toContain('Command failed')
})
})
describe('resolveSimulatorUdid', () => {
beforeEach(() => {
execFileMock.mockReset()
platformMock.mockReturnValue('darwin')
})
it('prefers an exact name over an earlier device whose name only contains it', async () => {
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callback(
null,
JSON.stringify({
devices: {
'com.apple.CoreSimulator.SimRuntime.iOS-27-0': [
{
name: 'iPhone 17e',
udid: '645A2204-ED7C-4253-B298-2749820B1B7E',
state: 'Shutdown'
},
{ name: 'iPhone 17', udid: '1BD4732B-EC1A-4074-8C70-6AC369607185', state: 'Shutdown' }
]
}
}),
''
)
})
const serveSim = { command: 'serve-sim', baseArgs: [], usesElectronAsNode: false }
await expect(resolveSimulatorUdid('iPhone 17', serveSim)).resolves.toBe(
'1BD4732B-EC1A-4074-8C70-6AC369607185'
)
await expect(resolveSimulatorUdid('iPHONE 17', serveSim)).resolves.toBe(
'1BD4732B-EC1A-4074-8C70-6AC369607185'
)
await expect(resolveSimulatorUdid('17e', serveSim)).resolves.toBe(
'645A2204-ED7C-4253-B298-2749820B1B7E'
)
})
})
describe('shutdownSimulatorDevice', () => {
beforeEach(() => {
execFileMock.mockReset()
platformMock.mockReset()
platformMock.mockReturnValue('darwin')
})
it('treats an already-shut-down device as success', async () => {
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callback(
Object.assign(
new Error('Command failed: xcrun simctl shutdown AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE'),
{ code: 164 }
),
'',
'Unable to shutdown device in current state: Shutdown'
)
})
await expect(
shutdownSimulatorDevice('AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE')
).resolves.toBeUndefined()
})
it('propagates real shutdown failures instead of swallowing them', async () => {
// Command text always contains shutdown; only the actual device state proves it is already off.
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callback(
Object.assign(
new Error('Command failed: xcrun simctl shutdown AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE'),
{ killed: true, signal: 'SIGTERM' }
),
'',
''
)
})
await expect(
shutdownSimulatorDevice('AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE')
).rejects.toMatchObject({ code: 'emulator_error' })
})
it('rejects current-state failures that are not already shut down', async () => {
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callback(
Object.assign(
new Error('Command failed: xcrun simctl shutdown AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE'),
{ code: 164 }
),
'',
'Unable to shutdown device in current state: Booting'
)
})
await expect(
shutdownSimulatorDevice('AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE')
).rejects.toMatchObject({ code: 'emulator_error' })
})
})
@@ -53,7 +53,6 @@ import {
resetSshUserDisconnectsForTests
} from '../ssh/ssh-user-disconnect'
import { getRuntimeOwnedSshTargetId } from '../ssh/connection/ssh-connection-store'
import { forgetRecipeVmWakePairingsForTests, recipeVmWakePairing } from './recipe-vm-wake-pairings'
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recipe context reader is mocked, so the resume never reads this store.
const STORE = {} as Store
@@ -67,15 +66,6 @@ describe("waking an SSH-connected recipe VM and its host's Disconnect", () => {
})
// The reconnect can move the tunnel and re-pair the server; the open that waited expects that.
it('reconnects the host when nobody disconnected it, recording the pairing it left', async () => {
forgetRecipeVmWakePairingsForTests()
await resumeEphemeralVmWorkspace(STORE, 'workspace-1', {
sshHeld: false,
sshUserDisconnects: 0
})
expect(mocks.connect).toHaveBeenCalledTimes(1)
expect(recipeVmWakePairing('env-vm')).toBe(9)
})
it('leaves a Disconnect made during the wake in place, with the VM up', async () => {
gates.resuming = Promise.withResolvers<void>()
@@ -117,12 +107,4 @@ describe("waking an SSH-connected recipe VM and its host's Disconnect", () => {
// Suspend tore down the VM's SSH transport, so the wake reconnects it even when the recipe had
// no resume step to run.
it('reconnects the host of a VM whose recipe resume was skipped', async () => {
gates.skipped = true
await resumeEphemeralVmWorkspace(STORE, 'workspace-1', {
sshHeld: false,
sshUserDisconnects: 0
})
expect(mocks.connect).toHaveBeenCalledTimes(1)
})
})
+2 -278
View File
@@ -9,18 +9,11 @@ vi.mock('../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
gitExecFileAsync: gitExecFileAsyncMock
}))
import {
getGiteaAuthStatus,
getGiteaPullRequestForBranch,
getGiteaPullRequestForBranchOrThrow,
normalizeGiteaApiBaseUrl
} from './client'
import { getGiteaPullRequestForBranch, getGiteaPullRequestForBranchOrThrow } from './client'
import { _resetGiteaRepoRefCache } from './repository-ref'
import {
_getGiteaPullRequestScanCacheSize,
_resetGiteaPullRequestScanCache,
scanGiteaPullRequests
_resetGiteaPullRequestScanCache
} from './pull-request-scan-cache'
import { __resetRepoDefaultBranchCacheForTests } from '../source-control/repo-default-branch'
@@ -70,50 +63,6 @@ describe('Gitea client', () => {
vi.unstubAllGlobals()
})
it('hides a stale closed PR whose source branch is the repo default branch (#9171)', async () => {
primeGitExecWithDefaultBranch()
const fetchMock = vi.fn(async (url: string) => {
const parsed = new URL(url)
if (parsed.pathname.endsWith('/status')) {
return Response.json({ state: 'success' })
}
return Response.json([{ ...giteaPr(7, 'main'), state: 'closed' }])
})
vi.stubGlobal('fetch', fetchMock)
await expect(getGiteaPullRequestForBranch('/repo', 'refs/heads/main')).resolves.toBeNull()
})
it('hides a stale merged PR on the default branch but keeps an open one', async () => {
primeGitExecWithDefaultBranch()
const fetchMock = vi.fn(async (url: string) => {
const parsed = new URL(url)
if (parsed.pathname.endsWith('/status')) {
return Response.json({ state: 'success' })
}
return Response.json([{ ...giteaPr(9, 'main'), merged: true }])
})
vi.stubGlobal('fetch', fetchMock)
await expect(getGiteaPullRequestForBranch('/repo', 'refs/heads/main')).resolves.toBeNull()
_resetGiteaPullRequestScanCache()
__resetRepoDefaultBranchCacheForTests()
const openFetchMock = vi.fn(async (url: string) => {
const parsed = new URL(url)
if (parsed.pathname.endsWith('/status')) {
return Response.json({ state: 'success' })
}
return Response.json([giteaPr(10, 'main')])
})
vi.stubGlobal('fetch', openFetchMock)
await expect(getGiteaPullRequestForBranch('/repo', 'refs/heads/main')).resolves.toMatchObject({
number: 10,
state: 'open'
})
})
it('discards a closed default-branch shadow and refetches the linked PR via the fallback (#9171)', async () => {
primeGitExecWithDefaultBranch()
const fetchMock = vi.fn(async (url: string) => {
@@ -133,124 +82,6 @@ describe('Gitea client', () => {
).resolves.toMatchObject({ number: 42 })
})
it('normalizes Gitea API base URLs', () => {
expect(normalizeGiteaApiBaseUrl('https://git.example.com')).toBe(
'https://git.example.com/api/v1'
)
expect(normalizeGiteaApiBaseUrl('https://git.example.com/api/v1/')).toBe(
'https://git.example.com/api/v1'
)
})
it('fetches a branch pull request and commit status', async () => {
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
const parsed = new URL(url)
if (!init) {
throw new Error('expected request init')
}
expect((init.headers as Record<string, string>).Authorization).toBe('token gitea-token')
if (parsed.pathname.endsWith('/commits/abc123/status')) {
return Response.json({ state: 'success' })
}
return Response.json([giteaPr()])
})
vi.stubGlobal('fetch', fetchMock)
await expect(
getGiteaPullRequestForBranch('/repo', 'refs/heads/feature/gitea')
).resolves.toEqual({
number: 7,
title: 'Add Gitea',
state: 'open',
url: 'https://git.example.com/team/repo/pulls/7',
status: 'success',
updatedAt: '2026-05-15T00:00:00Z',
mergeable: 'MERGEABLE',
headSha: 'abc123'
})
const listUrl = new URL(String(fetchMock.mock.calls[0]?.[0]))
expect(listUrl.origin).toBe('https://git.example.com')
expect(listUrl.pathname).toBe('/api/v1/repos/team/repo/pulls')
expect(listUrl.searchParams.get('state')).toBe('all')
expect(listUrl.searchParams.get('sort')).toBe('recentupdate')
expect(listUrl.searchParams.get('page')).toBe('1')
expect(listUrl.searchParams.get('limit')).toBe('50')
})
it('shares one /pulls scan across concurrent branch lookups (#8807)', async () => {
let listCalls = 0
const fetchMock = vi.fn(async (url: string) => {
const parsed = new URL(url)
if (parsed.pathname.endsWith('/status')) {
return Response.json({ state: 'success' })
}
listCalls++
return Response.json([giteaPr(7, 'feature/a'), giteaPr(8, 'feature/b')])
})
vi.stubGlobal('fetch', fetchMock)
const [a, b, missing] = await Promise.all([
getGiteaPullRequestForBranch('/repo', 'feature/a'),
getGiteaPullRequestForBranch('/repo', 'feature/b'),
getGiteaPullRequestForBranch('/repo', 'feature/none')
])
expect(a?.number).toBe(7)
expect(b?.number).toBe(8)
expect(missing).toBeNull()
expect(listCalls).toBe(1)
})
it('reuses the cached /pulls scan for lookups inside the TTL', async () => {
let listCalls = 0
const fetchMock = vi.fn(async (url: string) => {
const parsed = new URL(url)
if (parsed.pathname.endsWith('/status')) {
return Response.json({ state: 'success' })
}
listCalls++
return Response.json([giteaPr()])
})
vi.stubGlobal('fetch', fetchMock)
await getGiteaPullRequestForBranch('/repo', 'feature/gitea')
await getGiteaPullRequestForBranch('/repo', 'feature/gitea')
await getGiteaPullRequestForBranch('/repo', 'no-pr-branch')
expect(listCalls).toBe(1)
})
it('retries a failed /pulls scan after only the short failure cooldown', async () => {
vi.useFakeTimers()
try {
let listCalls = 0
const fetchMock = vi.fn(async (url: string) => {
const parsed = new URL(url)
if (parsed.pathname.endsWith('/status')) {
return Response.json({ state: 'success' })
}
listCalls++
return listCalls === 1
? Response.json({ message: 'temporary failure' }, { status: 503 })
: Response.json([giteaPr()])
})
vi.stubGlobal('fetch', fetchMock)
await expect(getGiteaPullRequestForBranch('/repo', 'feature/gitea')).resolves.toBeNull()
await expect(getGiteaPullRequestForBranch('/repo', 'feature/gitea')).resolves.toBeNull()
expect(listCalls).toBe(1)
await vi.advanceTimersByTimeAsync(3_001)
await expect(getGiteaPullRequestForBranch('/repo', 'feature/gitea')).resolves.toMatchObject({
number: 7
})
expect(listCalls).toBe(2)
} finally {
vi.useRealTimers()
}
})
it('surfaces a lookup failure via getGiteaPullRequestForBranchOrThrow instead of null (finding 4)', async () => {
const fetchMock = vi.fn(async () => Response.json({ message: 'unauthorized' }, { status: 403 }))
vi.stubGlobal('fetch', fetchMock)
@@ -279,16 +110,6 @@ describe('Gitea client', () => {
expect(requested).toEqual(['/api/v1/repos/team/repo/pulls', '/api/v1/repos/team/repo/pulls/42'])
})
it('resolves null without throwing when the scan returns an empty list', async () => {
// Gitea has no head-branch filter, so "branch has no PR" arrives as a 200
// with [] from the repo listing — never a 404. The throwing variant must
// still report that as a plain "no PR".
const fetchMock = vi.fn(async () => Response.json([]))
vi.stubGlobal('fetch', fetchMock)
await expect(getGiteaPullRequestForBranchOrThrow('/repo', 'feature/gitea')).resolves.toBeNull()
})
it('still throws on a 404 from the /pulls scan so a misconfigured host is not read as "no PR"', async () => {
const fetchMock = vi.fn(async () => Response.json({ message: 'not found' }, { status: 404 }))
vi.stubGlobal('fetch', fetchMock)
@@ -300,38 +121,6 @@ describe('Gitea client', () => {
)
})
it('expires successful scans and bounds retained repository listings', async () => {
vi.useFakeTimers()
try {
let listCalls = 0
const fetchMock = vi.fn(async (url: string) => {
const parsed = new URL(url)
if (parsed.pathname.endsWith('/status')) {
return Response.json({ state: 'success' })
}
listCalls++
return Response.json([giteaPr()])
})
vi.stubGlobal('fetch', fetchMock)
await getGiteaPullRequestForBranch('/repo', 'feature/gitea')
expect(_getGiteaPullRequestScanCacheSize()).toBe(1)
await vi.advanceTimersByTimeAsync(30_001)
expect(_getGiteaPullRequestScanCacheSize()).toBe(0)
await getGiteaPullRequestForBranch('/repo', 'feature/gitea')
expect(listCalls).toBe(2)
await Promise.all(
Array.from({ length: 40 }, (_, index) =>
scanGiteaPullRequests(`repo-${index}`, async () => [], 50, 5)
)
)
expect(_getGiteaPullRequestScanCacheSize()).toBe(32)
} finally {
vi.useRealTimers()
}
})
it('does not let an in-flight scan re-cache results from before an invalidation', async () => {
let releaseFirstScan!: () => void
const firstScanGate = new Promise<void>((resolve) => {
@@ -366,71 +155,6 @@ describe('Gitea client', () => {
expect(listCalls).toBe(2)
})
it('uses an API base URL override for subpath or non-standard deployments', async () => {
process.env.ORCA_GITEA_API_BASE_URL = 'https://git.example.com/code'
const fetchMock = vi.fn(async (url: string | URL) => {
if (String(url).includes('/commits/abc123/status')) {
return Response.json({ state: 'pending' })
}
return Response.json([giteaPr()])
})
vi.stubGlobal('fetch', fetchMock)
await expect(getGiteaPullRequestForBranch('/repo', 'feature/gitea')).resolves.toMatchObject({
number: 7,
status: 'pending'
})
expect(String(fetchMock.mock.calls[0]?.[0])).toContain(
'https://git.example.com/code/api/v1/repos/team/repo/pulls'
)
})
it('falls back to a linked PR number when branch lookup misses', async () => {
const fetchMock = vi.fn(async (url: string | URL) => {
const requestUrl = String(url)
if (requestUrl.includes('/commits/abc123/status')) {
return Response.json({ state: 'success' })
}
if (requestUrl.endsWith('/pulls/42')) {
return Response.json(giteaPr(42, 'renamed-local-branch'))
}
return Response.json([])
})
vi.stubGlobal('fetch', fetchMock)
await expect(getGiteaPullRequestForBranch('/repo', 'local-name', 42)).resolves.toMatchObject({
number: 42,
status: 'success'
})
})
it('reports configured token auth without a global API base URL', async () => {
await expect(getGiteaAuthStatus()).resolves.toEqual({
configured: true,
authenticated: true,
account: null,
baseUrl: null,
tokenConfigured: true
})
})
it('verifies token auth when a global API base URL is configured', async () => {
process.env.ORCA_GITEA_API_BASE_URL = 'https://git.example.com'
const fetchMock = vi.fn(async (_url: string, _init?: RequestInit) =>
Response.json({ login: 'gitea-user' })
)
vi.stubGlobal('fetch', fetchMock)
await expect(getGiteaAuthStatus()).resolves.toEqual({
configured: true,
authenticated: true,
account: 'gitea-user',
baseUrl: 'https://git.example.com/api/v1',
tokenConfigured: true
})
expect(String(fetchMock.mock.calls[0]?.[0])).toBe('https://git.example.com/api/v1/user')
})
it('cancels unread error-response bodies so bundled undici cannot crash on socket close', async () => {
let cancelledBodies = 0
const fetchMock = vi.fn(async () =>
@@ -1,67 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { uploadGitHubImage } from './image-attachment-upload'
const { gh, acquire, release } = vi.hoisted(() => ({
gh: vi.fn(),
acquire: vi.fn(),
release: vi.fn()
}))
vi.mock('./gh-utils', () => ({
ghExecFileAsync: gh,
acquire,
release,
extractExecError: (error: unknown) => ({ stdout: '', stderr: String(error) })
}))
const repository = {
ownerRepo: { owner: 'octo', repo: 'images', host: 'github.com' },
ghOptions: { host: 'github.com' }
}
beforeEach(() => {
vi.resetAllMocks()
})
describe('GitHub image attachment upload', () => {
it('posts unchanged binary bytes with their exact content length in the resolved gh context', async () => {
gh.mockResolvedValueOnce({ stdout: '{"id":123}' }).mockResolvedValueOnce({
stdout: '{"url":"https://github.com/user-attachments/assets/a"}'
})
await expect(
uploadGitHubImage({
fileName: 'screen.png',
contentType: 'image/png',
bytes: Buffer.from([0, 128, 255]),
resolve: async () => repository
})
).resolves.toEqual({ url: 'https://github.com/user-attachments/assets/a' })
expect(gh.mock.calls[0][0]).toEqual(['api', '--cache', '60s', 'repos/octo/images'])
expect(gh.mock.calls[1][0]).toEqual([
'api',
'-X',
'POST',
'https://uploads.github.com/user-attachments/assets?name=screen.png&content_type=image%2Fpng&repository_id=123',
'-H',
'Content-Type: application/octet-stream',
'-H',
'Content-Length: 3',
'--input',
'-'
])
expect(gh.mock.calls[1][1]).toMatchObject({
host: 'github.com',
idempotent: false,
stdin: Buffer.from([0, 128, 255])
})
})
it('reports a rejected upload without retrying the POST and releases admission', async () => {
gh.mockResolvedValueOnce({ stdout: '{"id":123}' }).mockRejectedValueOnce(new Error('HTTP 403'))
await expect(
uploadGitHubImage({
fileName: 'screen.png',
contentType: 'image/png',
bytes: Buffer.from([1]),
resolve: async () => repository
})
).rejects.toThrow('selected account')
expect(gh.mock.calls.map((call) => call[0][0])).toEqual(['api', 'api'])
expect(release).toHaveBeenCalledOnce()
})
})
-281
View File
@@ -1,281 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GlUtils from './gl-utils'
const {
glabExecFileAsyncMock,
getGlabKnownHostsMock,
getProjectRefMock,
acquireMock,
releaseMock
} = vi.hoisted(() => ({
glabExecFileAsyncMock: vi.fn(),
getGlabKnownHostsMock: vi.fn(),
getProjectRefMock: vi.fn(),
acquireMock: vi.fn(),
releaseMock: vi.fn()
}))
vi.mock('./gl-utils', async () => {
const actual = await vi.importActual<typeof GlUtils>('./gl-utils')
return {
...actual,
glabExecFileAsync: glabExecFileAsyncMock,
getGlabKnownHosts: getGlabKnownHostsMock,
getProjectRef: getProjectRefMock,
acquire: acquireMock,
release: releaseMock
}
})
import { getAuthenticatedViewer, getWorkItemByProjectRef, listTodos } from './client'
describe('gitlab client — viewer & paste-URL lookup', () => {
beforeEach(() => {
glabExecFileAsyncMock.mockReset()
getGlabKnownHostsMock.mockReset()
getProjectRefMock.mockReset()
acquireMock.mockReset()
releaseMock.mockReset()
acquireMock.mockResolvedValue(undefined)
getGlabKnownHostsMock.mockResolvedValue(['gitlab.com'])
getProjectRefMock.mockResolvedValue(null)
})
describe('getAuthenticatedViewer', () => {
it('returns username + email when glab api user succeeds', async () => {
glabExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({ username: 'alice', email: 'alice@example.com' })
})
await expect(getAuthenticatedViewer()).resolves.toEqual({
username: 'alice',
email: 'alice@example.com'
})
expect(glabExecFileAsyncMock).toHaveBeenCalledWith(['api', 'user'])
expect(acquireMock).toHaveBeenCalledOnce()
expect(releaseMock).toHaveBeenCalledOnce()
})
it('coerces a missing email to null', async () => {
glabExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({ username: 'alice', email: null })
})
await expect(getAuthenticatedViewer()).resolves.toEqual({
username: 'alice',
email: null
})
})
it('returns null when glab fails', async () => {
glabExecFileAsyncMock.mockRejectedValueOnce(new Error('not authenticated'))
await expect(getAuthenticatedViewer()).resolves.toBeNull()
expect(releaseMock).toHaveBeenCalledOnce()
})
it('returns null when username is empty', async () => {
glabExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({ username: ' ', email: null })
})
await expect(getAuthenticatedViewer()).resolves.toBeNull()
})
})
describe('getWorkItemByProjectRef', () => {
it('fetches an MR and maps to GitLabWorkItem', async () => {
glabExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
id: 100,
iid: 5,
title: 't',
state: 'opened',
web_url: 'https://gitlab.com/g/p/-/merge_requests/5',
source_branch: 'feat',
target_branch: 'main'
})
})
const item = await getWorkItemByProjectRef(
'/repo',
{ host: 'gitlab.com', path: 'g/p' },
5,
'mr'
)
expect(item).toMatchObject({ type: 'mr', number: 5, branchName: 'feat' })
expect(glabExecFileAsyncMock).toHaveBeenCalledWith(
['api', '--hostname', 'gitlab.com', 'projects/g%2Fp/merge_requests/5'],
{ cwd: '/repo' }
)
})
it('fetches an issue and maps to GitLabWorkItem', async () => {
glabExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
id: 200,
iid: 9,
title: 'bug',
state: 'opened',
web_url: 'https://gitlab.com/g/p/-/issues/9'
})
})
const item = await getWorkItemByProjectRef(
'/repo',
{ host: 'gitlab.com', path: 'g/p' },
9,
'issue'
)
expect(item).toMatchObject({ type: 'issue', number: 9 })
expect(glabExecFileAsyncMock).toHaveBeenCalledWith(
['api', '--hostname', 'gitlab.com', 'projects/g%2Fp/issues/9'],
{ cwd: '/repo' }
)
})
it('passes self-hosted hostname for local pasted URL lookups', async () => {
glabExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
id: 201,
iid: 9,
title: 'bug',
state: 'opened',
web_url: 'https://gitlab.internal/g/p/-/issues/9'
})
})
await getWorkItemByProjectRef('/repo', { host: 'gitlab.internal', path: 'g/p' }, 9, 'issue')
expect(glabExecFileAsyncMock).toHaveBeenCalledWith(
['api', '--hostname', 'gitlab.internal', 'projects/g%2Fp/issues/9'],
{ cwd: '/repo' }
)
})
it('routes local WSL pasted-project lookup through glab execution options', async () => {
const localGitOptions = { wslDistro: 'Ubuntu' }
glabExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
id: 201,
iid: 9,
title: 'WSL bug',
state: 'opened',
web_url: 'https://gitlab.com/g/p/-/issues/9'
})
})
await getWorkItemByProjectRef(
'/repo',
{ host: 'gitlab.com', path: 'g/p' },
9,
'issue',
null,
localGitOptions
)
expect(glabExecFileAsyncMock).toHaveBeenCalledWith(
['api', '--hostname', 'gitlab.com', 'projects/g%2Fp/issues/9'],
{
cwd: '/repo',
wslDistro: 'Ubuntu'
}
)
})
it('returns null when the API errors', async () => {
glabExecFileAsyncMock.mockRejectedValueOnce(new Error('not found'))
const item = await getWorkItemByProjectRef(
'/repo',
{ host: 'gitlab.com', path: 'g/p' },
9,
'issue'
)
expect(item).toBeNull()
})
})
describe('listTodos', () => {
it('maps glab todos response to GitLabTodo shape', async () => {
glabExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify([
{
id: 1,
action_name: 'assigned',
target_type: 'MergeRequest',
target: {
iid: 42,
title: 'Add feature',
web_url: 'https://gitlab.com/g/p/-/merge_requests/42'
},
target_url: 'https://gitlab.com/g/p/-/merge_requests/42',
author: { username: 'alice', avatar_url: 'https://example.com/a.png' },
project: { path_with_namespace: 'g/p' },
updated_at: '2026-05-08T10:00:00Z',
state: 'pending'
}
])
})
await expect(listTodos('/repo')).resolves.toEqual([
{
id: 1,
actionName: 'assigned',
targetType: 'MergeRequest',
targetIid: 42,
targetTitle: 'Add feature',
targetUrl: 'https://gitlab.com/g/p/-/merge_requests/42',
projectPath: 'g/p',
authorUsername: 'alice',
authorAvatarUrl: 'https://example.com/a.png',
updatedAt: '2026-05-08T10:00:00Z',
state: 'pending'
}
])
expect(glabExecFileAsyncMock).toHaveBeenCalledWith(
['api', 'todos?state=pending&per_page=50'],
{ cwd: '/repo' }
)
})
it('routes local WSL todos through project resolution and glab execution options', async () => {
const localGitOptions = { wslDistro: 'Ubuntu' }
getProjectRefMock.mockResolvedValueOnce({ host: 'gitlab.com', path: 'g/p' })
glabExecFileAsyncMock.mockResolvedValueOnce({ stdout: '[]' })
await expect(listTodos('/repo', null, localGitOptions)).resolves.toEqual([])
expect(getProjectRefMock).toHaveBeenCalledWith('/repo', ['gitlab.com'], null, localGitOptions)
expect(glabExecFileAsyncMock).toHaveBeenCalledWith(
['api', 'todos?state=pending&per_page=50'],
{ cwd: '/repo', wslDistro: 'Ubuntu' }
)
})
it('coerces non-pending state values to pending (defensive)', async () => {
// Preserve the pending/done union even if future API output ignores the pending filter.
glabExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify([
{ id: 2, action_name: 'mentioned', target_type: 'Issue', state: 'weird' }
])
})
const result = await listTodos('/repo')
expect(result[0].state).toBe('pending')
})
it('falls back to empty list when glab errors', async () => {
glabExecFileAsyncMock.mockRejectedValueOnce(new Error('auth failed'))
await expect(listTodos('/repo')).resolves.toEqual([])
})
it('handles missing target / project / author fields gracefully', async () => {
// Commit/Note todos can omit target; retain a well-formed display record.
glabExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify([{ id: 3, action_name: 'build_failed', target_type: 'Commit' }])
})
const result = await listTodos('/repo')
expect(result[0]).toMatchObject({
targetIid: null,
targetTitle: '',
targetUrl: '',
projectPath: '',
authorUsername: '',
authorAvatarUrl: ''
})
})
})
})
+1 -161
View File
@@ -26,13 +26,8 @@ vi.mock('os', async () => {
homedir: homedirMock
}
})
import { getGrokToolEventMatcherForTests, GrokHookService } from './hook-service'
import { GrokHookService } from './hook-service'
import { buildWindowsGrokHookScript } from './windows-grok-hook-script'
import {
POSIX_HOOK_JSON_STDIN_PRELUDE,
POSIX_HOOK_JSON_STDIN_READER
} from '../agent-hooks/hook-stdin-contract'
const GROK_SCRIPT_FILE_NAME = process.platform === 'win32' ? 'grok-hook.cmd' : 'grok-hook.sh'
const WINDOWS_POWERSHELL_LAUNCHER =
@@ -129,21 +124,6 @@ describe('GrokHookService', () => {
rmSync(homeDir, { recursive: true, force: true })
})
it('guards Windows GROK_HOME substring checks when empty (#9358)', () => {
const script = buildWindowsGrokHookScript()
expect(script).toContain('set "ORCA_GROK_HOME="')
expect(script).toContain('if not defined GROK_HOME goto :orca_grok_home_ready')
expect(script).toContain('%GROK_HOME:~4096,1%')
expect(script).toContain('set "ORCA_GROK_HOME=%GROK_HOME:"=%"')
expect(script).toContain('%ORCA_GROK_HOME:~4096,1%')
expect(script).toContain(':orca_grok_home_ready')
expect(script).toContain('if not defined ORCA_GROK_HOME goto :orca_grok_home_ready')
expect(script).toContain('if "%ORCA_GROK_HOME:~-1%"=="\\"')
expect(script).toContain('if not "%GROK_HOME:~4096,1%"=="" goto :orca_grok_home_ready')
// Parenthesized blocks still expand percent variables before their guard executes.
expect(script).not.toMatch(/if defined GROK_HOME \(/)
})
it.skipIf(process.platform !== 'win32')(
'generated grok-hook.cmd exits 0 when GROK_HOME is unset (#9358)',
async () => {
@@ -229,126 +209,7 @@ describe('GrokHookService', () => {
}
)
it('installs a dedicated global Grok hook config and managed script', () => {
const status = new GrokHookService().install()
expect(status.state).toBe('installed')
expect(status.configPath).toBe(join(homeDir, '.grok', 'hooks', 'orca-status.json'))
expect(status.managedHooksPresent).toBe(true)
const config = JSON.parse(
readFileSync(join(homeDir, '.grok', 'hooks', 'orca-status.json'), 'utf8')
) as {
hooks: Record<string, { matcher?: string; hooks: { command: string }[] }[]>
}
expect(Object.keys(config.hooks).sort()).toEqual(
[
'Notification',
'PreToolUse',
'PostToolUse',
'PostToolUseFailure',
'SessionEnd',
'SessionStart',
'Stop',
'StopCancelled',
'StopFailure',
'UserPromptSubmit'
].sort()
)
// PreToolUse is needed for in-flight tools and question waits.
expect(config.hooks.PreToolUse[0].matcher).toBe('.*')
expect(registersManagedGrokScript(config.hooks.PreToolUse[0].hooks[0].command)).toBe(true)
// Grok uses real regex matchers; bare * is invalid.
expect(config.hooks.PostToolUseFailure[0].matcher).toBe('.*')
expect(config.hooks.PostToolUse[0].matcher).toBe('.*')
expect(config.hooks.StopCancelled[0].matcher).toBeUndefined()
expect(config.hooks.StopFailure[0].matcher).toBeUndefined()
expect(config.hooks.Notification[0].matcher).toBeUndefined()
expect(getGrokToolEventMatcherForTests()).toBe('.*')
expect(getGrokToolEventMatcherForTests()).not.toBe('*')
expect(new RegExp(getGrokToolEventMatcherForTests()).test('run_terminal_command')).toBe(true)
// Construct invalid regex dynamically to avoid static lint rejection.
const bareStar = ['*', ''].join('')
expect(() => new RegExp(bareStar)).toThrow()
expect(registersManagedGrokScript(config.hooks.PostToolUse[0].hooks[0].command)).toBe(true)
if (process.platform !== 'win32') {
const command = config.hooks.PostToolUse[0].hooks[0].command
expect(command).toContain(join(homeDir, '.orca'))
expect(command).toMatch(/^if \[ -n "\$\{ORCA_PANE_KEY-\}" \] && /)
}
const script = readFileSync(
join(homeDir, '.orca', 'agent-hooks', GROK_SCRIPT_FILE_NAME),
'utf8'
)
expect(script).toContain('/hook/grok')
if (process.platform === 'win32') {
expect(script).toContain('%SystemRoot%\\System32\\curl.exe')
expect(script).toContain('set "ORCA_GROK_HOME=%GROK_HOME:"=%"')
expect(script).toContain('--data-urlencode "grokHome=%ORCA_GROK_HOME%"')
} else {
// Stream payload to curl stdin; reader variables must be initialized before capture.
const prelude = POSIX_HOOK_JSON_STDIN_PRELUDE.join('\n')
expect(script).toContain(prelude)
expect(script.indexOf(prelude)).toBeLessThan(
script.indexOf(`payload=$(${POSIX_HOOK_JSON_STDIN_READER})`)
)
expect(script).toContain(`payload=$(${POSIX_HOOK_JSON_STDIN_READER})`)
expect(script).toContain('printf \'%s\' "$payload" | LC_ALL= LC_NUMERIC=C curl')
expect(script).toContain('--data-urlencode "payload@-"')
expect(script).toContain('${#GROK_HOME}" -le 4096')
expect(script).toContain('--data-urlencode "grokHome=${grok_home}"')
expect(script).not.toContain('--data-urlencode "payload=${payload}"')
}
})
// Profile-path spaces need the encoded fallback so Grok cannot split the managed path.
it.skipIf(process.platform !== 'win32')(
'wraps the managed hook command to survive spaces in the profile path (#6078)',
() => {
const spaceHome = join(tmpdir(), 'orca grok home with spaces')
mkdirSync(spaceHome, { recursive: true })
homedirMock.mockReturnValue(spaceHome)
try {
expect(new GrokHookService().install().state).toBe('installed')
const config = JSON.parse(
readFileSync(join(spaceHome, '.grok', 'hooks', 'orca-status.json'), 'utf8')
) as { hooks: Record<string, { hooks: { command: string }[] }[]> }
for (const eventName of ['SessionStart', 'UserPromptSubmit', 'Stop']) {
const command = config.hooks[eventName]?.[0]?.hooks?.[0]?.command
expect(command).toMatch(WINDOWS_POWERSHELL_LAUNCHER)
}
} finally {
rmSync(spaceHome, { recursive: true, force: true })
}
}
)
it('installs hooks under GROK_HOME when set', () => {
const grokHome = mkdtempSync(join(tmpdir(), 'orca-grok-home-env-'))
const previous = process.env.GROK_HOME
process.env.GROK_HOME = grokHome
try {
const status = new GrokHookService().install()
expect(status.state).toBe('installed')
expect(status.configPath).toBe(join(grokHome, 'hooks', 'orca-status.json'))
expect(readFileSync(join(grokHome, 'hooks', 'orca-status.json'), 'utf8')).toContain(
'SessionStart'
)
expect(() =>
readFileSync(join(homeDir, '.grok', 'hooks', 'orca-status.json'), 'utf8')
).toThrow()
} finally {
if (previous === undefined) {
delete process.env.GROK_HOME
} else {
process.env.GROK_HOME = previous
}
rmSync(grokHome, { recursive: true, force: true })
}
})
it('does not restore a user-cleared global hook config on the next launch', () => {
const service = new GrokHookService()
@@ -375,15 +236,6 @@ describe('GrokHookService', () => {
expect(readFileSync(configPath, 'utf8')).toBe(userClearedConfig)
})
it('removes the empty managed config when hooks are disabled', () => {
const service = new GrokHookService()
const configPath = join(homeDir, '.grok', 'hooks', 'orca-status.json')
service.install()
expect(service.remove().state).toBe('not_installed')
expect(() => readFileSync(configPath, 'utf8')).toThrow()
})
it('removes the managed config even when a non-hook key remains', () => {
const service = new GrokHookService()
const configPath = join(homeDir, '.grok', 'hooks', 'orca-status.json')
@@ -400,18 +252,6 @@ describe('GrokHookService', () => {
})
// Respect startup opt-out while allowing explicit re-enabling to override it.
it('leaves a user-cleared config alone on a startup install', () => {
const service = new GrokHookService()
const configPath = join(homeDir, '.grok', 'hooks', 'orca-status.json')
const userCleared = '{ "hooks" : {} }\n'
expect(service.install().state).toBe('installed')
writeFileSync(configPath, userCleared, 'utf8')
service.install()
expect(readFileSync(configPath, 'utf8')).toBe(userCleared)
})
it('restores a user-cleared config when the user turns hooks back on', () => {
const service = new GrokHookService()
-50
View File
@@ -31,24 +31,6 @@ describe('HermesHookService', () => {
rmSync(homeDir, { recursive: true, force: true })
})
it('installs the managed Hermes plugin and enables it in config.yaml', () => {
const status = new HermesHookService().install()
expect(status).toMatchObject({
agent: 'hermes',
state: 'installed',
managedHooksPresent: true,
detail: null
})
const pluginDir = join(homeDir, 'plugins', _internals.HERMES_PLUGIN_NAME)
expect(readFileSync(join(pluginDir, 'plugin.yaml'), 'utf-8')).toContain('provides_hooks:')
expect(readFileSync(join(pluginDir, '__init__.py'), 'utf-8')).toContain('/hook/hermes')
const config = parse(readFileSync(join(homeDir, 'config.yaml'), 'utf-8')) as {
plugins: { enabled: string[] }
}
expect(config.plugins.enabled).toContain(_internals.HERMES_PLUGIN_NAME)
})
it('preserves other enabled plugins and removes Orca from disabled list', () => {
writeFileSync(
join(homeDir, 'config.yaml'),
@@ -217,38 +199,6 @@ describe('HermesHookService', () => {
expect(config.plugins.disabled).toBe('not-a-list')
})
it('reports partial when the plugin exists but is not enabled', () => {
new HermesHookService().install()
const update = _internals.updateConfigContent(
readFileSync(join(homeDir, 'config.yaml'), 'utf-8'),
_internals.disablePlugin
)
expect(update.content).toBeTruthy()
writeFileSync(join(homeDir, 'config.yaml'), update.content!, 'utf-8')
const status = new HermesHookService().getStatus()
expect(status.state).toBe('partial')
expect(status.detail).toContain('not enabled')
})
it('is visible to the real hermes CLI when hermes is installed', () => {
const hermesAvailable = spawnSync('hermes', ['--version'], { encoding: 'utf-8' }).status === 0
if (!hermesAvailable) {
return
}
new HermesHookService().install()
const output = execFileSync('hermes', ['plugins', 'list'], {
env: { ...process.env, HERMES_HOME: homeDir },
encoding: 'utf-8',
timeout: 15_000
})
expect(output).toContain(_internals.HERMES_PLUGIN_NAME)
expect(output.toLowerCase()).toContain('enabled')
}, 20_000)
it('registered plugin hooks post normalized JSON to Orca', async () => {
const pythonAvailable = spawnSync('python3', ['--version'], { encoding: 'utf-8' }).status === 0
if (!pythonAvailable) {
@@ -5,8 +5,6 @@ vi.mock('electron', () => ({ ipcMain: { handle: vi.fn(), removeHandler: vi.fn()
import type { SshTarget } from '../../../shared/ssh-types'
import {
disallowDesktopBrowserBridgeHost,
noticeDesktopBrowserBridgeHostControl,
stopDesktopBrowserBridgeHost,
type DesktopBrowserBridgeHostControlDeps
} from './desktop-browser-bridge-host-control'
import {
@@ -15,8 +13,6 @@ import {
setDesktopBrowserBridgeHostStopped
} from './desktop-browser-bridge-reconcile'
const NOTICE_CHANNEL = 'browser:remoteHostControlNotice'
function setup(targets: SshTarget[] = []) {
let noticed: string[] | undefined
const byId = new Map(targets.map((target) => [target.id, target]))
@@ -74,52 +70,6 @@ describe('desktop browser bridge host control', () => {
setDesktopBrowserBridgeControl(null)
})
it('tells the person once per host, and waits for a window before counting it shown', () => {
const { deps, sent, mainWindow } = setup([userHost, orcaOwnedHost])
const host = { targetId: userHost.id, hostLabel: 'build-box' }
noticeDesktopBrowserBridgeHostControl(host, () => null, deps)
expect(sent).toEqual([])
noticeDesktopBrowserBridgeHostControl(host, mainWindow, deps)
noticeDesktopBrowserBridgeHostControl(host, mainWindow, deps)
noticeDesktopBrowserBridgeHostControl(
{ targetId: orcaOwnedHost.id, hostLabel: 'sandbox' },
mainWindow,
deps
)
expect(sent).toEqual([
{
channel: NOTICE_CHANNEL,
payload: { targetId: 'ssh-1', hostLabel: 'build-box', canDisallow: true }
},
{
channel: NOTICE_CHANNEL,
payload: { targetId: 'runtime-ssh-1', hostLabel: 'sandbox', canDisallow: false }
}
])
expect(deps.settings?.getSettings().desktopBrowserBridgeNoticedTargetIds).toEqual([
'ssh-1',
'runtime-ssh-1'
])
})
it('Stop detaches the host and clears its tabs without touching its setting', () => {
const { deps, reconcile, byId } = setup([userHost])
stopDesktopBrowserBridgeHost(userHost.id, deps)
expect(isDesktopBrowserBridgeHostStopped(userHost.id)).toBe(true)
expect(reconcile).toHaveBeenCalledTimes(1)
expect(deps.endHostActivity).toHaveBeenCalledWith('ssh-1')
expect(byId.get(userHost.id)?.allowRemoteCliControl).toBeUndefined()
setDesktopBrowserBridgeHostStopped(userHost.id, false)
expect(isDesktopBrowserBridgeHostStopped(userHost.id)).toBe(false)
expect(reconcile).toHaveBeenCalledTimes(2)
})
it("Don't allow turns the host's setting off, and only stops hosts Orca created", () => {
const { deps, byId } = setup([userHost, orcaOwnedHost])
-10
View File
@@ -106,14 +106,4 @@ describe('classifyLinkedFile', () => {
}
expect(classifyLinkedFile(facts, platform, PATHEXT)).toBe(expected)
})
it('reveals sockets, devices and other non-regular files', () => {
const facts = {
names: ['/x/sock'],
isFile: false,
isDirectory: false,
mode: 0o755
}
expect(classifyLinkedFile({ ...facts, head: new Uint8Array() }, 'linux')).toBe('reveal')
})
})
@@ -24,11 +24,6 @@ describe.skipIf(process.platform === 'win32')('inspectLinkedFile', () => {
return path
}
it('opens documents and shows scripts as text', async () => {
expect(await inspectLinkedFile(await file('notes.md', '# Notes\n'))).toBe('open')
expect(await inspectLinkedFile(await file('run.sh', 'echo hi\n'))).toBe('view-as-text')
})
it('reads the content of +x files whatever their name', async () => {
expect(await inspectLinkedFile(await file('notes.md', '#!/bin/sh\n', 0o755))).toBe(
'view-as-text'
@@ -1,263 +0,0 @@
import '../runtime/rpc/unused-default-rpc-methods.test-fixture'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as NodeOs from 'node:os'
// Pins the desktop's Settings pairing handlers: every branch's reply and the runtime calls it makes.
const { handleMock, networkInterfacesMock } = vi.hoisted(() => ({
handleMock: vi.fn(),
networkInterfacesMock: vi.fn()
}))
vi.mock('electron', () => ({
app: { isPackaged: false },
ipcMain: { handle: handleMock },
shell: { openExternal: vi.fn() }
}))
vi.mock('os', async (importOriginal) => ({
...(await importOriginal<typeof NodeOs>()),
networkInterfaces: networkInterfacesMock
}))
import type { MobilePairingQrResult } from '../runtime/rpc/server/pairing/mobile-pairing-qr'
import {
MOBILE_PAIRING_OFFER_TTL_MS,
RUNTIME_PAIRING_OFFER_TTL_MS
} from '../runtime/rpc/server/pairing/pairing-offer-expiry'
import { registerMobileHandlers } from './mobile'
const LAN = {
en0: [{ address: '192.168.1.20', family: 'IPv4', internal: false, netmask: '255.255.255.0' }]
}
type Call = { method: string; args?: unknown }
function fakeServer(options: {
mobileOffer?: Record<string, unknown>
runtimeOffer?: Record<string, unknown>
exposureFails?: boolean
}) {
const calls: Call[] = []
const server = {
getNetworkExposureBindHost: () => '0.0.0.0',
ensureNetworkExposure: async () => {
calls.push({ method: 'ensureNetworkExposure' })
if (options.exposureFails) {
throw new Error('bind refused')
}
},
createMobilePairingOffer: async (args: Record<string, unknown>) => {
calls.push({ method: 'createMobilePairingOffer', args: { ...args, name: typeof args.name } })
return options.mobileOffer
},
createPairingOffer: (args: Record<string, unknown>) => {
calls.push({ method: 'createPairingOffer', args: { ...args, name: typeof args.name } })
return options.runtimeOffer
}
}
return { server, calls }
}
const MOBILE_OFFER = {
available: true,
pairingUrl: 'orca://pair#x',
endpoint: 'ws://192.168.1.20:6768',
deviceId: 'dev-1',
connectionMode: 'local-only',
webClientUrl: null,
offerExpiresAt: 1_000
}
const RUNTIME_OFFER = {
available: true,
pairingUrl: 'orca://pair#r',
webClientUrl: 'http://192.168.1.20:6768/#r',
endpoint: 'ws://192.168.1.20:6768',
deviceId: 'dev-2',
offerExpiresAt: 2_000
}
describe('desktop pairing IPC handlers', () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>()
const encodePairingQr = async (): Promise<MobilePairingQrResult> => ({
ok: true,
qrDataUrl: 'data:qr',
qrSize: 21
})
beforeEach(() => {
handlers.clear()
handleMock.mockReset()
handleMock.mockImplementation((channel: string, handler: (...args: unknown[]) => unknown) => {
handlers.set(channel, handler)
})
networkInterfacesMock.mockReset().mockReturnValue(LAN)
vi.spyOn(console, 'error').mockImplementation(() => {})
})
async function call(
channel: string,
server: ReturnType<typeof fakeServer>['server'],
args?: unknown
) {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handlers under test call only the four methods the fake implements.
registerMobileHandlers(server as never, {
encodePairingQr,
getDefaultRouteInterfaceNames: async () => new Set(['en0'])
})
return handlers.get(channel)?.({}, args)
}
describe('mobile:getPairingQR', () => {
it('mints at an explicit address', async () => {
const { server, calls } = fakeServer({ mobileOffer: MOBILE_OFFER })
expect(
await call('mobile:getPairingQR', server, {
address: '100.64.1.20',
connectionMode: 'local-only',
rotate: true
})
).toEqual({
available: true,
qrDataUrl: 'data:qr',
qrSize: 21,
pairingUrl: 'orca://pair#x',
endpoint: 'ws://192.168.1.20:6768',
deviceId: 'dev-1',
connectionMode: 'local-only',
expiresAt: 1_000
})
expect(calls).toEqual([
{
method: 'createMobilePairingOffer',
args: {
address: '100.64.1.20',
connectionMode: 'local-only',
rotate: true,
name: 'string',
offerTtlMs: MOBILE_PAIRING_OFFER_TTL_MS
}
}
])
})
it('falls back to the default LAN address', async () => {
const { server, calls } = fakeServer({ mobileOffer: MOBILE_OFFER })
await call('mobile:getPairingQR', server, { connectionMode: 'automatic' })
expect(calls[0]?.args).toMatchObject({ address: '192.168.1.20' })
})
it('refuses LAN-only pairing with no usable address and mints nothing', async () => {
networkInterfacesMock.mockReturnValue({})
const { server, calls } = fakeServer({ mobileOffer: MOBILE_OFFER })
expect(await call('mobile:getPairingQR', server, { connectionMode: 'local-only' })).toEqual({
available: false,
reason: 'invalid_advertised_endpoint',
guidance:
'No reachable network address is available for pairing. Connect to Wi‑Fi or Tailscale, or pick an address manually.'
})
expect(calls).toEqual([])
})
it('still asks for a Relay offer with no address, and hides the unadvertised endpoint', async () => {
networkInterfacesMock.mockReturnValue({})
const { server, calls } = fakeServer({ mobileOffer: MOBILE_OFFER })
expect(await call('mobile:getPairingQR', server, { connectionMode: 'automatic' })).toEqual(
expect.objectContaining({ available: true, endpoint: null })
)
expect(calls[0]?.args).toMatchObject({ address: null, connectionMode: 'automatic' })
})
it('passes through an unavailable offer with its Relay failure', async () => {
const relayFailure = { kind: 'install_missing' }
const { server } = fakeServer({
mobileOffer: { available: false, reason: 'relay_unavailable', guidance: 'g', relayFailure }
})
expect(await call('mobile:getPairingQR', server, { connectionMode: 'automatic' })).toEqual({
available: false,
reason: 'relay_unavailable',
guidance: 'g',
relayFailure
})
})
})
describe('mobile:getRuntimePairingUrl', () => {
it('widens, then mints a network-reach runtime link', async () => {
const { server, calls } = fakeServer({ runtimeOffer: RUNTIME_OFFER })
expect(await call('mobile:getRuntimePairingUrl', server, { rotate: true })).toEqual({
available: true,
pairingUrl: 'orca://pair#r',
webClientUrl: 'http://192.168.1.20:6768/#r',
endpoint: 'ws://192.168.1.20:6768',
deviceId: 'dev-2',
expiresAt: 2_000
})
expect(calls).toEqual([
{ method: 'ensureNetworkExposure' },
{
method: 'createPairingOffer',
args: {
address: '192.168.1.20',
rotate: true,
name: 'string',
scope: 'runtime',
reach: 'network',
offerTtlMs: RUNTIME_PAIRING_OFFER_TTL_MS
}
}
])
})
it('keeps this-computer reach on loopback without widening', async () => {
const { server, calls } = fakeServer({ runtimeOffer: RUNTIME_OFFER })
await call('mobile:getRuntimePairingUrl', server, {
address: '127.0.0.1',
reach: 'this-computer'
})
expect(calls).toEqual([
{
method: 'createPairingOffer',
args: expect.objectContaining({ address: '127.0.0.1', reach: 'this-computer' })
}
])
})
it('widens for this-computer intent at a non-loopback address', async () => {
const { server, calls } = fakeServer({ runtimeOffer: RUNTIME_OFFER })
await call('mobile:getRuntimePairingUrl', server, {
address: '192.168.1.20',
reach: 'this-computer'
})
expect(calls.map((entry) => entry.method)).toEqual([
'ensureNetworkExposure',
'createPairingOffer'
])
expect(calls[1]?.args).toMatchObject({ reach: 'network' })
})
it('reports a failed widening with guidance and mints nothing', async () => {
const { server, calls } = fakeServer({ runtimeOffer: RUNTIME_OFFER, exposureFails: true })
expect(await call('mobile:getRuntimePairingUrl', server)).toEqual({
available: false,
reason: 'network_exposure_failed',
guidance: expect.any(String)
})
expect(calls).toEqual([{ method: 'ensureNetworkExposure' }])
})
it('answers a bare unavailable with no address or no offer', async () => {
networkInterfacesMock.mockReturnValue({})
const none = fakeServer({ runtimeOffer: RUNTIME_OFFER })
expect(await call('mobile:getRuntimePairingUrl', none.server)).toEqual({ available: false })
expect(none.calls).toEqual([])
networkInterfacesMock.mockReturnValue(LAN)
const refused = fakeServer({
runtimeOffer: { available: false, reason: 'device_registry_unavailable', guidance: 'g' }
})
expect(await call('mobile:getRuntimePairingUrl', refused.server)).toEqual({
available: false
})
})
})
})
@@ -109,14 +109,6 @@ describe('a request to a recipe VM environment', () => {
rmSync(userDataPath, { recursive: true, force: true })
})
it('goes through to a running VM, holding a workspace open with its owner', async () => {
recordVm('running')
await expect(codeOf('none')).resolves.toBeNull()
expect(holdOpen).not.toHaveBeenCalled()
await expect(codeOf('wake')).resolves.toBeNull()
expect(holdOpen).toHaveBeenCalledWith('workspace-of-vm-1', false, 180_000, undefined)
})
it.each(['suspended', 'resume_failed'] as const)(
'leaves a %s VM asleep for a request that does not open its workspace',
async (status) => {
@@ -127,18 +119,6 @@ describe('a request to a recipe VM environment', () => {
}
)
it.each([
['up', null],
['waking', 'runtime_environment_waking'],
['wake-failed', 'runtime_environment_wake_failed'],
['slept', 'runtime_environment_asleep']
] as const)('answers a held open that ended %s', async (opening, code) => {
recordVm('suspended')
ownerOpening = opening
await expect(codeOf('wake')).resolves.toBe(code)
expect(holdOpen).toHaveBeenCalledWith('workspace-of-vm-1', true, 180_000, undefined)
})
it.each([
['waking', 'runtime_environment_waking'],
['asleep', 'runtime_environment_asleep']
@@ -152,11 +132,6 @@ describe('a request to a recipe VM environment', () => {
)
// A phone gives up after 30 seconds, so its open is held for less.
it("holds an open only as long as its caller's preparation allows", async () => {
recordVm('suspended')
await prepareRecipeVm(userDataPath, VM_ENVIRONMENT, 'wake', 25_000)
expect(holdOpen).toHaveBeenCalledWith('workspace-of-vm-1', true, 25_000, undefined)
})
// A Disconnect made before the open holds: no client's open wakes the VM behind that server.
it('refuses an open of a server the user disconnected, before waking anything', async () => {
@@ -211,10 +186,4 @@ describe('a request to a recipe VM environment', () => {
await expect(codeOf('none', { id: 'env-manual', source: 'manual' })).resolves.toBeNull()
expect(state).not.toHaveBeenCalled()
})
it('says a suspended VM is asleep on a host that cannot wake it', async () => {
registerRecipeVmWaker(null)
recordVm('suspended')
await expect(codeOf('wake')).resolves.toBe('runtime_environment_asleep')
})
})
@@ -43,35 +43,6 @@ afterEach(() => {
rmSync(userDataPath, { recursive: true, force: true })
})
it('removes a managed entry through forgetRedundant while another entry manages the server', async () => {
const environment = addManaged('managed', 'ssh-1', 46_768, 'public-key')
addManaged('managed-alias', 'ssh-2', 46_769, 'public-key')
forgetRedundant.mockResolvedValueOnce({
outcome: 'forgotten',
verdict: 'unverifiable',
environmentId: 'managed',
sshTargetId: 'ssh-1'
})
await expect(removeRedundantManagedPairing(userDataPath, environment)).resolves.toMatchObject({
removed: { id: 'managed' }
})
expect(forgetRedundant).toHaveBeenCalledWith('managed')
})
it('surfaces a refusal instead of reporting the entry removed', async () => {
const environment = addManaged('managed', 'ssh-1', 46_768, 'public-key')
addManaged('managed-alias', 'ssh-2', 46_769, 'public-key')
forgetRedundant.mockResolvedValueOnce({
outcome: 'refused',
verdict: 'live',
code: 'orcad_forget_no_managed_twin',
reason: 'No other saved entry manages this server anymore.'
})
await expect(removeRedundantManagedPairing(userDataPath, environment)).rejects.toThrow(
'No other saved entry manages this server anymore.'
)
})
it('leaves the pairing to the store guard when the other entry manages a different server', () => {
const environment = addManaged('managed', 'ssh-1', 46_768, 'public-key')
addManaged('other', 'ssh-2', 46_769, 'other-key')
@@ -55,12 +55,4 @@ describe('a request whose own recipe VM wake re-paired its server', () => {
}
expect((await fence(1, 'before')).failure).toMatchObject({ ok: false })
})
it('holds a request that woke nothing to the current pairing', async () => {
prepared.current = { environment: AFTER_WAKE, before: AFTER_WAKE, wokenPairingRevision: null }
await expect(fence(2, 'after')).resolves.toMatchObject({
failure: null,
expected: { pairingRevision: 2, runtimeId: 'after' }
})
})
})
@@ -1,111 +0,0 @@
// #36923: the desktop's own calls and subscriptions tell the server preparation which request
// needs the server (only one that opens a workspace may wake a sleeping recipe VM) and hand it
// their abort signal, so a caller that gives up stops waiting on a wake.
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
resolveManaged: vi.fn(
async (
_userDataPath: string,
id: string,
_preparation?: unknown,
_method?: string,
_signal?: AbortSignal
) => ({ id, pairingRevision: 1 })
),
// The server comes back re-paired, so the caller's pairing fence ends the call right after
// its preparation: only what preparation was handed is under test.
prepareRequest: vi.fn(
async (
_userDataPath: string,
id: string,
_preparation?: unknown,
_method?: string,
_signal?: AbortSignal
) => ({
environment: { id, pairingRevision: 2, createdAt: 0 },
before: { id, pairingRevision: 2, createdAt: 0 },
wokenPairingRevision: null
})
)
}))
vi.mock('electron', () => ({ app: { getPath: () => '/user-data' } }))
vi.mock('../../../shared/remote-runtime/host-store/runtime-environment-store', () => ({
resolveEnvironment: (_userDataPath: string, id: string) => ({ id, pairingRevision: 1 }),
markEnvironmentUsed: vi.fn()
}))
vi.mock('../../../shared/remote-runtime/host-catalog/runtime-environments', () => ({
getPreferredPairedRuntimeConnection: () => ({ endpoint: 'ws://vm:6768' })
}))
vi.mock('./runtime-environment-managed-tunnel', () => ({
resolveManagedRuntimeEnvironment: mocks.resolveManaged,
prepareManagedRuntimeRequest: mocks.prepareRequest,
requireWslHostWithoutStart: vi.fn()
}))
vi.mock('../../../shared/remote-runtime/remote-runtime-client', () => ({
subscribeRemoteRuntimeRequest: vi.fn(async () => ({ close: vi.fn() }))
}))
vi.mock('./runtime-environment-support-routing', () => ({
shouldRouteSubscriptionBySupport: () => false
}))
import {
callRuntimeEnvironment,
subscribeRuntimeEnvironment
} from './runtime-environment-transport-routing'
const callbacks = { onEvent: vi.fn(), onClose: vi.fn() }
describe('runtime environment transport and a sleeping recipe VM', () => {
beforeEach(() => {
mocks.resolveManaged.mockClear()
mocks.prepareRequest.mockClear()
})
it.each(['worktree.activate', 'worktree.ps'])(
'prepares the server for a %s call with its method and signal',
async (method) => {
const signal = new AbortController().signal
const response = await callRuntimeEnvironment(
'/user-data',
'env-vm',
method,
{},
1000,
1,
undefined,
{ preparation: { wslStart: 'never' }, signal }
)
expect(response).toMatchObject({ ok: false, error: { code: 'runtime_environment_changed' } })
expect(mocks.prepareRequest).toHaveBeenCalledWith(
'/user-data',
'env-vm',
{ wslStart: 'never' },
method,
signal
)
}
)
it('prepares the server for a subscription with its method and signal', async () => {
const signal = new AbortController().signal
await subscribeRuntimeEnvironment(
'/user-data',
'env-vm',
'runtime.clientEvents.subscribe',
undefined,
1000,
callbacks,
() => true,
signal
)
expect(mocks.resolveManaged).toHaveBeenCalledWith(
'/user-data',
'env-vm',
undefined,
'runtime.clientEvents.subscribe',
signal
)
})
})
-100
View File
@@ -1,100 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
handleMock,
fromWebContentsMock,
getSpeechModelManagerMock,
getSpeechSttServiceMock,
deleteLocalSpeechModelMock
} = vi.hoisted(() => ({
handleMock: vi.fn(),
fromWebContentsMock: vi.fn(),
getSpeechModelManagerMock: vi.fn(),
getSpeechSttServiceMock: vi.fn(),
deleteLocalSpeechModelMock: vi.fn()
}))
vi.mock('electron', () => ({
app: { getPath: vi.fn(() => '/tmp/orca-speech-test') },
BrowserWindow: { fromWebContents: fromWebContentsMock },
ipcMain: { handle: handleMock, on: vi.fn() },
safeStorage: {
decryptString: vi.fn(),
encryptString: vi.fn(() => Buffer.from('encrypted')),
isEncryptionAvailable: vi.fn(() => true)
},
systemPreferences: {
getMediaAccessStatus: vi.fn(() => 'granted'),
askForMediaAccess: vi.fn(() => Promise.resolve(true))
}
}))
vi.mock('../speech/model-catalog', () => ({
SPEECH_MODEL_CATALOG: [],
getCatalogModel: vi.fn(() => ({ id: 'model-1' }))
}))
vi.mock('../speech/speech-runtime-service', () => ({
getSpeechModelManager: getSpeechModelManagerMock,
getSpeechSttService: getSpeechSttServiceMock
}))
vi.mock('../speech/speech-model-deletion', () => ({
deleteLocalSpeechModel: deleteLocalSpeechModelMock
}))
import { registerSpeechHandlers } from './speech'
type SpeechDownloadHandler = (event: { sender: { id: number } }, modelId: string) => Promise<void>
function getHandler(channel: string): SpeechDownloadHandler {
const call = handleMock.mock.calls.find((entry) => entry[0] === channel)
if (!call) {
throw new Error(`${channel} handler not registered`)
}
return call[1] as SpeechDownloadHandler
}
describe('registerSpeechHandlers', () => {
beforeEach(() => {
handleMock.mockReset()
fromWebContentsMock.mockReset()
getSpeechModelManagerMock.mockReset()
getSpeechSttServiceMock.mockReset()
deleteLocalSpeechModelMock.mockReset()
})
it('clears the model download progress callback when the window closes', async () => {
const clearProgressCallback = vi.fn()
let resolveDownload: () => void = () => {}
const closeHandlers: (() => void)[] = []
const manager = {
setProgressCallback: vi.fn(() => clearProgressCallback),
downloadModel: vi.fn(
() =>
new Promise<void>((resolve) => {
resolveDownload = resolve
})
)
}
const window = {
isDestroyed: vi.fn(() => false),
webContents: { send: vi.fn() },
once: vi.fn((_event: string, handler: () => void) => {
closeHandlers.push(handler)
}),
off: vi.fn()
}
getSpeechModelManagerMock.mockReturnValue(manager)
fromWebContentsMock.mockReturnValue(window)
registerSpeechHandlers({} as never)
const pending = getHandler('speech:downloadModel')({ sender: { id: 7 } }, 'model-1')
closeHandlers[0]?.()
resolveDownload()
await pending
expect(clearProgressCallback).toHaveBeenCalledTimes(1)
expect(window.off).toHaveBeenCalledWith('closed', expect.any(Function))
})
})
@@ -1,70 +0,0 @@
// #37949: every removal of a saved SSH target (Settings, `orca host rm`, a paired client, a recipe
// VM's cleanup) ends in removeRegisteredSshTarget, so the window's cleanup must happen there.
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => {
const settings: { hostSettingOverrides?: Record<string, Record<string, string>> } = {}
return {
removeTarget: vi.fn(),
send: vi.fn(),
settings,
updateSettings: vi.fn()
}
})
vi.mock('../../ssh/targets/ssh-target-registry', () => ({
getSshTargetRegistryStore: () => ({ removeTarget: mocks.removeTarget })
}))
vi.mock('./ssh-ipc-context', () => ({
getCurrentMainWindow: () => ({ isDestroyed: () => false, webContents: { send: mocks.send } }),
connectionManager: { disconnect: vi.fn(async () => {}) },
portForwardManager: null,
persistedStore: {
getSettings: () => mocks.settings,
updateSettings: mocks.updateSettings,
removeSshRemotePtyLeases: vi.fn()
}
}))
vi.mock('./ssh-target-lifecycle-queue', () => ({
runTargetLifecycle: (_targetId: string, run: () => Promise<void>) => run()
}))
vi.mock('../../managed-server/lifecycle/orcad-managed-tunnel', () => ({
closeOrcadManagedTunnelsForTarget: vi.fn(async () => {})
}))
vi.mock('../../browser/routing/local-host-partitions/local-ssh-browser-partitions', () => ({
releaseLocalSshBrowserPartitionsForTarget: vi.fn(async () => {})
}))
vi.mock('../../browser/routing/partitions/browser-route-partition-storage-runtime', () => ({
clearBrowserRoutePartitionStorageForLocalSshTarget: vi.fn(async () => {})
}))
const { removeRegisteredSshTarget } = await import('./ssh-session-teardown')
describe('removing a saved SSH target', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.settings.hostSettingOverrides = {
'ssh:ssh-1': { displayLabel: 'Renamed', defaultWorktreeLocation: '/srv' },
'ssh:ssh-2': { displayLabel: 'Other' }
}
})
it("tells the window and drops the host's rename after the target is gone", async () => {
await removeRegisteredSshTarget('ssh-1')
expect(mocks.removeTarget).toHaveBeenCalledWith('ssh-1')
expect(mocks.send).toHaveBeenCalledWith('ssh:target-removed', { targetId: 'ssh-1' })
expect(mocks.removeTarget.mock.invocationCallOrder[0]).toBeLessThan(
mocks.send.mock.invocationCallOrder[0]!
)
expect(mocks.updateSettings).toHaveBeenCalledWith(
{
hostSettingOverrides: {
'ssh:ssh-1': { defaultWorktreeLocation: '/srv' },
'ssh:ssh-2': { displayLabel: 'Other' }
}
},
{ notifyListeners: true }
)
})
})
+2 -133
View File
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { parse as parseToml } from 'smol-toml'
@@ -17,9 +17,7 @@ import { JcodeHookService } from './hook-service'
import {
getJcodeConfigPath,
getJcodeManagedCommand,
getJcodeManagedScriptFileName,
getJcodeManagedScriptPath,
JCODE_HOOK_EVENTS
getJcodeManagedScriptPath
} from './hook-settings'
import { tomlQuoteString } from './hook-config'
@@ -39,36 +37,6 @@ describe('JcodeHookService', () => {
rmSync(homeDir, { recursive: true, force: true })
})
it('reports not_installed before any install', () => {
const status = new JcodeHookService().getStatus()
expect(status).toMatchObject({
agent: 'jcode',
state: 'not_installed',
configPath: getJcodeConfigPath()
})
})
it('installs managed hooks into jcode config.toml and posts to /hook/jcode', () => {
const status = new JcodeHookService().install()
expect(status.state).toBe('installed')
expect(status.agent).toBe('jcode')
expect(status.configPath).toBe(getJcodeConfigPath())
expect(status.managedHooksPresent).toBe(true)
const config = readFileSync(getJcodeConfigPath(), 'utf8')
for (const event of JCODE_HOOK_EVENTS) {
// Commands are shell-quoted for jcode, then TOML-quoted for config storage.
expect(config).toContain(
`${event} = ${tomlQuoteString(getJcodeManagedCommand(getJcodeManagedScriptPath()))}`
)
}
const script = readFileSync(getJcodeManagedScriptPath(), 'utf8')
expect(script).toContain('/hook/jcode')
expect(script).toContain('payload@-')
expect(script).toContain('$JCODE_HOOK_PAYLOAD')
expect(script).toContain('payload="$JCODE_HOOK_PAYLOAD"')
})
it('reports a missing managed script and repairs it without changing config', () => {
const service = new JcodeHookService()
service.install()
@@ -86,30 +54,6 @@ describe('JcodeHookService', () => {
expect(readFileSync(getJcodeConfigPath(), 'utf8')).toBe(config)
})
it('reports missing scripts alongside incomplete event coverage and user hooks', () => {
const service = new JcodeHookService()
service.install()
writeFileSync(
getJcodeConfigPath(),
`[hooks]\nsession_start = ${tomlQuoteString(getJcodeManagedCommand(getJcodeManagedScriptPath()))}\nturn_end = "~/bin/my-turn-notify"\n`,
'utf8'
)
rmSync(getJcodeManagedScriptPath())
const status = service.getStatus()
expect(status.state).toBe('partial')
expect(status.detail).toContain('Managed hook script missing')
expect(status.detail).toContain(
'Managed hook missing for events: turn_start, pre_tool, post_tool, session_end'
)
expect(status.detail).toContain('User-owned hooks kept for events: turn_end')
expect(service.install().state).toBe('partial')
expect(service.getStatus().detail).not.toContain('script missing')
expect(readFileSync(getJcodeConfigPath(), 'utf8')).toContain(
'turn_end = "~/bin/my-turn-notify"'
)
})
it.each([
'/Users/previous/.orca/agent-hooks/jcode-hook.sh',
'C:\\Users\\previous\\.orca\\agent-hooks\\jcode-hook.cmd'
@@ -133,60 +77,6 @@ describe('JcodeHookService', () => {
expect(readFileSync(configPath, 'utf8')).not.toContain('previous')
})
it('does not report stale managed commands as installed beside the current one', () => {
const service = new JcodeHookService()
service.install()
const staleCommand = getJcodeManagedCommand('/Users/previous/.orca/agent-hooks/jcode-hook.sh')
writeFileSync(
getJcodeConfigPath(),
`[hooks]\n${JCODE_HOOK_EVENTS.map((event) => `${event} = [${tomlQuoteString(staleCommand)}, ${tomlQuoteString(getJcodeManagedCommand(getJcodeManagedScriptPath()))}]`).join('\n')}\n`,
'utf8'
)
const status = service.getStatus()
expect(status.state).toBe('partial')
expect(status.managedHooksPresent).toBe(true)
expect(status.detail).toBe(
`Managed hook command outdated for events: ${JCODE_HOOK_EVENTS.join(', ')}`
)
expect(service.install().state).toBe('installed')
expect(readFileSync(getJcodeConfigPath(), 'utf8')).not.toContain('previous')
})
it('reports legacy unquoted commands as outdated even when the script exists', () => {
const service = new JcodeHookService()
service.install()
writeFileSync(
getJcodeConfigPath(),
`[hooks]\n${JCODE_HOOK_EVENTS.map((event) => `${event} = ${tomlQuoteString(getJcodeManagedScriptPath())}`).join('\n')}\n`,
'utf8'
)
const status = service.getStatus()
expect(status.state).toBe('partial')
expect(status.detail).toContain('Managed hook command outdated for events:')
expect(status.detail).not.toContain('script missing')
expect(status.detail).not.toContain('User-owned')
expect(service.install().state).toBe('installed')
})
it('does not mistake a user command mentioning the managed script in a comment for Orca ownership', () => {
const configPath = getJcodeConfigPath()
mkdirSync(dirname(configPath), { recursive: true })
writeFileSync(
configPath,
`[hooks]\nturn_end = "~/bin/my-turn-notify" # replaces agent-hooks/${getJcodeManagedScriptFileName()}\n`,
'utf8'
)
const status = new JcodeHookService().getStatus()
expect(status.state).toBe('partial')
expect(status.managedHooksPresent).toBe(false)
expect(status.detail).toContain('User-owned hooks kept for events: turn_end')
expect(status.detail).not.toContain('command outdated')
expect(status.detail).not.toContain('script missing')
})
it('preserves unrelated config tables when installing hooks', () => {
const configPath = getJcodeConfigPath()
mkdirSync(dirname(configPath), { recursive: true })
@@ -207,15 +97,6 @@ describe('JcodeHookService', () => {
expect(readFileSync(configPath, 'utf8')).toContain('turn_end = "~/bin/my-turn-notify"')
})
it('reports error when the [hooks] table holds a non-scalar value', () => {
const configPath = getJcodeConfigPath()
mkdirSync(dirname(configPath), { recursive: true })
writeFileSync(configPath, '[hooks]\nturn_end = """\nmultiline\n"""\n', 'utf8')
const status = new JcodeHookService().install()
expect(status.state).toBe('error')
expect(status.detail).toContain('config.toml')
})
it('appends to user hook arrays and removes only its own element', () => {
const configPath = getJcodeConfigPath()
const source = `[hooks]\npre_tool = [\n "~/gates/pre-tool.sh", # policy\n "~/gates/memory-gate.sh",\n]\npre_tool_timeout_ms = 5000\n`
@@ -244,16 +125,4 @@ describe('JcodeHookService', () => {
'pre_tool = [\n "~/gates/pre-tool.sh", # policy\n "~/gates/memory-gate.sh",\n]\n'
)
})
it('remove clears only the managed entries', () => {
new JcodeHookService().install()
const before = readFileSync(getJcodeConfigPath(), 'utf8')
expect(before).toContain('turn_end')
const status = new JcodeHookService().remove()
expect(status.state).toBe('not_installed')
expect(status.detail).toBeNull()
expect(existsSync(getJcodeManagedScriptPath())).toBe(true)
const after = readFileSync(getJcodeConfigPath(), 'utf8')
expect(after).not.toContain(getJcodeManagedScriptPath())
})
})
-299
View File
@@ -157,35 +157,6 @@ describe('Jira client credential storage', () => {
)
})
it('sends a non-browser User-Agent on Jira POST requests', async () => {
// Electron's Chrome UA trips Atlassian XSRF checks on token-authenticated writes.
const siteId = 'site-alpha'
writeJiraFiles(siteId, 'token-alpha')
netFetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ issues: [] }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
)
const jira = await loadClientModule({ encryptionAvailable: true })
const client = jira.getClients(siteId)[0]
if (!client) {
throw new Error('Expected stored Jira client')
}
await jira.jiraRequest(client, '/rest/api/3/search/jql', {
method: 'POST',
body: JSON.stringify({ jql: 'project = ALP' })
})
const headers = netFetchMock.mock.calls[0]?.[1]?.headers as Headers
const userAgent = headers.get('User-Agent') ?? ''
expect(netFetchMock.mock.calls[0]?.[1]?.method).toBe('POST')
expect(userAgent).toBe('Orca')
expect(userAgent).not.toMatch(/Mozilla|Chrome|Safari|AppleWebKit/i)
})
it('removes cancelled queued reads so the request pool recovers', async () => {
const jira = await loadClientModule()
await Promise.all([jira.acquire(), jira.acquire(), jira.acquire(), jira.acquire()])
@@ -351,21 +322,6 @@ describe('Jira client credential storage', () => {
expect(jira.getStatus().credentialError).toBeUndefined()
})
it('treats empty Jira token files as missing credentials', async () => {
const siteId = 'site-alpha'
writeJiraFiles(siteId, Buffer.alloc(0))
const jira = await loadClientModule({ encryptionAvailable: false })
await expect(jira.testConnection(siteId)).resolves.toEqual({
ok: false,
error: 'Not connected to Jira.',
errorCode: 'jira.notConnected'
})
expect(fetchMock).not.toHaveBeenCalled()
expect(jira.getStatus()).toMatchObject({ connected: false })
})
it('keeps healthy sites under the "all" selection when one site cannot be decrypted', async () => {
writeMultiSiteFiles(
[
@@ -389,24 +345,6 @@ describe('Jira client credential storage', () => {
expect(jira.getStatus().credentialError).toContain('Could not decrypt')
})
it('rethrows the decrypt error for a specific site selection', async () => {
writeMultiSiteFiles(
[
{ id: 'good', token: 'token-good' },
{ id: 'bad', token: Buffer.from([0x76, 0x31, 0x30, 0xff, 0xfe]) }
],
'bad'
)
const jira = await loadClientModule({
encryptionAvailable: true,
decryptString: () => {
throw new Error('userCanceledErr')
}
})
expect(() => jira.getClients('bad')).toThrow('Could not decrypt')
})
it('does not clear credentials when Electron transport fails after a network change', async () => {
const siteId = 'site-alpha'
const tokenPath = tokenPathForSite(siteId)
@@ -432,156 +370,6 @@ describe('Jira client credential storage', () => {
})
})
it('does not treat Jira permission failures as credential revocation', async () => {
const jira = await loadClientModule()
expect(jira.isAuthError(new jira.JiraApiError('Unauthorized', 401))).toBe(true)
expect(jira.isAuthError(new jira.JiraApiError('Forbidden', 403))).toBe(false)
})
it('connects to self-hosted Jira with a Bearer PAT against REST v2', async () => {
netFetchMock.mockResolvedValueOnce(
new Response(
JSON.stringify({
name: 'wquintal',
key: 'JIRAUSER10101',
displayName: 'William',
emailAddress: 'william@example.com'
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
)
)
const jira = await loadClientModule()
await expect(
jira.connect({
siteUrl: 'jira.example.com',
email: '',
apiToken: 'pat-token',
authType: 'server'
})
).resolves.toMatchObject({
ok: true,
// Server /myself has no accountId; the username stands in for it.
viewer: { displayName: 'William', accountId: 'wquintal' }
})
expect(netFetchMock).toHaveBeenCalledWith(
'https://jira.example.com/rest/api/2/myself',
expect.objectContaining({ headers: expect.any(Headers) })
)
const headers = netFetchMock.mock.calls[0]?.[1]?.headers as Headers
expect(headers.get('Authorization')).toBe('Bearer pat-token')
})
it('connects to self-hosted Jira with Basic username/password against REST v2', async () => {
netFetchMock.mockResolvedValueOnce(
new Response(
JSON.stringify({ name: 'jdoe', key: 'JIRAUSER20202', displayName: 'Jane Doe' }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
)
)
const jira = await loadClientModule()
await expect(
jira.connect({
siteUrl: 'jira.example.com',
// Username selects classic Server/DC Basic auth; the token slot holds the password.
email: 'jdoe',
apiToken: 'account-password',
authType: 'server'
})
).resolves.toMatchObject({
ok: true,
viewer: { displayName: 'Jane Doe', accountId: 'jdoe' }
})
expect(netFetchMock).toHaveBeenCalledWith(
'https://jira.example.com/rest/api/2/myself',
expect.objectContaining({ headers: expect.any(Headers) })
)
const headers = netFetchMock.mock.calls[0]?.[1]?.headers as Headers
expect(headers.get('Authorization')).toBe(
`Basic ${Buffer.from('jdoe:account-password').toString('base64')}`
)
})
it('uses Basic auth for stored self-hosted sites that carry a username', async () => {
const siteId = 'site-server-basic'
const orcaDir = join(tempHome, '.orca')
mkdirSync(join(orcaDir, 'jira-tokens'), { recursive: true })
writeFileSync(
join(orcaDir, 'jira-sites.json'),
JSON.stringify({
version: 1,
activeSiteId: siteId,
selectedSiteId: siteId,
sites: [
{
id: siteId,
siteUrl: 'https://jira.example.com',
email: 'jdoe',
displayName: 'Jane Doe',
accountId: 'jdoe',
authType: 'server'
}
]
}),
{ encoding: 'utf-8' }
)
writeFileSync(tokenPathForSite(siteId), 'account-password')
netFetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ name: 'jdoe', displayName: 'Jane Doe' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
)
const jira = await loadClientModule()
await expect(jira.testConnection(siteId)).resolves.toMatchObject({ ok: true })
const headers = netFetchMock.mock.calls[0]?.[1]?.headers as Headers
expect(headers.get('Authorization')).toBe(
`Basic ${Buffer.from('jdoe:account-password').toString('base64')}`
)
})
it('requires a token but not an email for self-hosted connections', async () => {
const jira = await loadClientModule()
await expect(
jira.connect({
siteUrl: 'jira.example.com',
email: '',
apiToken: '',
authType: 'server'
})
).resolves.toEqual({
ok: false,
error: 'Personal access token is required.',
errorCode: 'jira.personalTokenRequired'
})
expect(netFetchMock).not.toHaveBeenCalled()
})
it('requires a password when a username is present on self-hosted', async () => {
const jira = await loadClientModule()
await expect(
jira.connect({
siteUrl: 'jira.example.com',
email: 'jdoe',
apiToken: '',
authType: 'server'
})
).resolves.toEqual({
ok: false,
error: 'Password is required.',
errorCode: 'jira.passwordRequired'
})
expect(netFetchMock).not.toHaveBeenCalled()
})
it('keeps distinct self-hosted PAT accounts on one host as separate sites', async () => {
netFetchMock
.mockResolvedValueOnce(
@@ -620,79 +408,6 @@ describe('Jira client credential storage', () => {
expect(stored.sites).toHaveLength(2)
expect(stored.sites.map((site) => site.accountId).sort()).toEqual(['alice', 'bot'])
})
it('uses Bearer auth and REST v2 for stored self-hosted sites', async () => {
const siteId = 'site-server'
const orcaDir = join(tempHome, '.orca')
mkdirSync(join(orcaDir, 'jira-tokens'), { recursive: true })
writeFileSync(
join(orcaDir, 'jira-sites.json'),
JSON.stringify({
version: 1,
activeSiteId: siteId,
selectedSiteId: siteId,
sites: [
{
id: siteId,
siteUrl: 'https://jira.example.com',
email: '',
displayName: 'William',
accountId: 'wquintal',
authType: 'server'
}
]
}),
{ encoding: 'utf-8' }
)
writeFileSync(tokenPathForSite(siteId), 'pat-token')
netFetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ name: 'wquintal', displayName: 'William' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
)
const jira = await loadClientModule()
await expect(jira.testConnection(siteId)).resolves.toMatchObject({
ok: true,
viewer: { displayName: 'William' }
})
expect(netFetchMock).toHaveBeenCalledWith(
'https://jira.example.com/rest/api/2/myself',
expect.objectContaining({ headers: expect.any(Headers) })
)
const headers = netFetchMock.mock.calls[0]?.[1]?.headers as Headers
expect(headers.get('Authorization')).toBe('Bearer pat-token')
})
it('bridges proxy environment settings before Jira connect requests', async () => {
netFetchMock.mockResolvedValueOnce(
new Response(
JSON.stringify({
accountId: 'account-alpha',
displayName: 'Ada',
emailAddress: 'ada@example.com'
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
)
)
const jira = await loadClientModule()
await expect(
jira.connect({
siteUrl: 'example.atlassian.net',
email: 'ada@example.com',
apiToken: 'token-alpha'
})
).resolves.toMatchObject({ ok: true, viewer: { displayName: 'Ada' } })
expect(resolveProxyMock).toHaveBeenCalledWith('https://example.atlassian.net/rest/api/3/myself')
expect(netFetchMock).toHaveBeenCalledTimes(1)
const headers = netFetchMock.mock.calls[0]?.[1]?.headers as Headers
expect(headers.get('User-Agent')).toBe('Orca')
expect(fetchMock).not.toHaveBeenCalled()
})
})
describe('Jira site file cross-process visibility', () => {
@@ -813,18 +528,4 @@ describe('Jira site file cross-process visibility', () => {
expect(jira.getStatus()).toMatchObject({ connected: false, sites: [] })
})
it('follows the selected site changing underneath it', async () => {
const sites = [
{ id: 'site-alpha', token: 'token-alpha' },
{ id: 'site-beta', token: 'token-beta' }
]
writeMultiSiteFiles(sites, 'site-alpha')
const jira = await loadClientModule()
expect(jira.getStatus()).toMatchObject({ selectedSiteId: 'site-alpha' })
writeMultiSiteFiles(sites, 'site-beta')
expect(jira.getStatus()).toMatchObject({ selectedSiteId: 'site-beta' })
})
})
+2 -113
View File
@@ -1,10 +1,9 @@
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { createManagedHookLocalFilesystem } from '../agent-hooks/managed-hook-local-filesystem.test-fixture'
import { isPlainObject } from '../agent-hooks/installer-utils'
import { wrapWindowsBatchHookCommand } from '../agent-hooks/windows-direct-cmd-hook-command'
import { KiroHookService } from './hook-service'
import { getKiroManagedCommandMatcher, KIRO_HOOK_EVENTS } from './hook-settings'
@@ -45,13 +44,6 @@ afterEach(() => {
const agentsDir = (): string => join(home, '.kiro', 'agents')
const agentPath = (name: string): string => join(agentsDir(), `${name}.json`)
const scriptPath = (): string =>
join(
home,
'.orca',
'agent-hooks',
process.platform === 'win32' ? 'kiro-hook.cmd' : 'kiro-hook.sh'
)
function writeAgent(name: string, config: unknown): void {
mkdirSync(agentsDir(), { recursive: true })
@@ -65,15 +57,6 @@ function readAgent(name: string): Record<string, unknown> {
const USER_STOP_HOOK = { command: 'say done' }
describe('KiroHookService', () => {
it("reports the V2 default agent's gap as partial when no custom agents exist", () => {
const result = new KiroHookService().install()
expect(result.state).toBe('partial')
expect(result.detail).toContain('kiro-cli agent create')
expect(result.detail).toContain(agentsDir())
// The launcher is still laid down so the refresher can keep it current.
expect(existsSync(scriptPath())).toBe(true)
})
it('adds one managed hook per event to every agent and keeps user hooks', () => {
writeAgent('caverna', { name: 'caverna', tools: ['*'], hooks: { stop: [USER_STOP_HOOK] } })
writeAgent('reviewer', { name: 'reviewer', tools: ['read'] })
@@ -118,29 +101,6 @@ describe('KiroHookService', () => {
expect(readFileSync(agentPath('broken'), 'utf-8')).toBe('{ not json')
})
it('reports partial when an agent created after install has no hooks yet', () => {
writeAgent('first', { name: 'first' })
const service = new KiroHookService()
service.install()
writeAgent('later', { name: 'later' })
const result = service.getStatus()
expect(result.state).toBe('partial')
expect(result.detail).toContain('later.json has no Orca hooks')
})
it('reports a missing launcher as partial instead of installed', () => {
writeAgent('tracked', { name: 'tracked' })
const service = new KiroHookService()
service.install()
rmSync(scriptPath())
expect(service.getStatus()).toMatchObject({
state: 'partial',
managedHooksPresent: true,
detail: expect.stringContaining('script missing')
})
})
// Shape `kiro-cli agent upgrade` (2.29+) writes; edits to `hooks` keep the file "universal".
it('converges list-form hooks in place and removes only its entries, locally and remotely', async () => {
const orcaHook = (trigger: string, command: string) => ({
@@ -231,15 +191,6 @@ describe('KiroHookService', () => {
expect(readAgent('plain')).toEqual({ name: 'plain' })
})
it('patches the agents under $KIRO_HOME instead of ~/.kiro', () => {
const kiroHome = join(home, 'custom-kiro')
mkdirSync(join(kiroHome, 'agents'), { recursive: true })
writeFileSync(join(kiroHome, 'agents', 'relocated.json'), JSON.stringify({ name: 'relocated' }))
process.env.KIRO_HOME = kiroHome
expect(new KiroHookService().install().state).toBe('installed')
expect(existsSync(agentsDir())).toBe(false)
})
it('patches the remote agents under the probed KIRO_HOME, not ~/.kiro', async () => {
const kiroHome = join(home, 'remote-kiro')
mkdirSync(join(kiroHome, 'agents'), { recursive: true })
@@ -258,67 +209,5 @@ describe('KiroHookService', () => {
expect(readAgent('stale').hooks).toBeUndefined()
})
it('falls back to <remoteHome>/.kiro/agents when no KIRO_HOME was probed', async () => {
writeAgent('default', { name: 'default' })
const result = await new KiroHookService().installRemote(
createManagedHookLocalFilesystem(),
home
)
expect(result.state).toBe('installed')
expect(readAgent('default').hooks).toBeDefined()
})
it('reports a remote agent deleted after the listing as unreadable, not invalid JSON', async () => {
writeAgent('kept', { name: 'kept' })
writeAgent('gone', { name: 'gone' })
const sftp = createManagedHookLocalFilesystem()
const readFile = sftp.readFile
Object.assign(sftp, {
readFile: (
path: string,
encoding: BufferEncoding,
callback: (error: Error | undefined, contents: Buffer) => void
) => {
if (path.endsWith('gone.json')) {
rmSync(path)
}
return readFile(path, encoding, callback)
}
})
const result = await new KiroHookService().installRemote(sftp, home)
expect(result.state).toBe('partial')
expect(result.detail).toContain('gone.json is could not be read')
expect(result.detail).not.toContain('not valid JSON')
expect(readAgent('kept').hooks).toBeDefined()
})
// Check command shapes everywhere; windows-hook-payload-delivery.test.ts executes them under Kiro's shells.
it('registers the Windows batch hook command for every event', () => {
const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
try {
writeAgent('mine', { name: 'mine' })
expect(new KiroHookService().install().state).toBe('installed')
const hooks = readAgent('mine').hooks
if (!isPlainObject(hooks)) {
throw new Error('Expected hooks object')
}
const command = wrapWindowsBatchHookCommand(scriptPath())
for (const event of KIRO_HOOK_EVENTS) {
expect(hooks[event]).toEqual([expect.objectContaining({ command })])
}
} finally {
platform.mockRestore()
}
})
it('ignores non-JSON files such as the shipped example config', () => {
mkdirSync(agentsDir(), { recursive: true })
writeFileSync(join(agentsDir(), 'agent_config.json.example'), '{}')
expect(new KiroHookService().install().state).toBe('partial')
expect(readFileSync(join(agentsDir(), 'agent_config.json.example'), 'utf-8')).toBe('{}')
})
})
-81
View File
@@ -156,21 +156,6 @@ function mkdtempLike(prefix: string): string {
}
describe('Linear client workspace storage', () => {
it('forwards an optional detail deadline to both SDK clients', async () => {
const linear = await loadClientModule()
await linear.connect('token-alpha')
const signal = new AbortController().signal
const entry = linear.getClients('org-alpha', signal)[0]
expect(linearClientMock).toHaveBeenLastCalledWith({ apiKey: 'token-alpha', signal })
linear.getPublicFileUrlClient(entry, signal)
expect(linearClientMock).toHaveBeenLastCalledWith({
apiKey: 'token-alpha',
signal,
headers: {
'public-file-urls-expire-in': String(linear.LINEAR_PUBLIC_FILE_URL_EXPIRY_SECONDS)
}
})
})
it('requests temporary file URLs using only the selected workspace credential', async () => {
const linear = await loadClientModule()
await linear.connect('token-alpha')
@@ -214,22 +199,6 @@ describe('Linear client workspace storage', () => {
})
})
it('reports a legacy single-token workspace without constructing a Linear client', async () => {
writeLegacyLinearFiles('token-alpha', {
displayName: 'Ada',
email: 'ada@example.com',
organizationName: 'Alpha'
})
const linear = await loadClientModule()
expect(linear.getStatus()).toMatchObject({
connected: true,
selectedWorkspaceId: 'legacy',
workspaces: [{ id: 'legacy', organizationName: 'Alpha', isLegacy: true }]
})
expect(linearClientMock).not.toHaveBeenCalled()
})
it('migrates legacy token storage to a real workspace id when explicitly tested', async () => {
writeLegacyLinearFiles('token-alpha', {
displayName: 'Ada',
@@ -364,24 +333,6 @@ describe('Linear client workspace storage', () => {
expect(linear.getStatus().credentialError).toBeUndefined()
})
it('treats empty Linear token files as missing credentials', async () => {
writeLegacyLinearToken(Buffer.alloc(0), {
displayName: 'Ada',
email: 'ada@example.com',
organizationName: 'Alpha'
})
const linear = await loadClientModule({ encryptionAvailable: false })
await expect(linear.testConnection('legacy')).resolves.toEqual({
ok: false,
error: 'No API key stored.',
errorCode: 'linear.noApiKey'
})
expect(linearClientMock).not.toHaveBeenCalled()
expect(linear.getStatus()).toMatchObject({ connected: false })
})
it('keeps healthy workspaces under the "all" selection when one cannot be decrypted', async () => {
writeMultiWorkspaceFiles(
[
@@ -409,24 +360,6 @@ describe('Linear client workspace storage', () => {
expect(clients.map((client) => client.workspace.id)).toEqual(['good'])
expect(linear.getStatus().credentialError).toContain('Could not decrypt')
})
it('rethrows the decrypt error for a specific workspace selection', async () => {
writeMultiWorkspaceFiles(
[
{ id: 'good', token: 'token-good' },
{ id: 'bad', token: Buffer.from([0x76, 0x31, 0x30, 0xff, 0xfe]) }
],
'bad'
)
const linear = await loadClientModule({
encryptionAvailable: true,
decryptString: () => {
throw new Error('userCanceledErr')
}
})
expect(() => linear.getClients('bad')).toThrow('Could not decrypt')
})
})
describe('Linear workspace file cross-process visibility', () => {
@@ -548,18 +481,4 @@ describe('Linear workspace file cross-process visibility', () => {
activeWorkspaceId: 'ws-alpha'
})
})
it('follows the selected workspace changing underneath it', async () => {
const workspaces = [
{ id: 'ws-alpha', token: 'token-alpha' },
{ id: 'ws-beta', token: 'token-beta' }
]
writeMultiWorkspaceFiles(workspaces, 'ws-alpha')
const linear = await loadClientModule()
expect(linear.getStatus()).toMatchObject({ selectedWorkspaceId: 'ws-alpha' })
writeMultiWorkspaceFiles(workspaces, 'ws-beta')
expect(linear.getStatus()).toMatchObject({ selectedWorkspaceId: 'ws-beta' })
})
})
@@ -7,9 +7,7 @@ vi.mock('../ipc/runtime-environments/runtime-environment-transport-routing', ()
vi.mock('../ipc/runtime-environments/runtime-environment-registry-root', () => ({
getRuntimeEnvironmentRegistryRoot: () => '/user-data'
}))
import {
assertRemoteChunkReadsSupported,
readRemotePreviewRange,
readRemotePreviewChunk,
renewRemotePreviewGrant
@@ -40,53 +38,7 @@ describe('remote file preview reader', () => {
await expect(readRemotePreviewChunk(address, 0, 4, signal)).rejects.toThrow('corrupted')
})
it('proves chunk reads with one byte once per pairing, and re-proves after a re-pair', async () => {
callRuntimeEnvironment.mockResolvedValue(
ok({ contentBase64: 'AA==', bytesRead: 1, eof: false })
)
const pinned = { ...address, environmentId: 'env-probe' }
await assertRemoteChunkReadsSupported(pinned, signal)
await assertRemoteChunkReadsSupported(pinned, signal)
expect(callRuntimeEnvironment).toHaveBeenCalledTimes(1)
expect(callRuntimeEnvironment).toHaveBeenCalledWith(
'/user-data',
'env-probe',
'files.readChunk',
expect.objectContaining({ offset: 0, length: 1 }),
20_000,
7,
undefined,
{ signal }
)
await assertRemoteChunkReadsSupported(
{ ...pinned, expectedEnvironmentPairingRevision: 8 },
signal
)
expect(callRuntimeEnvironment).toHaveBeenCalledTimes(2)
})
// Why: a host can answer files.readChunk yet lack the grant's paging method.
it('pages a granted file through its grant, proven apart from workspace reads', async () => {
callRuntimeEnvironment.mockResolvedValue(
ok({ contentBase64: 'AA==', bytesRead: 1, eof: false })
)
const workspace = { ...address, environmentId: 'env-kinds' }
const { relativePath: _relativePath, ...base } = workspace
const granted = { ...base, grantId: 'g-1', absolutePath: '/tmp/clip.mp4' }
await assertRemoteChunkReadsSupported(workspace, signal)
await assertRemoteChunkReadsSupported(granted, signal)
expect(callRuntimeEnvironment).toHaveBeenLastCalledWith(
'/user-data',
'env-kinds',
'files.readTerminalArtifactChunk',
{ worktree: 'id:wt-1', grantId: 'g-1', absolutePath: '/tmp/clip.mp4', offset: 0, length: 1 },
20_000,
7,
undefined,
{ signal }
)
expect(callRuntimeEnvironment).toHaveBeenCalledTimes(2)
})
it('renews a granted file only at the path its first grant named', async () => {
const { relativePath: _relativePath, ...base } = address
@@ -1,53 +0,0 @@
import { resolveHiddenProbeCwd } from '../../rate-limits/hidden-probe-cwd'
import { describe, expect, it } from 'vitest'
import { applyCodexConfiguredLaunchDefaults } from '../../codex/structured/launch/codex-configured-launch-defaults'
import type { AgentSessionModelCatalogResult } from '../../../shared/agent-session/agent-session-wire'
import { harness } from './agent-model-catalog-account-list-test-support'
for (const agent of ['claude', 'codex'] as const) {
describe(`${agent} project defaults on the account listing`, () => {
it('shows the account’s configured model and effort in every folder, whatever its own settings pick', async () => {
// Every folder's settings may pick both; the picker still follows the account.
const h = harness(agent, { workspaceMayOverrideDefaultModel: async () => true })
const efforts = ['low', 'medium', 'high'].map((value) => ({ value, label: value }))
const raw = [
{
id: h.full[0],
label: 'Model',
isDefault: true,
efforts,
defaultEffort: 'medium',
declaredDefaultEffort: 'medium'
}
]
h.byFolder[resolveHiddenProbeCwd()] = {
origin: 'probe',
complete: true,
configured: { model: h.full[0], effort: 'high' },
// Codex folds configured defaults into rows before the catalog service sees them.
models: applyCodexConfiguredLaunchDefaults(raw, { model: h.full[0], effort: 'high' })
}
await h.service.read({ agent, waitForListing: true })
const row = (result: AgentSessionModelCatalogResult) =>
result.origin === 'unknown' ? null : result.models[0]
for (const id of ['a', 'e']) {
expect(row(await h.read(id, { waitForListing: true }))).toMatchObject({
isDefault: true,
defaultEffort: 'high'
})
expect(h.service.peek(id)?.configured).toMatchObject({ modelId: h.full[0] })
}
expect(h.runs).toEqual([resolveHiddenProbeCwd()])
})
it('lists a workspace this host cannot inspect as the account does, never where Orca runs', async () => {
const h = harness(agent)
const result = await h.service.read({ agent, workspacePath: null, waitForListing: true })
expect(result.origin === 'unknown' ? null : result.models).toEqual(
h.full.map((id) => expect.objectContaining({ id }))
)
expect(h.runs).toEqual([resolveHiddenProbeCwd()])
expect(h.unresolvedRuns).toEqual([])
})
})
}
@@ -256,27 +256,6 @@ it('keeps a shared listing for an earlier-ending reader of the same folder when
expect(h.run).toHaveBeenCalledOnce()
})
it('keeps a later reader of the same folder waiting for its own deadline, not the first one', async () => {
vi.useFakeTimers()
onTestFinished(() => {
vi.useRealTimers()
})
const h = sharedListingHarness()
void h.service.demand({ agent: 'codex', workspacePath: '/a', deadline: Date.now() + 1_000 })
await vi.waitFor(() => expect(h.run).toHaveBeenCalledOnce())
const later = h.service.read({
agent: 'codex',
workspacePath: '/a',
waitForListing: true,
deadline: Date.now() + 60_000
})
await vi.advanceTimersByTimeAsync(2_000)
h.discovery.resolve({ ...listing('shared'), complete: true, configured: {} })
await vi.advanceTimersByTimeAsync(0)
expect(await later).toMatchObject({ models: [{ id: 'shared' }] })
expect(h.run).toHaveBeenCalledOnce()
})
function readOperation(h: ReturnType<typeof sharedListingHarness>, deadline: number) {
const controller = new AbortController()
let current = true
@@ -272,34 +272,6 @@ it('ends a shorter reader at its own deadline, and a stalled refresh lists nothi
expect(h.run).not.toHaveBeenCalled()
})
it.each([10, 100])(
'resolves setup and project settings once for %i readers of one request, before and after it settles',
async (readers) => {
const h = harness()
const gate = Promise.withResolvers<void>()
h.folder.gate = gate
const reads = Array.from({ length: readers }, () =>
h.service.demand({ agent: 'codex', sessionId: 'b', deadline: Date.now() + 30_000 })
)
gate.resolve()
await vi.waitFor(() => expect(h.run).toHaveBeenCalledOnce())
// More readers arrive while the listing that setup found is still running.
reads.push(
...Array.from({ length: readers }, () =>
h.service.demand({ agent: 'codex', sessionId: 'b', deadline: Date.now() + 30_000 })
)
)
h.discovery.resolve(listing('shared'))
await Promise.all(reads)
// One request needs one folder lookup, one CLI resolution and one project-settings check.
expect(h.recordWorkspacePath).toHaveBeenCalledOnce()
// A due listing resolves the CLI twice (a snapshot, then a fresh lookup), once per setup.
expect(h.resolve.mock.calls.length).toBeLessThanOrEqual(2)
expect(h.workspaceMayOverrideDefaultModel.mock.calls.length).toBeLessThanOrEqual(1)
expect(h.run).toHaveBeenCalledOnce()
}
)
it('holds readers of a stalled refresh only to their own deadlines, and starts no other until asked', async () => {
vi.useFakeTimers()
onTestFinished(() => {
@@ -331,18 +303,6 @@ it('holds readers of a stalled refresh only to their own deadlines, and starts n
expect(await c).toMatchObject({ models: [{ id: 'fresh' }] })
})
it("resolves a moved folder afresh instead of reusing the old folder's setup", async () => {
const h = harness()
h.folder.cli = Promise.withResolvers<void>()
void h.service.demand({ agent: 'codex', sessionId: 'b' })
await vi.waitFor(() => expect(h.resolve).toHaveBeenCalledOnce())
h.folder.path = '/new'
h.service.retireContext('ws')
void h.service.demand({ agent: 'codex', sessionId: 'b' })
await vi.waitFor(() => expect(h.run).toHaveBeenCalledOnce())
expect(h.resolve.mock.calls.at(-1)?.[4]).toBe('/new')
})
it.each(['cancelled', 'stopped'] as const)(
'settles a reader at once when it is %s while its setup is stalled',
async (how) => {
@@ -396,22 +356,6 @@ it("keeps the folder's defaults when they arrive after a shorter last reader lef
expect(await a).toMatchObject({ models: [{ id: 'listed', isDefault: true }] })
})
it("reuses a settled setup after its starting reader's deadline while its listing still runs", async () => {
vi.useFakeTimers()
onTestFinished(() => {
vi.useRealTimers()
})
const h = harness(agent)
void h.service.demand({ agent, sessionId: 'a', deadline: Date.now() + 1_000 })
void h.service.demand({ agent, sessionId: 'b', deadline: Date.now() + 30_000 })
await vi.waitFor(() => expect(h.run).toHaveBeenCalledOnce())
await vi.advanceTimersByTimeAsync(2_000)
void h.service.demand({ agent, sessionId: 'b', deadline: Date.now() + 30_000 })
await vi.advanceTimersByTimeAsync(0)
expect(h.recordWorkspacePath).toHaveBeenCalledOnce()
expect(h.workspaceMayOverrideDefaultModel).toHaveBeenCalledOnce()
})
it("keeps the listing when the folder's settings stall, naming no default, within the reader's budget", async () => {
vi.useFakeTimers()
onTestFinished(() => {
@@ -442,45 +442,8 @@ describe.each([
})
// Steal focus after resume to exercise the grab’s caret restore; otherwise select-all retries can escape detection.
it('restores the caret, not a select-all, when the guest takes the bar back mid-grab', () => {
renderWorkspacePane()
const staged = startEditing('example.int')
act(() => staged.setSelectionRange(3, 7))
act(() => adoptOntoClient())
act(() => flushFrames(1))
const adopted = addressBar()
expect(document.activeElement).toBe(adopted)
act(() => webview.focus())
expect(document.activeElement).toBe(webview)
act(() => flushFrames())
expect(document.activeElement).toBe(adopted)
expect(adopted.value).toBe('example.int')
expect([adopted.selectionStart, adopted.selectionEnd]).toEqual([3, 7])
})
// An open list survives focus naturally; only a dismissed list proves dropdown state crosses the swap.
it('keeps a dismissed suggestion list dismissed through the swap', () => {
renderWorkspacePane()
const staged = startEditing('example.int')
act(() => {
fireEvent.keyDown(staged, { key: 'Escape' })
})
expect(staged.getAttribute('aria-expanded')).toBe('false')
expect(document.activeElement).toBe(staged)
act(() => adoptOntoClient())
act(() => flushFrames())
const adopted = addressBar()
expect(adopted.value).toBe('example.int')
expect(document.activeElement).toBe(adopted)
expect(adopted.getAttribute('aria-expanded')).toBe('false')
expect(screen.queryByRole('option', { name: /Example docs/ })).toBeNull()
})
// Only same-commit swaps may reclaim focus; later worktree revisits must not resume old edits.
it('does not resume an edit into a pane that mounts in a later task', async () => {
@@ -496,58 +459,9 @@ describe.each([
expect(document.activeElement).not.toBe(remounted)
})
it('leaves an idle address bar alone when the same swap happens', () => {
renderWorkspacePane()
const streamed = addressBar()
expect(document.activeElement).not.toBe(streamed)
act(() => adoptOntoClient())
act(() => flushFrames())
const adopted = addressBar()
expect(adopted.value).toBe('')
// Adoption without an active edit must not seize chrome focus or open suggestions.
expect(document.activeElement).not.toBe(adopted)
expect(adopted.getAttribute('aria-expanded')).toBe('false')
expect(screen.queryByRole('option', { name: /Example docs/ })).toBeNull()
})
// Generation changes reattach in place; edits must survive without remounting.
it('carries a live edit through a host restart that bumps the page generation', () => {
renderWorkspacePane()
act(() => adoptOntoClient())
act(() => flushFrames())
const before = startEditing('rebooting.internal')
act(() => before.setSelectionRange(4, 4))
act(() => adoptOntoClient(CLIENT_PLACEMENT.pageHostGeneration + 1))
act(() => flushFrames())
const after = addressBar()
// The registry can rescue drafts across remounts; node identity is needed to prove the chrome was never rebuilt.
expect(after).toBe(before)
expect(after.value).toBe('rebooting.internal')
expect(document.activeElement).toBe(after)
expect([after.selectionStart, after.selectionEnd]).toEqual([4, 4])
})
// Headless adoption stays streamed and must preserve the same live edit.
it('keeps a live edit when a headless host adopts the page in place', () => {
renderWorkspacePane()
const streamed = startEditing('headless.internal')
act(() => streamed.setSelectionRange(8, 8))
act(() => adoptOntoServer())
act(() => flushFrames())
const after = addressBar()
expect(screen.getByTestId('streamed-viewport')).not.toBeNull()
expect(after).toBe(streamed)
expect(after.value).toBe('headless.internal')
expect(document.activeElement).toBe(after)
expect([after.selectionStart, after.selectionEnd]).toEqual([8, 8])
})
it('holds a URL submitted against the staged page and loads it once the guest attaches', () => {
renderWorkspacePane()
@@ -567,30 +481,6 @@ describe.each([
})
// Only input identity exposes chrome remounts; save/resume can otherwise hide the repeated dropdown animation.
it('adopts a client-hosted page without remounting the chrome', () => {
stageClientHostedHandle()
renderWorkspacePane()
// The client-hosted pane mounts from the first frame, so there is no pane to swap at adoption.
expect(screen.queryByTestId('streamed-viewport')).toBeNull()
// Do not attach before the host publishes the page; the retained registry would reject the unknown ID.
expect(mocks.attach).not.toHaveBeenCalled()
expect(screen.queryByText('Client-hosted browser unavailable')).toBeNull()
const staged = startEditing('example.int')
act(() => staged.setSelectionRange(3, 7))
expect(staged.getAttribute('aria-expanded')).toBe('true')
act(() => adoptOntoClient())
act(() => flushFrames())
expect(addressBar()).toBe(staged)
expect(staged.getAttribute('aria-expanded')).toBe('true')
expect(staged.value).toBe('example.int')
expect(document.activeElement).toBe(staged)
expect([staged.selectionStart, staged.selectionEnd]).toEqual([3, 7])
expect(mocks.attach).toHaveBeenCalled()
})
// Continuity alone passes if nothing happens; verify generation changes really reattach the guest.
it('re-attaches the guest when a host restart bumps the page generation', () => {
@@ -618,23 +508,6 @@ describe.each([
})
// Do not consume a one-time tour before placement makes its controls usable.
it('does not burn the intro tour on a pane that is still connecting', () => {
const recordFeatureInteraction = vi.fn(async () => {})
useAppStore.setState({
persistedUIReady: true,
contextualToursSeenIds: ['client-hosted-browser'],
recordFeatureInteraction
} as unknown as Parameters<typeof useAppStore.setState>[0])
stageClientHostedHandle()
renderWorkspacePane()
expect(recordFeatureInteraction).not.toHaveBeenCalled()
act(() => adoptOntoClient())
act(() => flushFrames())
expect(recordFeatureInteraction).toHaveBeenCalledWith('client-hosted-browser')
})
// Live host status can override cached client placement and swap to streaming; retain edit-session recovery for this path.
it('carries a live edit when a staged client-hosted page is adopted onto the server', () => {
@@ -656,22 +529,6 @@ describe.each([
expect([adopted.selectionStart, adopted.selectionEnd]).toEqual([4, 4])
})
it('holds a URL submitted against a staged client-hosted page until its guest attaches', () => {
stageClientHostedHandle()
renderWorkspacePane()
const staged = startEditing('https://example.internal/client-deferred')
act(() => {
fireEvent.submit(staged.closest('form') ?? staged)
})
expect(webview.loadURL).not.toHaveBeenCalled()
act(() => adoptOntoClient())
act(() => flushFrames())
expect(webview.loadURL).toHaveBeenCalledWith('https://example.internal/client-deferred')
})
// Restored client-placed pages must await recovery locally; hosts reject server screencasts for them.
it('mounts a restored client-hosted page on the client pane while the host recovers it', () => {
restoreClientHostedHandle()
@@ -11,12 +11,10 @@ import { act } from 'react'
import { createRoot } from 'react-dom/client'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
import { GitHistoryPanel } from '@/components/right-sidebar/source-control/sync/git-history-panel'
import { GitGraphTab } from './git-graph-tab'
import { GitGraphFilter } from './git-graph-filter'
import { useGitGraphHistory, gitGraphFilterSupported } from './use-git-graph-history'
import { useGitGraphHistory } from './use-git-graph-history'
import type { GitHistoryResult } from '../../../../../shared/git/history/git-history-types'
import * as graph from '../../../../../shared/git/history/git-history-graph'
const { getHistory, getRefs } = vi.hoisted(() => ({ getHistory: vi.fn(), getRefs: vi.fn() }))
vi.mock('@/runtime/source-control/git/runtime-git-status-client', async (original) => ({
@@ -99,74 +97,6 @@ const result: GitHistoryResult = {
limit: 50
}
it('keeps view models on expand and resize, rebuilding when history changes', async () => {
const build = vi.spyOn(graph, 'buildGitHistoryViewModels')
const render = (history: GitHistoryResult) => (
<TooltipProvider>
<GitHistoryPanel
state={{ status: 'ready', result: history }}
collapsed={false}
onToggle={() => {}}
onRefresh={() => {}}
onLoadCommitFiles={async () => []}
onOpenCommitFile={() => {}}
/>
</TooltipProvider>
)
await act(async () => root.render(render(result)))
expect(build).toHaveBeenCalledTimes(1)
await act(async () =>
container.querySelector<HTMLButtonElement>('[data-testid="git-history-row"]')!.click()
)
await act(async () =>
container
.querySelector('[role="separator"]')!
.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }))
)
expect(build).toHaveBeenCalledTimes(1)
await act(async () =>
root.render(
render({
...result,
items: [...result.items, { id: 'b', subject: 'next', message: 'next', parentIds: [] }]
})
)
)
expect(build).toHaveBeenCalledTimes(2)
expect(
container.querySelector('[data-testid="git-history-row"]')?.getAttribute('aria-expanded')
).toBe('true')
})
it('disables the branch filter when an older host omits the refs echo', async () => {
await act(async () =>
root.render(
<TooltipProvider>
<GitGraphFilter
worktreeId={workspaceId}
filter="all"
supported={gitGraphFilterSupported(result)}
onChange={() => {}}
/>
</TooltipProvider>
)
)
expect(container.querySelector('button')?.disabled).toBe(true)
await act(async () =>
root.render(
<TooltipProvider>
<GitGraphFilter
worktreeId={workspaceId}
filter="all"
supported={gitGraphFilterSupported({ ...result, refs: 'all' })}
onChange={() => {}}
/>
</TooltipProvider>
)
)
expect(container.querySelector('button')?.disabled).toBe(false)
})
it('recovers silently after each later token expiry and ignores refreshes for other worktrees', async () => {
let history: ReturnType<typeof useGitGraphHistory>
function History() {
@@ -335,33 +265,6 @@ async function loadOlderHistory() {
return { controller, items, page }
}
it('keeps a selection pushed past the loaded depth with exactly one extra page', async () => {
const { controller, items } = await loadOlderHistory()
await act(async () => {
Array.from(container.querySelectorAll('button'))
.find((button) => button.textContent === 'Commit 149')!
.click()
})
const refreshed = [
{ id: 'new-tip', subject: 'New commit', message: 'New commit', parentIds: [] },
...items
]
getHistory.mockImplementation(async (_owner, options) => ({
...result,
refs: 'all',
historyToken: 'refreshed',
items: refreshed.slice(options.skip, options.skip + 50),
hasMore: true
}))
await act(async () => {
await controller.refresh()
})
expect(getHistory).toHaveBeenCalledTimes(4)
expect(container.querySelectorAll('[data-current]')).toHaveLength(200)
expect(container.querySelector('[data-current="true"] button[title="Commit 149"]')).not.toBeNull()
expect(container.querySelector('aside h2')?.textContent).toBe('Commit 149')
})
it('bounds refresh to one extra page when the selected commit has vanished', async () => {
const { controller, items } = await loadOlderHistory()
const remaining = items.filter((item) => item.id !== 'commit-110')
@@ -121,45 +121,6 @@ describe('EmulatorDeviceFrame window-visibility gating', () => {
expect(startFrameStream).toHaveBeenCalledTimes(2)
})
it('does not tear down on a quick hide/show within the park delay', async () => {
vi.useFakeTimers()
await renderFrame(true)
expect(startFrameStream).toHaveBeenCalledTimes(1)
await act(async () => {
setDocumentVisibility('hidden')
})
await act(async () => {
vi.advanceTimersByTime(EMULATOR_STREAM_PARK_DELAY_MS - 100)
})
await act(async () => {
setDocumentVisibility('visible')
})
// Returning before the grace expires must cancel parking without reconnecting.
await act(async () => {
vi.advanceTimersByTime(EMULATOR_STREAM_PARK_DELAY_MS)
})
expect(stopFrameStream).not.toHaveBeenCalled()
expect(startFrameStream).toHaveBeenCalledTimes(1)
})
it('keeps streaming while hidden when occlusion state is proven stale (display-sleep wedge)', async () => {
vi.useFakeTimers()
await renderFrame(true)
expect(startFrameStream).toHaveBeenCalledTimes(1)
// Real input disproves a wedged hidden/occluded signal; streaming must continue instead of leaving a black frame.
await act(async () => {
setDocumentVisibility('hidden')
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }))
})
await act(async () => {
vi.advanceTimersByTime(EMULATOR_STREAM_PARK_DELAY_MS * 2)
})
expect(stopFrameStream).not.toHaveBeenCalled()
expect(startFrameStream).toHaveBeenCalledTimes(1)
})
it('resumes a parked stream when user input proves hidden visibility is stale', async () => {
vi.useFakeTimers()
await renderFrame(true)
@@ -1,61 +0,0 @@
import { describe, expect, it } from 'vitest'
import { groupEmulatorDevices } from './emulator-device-picker-groups'
import { toSimulatorDeviceRows } from './emulator-device-row-mapping'
describe('emulator picker inventory', () => {
it('keeps unavailable records out of used devices and retains every duplicate identity', () => {
const rows = toSimulatorDeviceRows([
{
id: 'old',
name: 'iPhone 18 Pro',
backend: 'ios',
state: 'shutdown',
detail: 'com.apple.CoreSimulator.SimRuntime.iOS-27-0',
isAvailable: false,
lastUsedAt: '2026-10-30',
availabilityError: 'runtime removed'
},
{
id: 'used',
name: 'iPhone 18 Pro',
backend: 'ios',
state: 'shutdown',
detail: 'com.apple.CoreSimulator.SimRuntime.iOS-27-2',
isAvailable: true,
lastUsedAt: '2026-10-31'
},
{
id: 'unused',
name: 'iPhone 18 Pro',
backend: 'ios',
state: 'shutdown',
detail: 'com.apple.CoreSimulator.SimRuntime.iOS-27-2',
isAvailable: true
},
{
id: 'pixel',
name: 'Pixel',
backend: 'android',
state: 'booted',
osVersion: '16',
apiLevel: '36',
isAvailable: true
}
])
const groups = groupEmulatorDevices(rows, 'old')
expect(groups.used.map((d) => d.udid)).toEqual(['used', 'pixel'])
expect(groups.other.map((d) => d.udid)).toEqual(['unused'])
expect(groups.unavailable.map((d) => d.udid)).toEqual(['old'])
expect(groups.unavailable[0].availabilityError).toBe('runtime removed')
})
it('includes the selected shutdown device and tolerates metadata from older hosts', () => {
const rows = toSimulatorDeviceRows([
{ id: 'legacy', name: 'Phone', state: 'shutdown' },
{ id: 'selected', name: 'Tablet', state: 'shutdown' }
])
const groups = groupEmulatorDevices(rows, 'selected')
expect(groups.used.map((d) => d.udid)).toEqual(['selected'])
expect(groups.other.map((d) => d.udid)).toEqual(['legacy'])
})
})
@@ -1,48 +0,0 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { EmulatorDevicePicker } from './emulator-device-picker'
afterEach(cleanup)
describe('EmulatorDevicePicker', () => {
it('reveals other devices during search, keeps unavailable rows disabled, and selects the exact duplicate', async () => {
const onSelect = vi.fn()
render(
<EmulatorDevicePicker
selectedUdid="active"
onSelectDevice={onSelect}
devices={[
{ name: 'Pixel', udid: 'active', state: 'Booted', backend: 'android' },
{
name: 'iPhone',
udid: 'old',
state: 'Shutdown',
runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-26-0',
isAvailable: false,
availabilityError: 'Missing runtime'
},
{
name: 'iPhone',
udid: 'new',
state: 'Shutdown',
runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-27-2'
}
]}
/>
)
fireEvent.click(screen.getByRole('combobox'))
expect(screen.queryByText('iPhone')).toBeNull()
fireEvent.change(screen.getByPlaceholderText('Search devices…'), {
target: { value: 'iPhone' }
})
await waitFor(() => expect(screen.getAllByText('iPhone')).toHaveLength(2))
const unavailable = screen.getByText('Missing runtime').closest('[cmdk-item]')
expect(unavailable?.getAttribute('aria-disabled')).toBe('true')
fireEvent.click(unavailable!)
expect(onSelect).not.toHaveBeenCalled()
fireEvent.click(screen.getByText('iOS 27.2 · Stopped'))
expect(onSelect).toHaveBeenCalledWith('new')
await waitFor(() => expect(screen.queryByPlaceholderText('Search devices…')).toBeNull())
})
})
@@ -1,6 +1,5 @@
import * as emulatorPreferences from '@/lib/emulator-opening-preferences-capability'
import { setLocalRuntimeCapabilitiesForTests } from '@/runtime/local-runtime-capabilities'
import { EMULATOR_OPENING_PREFERENCES_CAPABILITY } from '../../../../shared/emulator-runtime-capabilities'
// @vitest-environment happy-dom
import { act } from 'react'
@@ -165,31 +164,6 @@ describe('useEmulatorPaneSession', () => {
vi.restoreAllMocks()
})
it('asks the capable host on automatic open without sending a guessed device', async () => {
consumePrelaunchedSimulatorSession(WORKTREE_ID)
setLocalRuntimeCapabilitiesForTests([EMULATOR_OPENING_PREFERENCES_CAPABILITY])
vi.mocked(window.api.runtime.call).mockImplementation(async (request) => {
if (request.method === 'emulator.listDevices') {
return runtimeSuccess(deviceList)
}
return runtimeSuccess({ attached: false, selectionRequired: 'first-use' })
})
await act(async () => {
root.render(<AutoAttachProbe />)
})
await flushEffects()
expect(window.api.runtime.call).toHaveBeenCalledWith(
expect.objectContaining({
method: 'emulator.attach',
params: { worktree: WORKTREE_ID, focus: false, userInitiated: true }
})
)
expect(latest?.loading).toBe(false)
expect(latest?.selectedUdid).toBeNull()
expect(latest?.selectionRequired).toBe('first-use')
expect(latest?.error).toBeNull()
})
it('retains an explicit session when an older automatic capability lookup finishes', async () => {
consumePrelaunchedSimulatorSession(WORKTREE_ID)
const capability = createDeferred<boolean>()
@@ -242,45 +216,6 @@ describe('useEmulatorPaneSession', () => {
expect(latest?.displayName).toBe('iPhone B')
})
it('shows a connecting state instead of the old live preview while switching devices', async () => {
await act(async () => {
root.render(<Probe />)
})
await flushEffects()
expect(latest?.isLive).toBe(true)
expect(latest?.previewUrl).toBe('http://127.0.0.1:3100/stream.mjpeg')
await act(async () => {
container.querySelector('button')?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await Promise.resolve()
})
expect(latest?.loading).toBe(true)
expect(latest?.isLive).toBe(false)
expect(latest?.previewUrl).toBeUndefined()
expect(latest?.displayName).toBe('iPhone B')
expect(latest?.selectedUdid).toBe('device-b')
await act(async () => {
attachDeferred.resolve({
attached: true,
info: {
deviceUdid: 'device-b',
displayName: 'iPhone B',
streamUrl: 'http://127.0.0.1:3200/stream.mjpeg',
wsUrl: 'ws://127.0.0.1:3200/ws'
}
})
await attachDeferred.promise
await Promise.resolve()
})
expect(latest?.loading).toBe(false)
expect(latest?.isLive).toBe(true)
expect(latest?.previewUrl).toBe('http://127.0.0.1:3200/stream.mjpeg')
})
it('ignores a rotate response from the previous session after switching devices', async () => {
await act(async () => {
root.render(<Probe />)
@@ -64,51 +64,3 @@ it('refuses image intake on an older runtime before sending any bytes', async ()
).rejects.toThrow('cannot upload images yet')
expect(rpc.mock.calls.map((call) => call[1])).toEqual(['status.get'])
})
it.each([2, 20])('checks capability once and uploads each of %i accepted images', async (count) => {
rpc.mockImplementation(async (_runtime, method) => {
if (method === 'status.get') {
return { capabilities: ['github.image-attachments.v1'] }
}
if (method === 'clipboard.startImageUpload') {
return { uploadId: 'image' }
}
if (method === 'github.uploadAttachment') {
return { url: 'https://github.com/user-attachments/assets/image' }
}
return {}
})
const upload = captureGitHubImageUpload({
runtime: { kind: 'local' },
target: { kind: 'repo', repo: 'repo', source: 'origin' }
})
for (let index = 0; index < count; index++) {
await upload(new File(['a'], `${index}.png`, { type: 'image/png' }))
}
expect(rpc.mock.calls.filter((call) => call[1] === 'status.get')).toHaveLength(1)
expect(rpc.mock.calls.filter((call) => call[1] === 'github.uploadAttachment')).toHaveLength(count)
})
it('stages a 5 MiB image in fourteen bounded chunks without putting bytes in Markdown', async () => {
rpc.mockImplementation(async (_runtime, method) => {
if (method === 'status.get') {
return { capabilities: ['github.image-attachments.v1'] }
}
if (method === 'clipboard.startImageUpload') {
return { uploadId: 'large' }
}
if (method === 'github.uploadAttachment') {
return { url: 'https://github.com/user-attachments/assets/large' }
}
return {}
})
const url = await captureGitHubImageUpload({
runtime: { kind: 'local' },
target: { kind: 'repo', repo: 'repo', source: 'issue' }
})(new File([new Uint8Array(5 * 1024 * 1024)], 'large.png', { type: 'image/png' }))
expect(url).toBe('https://github.com/user-attachments/assets/large')
expect(
rpc.mock.calls.filter((call) => call[1] === 'clipboard.appendImageUploadChunk')
).toHaveLength(14)
expect(rpc.mock.calls.filter((call) => call[1] === 'status.get')).toHaveLength(1)
})
@@ -132,55 +132,6 @@ it('delivers a File drop through the registered owner and detaches an externally
expect(view.container.querySelector('output')?.textContent).toBe('Replacement')
})
it.each([2, 20])(
'keeps typing independent of %i historical labels and retires them with the draft',
async (count) => {
upload.mockReset().mockResolvedValue('https://github.com/user-attachments/assets/completed')
const changed = vi.fn()
const view = render(<Draft initialBody={'text '.repeat(count * 100)} onChange={changed} />)
const field = textareaField(view.getByRole('textbox'))
field.setSelectionRange(field.value.length, field.value.length)
fireEvent.paste(field, {
clipboardData: {
files: Array.from(
{ length: count },
(_, index) => new File(['png'], `history-${index}.png`, { type: 'image/png' })
)
}
})
await waitFor(() => expect(upload).toHaveBeenCalledTimes(count))
await waitFor(() => expect(field.value).not.toContain('Uploading'))
let projectionPasses = 0
const originalReplace = String.prototype.replace
const replace = vi.spyOn(String.prototype, 'replace').mockImplementation(function (
this: string,
pattern: Parameters<typeof originalReplace>[0],
replacement: Parameters<typeof originalReplace>[1]
) {
if (pattern instanceof RegExp && pattern.source.startsWith('!\\[Uploading ')) {
projectionPasses++
}
return originalReplace.call(this, pattern, replacement)
})
changed.mockClear()
try {
fireEvent.change(field, { target: { value: `${field.value} typed` } })
expect(projectionPasses).toBe(1)
expect(changed).toHaveBeenCalledOnce()
expect(field.value.endsWith(' typed')).toBe(true)
} finally {
replace.mockRestore()
}
fireEvent.change(field, { target: { value: `${field.value}![Uploading history-0.png…]()` } })
expect(field.value).not.toContain('Uploading')
fireEvent.click(view.getByRole('button', { name: 'Replace' }))
fireEvent.change(field, { target: { value: 'Replacement ![Uploading history-0.png…]()' } })
expect(view.container.querySelector('output')?.textContent).toBe(
'Replacement ![Uploading history-0.png…]()'
)
}
)
it('removes an undo-restored owned label after an incomplete user label prefix', async () => {
const held = Promise.withResolvers<string>()
upload.mockReset().mockReturnValue(held.promise)
@@ -44,21 +44,6 @@ it('removes the previous image immediately when its mounted owner or reference c
expect(release).toHaveBeenCalledOnce()
})
it('lets a new reference load after the previous reference failed', async () => {
acquire
.mockRejectedValueOnce(new Error('unavailable'))
.mockResolvedValueOnce({ url: 'blob:second', release })
const view = render(
<JiraAttachmentImage target={target} reference="first" alt="shot" compact={false} />
)
await waitFor(() => expect(acquire).toHaveBeenCalledOnce())
await new Promise((resolve) => setTimeout(resolve, 0))
view.rerender(
<JiraAttachmentImage target={target} reference="second" alt="shot" compact={false} />
)
await waitFor(() => expect(screen.getByRole('img').getAttribute('src')).toBe('blob:second'))
})
it('reacquires the same reference after disconnect and reconnect invalidations', async () => {
acquire
.mockResolvedValueOnce({ url: 'blob:first', release })
@@ -93,31 +93,6 @@ beforeEach(() => {
boundary.search.mockResolvedValue(users)
})
describe('Existing Jira issue assignee', () => {
it('offers self first, alphabetic candidates, issue-scoped search, and unassignment', async () => {
const { mutateIssue } = mount()
fireEvent.click(screen.getByRole('button', { name: 'Assignee' }))
const search = await screen.findByPlaceholderText('Search users')
await screen.findByRole('button', { name: 'Ada' })
const rows = screen.getAllByRole('button').map((button) => button.textContent)
expect(rows.indexOf('Zoe me')).toBeLessThan(rows.indexOf('Ada'))
expect(rows.indexOf('Ada')).toBeLessThan(rows.indexOf('Zoe'))
boundary.search.mockResolvedValue([{ accountId: 'beth', displayName: 'Beth' }])
fireEvent.change(search, { target: { value: 'Beth' } })
fireEvent.click(await screen.findByRole('button', { name: 'Beth' }))
expect(boundary.search).toHaveBeenLastCalledWith({ kind: 'local' }, 'ENG-1', 'Beth', 'site-1')
expect(mutateIssue).toHaveBeenCalledWith(
'assignee',
{ assigneeAccountId: 'beth' },
{ assignee: { accountId: 'beth', displayName: 'Beth' } }
)
fireEvent.click(screen.getByRole('button', { name: 'Assignee' }))
fireEvent.click(screen.getByRole('button', { name: 'Unassigned' }))
expect(mutateIssue).toHaveBeenLastCalledWith(
'assignee',
{ assigneeAccountId: null },
{ assignee: undefined }
)
})
it('loads the viewer from the selected source and hides it during an account switch', async () => {
let resolve = (_status: JiraConnectionStatus) => {}
boundary.readJiraStatus.mockReturnValue(
@@ -210,12 +185,4 @@ describe('Existing Jira issue assignee', () => {
'site-1'
)
})
it('ignores assignment from a previously opened picker while another field saves', async () => {
const { props, rerender, mutateIssue } = mount()
fireEvent.click(screen.getByRole('button', { name: 'Assignee' }))
await screen.findByRole('button', { name: 'Ada' })
rerender(<JiraIssueMetadataBar {...props} pendingField="title" />)
fireEvent.click(screen.getByRole('button', { name: 'Ada' }))
expect(mutateIssue).not.toHaveBeenCalled()
})
})
@@ -23,26 +23,6 @@ describe('usePairingOfferExpiry', () => {
vi.useRealTimers()
})
it('fires one minute before expiry', () => {
const onDue = vi.fn()
renderHook(() => usePairingOfferExpiry(EXPIRES_AT, onDue))
act(() => vi.advanceTimersByTime(EXPIRES_AT - 60_000 - T0 - 1))
expect(onDue).not.toHaveBeenCalled()
act(() => vi.advanceTimersByTime(1))
expect(onDue).toHaveBeenCalledOnce()
})
it('waits while hidden and fires on return once due', () => {
const onDue = vi.fn()
renderHook(() => usePairingOfferExpiry(EXPIRES_AT, onDue))
act(() => setVisibility('hidden'))
act(() => vi.advanceTimersByTime(EXPIRES_AT - T0))
expect(onDue).not.toHaveBeenCalled()
act(() => setVisibility('visible'))
act(() => vi.advanceTimersByTime(0))
expect(onDue).toHaveBeenCalledOnce()
})
it('fires soon after the computer wakes from sleep past the refresh time', () => {
const onDue = vi.fn()
renderHook(() => usePairingOfferExpiry(EXPIRES_AT, onDue))
@@ -51,22 +31,4 @@ describe('usePairingOfferExpiry', () => {
act(() => vi.advanceTimersByTime(15_000))
expect(onDue).toHaveBeenCalledOnce()
})
it('rechecks when the window regains focus', () => {
const onDue = vi.fn()
renderHook(() => usePairingOfferExpiry(EXPIRES_AT, onDue))
vi.setSystemTime(EXPIRES_AT)
act(() => window.dispatchEvent(new Event('focus')))
act(() => vi.advanceTimersByTime(0))
expect(onDue).toHaveBeenCalledOnce()
})
it('stops after unmount and never runs without an expiry', () => {
const onDue = vi.fn()
const { unmount } = renderHook(() => usePairingOfferExpiry(EXPIRES_AT, onDue))
renderHook(() => usePairingOfferExpiry(null, onDue))
unmount()
act(() => vi.advanceTimersByTime(EXPIRES_AT))
expect(onDue).not.toHaveBeenCalled()
})
})
@@ -92,31 +92,3 @@ it('holds Continue back from the settings wait until the opted-in launch names i
expect(seen.at(-1)).toEqual({ pending: false, resuming: ['a', 'b'] })
expect(seen.some((entry) => !entry.pending && entry.resuming.length === 0)).toBe(false)
})
it('holds nothing back once the launch found nothing to resume', async () => {
settings(true)
rpc.mockResolvedValue({ sessions: [] })
await act(async () => root.render(<Probe launch />))
expect(seen.at(-1)).toEqual({ pending: false, resuming: [] })
})
it('holds nothing back when the launch asks instead of resuming', () => {
settings(false)
act(() => root.render(<Probe launch={false} />))
expect(seen.at(-1)).toEqual({ pending: false, resuming: [] })
})
it('waits for settings when held chats enable the launch first, then resumes by default', async () => {
rpc.mockImplementation((_target, method) =>
method === 'agentSession.restartResumable'
? Promise.resolve({ sessions: offered })
: new Promise(() => {})
)
await act(async () => root.render(<Probe launch />))
expect(rpc).not.toHaveBeenCalledWith(expect.anything(), 'agentSession.restartResumable')
settings(undefined)
await act(async () => root.render(<Probe launch />))
expect(seen.at(-1)).toEqual({ pending: false, resuming: ['a', 'b'] })
})
@@ -4,7 +4,6 @@ import { StrictMode } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { AgentSessionAccountUsage } from '../../../../shared/agent-session/agent-session-account-usage'
import NativeChatAccountUsageDialog from './NativeChatAccountUsageDialog'
import { openNativeChatAccountUsageModal } from './native-chat-account-usage-modal'
const mocks = vi.hoisted(() => ({
read: vi.fn(),
@@ -87,23 +86,6 @@ afterEach(() => {
})
describe('/usage modal', () => {
it('opens for the chat it was typed in and closes from its Close button', async () => {
mocks.read.mockResolvedValue(result)
openNativeChatAccountUsageModal({ sessionId: 'chat', target })
expect(mocks.openModal).toHaveBeenCalledWith('chat-account-usage', {
sessionId: 'chat',
target
})
render(<NativeChatAccountUsageDialog />)
expect(screen.getByRole('dialog', { name: 'Codex usage' })).toBeTruthy()
await screen.findByText('chat@example.com')
expect(mocks.read).toHaveBeenCalledWith(target, 'agentSession.accountUsage', {
sessionId: 'chat'
})
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
expect(mocks.closeModal).toHaveBeenCalled()
})
it('keeps the newer first read when a replayed mount’s older read fails late', async () => {
let failFirst!: (error: Error) => void
mocks.read
@@ -146,22 +128,6 @@ describe('/usage modal', () => {
await screen.findByText('Nothing to reset right now.')
})
it('keeps the reset outcome when the refresh after it fails, through Retry', async () => {
mocks.read
.mockResolvedValueOnce(result)
.mockRejectedValueOnce(new Error('connection lost'))
.mockResolvedValueOnce(result)
mocks.reset.mockResolvedValueOnce({ outcome: 'reset' })
render(<NativeChatAccountUsageDialog />)
await screen.findByText('chat@example.com')
fireEvent.click(screen.getByRole('button', { name: 'Reset now' }))
await screen.findByText('Codex usage couldn’t be loaded. Try again.')
expect(screen.getByText('Codex limits were reset.')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Retry' }))
await screen.findByText('chat@example.com')
expect(screen.getByText('Codex limits were reset.')).toBeTruthy()
})
it('reads nothing when a reset confirmed before closing finishes after it', async () => {
let finish!: (value: unknown) => void
mocks.read.mockResolvedValue(result)
@@ -179,22 +145,6 @@ describe('/usage modal', () => {
expect(mocks.read).toHaveBeenCalledOnce()
})
it('re-reads the offer when the host’s Codex state lands after the modal opened', async () => {
const { reset: _offer, ...withoutOffer } = result
mocks.read.mockResolvedValueOnce(withoutOffer).mockResolvedValueOnce(result)
render(<NativeChatAccountUsageDialog />)
await screen.findByText('chat@example.com')
expect(screen.queryByRole('button', { name: 'Reset now' })).toBeNull()
act(() => hostStream.snapshot?.(hostState('chat', 0, 'fetching')))
expect(mocks.read).toHaveBeenCalledOnce()
act(() => hostStream.snapshot?.(hostState('chat', 1)))
await screen.findByRole('button', { name: 'Reset now' })
expect(mocks.read).toHaveBeenCalledTimes(2)
// An unchanged host state reads nothing more.
act(() => hostStream.snapshot?.(hostState('chat', 1)))
expect(mocks.read).toHaveBeenCalledTimes(2)
})
it('collapses host changes during a background read into one follow-up, and sends none after closing', async () => {
const finishes: ((value: AgentSessionAccountUsage) => void)[] = []
mocks.read.mockResolvedValueOnce(result).mockImplementation(
@@ -1,57 +0,0 @@
// @vitest-environment happy-dom
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
import { NativeChatComposerPlanChip, useNativeChatPlanMode } from './NativeChatComposerPlanChip'
import type { SessionOptionsSurface } from '../../../../shared/native-chat/input/native-chat-session-options'
import { structuredAgentSessionInteractionDescriptor } from '../../../../shared/agent-session/options/agent-session-interaction-mode'
import { createStructuredAgentSessionOptionState } from '../../../../shared/agent-session/options/structured-agent-session-options'
vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback }))
vi.mock('@/i18n/use-ui-language-revision', () => ({ useUiLanguageRevision: () => 0 }))
vi.mock('sonner', () => ({ toast: { error: vi.fn() } }))
afterEach(cleanup)
function PlanChip(props: Parameters<typeof useNativeChatPlanMode>[0]) {
return <NativeChatComposerPlanChip control={useNativeChatPlanMode(props)} />
}
it('shows only active Plan, exits with an absolute Build choice, and retains Plan after a failed exit', async () => {
const descriptor = structuredAgentSessionInteractionDescriptor({
...createStructuredAgentSessionOptionState('codex', null),
interactionModeSupported: true
})!
const setOption = vi
.fn<SessionOptionsSurface['setOption']>()
.mockRejectedValueOnce(new Error('Disconnected'))
.mockResolvedValue({ snapshot: [] })
const surface: SessionOptionsSurface = {
getSnapshot: () => [descriptor],
setOption,
invokeAction: async () => ({ snapshot: [] }),
subscribe: () => () => {}
}
const focus = vi.fn()
const renderChip = (snapshot: (typeof descriptor)[]) => (
<TooltipProvider>
<PlanChip surface={surface} snapshot={snapshot} focusComposer={focus} />
</TooltipProvider>
)
const view = render(renderChip([descriptor]))
expect(screen.queryByRole('button')).toBeNull()
const plan = {
...descriptor,
kind: { type: 'select' as const, currentValue: 'plan', choices: [] }
}
view.rerender(renderChip([plan]))
await act(async () => fireEvent.click(screen.getByRole('button', { name: 'Exit plan mode' })))
expect(setOption).toHaveBeenLastCalledWith('interactionMode', 'default')
expect(screen.getByRole('button', { name: 'Exit plan mode' })).toHaveProperty('disabled', false)
expect(focus).toHaveBeenCalledOnce()
await act(async () => fireEvent.click(screen.getByRole('button', { name: 'Exit plan mode' })))
expect(setOption).toHaveBeenLastCalledWith('interactionMode', 'default')
view.rerender(renderChip([descriptor]))
expect(screen.queryByRole('button')).toBeNull()
view.rerender(renderChip([]))
expect(screen.queryByRole('button')).toBeNull()
})
@@ -5,7 +5,6 @@ import { Editor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { focusMountedNativeChatEditor } from './native-chat-mounted-editor-view'
import { TooltipProvider } from '@/components/ui/tooltip'
import { NativeChatComposerToolbar } from './NativeChatComposerToolbar'
import { NativeChatComposerActions } from './NativeChatComposerActions'
import type {
SessionOptionDescriptor,
@@ -250,58 +249,3 @@ it('keeps current mode, permissions and Goal reachable through overflow, includi
view.unmount()
editor.destroy()
})
it('limits a footer resize to its own controls with five and fifty mounted composers', () => {
const callbacks = new Map<Element, () => void>()
vi.stubGlobal(
'ResizeObserver',
class {
constructor(private callback: ResizeObserverCallback) {}
observe(target: Element) {
callbacks.set(target, () => this.callback([], this))
}
unobserve(target: Element) {
callbacks.delete(target)
}
disconnect() {}
}
)
vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(120)
const measured: HTMLElement[] = []
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (
this: HTMLElement
) {
if (this.hasAttribute('data-composer-control')) {
measured.push(this)
}
return new DOMRect(0, 0, 40, 24)
})
for (const population of [5, 50]) {
const view = render(
<>
{Array.from({ length: population }, (_, index) => (
<section key={index} aria-label={`Chat ${index}`}>
<NativeChatComposerToolbar
left={[{ id: 'mode', priority: 1, content: <button>Plan</button> }]}
right={[{ id: 'model', content: <button>Model</button> }]}
primary={<button>Send</button>}
/>
</section>
))}
</>
)
const chat = screen.getByRole('region', { name: 'Chat 0' })
const toolbar = chat.querySelector('[data-compact]')
if (!toolbar) {
throw new Error('Expected the mounted composer toolbar')
}
measured.length = 0
act(() => callbacks.get(toolbar)?.())
// The changed footer needs at most full and compact widths for its two controls.
expect(measured.length).toBeGreaterThan(0)
expect(measured.length).toBeLessThanOrEqual(4)
expect(measured.every((control) => chat.contains(control))).toBe(true)
view.unmount()
callbacks.clear()
}
})
@@ -1,294 +0,0 @@
// @vitest-environment happy-dom
import { Profiler } from 'react'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import {
makeWorktree,
makeFolderWorkspace,
TEST_REPO
} from '@/store/slices/worktrees-slice-test-fixtures'
import { makeTab } from '@/store/slices/store-session-test-harness'
import { TooltipProvider } from '@/components/ui/tooltip'
import { NativeChatComposerWorkspace } from './NativeChatComposerWorkspace'
import { createGlobalSettingsFixture } from '../../../../shared/global-settings-test-fixture'
vi.mock('@/store', async () => {
const { createTestStore } = await import('@/store/slices/store-test-helpers')
return { useAppStore: createTestStore() }
})
const clipboard = vi.fn(async () => {})
const wt = makeWorktree({
id: 'wt-1',
repoId: TEST_REPO.id,
branch: 'refs/heads/feature/composer',
hostId: 'local'
})
beforeEach(() => {
vi.stubGlobal('api', undefined)
Object.defineProperty(window, 'api', {
configurable: true,
value: { ui: { writeClipboardText: clipboard } }
})
clipboard.mockClear()
useAppStore.setState({
settings: createGlobalSettingsFixture({ machineName: 'My laptop' }),
runtimeEnvironments: [],
sshTargetLabels: new Map(),
repos: [TEST_REPO],
worktreesByRepo: { [TEST_REPO.id]: [wt] },
detectedWorktreesByRepo: {},
tabsByWorktree: { [wt.id]: [makeTab({ id: 'tab-1', worktreeId: wt.id })] },
unifiedTabsByWorktree: {},
folderWorkspaces: [],
structuredSessionLaunchDirectoryByTabId: {},
activeWorktreeId: wt.id,
activeWorkspaceExecutionHostId: 'local'
})
})
afterEach(() => {
cleanup()
vi.unstubAllGlobals()
})
function Footer() {
return (
<TooltipProvider>
<NativeChatComposerWorkspace terminalTabId="tab-1" />
</TooltipProvider>
)
}
describe('chat composer workspace', () => {
it('shows the machine, sidebar checkout name and branch without a separate project label', async () => {
useAppStore.setState({
settings: createGlobalSettingsFixture(),
worktreesByRepo: {
[TEST_REPO.id]: [{ ...wt, displayName: 'my-checkout', isMainWorktree: true }]
}
})
const getStatus = vi.fn().mockResolvedValue({ machineName: 'Detected laptop' })
Object.defineProperty(window.api, 'runtime', {
configurable: true,
value: { getStatus }
})
render(<Footer />)
const machine = await screen.findByText('Detected laptop')
const checkout = screen.getByRole('button', { name: 'Copy my-checkout' })
const branch = screen.getByRole('button', { name: 'Copy feature/composer' })
expect(
machine.compareDocumentPosition(checkout) & Node.DOCUMENT_POSITION_FOLLOWING
).toBeTruthy()
expect(checkout.compareDocumentPosition(branch) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
fireEvent.click(checkout)
expect(clipboard).toHaveBeenCalledWith('my-checkout')
act(() => checkout.focus())
expect((await screen.findByRole('tooltip')).textContent).toBe(wt.path)
expect(screen.queryByText(TEST_REPO.displayName)).toBeNull()
act(() =>
useAppStore.setState({
settings: createGlobalSettingsFixture({ machineName: 'Renamed laptop' })
})
)
expect(screen.getByText('Renamed laptop')).toBeTruthy()
})
it('shows and copies the current branch, then follows the sidebar identity after checkout', async () => {
render(<Footer />)
fireEvent.click(screen.getByRole('button', { name: 'Copy feature/composer' }))
expect(clipboard).toHaveBeenCalledWith('feature/composer')
await act(async () => {})
act(() =>
useAppStore.setState({
worktreesByRepo: { [TEST_REPO.id]: [{ ...wt, branch: 'refs/heads/main' }] }
})
)
expect(screen.getByRole('button', { name: 'Copy main' })).toBeTruthy()
expect(screen.queryByRole('button', { name: 'Copy feature/composer' })).toBeNull()
})
it('shows a legacy chat with an opaque workspace ID before its unified bucket arrives', () => {
const opaque = makeWorktree({
id: 'constructor',
repoId: TEST_REPO.id,
hostId: 'local',
path: '/repo/opaque',
displayName: 'opaque-checkout',
branch: 'refs/heads/opaque-branch'
})
useAppStore.setState({
worktreesByRepo: { [TEST_REPO.id]: [opaque] },
tabsByWorktree: { constructor: [makeTab({ id: 'tab-1', worktreeId: opaque.id })] },
unifiedTabsByWorktree: {}
})
render(<Footer />)
expect(screen.getByRole('button', { name: 'Copy opaque-branch' })).toBeTruthy()
expect(screen.getByText('opaque-checkout')).toBeTruthy()
})
it('copies the full detached HEAD and never invents a branch for a folder', async () => {
useAppStore.setState({
worktreesByRepo: { [TEST_REPO.id]: [{ ...wt, branch: '', head: 'a1b2c3d4e5f6' }] }
})
render(<Footer />)
expect(screen.getByRole('button', { name: 'Copy Detached HEAD · a1b2c3d' })).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Copy Detached HEAD · a1b2c3d' }))
expect(clipboard).toHaveBeenCalledWith('a1b2c3d4e5f6')
await act(async () => {})
act(() =>
useAppStore.setState({
folderWorkspaces: [
makeFolderWorkspace({
name: 'notes',
folderPath: '/work/notes',
executionHostId: 'local'
})
],
tabsByWorktree: {
'folder:folder-1': [makeTab({ id: 'tab-1', worktreeId: 'folder:folder-1' })]
}
})
)
expect(screen.queryByRole('button', { name: 'Copy Detached HEAD · a1b2c3d' })).toBeNull()
expect(screen.getByText('notes')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Copy notes' }))
expect(clipboard).toHaveBeenCalledWith('notes')
})
it('identifies a folder project by its checkout name without Git identity', () => {
const folder = makeWorktree({
id: 'notes-main',
repoId: TEST_REPO.id,
path: '/work/notes',
displayName: 'plain-notes-folder',
isMainWorktree: true,
branch: '',
head: ''
})
useAppStore.setState({
repos: [{ ...TEST_REPO, kind: 'folder', displayName: 'Notes project', path: '/work/notes' }],
worktreesByRepo: { [TEST_REPO.id]: [folder] },
tabsByWorktree: { [folder.id]: [makeTab({ id: 'tab-1', worktreeId: folder.id })] }
})
render(<Footer />)
expect(screen.getByText('plain-notes-folder')).toBeTruthy()
expect(screen.queryByRole('button', { name: 'Copy feature/composer' })).toBeNull()
})
it('does not render for unrelated store writes or another workspace branch update', () => {
const commits = vi.fn()
render(
<Profiler id="footer" onRender={commits}>
<Footer />
</Profiler>
)
commits.mockClear()
act(() => useAppStore.setState({ activeWorktreeId: 'other' }))
expect(commits).not.toHaveBeenCalled()
act(() =>
useAppStore.setState({
worktreesByRepo: {
[TEST_REPO.id]: [
wt,
makeWorktree({ id: 'other', repoId: TEST_REPO.id, branch: 'other-branch' })
]
}
})
)
expect(commits).not.toHaveBeenCalled()
act(() =>
useAppStore.setState({
worktreesByRepo: { [TEST_REPO.id]: [{ ...wt, branch: 'refs/heads/changed' }] }
})
)
expect(screen.getByRole('button', { name: 'Copy changed' })).toBeTruthy()
expect(commits).toHaveBeenCalledTimes(1)
})
it('keeps same-ID host copies separate and leaves the other host unrendered', () => {
const remote = { ...wt, hostId: 'runtime:box' as const, branch: 'refs/heads/remote-branch' }
useAppStore.setState({
runtimeEnvironments: [
{
id: 'box',
name: 'Build Mac',
createdAt: 0,
updatedAt: 0,
lastUsedAt: null,
runtimeId: null,
endpoints: [],
preferredEndpointId: 'direct'
}
],
repos: [TEST_REPO, { ...TEST_REPO, executionHostId: 'runtime:box' }],
worktreesByRepo: { [TEST_REPO.id]: [wt, remote] },
activeWorkspaceExecutionHostId: 'runtime:box'
})
const localCommits = vi.fn()
const remoteCommits = vi.fn()
render(
<TooltipProvider>
<Profiler id="local" onRender={localCommits}>
<NativeChatComposerWorkspace
terminalTabId="tab-1"
structuredTransport={{ worktreeId: wt.id, runtimeEnvironmentId: null }}
/>
</Profiler>
<Profiler id="remote" onRender={remoteCommits}>
<NativeChatComposerWorkspace
terminalTabId="remote-tab"
structuredTransport={{ worktreeId: wt.id, runtimeEnvironmentId: 'box' }}
/>
</Profiler>
</TooltipProvider>
)
expect(screen.getByRole('button', { name: 'Copy feature/composer' })).toBeTruthy()
expect(screen.getByRole('button', { name: 'Copy remote-branch' })).toBeTruthy()
expect(screen.getByText('Build Mac')).toBeTruthy()
expect(screen.getByText('My laptop')).toBeTruthy()
localCommits.mockClear()
remoteCommits.mockClear()
act(() =>
useAppStore.getState().setEnvironmentSshConnectionState('box', 'target', {
targetId: 'target',
status: 'connecting',
error: null,
reconnectAttempt: 0
})
)
expect(localCommits).not.toHaveBeenCalled()
expect(remoteCommits).not.toHaveBeenCalled()
act(() =>
useAppStore.getState().setEnvironmentRemovedSshTargetLabels('other', { target: 'Other box' })
)
expect(localCommits).not.toHaveBeenCalled()
expect(remoteCommits).not.toHaveBeenCalled()
act(() =>
useAppStore.setState({
runtimeEnvironments: useAppStore.getState().runtimeEnvironments.map((environment) => ({
...environment,
name: 'Renamed server'
}))
})
)
expect(screen.getByText('Renamed server')).toBeTruthy()
expect(remoteCommits).toHaveBeenCalledTimes(1)
expect(localCommits).not.toHaveBeenCalled()
act(() =>
useAppStore.setState({
worktreesByRepo: { [TEST_REPO.id]: [wt, { ...remote, branch: 'refs/heads/remote-new' }] }
})
)
expect(screen.getByRole('button', { name: 'Copy remote-new' })).toBeTruthy()
expect(localCommits).not.toHaveBeenCalled()
act(() =>
useAppStore.setState({
worktreesByRepo: { [TEST_REPO.id]: [{ ...wt, branch: 'local-new' }, remote] }
})
)
expect(screen.getByRole('button', { name: 'Copy local-new' })).toBeTruthy()
expect(localCommits).toHaveBeenCalledTimes(1)
})
})
@@ -1,73 +0,0 @@
// @vitest-environment happy-dom
import { act, cleanup, render } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import type { FileLinkExists } from '../../../../shared/native-chat/native-chat-file-link-tree'
import { NativeChatPacedMarkdown } from './NativeChatPacedMarkdown'
import { NativeChatFileLinkExistenceContext } from './native-chat-file-link-existence'
import type { NativeChatFileLinkExistence } from '../../../../shared/native-chat/native-chat-file-link-existence'
import {
NativeChatReplyRevealsContext,
type NativeChatReplyReveals
} from './native-chat-reply-reveals'
import { NATIVE_CHAT_TEXT_REVEAL_DELAY_MS } from '../../../../shared/native-chat/transcript/native-chat-text-reveal'
import { NativeChatLinkClickContext } from './native-chat-link-click-context'
vi.mock('@/hooks/usePrefersReducedMotion', () => ({ usePrefersReducedMotion: () => false }))
vi.mock('./native-chat-visual-markdown-extension', () => ({
useNativeChatVisualMarkdownExtension: () => undefined
}))
const reveals: NativeChatReplyReveals = { begun: new Set(['reply']), drawn: new Map() }
const check = vi.fn<FileLinkExists>(() => true)
const peek = vi.fn<FileLinkExists>(() => false)
const snapshot = { check, peek }
const existence: NativeChatFileLinkExistence = {
watch: () => ({ subscribe: () => () => {}, getSnapshot: () => snapshot }),
recheck: () => {}
}
afterEach(() => {
cleanup()
vi.useRealTimers()
reveals.drawn.clear()
check.mockClear()
peek.mockClear()
})
it('asks the host about paths only once a paced reply has been fully revealed', () => {
vi.useFakeTimers({
toFake: [
'setTimeout',
'clearTimeout',
'requestAnimationFrame',
'cancelAnimationFrame',
'performance'
]
})
const onLinkClick = vi.fn()
const reply = (streaming: boolean) => (
<NativeChatFileLinkExistenceContext.Provider value={existence}>
<NativeChatReplyRevealsContext.Provider value={reveals}>
<NativeChatLinkClickContext value={onLinkClick}>
<NativeChatPacedMarkdown
rowKey="reply"
content="Edited src/app.ts for you."
streaming={streaming}
variant="document"
linkifyFilePaths
/>
</NativeChatLinkClickContext>
</NativeChatReplyRevealsContext.Provider>
</NativeChatFileLinkExistenceContext.Provider>
)
const view = render(reply(true))
act(() => vi.advanceTimersByTime(100))
// Why: the stream has ended but the text is still being drawn, so it counts as arriving.
view.rerender(reply(false))
expect(check).not.toHaveBeenCalled()
act(() => vi.advanceTimersByTime(NATIVE_CHAT_TEXT_REVEAL_DELAY_MS))
expect(check).toHaveBeenCalledWith(expect.objectContaining({ pathText: 'src/app.ts' }))
expect(view.container.querySelector('a')?.textContent).toBe('src/app.ts')
})
@@ -1,142 +0,0 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { NativeChatStructuredSession } from './NativeChatStructuredSession'
import { resetStructuredAgentLaunchRegistryForTests } from '@/lib/agent-session/launch/structured-agent-session-launch-registry'
import {
resetStructuredAgentLaunchPersistenceForTests,
writeStructuredAgentLaunchRecord
} from '@/lib/agent-session/launch/structured-agent-session-launch-persistence'
import { resetStructuredAgentSessionSendsForTests } from './structured-agent-session-message-sender'
import { structuredSessionNotices } from './native-chat-structured-session-notices'
import { NativeChatComposerNotices } from './NativeChatComposerNotices'
import { i18n } from '@/i18n/i18n'
import fr from '@/i18n/locales/fr.json'
import type * as Launch from '@/lib/agent-session/launch/structured-agent-session-launch'
const { mocks, moduleFactories, resetStructuredSessionMocks } = await vi.hoisted(async () =>
(await import('./NativeChatStructuredSession.test-harness')).createStructuredSessionMocks()
)
vi.mock('@/lib/agent-session/launch/structured-agent-session-launch', async (importOriginal) => ({
...(await importOriginal<typeof Launch>()),
retryStructuredAgentSessionLaunch: mocks.retryLaunch
}))
vi.mock('@/runtime/structured-agent-session-client', () =>
moduleFactories.structuredAgentSessionClient()
)
vi.mock('./use-structured-agent-session', () => moduleFactories.useStructuredAgentSession())
vi.mock('./use-native-chat-tab-owner', () => moduleFactories.useNativeChatTabOwner())
vi.mock('./use-native-chat-font-size', () => moduleFactories.useNativeChatFontSize())
vi.mock('./use-native-chat-file-link-context', () => moduleFactories.useNativeChatFileLinkContext())
vi.mock('./use-native-chat-file-link-click', () => moduleFactories.useNativeChatFileLinkClick())
vi.mock('./NativeChatMessageList', () => moduleFactories.nativeChatMessageList())
vi.mock('./NativeChatComposer', () => moduleFactories.nativeChatComposer())
vi.mock('./NativeChatApprovalCard', () => moduleFactories.nativeChatApprovalCard())
vi.mock('./NativeChatQuestionCard', () => moduleFactories.nativeChatQuestionCard())
afterEach(async () => {
cleanup()
resetStructuredSessionMocks()
resetStructuredAgentLaunchRegistryForTests()
resetStructuredAgentLaunchPersistenceForTests()
resetStructuredAgentSessionSendsForTests()
localStorage.clear()
await i18n.changeLanguage('en')
})
function restoreOpening() {
writeStructuredAgentLaunchRecord({
sessionId: 'opening-notice-session',
executionHostId: 'local',
agent: 'codex',
lifecycle: 'visibility-unknown',
clientOperationId: 'original-create',
payloadFingerprint: 'original-payload',
expectedRuntimeFence: null,
createMessageSupport: true,
firstMessage: {
clientMessageId: 'original-opening',
body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'Keep this opening' }] }
}
})
mocks.messages = [
{
id: 'orca:original-opening',
role: 'user',
source: 'transcript',
timestamp: 1,
blocks: [{ type: 'text', text: 'Keep this opening' }]
}
]
}
function renderPane() {
return render(
<NativeChatStructuredSession
isVisible
isFocusedGroup
tabId="opening-notice-tab"
sessionId="opening-notice-session"
target={{ kind: 'local' }}
agent="codex"
/>
)
}
it.each(['en', 'fr'])(
'keeps restored opening recovery with its launch notice in %s',
async (language) => {
i18n.addResourceBundle('fr', 'translation', fr)
await i18n.changeLanguage(language)
restoreOpening()
mocks.status = 'error'
mocks.readRefusal = {
code: 'agent_session_identity_required',
details: { reason: 'recordMissing' }
}
renderPane()
expect(mocks.controllerProps?.transportEnabled).toBe(true)
expect(mocks.composerProps?.notices?.map(({ key }) => key)).toEqual(['launch'])
fireEvent.click(screen.getByRole('button'))
expect(mocks.retryLaunch).toHaveBeenCalledExactlyOnceWith('wt-1', 'opening-notice-session')
}
)
it('keeps an unrelated read refusal with its history owner despite unresolved creation', () => {
restoreOpening()
mocks.status = 'error'
mocks.readRefusal = {
code: 'agent_session_journal_unreadable',
details: { reason: 'journalUnavailable' }
}
renderPane()
expect(mocks.composerProps?.notices?.map(({ key }) => key)).toEqual(['session'])
})
it('preserves the launch cause and its action instead of borrowing it for missing-history advice', () => {
const retry = vi.fn()
const notices = structuredSessionNotices({
launch: {
lifecycle: 'visibility-unknown',
failure: {
kind: 'refused',
code: 'agent_session_operation_invalid',
details: { reason: 'cliMissing' },
causeText: 'Codex needs its command installed.'
},
retry
},
agentLabel: 'Codex',
sessionError: 'Missing history advice',
readRefusal: {
code: 'agent_session_identity_required',
details: { reason: 'recordMissing' }
},
composerError: null
})
expect(notices.map(({ key }) => key)).toEqual(['launch'])
render(<NativeChatComposerNotices notices={notices} />)
expect(screen.getByText(/Codex needs its command installed/)).toBeTruthy()
fireEvent.click(screen.getByRole('button'))
expect(retry).toHaveBeenCalledOnce()
})
@@ -1,118 +0,0 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { NativeChatToolLine } from './NativeChatToolLine'
import { NativeChatDiffCard } from './NativeChatDiffCard'
import { createNativeChatFileHref } from '../../../../shared/native-chat/native-chat-href-routing'
import type { NativeChatEditFile } from '../../../../shared/native-chat/tools/native-chat-edit-model'
import { NativeChatLinkClickContext } from './native-chat-link-click-context'
afterEach(cleanup)
const editFile = (overrides: Partial<NativeChatEditFile>): NativeChatEditFile => ({
path: 'src/main.ts',
oldPath: null,
changeKind: 'edited',
lines: [{ kind: 'add', text: 'x', oldLineNumber: null, newLineNumber: 1 }],
added: 1,
removed: 0,
lineNumbersKnown: true,
truncated: false,
...overrides
})
function withLinks(ui: React.ReactElement, onLinkClick: () => void): React.ReactElement {
return <NativeChatLinkClickContext value={onLinkClick}>{ui}</NativeChatLinkClickContext>
}
describe('tool row file targets', () => {
it('opens a read file in Orca without toggling the row', () => {
const onLinkClick = vi.fn()
render(
withLinks(
<NativeChatToolLine
block={{ type: 'tool-call', name: 'Read', input: { file_path: '/repo/src/main.ts' } }}
result={{ type: 'tool-result', output: 'contents' }}
/>,
onLinkClick
)
)
const link = screen.getByRole('link', { name: '/repo/src/main.ts' })
expect(link).toHaveTextContent('main.ts')
fireEvent.click(link)
expect(onLinkClick).toHaveBeenCalledWith(
expect.anything(),
createNativeChatFileHref('/repo/src/main.ts', 'literal')
)
expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false')
expect(screen.getByRole('button')).toHaveAccessibleName(/Read.*\/repo\/src\/main.ts/)
})
it('opens from the keyboard', () => {
const onLinkClick = vi.fn()
render(
withLinks(
<NativeChatToolLine
block={{ type: 'tool-call', name: 'Write', input: { file_path: 'new.ts', content: 'a' } }}
/>,
onLinkClick
)
)
fireEvent.keyDown(screen.getByRole('link'), { key: 'Enter' })
expect(onLinkClick).toHaveBeenCalledWith(
expect.anything(),
createNativeChatFileHref('new.ts', 'literal')
)
})
it('leaves the name plain when the chat cannot open files, and for non-file tools', () => {
render(
<NativeChatToolLine
block={{ type: 'tool-call', name: 'Read', input: { file_path: '/repo/src/main.ts' } }}
/>
)
expect(screen.queryByRole('link')).toBeNull()
cleanup()
render(
withLinks(
<NativeChatToolLine
block={{ type: 'tool-call', name: 'Grep', input: { pattern: 'TODO', path: '/repo' } }}
/>,
vi.fn()
)
)
expect(screen.queryByRole('link')).toBeNull()
})
it('opens an edited file from its diff card without expanding the card', () => {
const onLinkClick = vi.fn()
render(withLinks(<NativeChatDiffCard file={editFile({})} />, onLinkClick))
fireEvent.click(screen.getByRole('link', { name: 'src/main.ts' }))
expect(onLinkClick).toHaveBeenCalledWith(
expect.anything(),
createNativeChatFileHref('src/main.ts', 'literal')
)
expect(screen.getByRole('button', { expanded: false })).toBeInTheDocument()
})
it('opens the new name of a renamed file, and nothing for a deleted one', () => {
render(
withLinks(
<NativeChatDiffCard
file={editFile({ changeKind: 'renamed', oldPath: 'old.ts', path: 'new.ts' })}
/>,
vi.fn()
)
)
expect(screen.getAllByRole('link').map((link) => link.textContent)).toEqual(['new.ts'])
cleanup()
render(
withLinks(
<NativeChatDiffCard file={editFile({ changeKind: 'deleted', lines: [] })} />,
vi.fn()
)
)
expect(screen.queryByRole('link')).toBeNull()
})
})
@@ -11,8 +11,6 @@ import {
NATIVE_CHAT_VISUAL_PING_TYPE,
NATIVE_CHAT_VISUAL_PONG_TIMEOUT_MS,
NATIVE_CHAT_VISUAL_PONG_TYPE,
NATIVE_CHAT_VISUAL_READER_SCROLL_TYPE,
NATIVE_CHAT_VISUAL_SIZE_TYPE,
NATIVE_CHAT_VISUAL_THEME_TYPE
} from '../../../../shared/native-chat/native-chat-visual-shell'
@@ -23,7 +21,6 @@ vi.mock('@/lib/browser/navigation/http-link-routing', () => ({
import { NativeChatVisualFrame } from './NativeChatVisualFrame'
import type { ReaderGesture } from './native-chat-autoscroll'
import { acquireWebviewsDragPassthrough } from '../browser-pane/host-guest/webview-drag-passthrough'
const visual = { revision: 'r1', html: '<p>chart</p>', path: '/v' }
@@ -123,21 +120,6 @@ afterEach(() => {
})
describe('NativeChatVisualFrame', () => {
it('lets a dragged tab pass over the frame while a tab drag holds pointer passthrough', () => {
const { container } = render(<Harness />)
const frame = frameOf(container)
expect(frame.style.pointerEvents).toBe('')
let release = (): void => {}
act(() => {
release = acquireWebviewsDragPassthrough()
})
expect(frame.style.pointerEvents).toBe('none')
act(() => release())
expect(frame.style.pointerEvents).toBe('')
})
it('runs the visual in a scripts-only sandbox, named for host registration, with its CSP first', () => {
const { container } = render(<Harness />)
const frame = frameOf(container)
@@ -148,62 +130,6 @@ describe('NativeChatVisualFrame', () => {
expect(channelOf(frame)).toMatch(/^[0-9a-f]{32}$/)
})
it('fits its height to what the frame reports, ignoring other windows and channels', () => {
const { container } = render(<Harness />)
const frame = frameOf(container)
const box = frame.parentElement
const channel = channelOf(frame)
post({ type: NATIVE_CHAT_VISUAL_SIZE_TYPE, channel, height: 420 }, frame.contentWindow)
expect(box).toHaveStyle({ height: '420px' })
post({ type: NATIVE_CHAT_VISUAL_SIZE_TYPE, channel, height: 900 }, window)
post({ type: NATIVE_CHAT_VISUAL_SIZE_TYPE, channel: 'other', height: 900 }, frame.contentWindow)
expect(box).toHaveStyle({ height: '420px' })
})
it('hands a wheel or touch its page could not take to the transcript as a reader gesture', () => {
const onReaderGesture = vi.fn()
const { container } = render(<Harness onReaderGesture={onReaderGesture} />)
const frame = frameOf(container)
const channel = channelOf(frame)
const scroll = { type: NATIVE_CHAT_VISUAL_READER_SCROLL_TYPE, channel }
post({ ...scroll, input: 'wheel', deltaY: -30 }, frame.contentWindow)
expect(onReaderGesture).toHaveBeenLastCalledWith({ kind: 'wheel', deltaY: -30, zoom: false })
post({ ...scroll, input: 'touch' }, frame.contentWindow)
expect(onReaderGesture).toHaveBeenLastCalledWith({ kind: 'touch-drag' })
post({ ...scroll, input: 'wheel', deltaY: -30 }, window)
post({ ...scroll, channel: 'other', input: 'wheel', deltaY: -30 }, frame.contentWindow)
expect(onReaderGesture).toHaveBeenCalledTimes(2)
})
it('restyles in place on a theme change instead of reloading the visual', () => {
const { container } = render(<Harness />)
const frame = frameOf(container)
const contentWindow = frame.contentWindow
if (!contentWindow) {
throw new Error('no content window')
}
const postMessage = vi.spyOn(contentWindow, 'postMessage')
act(() => {
frame.dispatchEvent(new Event('load'))
})
expect(postMessage).toHaveBeenCalledWith(
expect.objectContaining({ type: NATIVE_CHAT_VISUAL_THEME_TYPE, colorScheme: 'light' }),
'*'
)
const srcdoc = frame.getAttribute('srcdoc')
act(() => {
document.documentElement.classList.add('dark')
})
return vi.waitFor(() => {
expect(postMessage).toHaveBeenLastCalledWith(
expect.objectContaining({ type: NATIVE_CHAT_VISUAL_THEME_TYPE, colorScheme: 'dark' }),
'*'
)
expect(frameOf(container)).toBe(frame)
expect(frame.getAttribute('srcdoc')).toBe(srcdoc)
})
})
it('opens a link only while the frame has focus and the user has just acted', () => {
const { container } = render(<Harness />)
const frame = frameOf(container)
@@ -1,148 +0,0 @@
// @vitest-environment happy-dom
/** A phone’s input lease must visibly lock the composer and offer Take back regardless of draft contents. */
import { createRef } from 'react'
import { act, cleanup, fireEvent, render, renderHook, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
restoreTerminalFitToDesktop: vi.fn<(ptyId: string, settings: unknown) => Promise<boolean>>(),
refitAndRefreshAllTerminalPanes: vi.fn(),
settings: { activeRuntimeEnvironmentId: 'env-1' }
}))
vi.mock('@/i18n/i18n', () => ({
translate: (_key: string, fallback: string) => fallback
}))
vi.mock('@/store', () => ({
useAppStore: { getState: () => ({ settings: mocks.settings }) }
}))
vi.mock('../terminal-pane/viewport/terminal-fit-restore', () => ({
restoreTerminalFitToDesktop: mocks.restoreTerminalFitToDesktop
}))
vi.mock('@/lib/pane-manager/pane-manager-registry', () => ({
refitAndRefreshAllTerminalPanes: mocks.refitAndRefreshAllTerminalPanes
}))
vi.mock('./NativeChatComposerActions', () => ({
NativeChatComposerActions: () => <div data-testid="composer-actions" />
}))
vi.mock('./NativeChatAutocompleteMenus', () => ({
NativeChatMentionMenu: () => null,
NativeChatPickerMenu: () => null
}))
import { NativeChatComposerField } from './NativeChatComposerField'
import type { NativeChatComposerNotice } from './native-chat-composer-notice'
import { useImeEnterGestureOwnership } from '@/lib/ime-composition-keyboard-event'
import {
nativeChatMobileHoldNotice,
useNativeChatMobileHoldNotice
} from './use-native-chat-mobile-hold-notice'
beforeEach(() => {
mocks.restoreTerminalFitToDesktop.mockReset()
mocks.refitAndRefreshAllTerminalPanes.mockReset()
vi.stubGlobal('requestAnimationFrame', (callback: () => void) => {
callback()
return 0
})
})
afterEach(() => {
cleanup()
vi.unstubAllGlobals()
})
function HeldField({
notices,
goalActive
}: {
notices: readonly NativeChatComposerNotice[]
goalActive: boolean
}): React.JSX.Element {
const imeEnterGesture = useImeEnterGestureOwnership()
return (
<NativeChatComposerField
draftScopeKey="pane-held"
textareaRef={createRef<HTMLTextAreaElement>()}
draft="half-written reply"
disabled
hasPty
canSend={false}
autocomplete={{ mode: 'none' }}
activeSuggestion={0}
notices={notices}
imageAttachments={[]}
sendButtonDisabled
isWorking={false}
attachDisabled
dictationDisabled
isDictating={false}
isDictationHoldMode={false}
imeEnterGesture={imeEnterGesture}
goalMode={{ active: goalActive, exit: vi.fn(), interceptPick: (pick) => pick }}
onDraftChange={vi.fn()}
onTextareaSelect={vi.fn()}
onKeyDown={vi.fn()}
onImeSettled={vi.fn()}
onPaste={vi.fn()}
pickerListboxId="picker"
onChoosePickerItem={vi.fn()}
onRetrySkills={vi.fn()}
onChooseMentionFile={vi.fn()}
mentionFiles={{ files: [], loading: false, failed: false }}
onRemoveImageAttachment={vi.fn()}
onAttach={vi.fn()}
onDictationToggle={vi.fn()}
onDictationHoldStart={vi.fn()}
onDictationHoldEnd={vi.fn()}
onSend={vi.fn()}
sessionOptionsSurface={null}
sessionOptionsSnapshot={[]}
/>
)
}
describe('the chat composer while a phone holds the input', () => {
it('names the lock and offers Take back with a draft and goal mode on', () => {
const onTakeBack = vi.fn()
render(<HeldField notices={[nativeChatMobileHoldNotice(false, onTakeBack)]} goalActive />)
expect(screen.getByText('Input is held by another device.')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Take back' }))
expect(onTakeBack).toHaveBeenCalledTimes(1)
})
})
describe('useNativeChatMobileHoldNotice', () => {
it("runs the terminal's reclaim for this pty and refits once it lands", async () => {
let resolveRestore: (restored: boolean) => void = () => {}
mocks.restoreTerminalFitToDesktop.mockReturnValue(
new Promise<boolean>((resolve) => {
resolveRestore = resolve
})
)
const { result } = renderHook(() => useNativeChatMobileHoldNotice('pty-1', false))
expect(result.current?.kind).toBe('device')
expect(result.current?.action?.disabled).toBe(false)
act(() => result.current?.action?.onClick())
expect(mocks.restoreTerminalFitToDesktop).toHaveBeenCalledWith('pty-1', mocks.settings)
// Pending blocks a second reclaim while the first is in flight.
expect(result.current?.action?.disabled).toBe(true)
await act(async () => resolveRestore(true))
expect(mocks.refitAndRefreshAllTerminalPanes).toHaveBeenCalledTimes(1)
expect(result.current?.action?.disabled).toBe(false)
})
it('leaves the panes alone when there was nothing to reclaim', async () => {
mocks.restoreTerminalFitToDesktop.mockResolvedValue(false)
const { result } = renderHook(() => useNativeChatMobileHoldNotice('pty-1', false))
await act(async () => result.current?.action?.onClick())
expect(mocks.refitAndRefreshAllTerminalPanes).not.toHaveBeenCalled()
expect(result.current?.action?.disabled).toBe(false)
})
})
@@ -1,442 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createGlobalSettingsFixture } from '../../../../shared/global-settings-test-fixture'
import { makeDetectedResult } from '@/store/slices/worktrees-detected-listing-fixtures'
import {
hasCachedWindowsTerminalCapabilities,
resetWindowsTerminalCapabilitiesForTests
} from '@/lib/windows-terminal-capabilities'
import { createTestStore } from '@/store/slices/store-test-helpers'
import {
makeWorktree,
makeFolderWorkspace,
TEST_REPO
} from '@/store/slices/worktrees-slice-test-fixtures'
import { makeTab, makeUnifiedTab } from '@/store/slices/store-session-test-harness'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
import { createNativeChatComposerWorkspaceSelector } from './native-chat-composer-workspace'
vi.mock('@/lib/renderer-app-platform', () => ({ getRendererAppPlatform: () => 'win32' }))
afterEach(() => {
resetWindowsTerminalCapabilitiesForTests()
})
describe('composer workspace context', () => {
it('preserves a terminal chat SSH host even when a local twin is selected', () => {
const store = createTestStore()
store.setState({
sshTargetLabels: new Map([['box', 'Build server']]),
repos: [TEST_REPO, { ...TEST_REPO, executionHostId: 'ssh:box' }],
activeWorktreeId: 'wt-1',
activeWorkspaceExecutionHostId: 'local',
worktreesByRepo: {
[TEST_REPO.id]: [
makeWorktree({ id: 'wt-1', repoId: TEST_REPO.id, hostId: 'local', branch: 'local' }),
makeWorktree({
id: 'wt-1',
repoId: TEST_REPO.id,
hostId: 'ssh:box',
branch: 'ssh',
path: '/srv/work'
})
]
},
tabsByWorktree: {
'wt-1': [makeTab({ id: 'chat', worktreeId: 'wt-1' })]
},
unifiedTabsByWorktree: {
'wt-1': [
makeUnifiedTab({
id: 'chat',
worktreeId: 'wt-1',
groupId: 'group',
executionHostId: 'ssh:box'
})
]
}
})
expect(createNativeChatComposerWorkspaceSelector('chat')(store.getState())).toMatchObject({
branch: 'ssh',
hostId: 'ssh:box',
machine: 'Build server',
kind: 'worktree',
path: '/srv/work'
})
})
it.each([
['git', true, 'main'],
['git', false, 'worktree'],
['folder', true, 'folder']
] as const)('identifies a %s checkout before Git identity arrives', (repoKind, main, kind) => {
const store = createTestStore()
store.setState({
repos: [{ ...TEST_REPO, kind: repoKind }],
worktreesByRepo: {
[TEST_REPO.id]: [
makeWorktree({
id: 'wt',
repoId: TEST_REPO.id,
hostId: 'local',
displayName: 'Sidebar name',
isMainWorktree: main,
branch: '',
head: ''
})
]
}
})
expect(
createNativeChatComposerWorkspaceSelector('chat', {
worktreeId: 'wt',
runtimeEnvironmentId: null
})(store.getState())
).toMatchObject({ checkout: 'Sidebar name', kind })
})
it('uses this floating session pin and waits through missing or stale pins', () => {
const store = createTestStore()
const worktreeId = FLOATING_TERMINAL_WORKTREE_ID
store.setState({
floatingWorkspacePath: '/home/me/changed-setting',
unifiedTabsByWorktree: {
[worktreeId]: [
makeUnifiedTab({
id: 'chat',
worktreeId,
groupId: 'group',
entityId: 'session-1',
contentType: 'agent-session',
agentSessionAgent: 'codex'
})
]
},
structuredSessionLaunchDirectoryByTabId: {}
})
const select = createNativeChatComposerWorkspaceSelector('chat', {
worktreeId,
runtimeEnvironmentId: null
})
expect(select(store.getState()).path).toBeUndefined()
store.setState({
structuredSessionLaunchDirectoryByTabId: {
chat: { sessionId: 'old-session', launchDirectory: '/home/me/stale' }
}
})
expect(select(store.getState()).path).toBeUndefined()
store.setState({
structuredSessionLaunchDirectoryByTabId: {
chat: { sessionId: 'session-1', launchDirectory: '/home/me/pinned' }
}
})
expect(select(store.getState())).toMatchObject({ path: '/home/me/pinned', branch: '' })
store.setState({ floatingWorkspacePath: '/home/me/another-setting' })
expect(select(store.getState()).path).toBe('/home/me/pinned')
store.setState({
unifiedTabsByWorktree: {
[worktreeId]: [
makeUnifiedTab({
id: 'chat',
worktreeId,
groupId: 'group',
entityId: 'session-2',
contentType: 'agent-session',
agentSessionAgent: 'codex'
})
]
}
})
expect(select(store.getState()).path).toBeUndefined()
})
it('uses the addressed Windows project runtime without a capability probe or active-project fallback', () => {
const store = createTestStore()
const local = makeWorktree({
id: 'wt',
repoId: TEST_REPO.id,
projectId: 'local-project',
hostId: 'local',
path: 'C:\\src\\repo'
})
const remote = { ...local, projectId: 'remote-project', hostId: 'runtime:hub' as const }
const project = {
id: 'local-project',
displayName: 'Local project',
badgeColor: 'blue',
sourceRepoIds: [TEST_REPO.id],
createdAt: 0,
updatedAt: 0
}
store.setState({
settings: createGlobalSettingsFixture({
machineName: 'Windows PC',
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
}),
repos: [TEST_REPO, { ...TEST_REPO, executionHostId: 'runtime:hub' }],
projects: [
{
...project,
id: 'remote-project',
sourceRepoIds: [],
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Wrong distro' }
},
{ ...project, localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } }
],
activeRepoId: TEST_REPO.id,
activeWorktreeId: 'wt',
activeWorkspaceExecutionHostId: 'runtime:hub',
worktreesByRepo: { [TEST_REPO.id]: [remote, local] },
tabsByWorktree: { wt: [makeTab({ id: 'chat', worktreeId: 'wt' })] },
unifiedTabsByWorktree: {
wt: [
makeUnifiedTab({
id: 'chat',
worktreeId: 'wt',
groupId: 'group',
executionHostId: 'local'
})
]
}
})
const select = createNativeChatComposerWorkspaceSelector('chat')
expect(hasCachedWindowsTerminalCapabilities()).toBe(false)
expect(select(store.getState()).machine).toBe('Ubuntu')
store.setState({
projects: [{ ...project, localWindowsRuntimePreference: { kind: 'inherit-global' } }]
})
expect(select(store.getState()).machine).toBe('Ubuntu')
store.setState({
projects: [{ ...project, localWindowsRuntimePreference: { kind: 'windows-host' } }]
})
expect(select(store.getState()).machine).toBe('Windows PC')
expect(
createNativeChatComposerWorkspaceSelector('chat', {
worktreeId: 'wt',
runtimeEnvironmentId: null
})(store.getState()).machine
).toBe('Windows PC')
store.setState({
projects: [{ ...project, localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } }],
worktreesByRepo: {}
})
expect(select(store.getState()).machine).toBe('Windows PC')
})
it('keeps UNC and addressed runtime WSL names', () => {
const store = createTestStore()
store.setState({
repos: [TEST_REPO, { ...TEST_REPO, executionHostId: 'runtime:wsl' }],
runtimeEnvironments: [
{
id: 'wsl',
name: 'Windows host',
createdAt: 0,
updatedAt: 0,
lastUsedAt: null,
runtimeId: null,
endpoints: [],
preferredEndpointId: 'direct',
orcadDeployment: { kind: 'wsl', distro: 'Debian', localPort: 1234, remotePort: 1234 }
}
],
worktreesByRepo: {
[TEST_REPO.id]: [makeWorktree({ id: 'wt', repoId: TEST_REPO.id, hostId: 'runtime:wsl' })]
}
})
expect(
createNativeChatComposerWorkspaceSelector('chat', {
worktreeId: 'wt',
runtimeEnvironmentId: 'wsl'
})(store.getState()).machine
).toBe('Debian')
store.setState({
worktreesByRepo: {
[TEST_REPO.id]: [
makeWorktree({
id: 'wt',
repoId: TEST_REPO.id,
hostId: 'local',
path: '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo'
})
]
},
tabsByWorktree: { wt: [makeTab({ id: 'chat', worktreeId: 'wt' })] },
unifiedTabsByWorktree: {
wt: [
makeUnifiedTab({
id: 'chat',
worktreeId: 'wt',
groupId: 'group',
executionHostId: 'local'
})
]
}
})
expect(createNativeChatComposerWorkspaceSelector('chat')(store.getState()).machine).toBe(
'Ubuntu'
)
})
it('withholds contested owner bookkeeping instead of showing a local or routing name', () => {
const store = createTestStore()
store.setState({
settings: createGlobalSettingsFixture({ machineName: 'My laptop' }),
activeWorktreeId: null,
activeWorkspaceExecutionHostId: null,
repos: [TEST_REPO],
worktreesByRepo: {},
tabsByWorktree: { wt: [makeTab({ id: 'chat', worktreeId: 'wt' })] },
detectedWorktreesByRepo: {
[TEST_REPO.id]: makeDetectedResult(TEST_REPO.id, [
makeWorktree({ id: 'wt', repoId: TEST_REPO.id, hostId: 'runtime:one' }),
makeWorktree({ id: 'wt', repoId: TEST_REPO.id, hostId: 'runtime:two' })
])
}
})
const value = createNativeChatComposerWorkspaceSelector('chat')(store.getState())
expect(value.hostId).toBeUndefined()
expect(value.machine).toBeUndefined()
})
it.each([8, 80])(
'caches missing-folder resolution before catalog scans for %i retained consumers',
(size) => {
const store = createTestStore()
let visits = 0
const folders = Array.from({ length: size }, (_, index) => {
const id = `existing-${index}`
const folder = makeFolderWorkspace({ id, executionHostId: 'local' })
Object.defineProperty(folder, 'id', {
get: () => {
visits++
return id
}
})
return folder
})
const nested = makeWorktree({
id: 'nested',
repoId: TEST_REPO.id,
hostId: 'ssh:target',
runtimeOwnerEnvironmentId: 'hub'
})
Object.defineProperty(nested, 'path', {
get: () => {
visits++
return '/srv/nested'
}
})
store.setState({
settings: createGlobalSettingsFixture({ machineName: 'Old local name' }),
folderWorkspaces: folders,
repos: [{ ...TEST_REPO, executionHostId: 'ssh:target' }],
sshTargetLabels: new Map([['target', 'Client target']]),
runtimeEnvironments: [
{
id: 'hub',
name: 'Hub',
createdAt: 0,
updatedAt: 0,
lastUsedAt: null,
runtimeId: null,
endpoints: [],
preferredEndpointId: 'direct'
}
],
worktreesByRepo: { [TEST_REPO.id]: [nested] },
tabsByWorktree: {
nested: [makeTab({ id: 'nested-chat', worktreeId: 'nested' })],
'folder:unowned': [makeTab({ id: 'unowned-chat', worktreeId: 'folder:unowned' })]
},
unifiedTabsByWorktree: {
nested: [
makeUnifiedTab({
id: 'nested-chat',
worktreeId: 'nested',
groupId: 'group',
executionHostId: 'ssh:target'
})
]
}
})
const selectors = Array.from({ length: size }, (_, index) =>
createNativeChatComposerWorkspaceSelector(`chat-${index}`, {
worktreeId: `folder:missing-${index}`,
runtimeEnvironmentId: null
})
)
selectors.push(createNativeChatComposerWorkspaceSelector('nested-chat'))
selectors.push(createNativeChatComposerWorkspaceSelector('unowned-chat'))
const read = () => selectors.map((select) => select(store.getState()))
const first = read()
expect(visits).toBeGreaterThan(0)
visits = 0
store.setState({
activeWorktreeId: 'unrelated',
activeWorkspaceExecutionHostId: 'runtime:box'
})
read().forEach((value, index) => expect(value).toBe(first[index]))
expect(visits).toBe(0)
store.setState({
settings: createGlobalSettingsFixture({
machineName: 'Old local name',
activeRuntimeEnvironmentId: 'hub'
})
})
read().forEach((value, index) => expect(value).toBe(first[index]))
expect(visits).toBe(0)
store
.getState()
.setEnvironmentRemovedSshTargetLabels('unrelated-server', { target: 'Old target' })
read()
expect(visits).toBe(0)
for (const environmentId of ['unrelated-server', 'hub']) {
store.getState().setEnvironmentSshConnectionState(environmentId, 'target', {
targetId: 'target',
status: 'connecting',
error: null,
reconnectAttempt: 0
})
read()
store.getState().setEnvironmentSshConnectionState(environmentId, 'target', {
targetId: 'target',
status: 'connected',
error: null,
reconnectAttempt: 0
})
read()
}
expect(visits).toBe(0)
store.setState({
settings: createGlobalSettingsFixture({ machineName: 'New local name' }),
runtimeEnvironments: store
.getState()
.runtimeEnvironments.map((entry) => ({ ...entry, name: 'Renamed hub' }))
})
read()
expect(visits).toBe(0)
expect(read()[size].machine).toBe('target')
store.getState().setEnvironmentRemovedSshTargetLabels('hub', { target: 'Build server' })
expect(read()[size].machine).toBe('Build server')
store.getState().setEnvironmentRemovedSshTargetLabels('hub', { target: 'Renamed server' })
expect(read()[size].machine).toBe('Renamed server')
store
.getState()
.setEnvironmentSshTargetsMetadata('hub', [{ id: 'target', label: 'Current server' }])
expect(read()[size].machine).toBe('Current server')
store.getState().setEnvironmentSshTargetsMetadata('hub', [])
expect(read()[size].machine).toBe('Renamed server')
store.getState().removeEnvironmentSshState('hub')
expect(read()[size].machine).toBe('target')
expect(visits).toBe(0)
store.setState({
folderWorkspaces: [
...folders,
makeFolderWorkspace({ id: 'missing-0', folderPath: '/arrived', executionHostId: 'local' })
]
})
expect(read()[0]).toMatchObject({ path: '/arrived' })
expect(visits).toBeGreaterThan(0)
}
)
})
@@ -3,7 +3,6 @@
import { describe, expect, it } from 'vitest'
import { createDraftPasteReadyScanner } from './draft-paste-ready-scanner'
import type { DraftPasteReadySignal } from './agent-launch/catalog/tui-agent-config-types'
import { TUI_AGENT_CONFIG } from './agent-launch/catalog/tui-agent-config'
import {
COPILOT_STARTUP_PTY_TRACE,
COPILOT_TRUST_MENU_PTY_TRACE,
@@ -35,10 +34,6 @@ function replay(
}
describe('copilot-composer-prompt startup replay', () => {
it('is the signal Copilot launches wait on', () => {
expect(TUI_AGENT_CONFIG.copilot.draftPasteReadySignal).toBe('copilot-composer-prompt')
})
it('delivers on the composer row instead of after the loading spinner settles', () => {
expect(replay('render-quiet-after-bracketed-paste', COPILOT_STARTUP_PTY_TRACE)).toEqual({
readyAtMs: null,
@@ -1,14 +1,11 @@
import { describe, expect, it } from 'vitest'
import {
NATIVE_CHAT_VISUAL_CSP,
NATIVE_CHAT_VISUAL_HOST_CSP,
NATIVE_CHAT_VISUAL_OPEN_LINK_TYPE,
NATIVE_CHAT_VISUAL_PING_TYPE,
NATIVE_CHAT_VISUAL_PONG_TYPE,
NATIVE_CHAT_VISUAL_READER_SCROLL_TYPE,
NATIVE_CHAT_VISUAL_SIZE_TYPE,
buildNativeChatVisualDocument,
nativeChatVisualPingMessage,
nativeChatVisualThemeCss,
readNativeChatVisualFrameMessage,
readNativeChatVisualPong
@@ -19,59 +16,6 @@ const theme = {
tokens: { '--background': 'oklch(0.2 0 0)', '--chart-1': '#123456' }
}
function directives(policy = NATIVE_CHAT_VISUAL_CSP): Map<string, string> {
return new Map(
policy.split(';').map((part) => {
const [name, ...values] = part.trim().split(/\s+/)
return [name, values.join(' ')]
})
)
}
describe('NATIVE_CHAT_VISUAL_CSP', () => {
it('denies by default and closes every channel a chart does not need', () => {
const policy = directives()
expect(policy.get('default-src')).toBe("'none'")
for (const name of [
'connect-src',
'frame-src',
'child-src',
'worker-src',
'object-src',
'media-src',
'manifest-src',
'form-action',
'base-uri'
]) {
expect(policy.get(name)).toBe("'none'")
}
})
it('allows assets only inline, from data/blob, or from the pinned CDNs, and never eval', () => {
const policy = directives()
for (const name of ['script-src', 'style-src']) {
expect(policy.get(name)).toContain("'unsafe-inline'")
expect(policy.get(name)).not.toContain("'unsafe-eval'")
expect(policy.get(name)).not.toContain('*')
}
expect(policy.get('img-src')).toBe(
'data: blob: https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com https://esm.sh https://fonts.googleapis.com https://fonts.gstatic.com'
)
})
})
describe('NATIVE_CHAT_VISUAL_HOST_CSP', () => {
it('is the visual policy except that it admits only an about: base, for the shell', () => {
const host = directives(NATIVE_CHAT_VISUAL_HOST_CSP)
const visual = directives()
expect(host.get('base-uri')).toBe('about:')
expect(visual.get('base-uri')).toBe("'none'")
host.delete('base-uri')
visual.delete('base-uri')
expect(host).toEqual(visual)
})
})
describe('buildNativeChatVisualDocument', () => {
const html = '<!doctype html><html><head><title>x</title></head><body><p>hi</p></body></html>'
const document = buildNativeChatVisualDocument({ html, channel: 'abc123', theme })
@@ -83,15 +27,6 @@ describe('buildNativeChatVisualDocument', () => {
expect(cspAt).toBeLessThan(document.indexOf('<script>'))
expect(document.endsWith(html)).toBe(true)
})
it('resolves in-page links against the visual itself, ahead of the policy that bars any other base', () => {
const baseAt = document.indexOf('<base href="about:srcdoc">')
expect(baseAt).toBeGreaterThan(0)
expect(baseAt).toBeLessThan(document.indexOf('http-equiv="Content-Security-Policy"'))
// A srcdoc page otherwise resolves '#x' against Orca's own URL and navigates away.
expect(new URL('#overview', 'about:srcdoc').href).toBe('about:srcdoc#overview')
expect(() => new URL('page-2.html', 'about:srcdoc')).toThrow()
})
})
describe('nativeChatVisualThemeCss', () => {
@@ -113,32 +48,6 @@ describe('nativeChatVisualThemeCss', () => {
})
describe('readNativeChatVisualFrameMessage', () => {
it('accepts a finite size and an http(s) link on its own channel', () => {
expect(
readNativeChatVisualFrameMessage(
{ type: NATIVE_CHAT_VISUAL_SIZE_TYPE, channel: 'c', height: 321.5 },
'c'
)
).toEqual({ kind: 'size', height: 321.5 })
expect(
readNativeChatVisualFrameMessage(
{ type: NATIVE_CHAT_VISUAL_OPEN_LINK_TYPE, channel: 'c', url: 'https://example.com/a b' },
'c'
)
).toEqual({ kind: 'open-link', url: 'https://example.com/a%20b' })
})
it('accepts a wheel with a finite nonzero delta and a touch drag', () => {
const scroll = { type: NATIVE_CHAT_VISUAL_READER_SCROLL_TYPE, channel: 'c' }
expect(
readNativeChatVisualFrameMessage({ ...scroll, input: 'wheel', deltaY: -30 }, 'c')
).toEqual({ kind: 'reader-scroll', input: 'wheel', deltaY: -30 })
expect(readNativeChatVisualFrameMessage({ ...scroll, input: 'touch' }, 'c')).toEqual({
kind: 'reader-scroll',
input: 'touch'
})
})
it.each([
['another channel', { type: NATIVE_CHAT_VISUAL_SIZE_TYPE, channel: 'x', height: 10 }],
['no channel', { type: NATIVE_CHAT_VISUAL_SIZE_TYPE, height: 10 }],
@@ -189,16 +98,6 @@ describe('readNativeChatVisualFrameMessage', () => {
})
describe('readNativeChatVisualPong', () => {
it('reads the ping id on its own channel', () => {
const pong = { type: NATIVE_CHAT_VISUAL_PONG_TYPE, channel: 'c', id: 2 }
expect(readNativeChatVisualPong(pong, 'c')).toBe(2)
expect(nativeChatVisualPingMessage(2, 'c')).toEqual({
type: NATIVE_CHAT_VISUAL_PING_TYPE,
channel: 'c',
id: 2
})
})
it.each([
['another channel', { type: NATIVE_CHAT_VISUAL_PONG_TYPE, channel: 'x', id: 2 }],
['a ping', { type: NATIVE_CHAT_VISUAL_PING_TYPE, channel: 'c', id: 2 }],
@@ -210,14 +109,3 @@ describe('readNativeChatVisualPong', () => {
expect(readNativeChatVisualPong(data, 'c')).toBeNull()
})
})
it('keeps ordinary themed file guests scrollable without the inline containment shell', async () => {
const { nativeChatVisualGuestTheme } = await import('./native-chat-visual-shell')
const theme = nativeChatVisualGuestTheme({
colorScheme: 'dark',
tokens: { '--background': '#111' }
})
expect(theme.css).toContain('--background:#111')
expect(theme.css).not.toContain('scrollbar')
expect(theme.css).not.toContain('overflow:hidden')
})
@@ -1,11 +1,6 @@
import { describe, expect, it } from 'vitest'
import {
foldSubagentItems,
namedSubagents,
subagentFallbackText
} from './native-chat-subagent-items'
import { foldSubagentItems, subagentFallbackText } from './native-chat-subagent-items'
import { projectNativeChatTranscriptMessages } from './native-chat-transcript-projection'
import { formatWorkerTranscriptMessage } from '../../worker-transcript-text'
import type {
NativeChatMessage,
NativeChatSubagentBlock,
@@ -85,37 +80,6 @@ describe('a turn’s subagents drawn as one row', () => {
expect(new Set(rows.map((row) => groupOf(row).groupId)).size).toBe(3)
})
it('tells same-named siblings apart without taking a name a sibling already has', () => {
const rows = foldSubagentItems([
item(1, subagent('a', { label: 'Audit' })),
item(2, subagent('b', { label: 'Audit' })),
item(3, subagent('c', { label: 'Audit 2' })),
item(4, subagent('d', { label: 'Audit' })),
item(5, subagent('e', { label: 'Audit 4' }))
])
expect(groupOf(rows[0]).agents.map((agent) => agent.label)).toEqual([
'Audit',
'Audit 3',
'Audit 2',
'Audit 5',
'Audit 4'
])
})
it('hands back the same row while no sibling changed, and a new one when one did', () => {
const a = item(1, subagent('a'))
const b = item(2, subagent('b'))
const first = foldSubagentItems([a, b])[0]
expect(foldSubagentItems([a, b])[0]).toBe(first)
const settled = item(2, subagent('b', { state: 'completed' }))
const next = foldSubagentItems([a, settled])[0]
expect(next).not.toBe(first)
// The sibling that did not change keeps its line, so its memoized row stays put.
expect(groupOf(next).agents[0]).toBe(groupOf(first).agents[0])
})
it('leaves an older host’s roster row, and rows that are not one subagent, as they came', () => {
const roster: NativeChatMessage = {
...text(1, 'system', 'Ran 1 subagent'),
@@ -154,41 +118,3 @@ describe('a turn’s subagents drawn as one row', () => {
])
})
})
describe('the sentence an older client draws', () => {
it('claims no liveness it cannot keep, and names an adverse outcome', () => {
expect(subagentFallbackText(subagent('a', { label: 'Audit' }))).toBe(
'Kicked off subagent Audit'
)
expect(subagentFallbackText(subagent('a', { label: 'Audit', state: 'completed' }))).toBe(
'Ran subagent Audit'
)
expect(subagentFallbackText(subagent('a', { label: 'Audit', state: 'failed' }))).toBe(
'Ran subagent Audit (failed)'
)
})
it('is what a text surface prints, once, and the block alone still prints it', () => {
const own = item(1, subagent('a', { label: 'Audit' }))
expect(formatWorkerTranscriptMessage(own)).toBe('[system] Kicked off subagent Audit')
expect(formatWorkerTranscriptMessage({ ...own, blocks: own.blocks.slice(1) })).toBe(
'[system] [subagents] Kicked off subagent Audit'
)
})
})
describe('the subagents a row names', () => {
it('reads an older host’s roster and a subagent’s own row alike', () => {
expect(
namedSubagents(subagent('a', { run: 'r', outcomeBasis: 'stop-acknowledged', tokens: 3 }))
).toEqual([{ id: 'a', label: 'a', state: 'working', tokens: 3 }])
expect(
namedSubagents({
type: 'subagent-group',
groupId: 'g',
agents: [{ id: 'b', label: 'b', state: 'idle' }]
})
).toEqual([{ id: 'b', label: 'b', state: 'idle' }])
expect(namedSubagents({ type: 'text', text: 'x' })).toEqual([])
})
})
@@ -21,15 +21,6 @@ function row(overrides: Partial<NativeChatTurnFoldRow> = {}): NativeChatTurnFold
}
}
/** The turn in the reported session: a prompt, narration, work, then the answer. */
const TURN: NativeChatTurnFoldRow[] = [
row({ role: 'user' }),
row(),
row({ rendersProse: false }),
row(),
row()
]
/** An error-toned system row: the agent or host reporting the turn failed. */
const FAILURE = row({ role: 'system', reportsFailure: true })
@@ -105,57 +96,8 @@ describe('rendered plans in collapsed turns', () => {
})
})
describe('nativeChatTurnAnswerRows', () => {
it('ignores reasoning and system rows, which are never the agent answering', () => {
const rows = [row({ role: 'user' }), row(), row({ role: 'reasoning' }), row({ role: 'system' })]
expect([...nativeChatTurnAnswerRows(rows)]).toEqual([1])
})
})
describe('nativeChatTurnFold', () => {
it('folds a settled turn to its answer', () => {
const { foldedRows } = nativeChatTurnFold({
rows: TURN,
settledTurnKeys: SETTLED,
expandedTurnKeys: NONE
})
expect([...foldedRows].sort()).toEqual([1, 2, 3])
})
// A stored-only provider event draws nothing: a disclosure over it would open onto nothing.
it('offers no disclosure for a turn whose only other row draws nothing', () => {
const { foldedRows, foldableTurnKeys } = nativeChatTurnFold({
rows: [
row({ role: 'user' }),
row({ role: 'system', rendersProse: false, draws: false }),
row()
],
settledTurnKeys: SETTLED,
expandedTurnKeys: NONE
})
expect(foldedRows.size).toBe(0)
expect(foldableTurnKeys.size).toBe(0)
})
it('folds nothing while the turn is still running', () => {
const { foldedRows, foldableTurnKeys } = nativeChatTurnFold({
rows: TURN,
settledTurnKeys: NONE,
expandedTurnKeys: NONE
})
expect(foldedRows.size).toBe(0)
expect(foldableTurnKeys.size).toBe(0)
})
it('reveals every row of a turn the reader opened, and still reports it foldable', () => {
const { foldedRows, foldableTurnKeys } = nativeChatTurnFold({
rows: TURN,
settledTurnKeys: SETTLED,
expandedTurnKeys: SETTLED
})
expect(foldedRows.size).toBe(0)
expect([...foldableTurnKeys]).toEqual(['turn-1'])
})
it('keeps a spawn roster or background task out of the fold', () => {
const rows = [row({ role: 'user' }), row(), row({ outlivesTurn: true }), row()]
@@ -168,28 +110,6 @@ describe('nativeChatTurnFold', () => {
expect(foldedRows.has(1)).toBe(true)
})
it('folds a prose-less turn whole, so its commands do not return to the transcript', () => {
const rows = [row({ role: 'user' }), row({ rendersProse: false }), row({ rendersProse: false })]
const { foldedRows, foldableTurnKeys } = nativeChatTurnFold({
rows,
settledTurnKeys: SETTLED,
expandedTurnKeys: NONE
})
expect([...foldedRows].sort()).toEqual([1, 2])
expect([...foldableTurnKeys]).toEqual(['turn-1'])
})
it('offers no disclosure on a turn that is nothing but its answer', () => {
const rows = [row({ role: 'user' }), row()]
const { foldedRows, foldableTurnKeys } = nativeChatTurnFold({
rows,
settledTurnKeys: SETTLED,
expandedTurnKeys: NONE
})
expect(foldedRows.size).toBe(0)
expect(foldableTurnKeys.size).toBe(0)
})
it('folds each settled turn to its own answer and leaves a running turn alone', () => {
const rows = [
row({ turnKey: 'turn-1', role: 'user' }),
@@ -207,16 +127,6 @@ describe('nativeChatTurnFold', () => {
expect([...foldedRows]).toEqual([1])
})
it('leaves rows before the first prompt alone', () => {
const rows = [row({ turnKey: undefined }), row({ turnKey: undefined })]
const { foldedRows } = nativeChatTurnFold({
rows,
settledTurnKeys: SETTLED,
expandedTurnKeys: NONE
})
expect(foldedRows.size).toBe(0)
})
it('ends a failed turn on its error, folding the work and prose before it', () => {
const rows = [row({ role: 'user' }), row(), row({ rendersProse: false }), FAILURE]
const { foldedRows, foldableTurnKeys } = nativeChatTurnFold({
@@ -238,17 +148,6 @@ describe('nativeChatTurnFold', () => {
expect([...foldedRows].sort()).toEqual([1, 2])
})
it('shows a turn that is nothing but its error as that error, with no disclosure', () => {
const rows = [row({ role: 'user' }), FAILURE]
const { foldedRows, foldableTurnKeys } = nativeChatTurnFold({
rows,
settledTurnKeys: SETTLED,
expandedTurnKeys: NONE
})
expect(foldedRows.size).toBe(0)
expect(foldableTurnKeys.size).toBe(0)
})
it('never folds a compaction report, and keeps the last prose as the answer', () => {
const rows = [
row({ role: 'user' }),
-71
View File
@@ -1,71 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
NESTED_WORKER_MAX_DEPTH_DEFAULT,
migrateNestedWorkerMaxDepthDefault,
nestedWorkerDepthExceededMessage,
resolveNestedWorkerMaxDepth
} from './nested-worker-depth'
describe('resolveNestedWorkerMaxDepth', () => {
it('defaults to 4 when unset', () => {
expect(resolveNestedWorkerMaxDepth(undefined)).toBe(4)
expect(resolveNestedWorkerMaxDepth(null)).toBe(4)
expect(resolveNestedWorkerMaxDepth({})).toBe(4)
})
it('accepts whole numbers at or above 1', () => {
expect(resolveNestedWorkerMaxDepth({ nestedWorkerMaxDepth: 1 })).toBe(1)
expect(resolveNestedWorkerMaxDepth({ nestedWorkerMaxDepth: 3 })).toBe(3)
})
// Malformed settings must fall back to the cap, never disable nesting limits.
it.each([
['a numeric string', '2'],
['a boolean', true],
['zero', 0],
['negative', -1],
['fractional', 1.5],
['NaN', Number.NaN],
['Infinity', Number.POSITIVE_INFINITY],
['unsafe integer', Number.MAX_SAFE_INTEGER + 1],
['null', null]
])('falls back to the default for %s', (_label, value) => {
expect(resolveNestedWorkerMaxDepth({ nestedWorkerMaxDepth: value as unknown as number })).toBe(
NESTED_WORKER_MAX_DEPTH_DEFAULT
)
})
})
describe('migrateNestedWorkerMaxDepthDefault', () => {
it('moves the old default of 1 to 4 once', () => {
expect(migrateNestedWorkerMaxDepthDefault({ nestedWorkerMaxDepth: 1 })).toEqual({
nestedWorkerMaxDepth: 4,
nestedWorkerMaxDepthDefaultedToFour: true
})
expect(migrateNestedWorkerMaxDepthDefault(undefined).nestedWorkerMaxDepth).toBe(4)
})
it('keeps a deliberate value above 1', () => {
expect(
migrateNestedWorkerMaxDepthDefault({ nestedWorkerMaxDepth: 2 }).nestedWorkerMaxDepth
).toBe(2)
})
it('keeps 1 once the guard is stamped', () => {
expect(
migrateNestedWorkerMaxDepthDefault({
nestedWorkerMaxDepth: 1,
nestedWorkerMaxDepthDefaultedToFour: true
}).nestedWorkerMaxDepth
).toBe(1)
})
})
describe('depth-exceeded message', () => {
it('names the cap and the refused depth, and says to finish here', () => {
const message = nestedWorkerDepthExceededMessage(5, 4)
expect(message).toContain('at most 4 levels deep')
expect(message).toContain('(depth 5)')
expect(message).toContain('Finish this task here')
})
})
-208
View File
@@ -1,114 +1,8 @@
import { describe, it, expect } from 'vitest'
import { parseManualNetworkAddress } from './manual-address'
import { PAIRING_ENDPOINT_MAX_CHARACTERS } from '../remote-runtime/pairing/mobile-pairing-protocol-limits'
describe('parseManualNetworkAddress', () => {
describe('IPv4', () => {
it('accepts canonical IPv4', () => {
expect(parseManualNetworkAddress('192.168.1.24')).toEqual({
ok: true,
address: '192.168.1.24'
})
expect(parseManualNetworkAddress('100.64.1.20')).toEqual({
ok: true,
address: '100.64.1.20'
})
})
it('accepts the upper boundary and rejects the wildcard address', () => {
expect(parseManualNetworkAddress('0.0.0.0').ok).toBe(false)
expect(parseManualNetworkAddress('255.255.255.255').ok).toBe(true)
})
it('accepts an IPv4 address with a port suffix', () => {
expect(parseManualNetworkAddress('192.168.1.24:8080')).toEqual({
ok: true,
address: '192.168.1.24:8080'
})
})
it('rejects malformed IPv4', () => {
for (const bad of ['', ' ', '1.2.3', '1.2.3.4.5', '256.0.0.1']) {
expect(parseManualNetworkAddress(bad)).toEqual({
ok: false,
error: 'Enter an IPv4/IPv6 address, hostname, or HTTP(S)/WebSocket URL'
})
}
})
it('rejects leading zeros in octets', () => {
expect(parseManualNetworkAddress('01.02.03.04')).toEqual({
ok: false,
error: 'Enter an IPv4/IPv6 address, hostname, or HTTP(S)/WebSocket URL'
})
})
})
describe('Tailscale MagicDNS hostname', () => {
it('accepts short MagicDNS names', () => {
expect(parseManualNetworkAddress('my-mac.ts.net')).toEqual({
ok: true,
address: 'my-mac.ts.net'
})
})
it('accepts tailnet-qualified MagicDNS names', () => {
expect(parseManualNetworkAddress('my-mac.tail-abcd.ts.net')).toEqual({
ok: true,
address: 'my-mac.tail-abcd.ts.net'
})
expect(parseManualNetworkAddress('a.b.c.d.ts.net').ok).toBe(true)
})
it('is case-insensitive', () => {
expect(parseManualNetworkAddress('MY-MAC.TS.NET').ok).toBe(true)
})
it('rejects a MagicDNS-shaped hostname with a malformed label', () => {
// RFC 1123 forbids leading/trailing label hyphens even under ts.net.
expect(parseManualNetworkAddress('-foo.ts.net').ok).toBe(false)
})
})
describe('arbitrary hostnames (DDNS / self-hosted domains)', () => {
it('accepts a bare single-label hostname', () => {
expect(parseManualNetworkAddress('my-nas')).toEqual({
ok: true,
address: 'my-nas'
})
})
it('accepts a multi-label domain, e.g. a DDNS hostname', () => {
expect(parseManualNetworkAddress('home.example.com')).toEqual({
ok: true,
address: 'home.example.com'
})
expect(parseManualNetworkAddress('my-house.ddns.net').ok).toBe(true)
})
it('accepts a hostname with a port suffix', () => {
expect(parseManualNetworkAddress('home.example.com:8443')).toEqual({
ok: true,
address: 'home.example.com:8443'
})
})
it('is case-insensitive', () => {
expect(parseManualNetworkAddress('HOME.EXAMPLE.COM').ok).toBe(true)
})
it('rejects labels with leading or trailing hyphens', () => {
for (const bad of ['-foo.example.com', 'foo-.example.com', 'foo..com']) {
expect(parseManualNetworkAddress(bad).ok).toBe(false)
}
})
it('rejects hostnames with characters outside the RFC 1123 grammar', () => {
for (const bad of ['my_host.example.com', 'my host.example.com', 'example.com/path']) {
expect(parseManualNetworkAddress(bad).ok).toBe(false)
}
})
it('rejects a host whose last label is numeric as an ambiguous IPv4', () => {
// WHATWG URLs reinterpret numeric endings as IPv4 (123 → 0.0.0.123) or fail; accepting them would validate a host main dials differently.
const bad = [
@@ -126,62 +20,16 @@ describe('parseManualNetworkAddress', () => {
expect(parseManualNetworkAddress(value).ok).toBe(false)
}
})
it('still accepts a hostname whose last label merely contains digits', () => {
// Only wholly numeric/hex endings are ambiguous; nas1 remains a hostname.
expect(parseManualNetworkAddress('host2.example.com').ok).toBe(true)
expect(parseManualNetworkAddress('my-nas1').ok).toBe(true)
})
})
describe('port suffix', () => {
it('accepts boundary port values', () => {
expect(parseManualNetworkAddress('example.com:1').ok).toBe(true)
expect(parseManualNetworkAddress('example.com:65535').ok).toBe(true)
})
it('rejects out-of-range ports', () => {
expect(parseManualNetworkAddress('example.com:0').ok).toBe(false)
expect(parseManualNetworkAddress('example.com:65536').ok).toBe(false)
expect(parseManualNetworkAddress('example.com:99999').ok).toBe(false)
})
it('rejects a non-numeric or empty port', () => {
expect(parseManualNetworkAddress('example.com:abc').ok).toBe(false)
expect(parseManualNetworkAddress('example.com:').ok).toBe(false)
})
it('rejects a leading-zero (non-canonical, unbounded-length) port', () => {
expect(parseManualNetworkAddress('example.com:0080').ok).toBe(false)
expect(parseManualNetworkAddress(`example.com:${'0'.repeat(1000)}8080`).ok).toBe(false)
})
it('rejects malformed addresses with more than one colon', () => {
expect(parseManualNetworkAddress('example.com:80:90').ok).toBe(false)
})
})
describe('IPv6', () => {
it('accepts bare and bracketed IPv6 addresses with optional ports', () => {
expect(parseManualNetworkAddress('::1')).toEqual({ ok: true, address: '::1' })
expect(parseManualNetworkAddress('2001:db8::4')).toEqual({
ok: true,
address: '2001:db8::4'
})
expect(parseManualNetworkAddress('0:0:0:0:0:0:0:1')).toEqual({
ok: true,
address: '0:0:0:0:0:0:0:1'
})
expect(parseManualNetworkAddress('[2001:db8::4]:7443')).toEqual({
ok: true,
address: '[2001:db8::4]:7443'
})
expect(parseManualNetworkAddress('[0:0:0:0:0:0:0:1]:7443')).toEqual({
ok: true,
address: '[0:0:0:0:0:0:0:1]:7443'
})
})
it('rejects wildcard, malformed, and invalid-port IPv6 addresses', () => {
for (const bad of [
'::',
@@ -197,39 +45,9 @@ describe('parseManualNetworkAddress', () => {
expect(parseManualNetworkAddress(bad).ok).toBe(false)
}
})
it('accepts reachable IPv4-mapped and native IPv6 controls', () => {
expect(parseManualNetworkAddress('::ffff:192.168.1.24').ok).toBe(true)
expect(parseManualNetworkAddress('[::ffff:192.168.1.24]:8080').ok).toBe(true)
expect(parseManualNetworkAddress('2001:db8::24').ok).toBe(true)
})
})
describe('full URL', () => {
it('accepts ws:// and wss:// URLs with optional ports', () => {
expect(parseManualNetworkAddress('wss://example.com')).toEqual({
ok: true,
address: 'wss://example.com'
})
expect(parseManualNetworkAddress('wss://example.com:443')).toEqual({
ok: true,
address: 'wss://example.com:443'
})
expect(parseManualNetworkAddress('ws://example.com:8080')).toEqual({
ok: true,
address: 'ws://example.com:8080'
})
})
it('accepts reverse-proxy paths and query strings', () => {
expect(parseManualNetworkAddress('wss://example.com/orca?route=runtime').ok).toBe(true)
})
it('accepts HTTP URLs that the main process normalizes to WebSocket URLs', () => {
expect(parseManualNetworkAddress('http://example.com/orca').ok).toBe(true)
expect(parseManualNetworkAddress('https://example.com/orca?route=runtime').ok).toBe(true)
})
it('rejects URLs the pairing endpoint cannot advertise', () => {
for (const bad of [
'ftp://example.com',
@@ -246,30 +64,4 @@ describe('parseManualNetworkAddress', () => {
}
})
})
describe('length and whitespace', () => {
it('matches the shared pairing endpoint length boundary', () => {
const prefix = 'wss://example.com/'
const atLimit = `${prefix}${'a'.repeat(PAIRING_ENDPOINT_MAX_CHARACTERS - prefix.length)}`
const aboveLimit = `${atLimit}a`
expect(atLimit).toHaveLength(PAIRING_ENDPOINT_MAX_CHARACTERS)
expect(parseManualNetworkAddress(atLimit)).toEqual({ ok: true, address: atLimit })
expect(aboveLimit).toHaveLength(PAIRING_ENDPOINT_MAX_CHARACTERS + 1)
expect(parseManualNetworkAddress(aboveLimit).ok).toBe(false)
})
it('rejects inputs longer than 253 chars', () => {
const long = `${'a'.repeat(250)}.ts.net`
expect(long.length).toBeGreaterThan(253)
expect(parseManualNetworkAddress(long).ok).toBe(false)
})
it('trims leading and trailing whitespace before validating', () => {
expect(parseManualNetworkAddress(' 192.168.1.24 ')).toEqual({
ok: true,
address: '192.168.1.24'
})
})
})
})
@@ -1,36 +0,0 @@
import { describe, it, expect } from 'vitest'
import { PAIRING_ENDPOINT_MAX_CHARACTERS } from '../remote-runtime/pairing/mobile-pairing-protocol-limits'
import { parseServerShareAddress } from './server-share-address'
describe('parseServerShareAddress', () => {
it('accepts a bare hostname or IP', () => {
expect(parseServerShareAddress('my-host')).toEqual({ ok: true, value: 'my-host' })
expect(parseServerShareAddress('my-mac.tail-abcd.ts.net')).toEqual({
ok: true,
value: 'my-mac.tail-abcd.ts.net'
})
expect(parseServerShareAddress('192.168.1.50')).toEqual({
ok: true,
value: '192.168.1.50'
})
})
it('rejects empty, whitespace-containing, and malformed input', () => {
for (const bad of [
'',
' ',
'has space',
'http://my-host',
'wss://',
':6768',
'0.0.0.0',
'::',
'my-host:0',
'999.999.999.999',
'wss://user:password@my-host'
]) {
expect(parseServerShareAddress(bad).ok).toBe(false)
}
expect(parseServerShareAddress('a'.repeat(PAIRING_ENDPOINT_MAX_CHARACTERS + 1)).ok).toBe(false)
})
})
@@ -4,35 +4,6 @@ import { openCodeVariantRetryPlan } from './opencode-generation-command'
const rejection = 'Unrecognized flag: --variant in command opencode run'
describe('OpenCode generation commands', () => {
it.each(['opencode', 'opencode.exe', 'opencode.cmd', 'opencode2'])(
'keeps run before launch flags for %s',
(binary) => {
const result = planCommitMessageGeneration(
{ agentId: 'opencode', model: 'default', agentCommandOverride: `${binary} --auto` },
'PROMPT'
)
expect(result).toMatchObject({
ok: true,
plan: {
args: ['run', '--auto', '--agent', 'build', '--format', 'json'],
stdinPayload: 'PROMPT',
outputFormat: 'opencode-json'
}
})
}
)
it('keeps explicit formatted output recipes on their requested output format', () => {
const result = planCommitMessageGeneration(
{ agentId: 'opencode', model: 'default', agentArgs: '--format default' },
'PROMPT'
)
if (!result.ok) {
throw new Error(result.error)
}
expect(result.plan.outputFormat).toBeUndefined()
})
it('retries only the precise v2 argv rejection, retaining the prompt and selected model', () => {
const result = planCommitMessageGeneration(
{ agentId: 'opencode', model: 'fixture/chat', thinkingLevel: 'high' },
@@ -48,24 +19,6 @@ describe('OpenCode generation commands', () => {
expect(openCodeVariantRetryPlan(result.plan, 'provider rejected the request')).toBeNull()
expect(openCodeVariantRetryPlan(result.plan, 'Unrecognized flag: --other')).toBeNull()
})
it.each([
'--model=fixture/chat --variant=high',
'-mfixture/chat --variant=high',
'--variant high --model=fixture/chat',
'--variant=high -m fixture/chat'
])('retries the chosen recipe model and variant in %s', (agentArgs) => {
const result = planCommitMessageGeneration(
{ agentId: 'opencode', model: 'other/model', thinkingLevel: 'low', agentArgs },
'PROMPT'
)
if (!result.ok) {
throw new Error(result.error)
}
const retry = openCodeVariantRetryPlan(result.plan, rejection)
expect(retry?.args.join(' ')).toContain('fixture/chat#high')
expect(retry?.args.join(' ')).not.toContain('--variant')
expect(retry?.stdinPayload).toBe('PROMPT')
})
it('does not treat prompt tokens after the terminator as options', () => {
expect(
+1 -30
View File
@@ -1,25 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
bindHostIsNetworkExposed,
ORCAD_LOOPBACK_BIND_HOST,
OrcadBindAddressError,
resolveOrcadBindHost
} from './orcad-bind-address'
import { OrcadBindAddressError, resolveOrcadBindHost } from './orcad-bind-address'
describe('resolveOrcadBindHost', () => {
it('defaults to loopback when the operator asked for nothing', () => {
expect(resolveOrcadBindHost()).toBe(ORCAD_LOOPBACK_BIND_HOST)
expect(ORCAD_LOOPBACK_BIND_HOST).toBe('127.0.0.1')
})
it('accepts literal IPv4 and IPv6 addresses, including explicit wide binds', () => {
expect(resolveOrcadBindHost('0.0.0.0')).toBe('0.0.0.0')
expect(resolveOrcadBindHost('10.1.2.3')).toBe('10.1.2.3')
expect(resolveOrcadBindHost('::1')).toBe('::1')
expect(resolveOrcadBindHost('localhost')).toBe('127.0.0.1')
expect(resolveOrcadBindHost(' 127.0.0.1 ')).toBe('127.0.0.1')
})
it('refuses hostnames, because DNS would decide which interface got bound', () => {
expect(() => resolveOrcadBindHost('internal.example')).toThrow(OrcadBindAddressError)
expect(() => resolveOrcadBindHost('')).toThrow(OrcadBindAddressError)
@@ -29,14 +11,3 @@ describe('resolveOrcadBindHost', () => {
)
})
})
describe('bindHostIsNetworkExposed', () => {
it('separates local-only addresses from network-reachable ones', () => {
expect(bindHostIsNetworkExposed('127.0.0.1')).toBe(false)
expect(bindHostIsNetworkExposed('127.5.5.5')).toBe(false)
expect(bindHostIsNetworkExposed('::1')).toBe(false)
expect(bindHostIsNetworkExposed('0.0.0.0')).toBe(true)
expect(bindHostIsNetworkExposed('::')).toBe(true)
expect(bindHostIsNetworkExposed('10.1.2.3')).toBe(true)
})
})
@@ -110,14 +110,6 @@ describe('orcad migration manifest contract', () => {
})
// Retired draft-blob field from development builds; no released desktop sent it.
it('accepts and drops the retired editorDraftsBlob declaration', () => {
const dormantState = {
...dormantLineage('folder:folder-1', 'worktree:repo-1::/srv/repo-1'),
editorDraftsBlob: { sha256: digest, byteLength: 4, entries: [] }
}
const parsed = parseOrcadMigrationManifest(manifest({ dormantState }))
expect(parsed.payload.dormantState).not.toHaveProperty('editorDraftsBlob')
})
it('rejects a malformed workspace key rather than treating it as a worktree id', () => {
expect(() =>
@@ -6,7 +6,6 @@ import {
} from '../workspace-session/client-hosted-browser-page-record'
import type { BrowserWorkspace } from '../browser-workspace-types'
import { assertOrcadMigrationDormantWorkspaceSessionReferences } from './orcad-migration-dormant-session-validation'
import { parseOrcadMigrationClientState } from './orcad-migration-client-state'
import type { WorkspaceSessionState } from '../workspace-session/workspace-session-state-types'
const OWNER = 'repo-1::/srv/worktree'
@@ -62,10 +61,6 @@ function sessionWithPages(pages: ReturnType<typeof clientPage>[]): WorkspaceSess
}
describe('dormant client-hosted browser page migration validation', () => {
it('accepts held pages whose workspace id belongs to the transferred browser workspace', () => {
expect(() => validate(sessionWithPages([clientPage('page-1')]))).not.toThrow()
})
it('rejects a page that names a browser workspace outside its owner', () => {
expect(() => validate(sessionWithPages([clientPage('page-1', 'browser-missing')]))).toThrow(
'orcad_migration_dormant_workspace_session_client_page_scope_invalid'
@@ -78,52 +73,6 @@ describe('dormant client-hosted browser page migration validation', () => {
)
})
it('accepts scoped client-owned close intents alongside transferred pages', () => {
const session = sessionWithPages([clientPage('page-1')])
session.clientHostedBrowserCloseIntentsByEnvironment = {
'old-environment': [{ browserPageId: 'page-1', worktreeId: OWNER, closedAt: 10 }]
}
expect(() => validate(session)).not.toThrow()
})
it('parses migration close-intent envelopes with source environment identity', () => {
expect(
parseOrcadMigrationClientState({
clientHostedBrowserCloseIntents: [
{
sourceEnvironmentId: 'old-environment',
browserPageId: 'page-1',
worktreeId: OWNER,
closedAt: 10
}
]
})
).toEqual({
clientHostedBrowserCloseIntents: [
{
sourceEnvironmentId: 'old-environment',
browserPageId: 'page-1',
worktreeId: OWNER,
closedAt: 10
}
]
})
})
it('allows global browser URL history to remain client-owned', () => {
const session = sessionWithPages([])
session.browserUrlHistory = [
{
url: 'https://example.test/',
normalizedUrl: 'https://example.test/',
title: 'Example',
lastVisitedAt: 10,
visitCount: 1
}
]
expect(() => validate(session)).not.toThrow()
})
it('accepts markdown visibility only when it names a persisted open file', () => {
const session = sessionWithPages([])
session.openFilesByWorktree = {
@@ -2,32 +2,15 @@ import { describe, expect, it } from 'vitest'
import { getDefaultWorkspaceSession } from '../constants'
import {
assertOrcadMigrationScrollbackReferences,
MAX_ORCAD_MIGRATION_SCROLLBACK_SNAPSHOTS,
OrcadMigrationSnapshotChunkRequestSchema,
parseOrcadMigrationSnapshotChunkResult,
parseOrcadMigrationTerminalScrollbackSnapshots,
type OrcadMigrationSnapshotChunkRequest,
type OrcadMigrationTerminalScrollbackSnapshot
} from './orcad-migration-scrollback'
import { TERMINAL_SCROLLBACK_STORE_BYTE_LIMIT } from '../terminal/replay/terminal-scrollback-limits'
const DIGEST = 'a'.repeat(64)
describe('orcad migration scrollback wire validation', () => {
it('accepts only canonical non-empty bounded base64 chunks', () => {
const request = chunkRequest('YQ==')
expect(OrcadMigrationSnapshotChunkRequestSchema.safeParse(request).success).toBe(true)
expect(
OrcadMigrationSnapshotChunkRequestSchema.safeParse({ ...request, futureField: 1 }).success,
'a newer client field'
).toBe(true)
for (const bytesBase64 of ['', 'YQ', 'YQ=', 'A===', '***=']) {
expect(
OrcadMigrationSnapshotChunkRequestSchema.safeParse({ ...request, bytesBase64 }).success
).toBe(false)
}
})
it('requires an acknowledgment for exactly the decoded chunk length', () => {
const request = chunkRequest(Buffer.from('abc').toString('base64'), 7)
expect(
@@ -69,29 +52,6 @@ describe('orcad migration scrollback wire validation', () => {
).toThrow('orcad_migration_dormant_scrollback_snapshot_leaf_duplicate')
})
it('enforces descriptor count, per-file, and aggregate limits', () => {
expect(() =>
parseOrcadMigrationTerminalScrollbackSnapshots(
Array.from({ length: MAX_ORCAD_MIGRATION_SCROLLBACK_SNAPSHOTS + 1 }, (_, index) =>
descriptor(index + 1)
)
)
).toThrow('orcad_migration_dormant_scrollback_snapshots_too_many')
expect(() =>
parseOrcadMigrationTerminalScrollbackSnapshots([
{ ...descriptor(1), byteLength: TERMINAL_SCROLLBACK_STORE_BYTE_LIMIT + 1 }
])
).toThrow('orcad_migration_dormant_scrollback_size_invalid')
expect(() =>
parseOrcadMigrationTerminalScrollbackSnapshots(
Array.from({ length: 52 }, (_, index) => ({
...descriptor(index + 1),
byteLength: TERMINAL_SCROLLBACK_STORE_BYTE_LIMIT
}))
)
).toThrow('orcad_migration_dormant_scrollback_snapshots_too_large')
})
it('requires one exact descriptor for every published session ref', () => {
const session = getDefaultWorkspaceSession()
const first = descriptor(1)
-10
View File
@@ -6,11 +6,6 @@ const CANONICAL = makePaneKey('tab-1', '11111111-1111-4111-8111-111111111111')
const REMINTED = '$$MFRGGZDFMY:L$$'
describe('opaque reminted pane keys', () => {
it('accepts the exact $$<base32>:L$$ remint form', () => {
expect(isOpaqueRemintedPaneKey(REMINTED)).toBe(true)
expect(canRegisterPaneKeyAlias(REMINTED, CANONICAL)).toBe(true)
})
it('rejects canonical keys, numeric keys with a different tab, and unmatched tokens', () => {
expect(isOpaqueRemintedPaneKey(CANONICAL)).toBe(false)
expect(canRegisterPaneKeyAlias(CANONICAL, CANONICAL)).toBe(false)
@@ -21,9 +16,4 @@ describe('opaque reminted pane keys', () => {
expect(canRegisterPaneKeyAlias(REMINTED, '$$ONXW2ZJAON:L$$')).toBe(false)
expect(canRegisterPaneKeyAlias(REMINTED, 'tab-1:0')).toBe(false)
})
it('rejects oversized canonical destinations', () => {
const oversizedPane = `${'x'.repeat(180)}:11111111-1111-4111-8111-111111111111`
expect(canRegisterPaneKeyAlias(REMINTED, oversizedPane)).toBe(false)
})
})
@@ -1,10 +1,6 @@
import { describe, expect, it } from 'vitest'
import {
checkPluginLanguagePackForAuthoring,
parsePluginLanguagePackArtifact,
PLUGIN_LANGUAGE_CATALOG_MAX_DEPTH,
PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES,
validatePluginLanguagePackCatalog,
pluginLanguageResourceId
} from './plugin-language-pack-artifact'
import { englishSourceLookup } from '../languages/plugin-translation-string-policy'
@@ -21,24 +17,6 @@ describe('plugin language-pack artifacts', () => {
expect(first).not.toBe(second)
})
it('accepts nested string catalogs and reports their bounded entry count', () => {
expect(
parsePluginLanguagePackArtifact(
JSON.stringify({
settings: { appearance: { title: 'Aparência' } },
common: { save: 'Salvar' }
})
)
).toEqual({
ok: true,
catalog: {
settings: { appearance: { title: 'Aparência' } },
common: { save: 'Salvar' }
},
entries: 5
})
})
it('drops protected copy instead of rejecting the pack when loading tolerantly', () => {
const raw = JSON.stringify({
common: { save: 'Sávé' },
@@ -52,40 +30,6 @@ describe('plugin language-pack artifacts', () => {
})
})
it('counts protected copy for authors instead of rejecting, still rejecting other problems', () => {
expect(
checkPluginLanguagePackForAuthoring(
JSON.stringify({
common: { save: 'Sávé' },
components: { 'native-chat': { approval: { allow: 'Álló', deny: 'Déný' } } },
browser: { loadFailure: { proceedUnsafe: 'Continuar' } }
})
)
).toEqual({ ok: true, ignoredProtectedStrings: 3 })
expect(
checkPluginLanguagePackForAuthoring(
JSON.stringify({
common: { save: 'a\u202esave' },
components: { 'native-chat': { approval: { allow: 'Álló' } } }
})
)
).toMatchObject({ ok: false })
})
it.each([
['PluginsSettingsSection', 'title', 'Плагины'],
['PluginMarketplaceBrowser', 'refresh', 'Обновить'],
['PluginDevelopmentSection', 'title', 'Разработка']
])('lets a language pack translate %s.%s, which asserts nothing', (component, key, value) => {
expect(
parsePluginLanguagePackArtifact(
JSON.stringify({
auto: { components: { settings: { [component]: { [key]: value } } } }
})
).ok
).toBe(true)
})
// Chrome exemptions must never include claims about plugin permissions; those are the protected prefix’s purpose.
it.each([
['description', 'Plugins are sandboxed and cannot read your files.'],
@@ -100,36 +44,6 @@ describe('plugin language-pack artifacts', () => {
).toMatchObject({ ok: false, error: expect.stringContaining('protected security copy') })
})
it('still refuses the development-plugin permission promise', () => {
expect(
parsePluginLanguagePackArtifact(
JSON.stringify({
auto: {
components: {
settings: {
PluginDevelopmentSection: { help: 'Dev plugins skip permission review.' }
}
}
}
})
)
).toMatchObject({ ok: false, error: expect.stringContaining('protected security copy') })
})
it('still refuses install failure copy that reports a trust event', () => {
expect(
parsePluginLanguagePackArtifact(
JSON.stringify({
auto: {
components: {
settings: { PluginMarketplaceBrowser: { installFailed: 'Installed successfully.' } }
}
}
})
)
).toMatchObject({ ok: false, error: expect.stringContaining('protected security copy') })
})
it.each([
[
'the Official badge',
@@ -211,17 +125,6 @@ describe('plugin language-pack artifacts', () => {
})
})
it('still translates neighbours of protected components', () => {
expect(
parsePluginLanguagePackArtifact(
JSON.stringify({
auto: { components: { pluginCatalog: { PluginListRow: { install: 'Instalar' } } } },
components: { 'native-chat': { composer: { send: 'Enviar' } } }
})
).ok
).toBe(true)
})
it.each([
['array leaf', { settings: { choices: ['one'] } }],
['numeric leaf', { settings: { count: 1 } }],
@@ -231,36 +134,6 @@ describe('plugin language-pack artifacts', () => {
expect(parsePluginLanguagePackArtifact(JSON.stringify(catalog)).ok).toBe(false)
})
it('rejects excessive nesting without recursive validation', () => {
let catalog: Record<string, unknown> = { value: 'deep' }
for (let index = 0; index <= PLUGIN_LANGUAGE_CATALOG_MAX_DEPTH; index += 1) {
catalog = { nested: catalog }
}
expect(parsePluginLanguagePackArtifact(JSON.stringify(catalog))).toMatchObject({
ok: false,
error: expect.stringContaining('depth')
})
})
it('accepts the entry limit and rejects one additional entry', () => {
const catalog = Object.fromEntries(
Array.from({ length: PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES }, (_, index) => [
`key${index}`,
'value'
])
)
expect(validatePluginLanguagePackCatalog(catalog)).toMatchObject({
ok: true,
entries: PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES
})
catalog.overflow = 'value'
expect(validatePluginLanguagePackCatalog(catalog)).toMatchObject({
ok: false,
error: expect.stringContaining(`${PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES} entries`)
})
})
describe('string limits', () => {
const english = englishSourceLookup({
common: { retry: 'Retry', detail: 'Line one\nLine two' },
@@ -269,21 +142,6 @@ describe('plugin language-pack artifacts', () => {
const parse = (catalog: unknown, strictness?: 'strict' | 'tolerant') =>
parsePluginLanguagePackArtifact(JSON.stringify(catalog), strictness, english)
it('caps a translation at 4x its English text with a 64-character floor', () => {
expect(parse({ common: { retry: 'x'.repeat(64) } }).ok).toBe(true)
expect(parse({ common: { retry: 'x'.repeat(65) } })).toMatchObject({
ok: false,
error: expect.stringContaining('common.retry exceeds 64 characters')
})
const detail = 'x'.repeat(4 * 'Line one\nLine two'.length + 1)
expect(parse({ common: { detail } }).ok).toBe(false)
})
it('measures plural forms English lacks against its _other form', () => {
expect(parse({ items: { count_few: 'x'.repeat(64) } }).ok).toBe(true)
expect(parse({ items: { count_few: 'x'.repeat(65) } }).ok).toBe(false)
})
it.each([
['a right-to-left override', 'Delete\u202Eetelpmoc'],
['an isolate', 'Retry\u2066x\u2069'],
@@ -1,17 +1,8 @@
import { describe, expect, it } from 'vitest'
import { legacyPluginCatalog } from '../../../../config/scripts/plugin-authoring/content-validation/export-legacy-plugin-catalog.mjs'
import {
PLUGIN_CATALOG_RELEASE_LIMIT,
pluginCatalogSchema,
pluginCatalogVersionError,
selectPluginCatalogRelease
} from './plugin-catalog'
import { pluginCatalogSchema, selectPluginCatalogRelease } from './plugin-catalog'
import { pluginCatalogReleaseSchema } from './plugin-catalog-release'
import {
PLUGIN_MARKETPLACE_ENTRY_LIMIT,
pluginMarketplaceListingSchema,
pluginMarketplaceSchema
} from './plugin-marketplace'
import { pluginMarketplaceSchema } from './plugin-marketplace'
function release(version: string, minimumOrca = '1.4.0') {
return pluginCatalogReleaseSchema.parse({
@@ -51,70 +42,6 @@ describe('plugin catalog', () => {
}
)
it('keeps what it can read and skips the rest', () => {
const parsed = pluginCatalogSchema.parse(
catalog(
[
entry('acme.one', { surprise: true, description: 'x'.repeat(5_000) }),
entry('acme.one', { categories: ['other'] }),
entry('acme.two', { releases: [{ ...release('2.0.0'), types: ['widget'] }] }),
entry('acme.three', {
categories: [
'themes',
'themes',
'Not A Slug',
...Array.from({ length: 30 }, (_, i) => `c${i}`)
],
releases: [{ broken: true }, release('1.1.0')]
}),
{ id: 5 }
],
{ future: { anything: true } }
)
)
expect(parsed.plugins.map((plugin) => plugin.id)).toEqual(['acme.one', 'acme.three'])
expect(parsed.plugins[0]).not.toHaveProperty('surprise')
expect(parsed.plugins[0]?.description).toBeUndefined()
expect(parsed.plugins[0]?.categories).toEqual(['themes'])
expect(parsed.plugins[1]?.categories).toHaveLength(16)
expect(parsed.plugins[1]?.categories.slice(0, 2)).toEqual(['themes', 'c0'])
expect(parsed.plugins[1]?.releases.map((r) => r.version)).toEqual(['1.1.0'])
})
it('reads the owner id without carrying it into the cached listing', () => {
const parsed = pluginCatalogSchema.parse(
catalog([entry('acme.one', { ownerId: 1_234 }), entry('acme.two', { ownerId: 'acme' })])
)
expect(parsed.plugins.map((plugin) => plugin.id)).toEqual(['acme.one', 'acme.two'])
expect(parsed.plugins[0]).not.toHaveProperty('ownerId')
// Why strict: older Orca reads the cached listing back with this shape after a downgrade.
const { name, owner, plugins } = parsed
expect(pluginMarketplaceListingSchema.safeParse({ name, owner, plugins }).success).toBe(true)
})
it('trims an oversized catalog and release list instead of refusing it', () => {
const many = Array.from({ length: PLUGIN_MARKETPLACE_ENTRY_LIMIT + 5 }, (_, index) =>
entry(`acme.p${index}`, {
releases: Array.from({ length: PLUGIN_CATALOG_RELEASE_LIMIT + 3 }, (_, r) =>
release(`1.0.${r}`)
)
})
)
const parsed = pluginCatalogSchema.parse(catalog(many))
expect(parsed.plugins).toHaveLength(PLUGIN_MARKETPLACE_ENTRY_LIMIT)
expect(parsed.plugins[0]?.releases).toHaveLength(PLUGIN_CATALOG_RELEASE_LIMIT)
})
it('refuses a newer catalog format with the fix, and requires the version', () => {
expect(pluginCatalogVersionError(catalog([], { catalogVersion: 3 }))).toBe(
'uses catalog format 3; update Orca to browse this catalog'
)
expect(pluginCatalogVersionError(catalog([]))).toBeNull()
expect(
pluginCatalogSchema.safeParse({ name: 'Acme', owner: 'acme', plugins: [] }).success
).toBe(false)
})
it('offers the newest release this Orca can install', () => {
const releases = [
release('3.0.0', '2.0.0'),
@@ -131,20 +58,6 @@ describe('plugin catalog', () => {
).toBe('8.0.0')
})
it('exports a frozen v1 file that old clients accept', () => {
const parsed = pluginCatalogSchema.parse(
catalog([entry('acme.one', { description: 'One theme.' }), entry('acme.two')])
)
const legacy = legacyPluginCatalog({ ...parsed, plugins: parsed.plugins })
expect(pluginMarketplaceSchema.safeParse(legacy).success).toBe(true)
expect(legacy.plugins[0]).toEqual({
id: 'acme.one',
source: { kind: 'git', url: 'https://github.com/acme/acme.one.git', ref: 'main' },
description: 'One theme.',
categories: ['themes']
})
})
it('lists plugins from directories of one repository and refuses unsafe directories', () => {
const at = (path: string, index: number) =>
entry(`acme.p${index}`, {
@@ -4,43 +4,6 @@ import { classifyPluginGitAccessFailure } from './plugin-git-access-failure'
const HOST = 'the Orca host (build-box)'
describe('classifyPluginGitAccessFailure', () => {
it('explains a private GitHub repository over HTTPS with gh on the named host', () => {
const failure = classifyPluginGitAccessFailure({
error:
"Cloning into 'repository'...\nremote: Repository not found.\nfatal: repository 'https://github.com/acme/private-notes/' not found",
url: 'https://github.com/acme/private-notes',
executionHost: HOST
})
expect(failure).toMatchObject({
transport: 'https',
gitHost: 'github.com',
executionHost: HOST
})
expect(failure?.nextSteps[0]).toBe(
`On ${HOST}, sign Git in to GitHub without a prompt: \`gh auth login\` then \`gh auth setup-git\` (or any Git credential helper).`
)
expect(failure?.nextSteps[1]).toContain('git@github.com:acme/private-notes.git')
})
it('recognises a disabled terminal prompt and an SSH key refusal', () => {
expect(
classifyPluginGitAccessFailure({
error: "fatal: could not read Username for 'https://gitlab.com': terminal prompts disabled",
url: 'https://gitlab.com/acme/tools/notes',
executionHost: HOST
})?.nextSteps[0]
).toContain('glab auth login')
const ssh = classifyPluginGitAccessFailure({
error:
'git@example.com: Permission denied (publickey).\nfatal: Could not read from remote repository.',
url: 'git@example.com:acme/notes.git',
executionHost: HOST
})
expect(ssh?.transport).toBe('ssh')
expect(ssh?.nextSteps[0]).toContain('ssh-agent')
})
it('leaves other failures alone and strips credentials from the detail', () => {
expect(
classifyPluginGitAccessFailure({
@@ -1,44 +0,0 @@
import { describe, expect, it } from 'vitest'
import { pluginNameClashKeys } from './plugin-name-clashes'
const item = (key: string, name: string, official = false) => ({
key,
name,
publisher: key.split('.')[0],
official
})
describe('pluginNameClashKeys', () => {
it('marks only the non-official side of a clash', () => {
expect(
pluginNameClashKeys([
item('stablyai.orca-catppuccin-theme', 'Catppuccin for Orca', true),
item('vale-c.catppuccin', 'Catppuccin for Orca')
])
).toEqual(new Set(['vale-c.catppuccin']))
})
it('marks every side when none is official, ignoring case and spacing', () => {
expect(
pluginNameClashKeys([
item('acme.mocha', 'Mocha'),
item('beta.mocha', ' mocha '),
item('c.x', 'X')
])
).toEqual(new Set(['acme.mocha', 'beta.mocha']))
})
it('leaves unique names and official clashes plain', () => {
expect(
pluginNameClashKeys([
item('stablyai.a', 'Same', true),
item('stablyai.b', 'Same', true),
item('acme.c', 'Other')
]).size
).toBe(0)
})
it('does not count one item listed twice as a clash', () => {
expect(pluginNameClashKeys([item('acme.x', 'X'), item('acme.x', 'X')]).size).toBe(0)
})
})
@@ -2,16 +2,6 @@ import { describe, expect, it } from 'vitest'
import { createTerminalAgentStatusProcessor } from './terminal-agent-status-processor'
describe('createTerminalAgentStatusProcessor', () => {
it('appends OSC 7501 projections, marks them, and leaves the sequence in the stream', () => {
const process = createTerminalAgentStatusProcessor()
const data = 'x\x1b]7501;state=working:app=codex\x1b\\y'
const result = process(data)
expect(result.cleanData).toBe(data)
expect(result.payloads).toMatchObject([{ state: 'working', agentType: 'codex' }])
expect(result.programStatusPayloads?.has(result.payloads[0])).toBe(true)
expect(result.lastPayloadCleanOffset).toBe(data.length - 1)
})
it('merges OSC 9999 and OSC 7501 in one chunk, leaving titles untouched', () => {
const process = createTerminalAgentStatusProcessor()
const report = '\x1b]7501;state=working:app=claude-code\x1b\\'
@@ -37,14 +27,6 @@ describe('createTerminalAgentStatusProcessor', () => {
expect(activity(osc('state=clear'))).toEqual([null])
})
it('places each activity change at the end of its report in the clean output', () => {
const process = createTerminalAgentStatusProcessor()
const report = '\x1b]7501;state=working:app=x\x1b\\'
expect(process(`ab${report}cd`).programActivity).toEqual([
{ offset: 2 + report.length, activity: 'working' }
])
})
it('keeps OSC 9999 and OSC 7501 payloads in byte order', () => {
const process = createTerminalAgentStatusProcessor()
const osc9999Report = (state: string) => `\x1b]9999;{"state":"${state}"}\x07`
@@ -148,42 +148,3 @@ it('keeps the existing held/failure/expired-to-held request protocol without ter
expect(vi.getTimerCount()).toBe(0)
warn.mockRestore()
})
it.each([10, 100])(
'terminal completion for %i independent leases delivers once per owner with no followup requests',
async (n) => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
const onLost = vi.fn()
const request = vi.fn(
async (
params: AgentSessionQueuedMessageEditHoldParams
): Promise<AgentSessionQueuedMessageEditHoldResult> =>
params.action === 'release' ? { status: 'released' } : { status: 'gone' }
)
const leases = Array.from({ length: n }, (_, index) =>
startQueuedEditLease({
sessionId: `session-${index}`,
messageId: 'card',
editId: `edit-${index}`,
expectedBodyFingerprint: `base-${index}`,
request,
onLost
})
)
await Promise.all(leases.map((lease) => lease.acquired))
expect(onLost).toHaveBeenCalledTimes(n)
leases.forEach((lease, index) =>
expect(onLost.mock.calls[index]?.[0]).toEqual({
lease,
expected: `base-${index}`,
status: 'gone'
})
)
expect(request).toHaveBeenCalledTimes(n)
leases.forEach((lease) => lease.end())
await vi.advanceTimersByTimeAsync(4 * QUEUED_EDIT_LEASE_RENEW_MS)
expect(onLost).toHaveBeenCalledTimes(n)
expect(request).toHaveBeenCalledTimes(2 * n)
expect(vi.getTimerCount()).toBe(0)
}
)
@@ -8,52 +8,6 @@ import {
afterEach(closeSharedControlTestServers)
it('reports a silent socket as a keepalive timeout, with its uptime', async () => {
const server = await createServer({ disableAutoPong: true })
const closes: SharedControlSocketCloseRecord[] = []
const connection = new RemoteRuntimeSharedControlConnection(server.pairing, {
liveness: { pingIntervalMs: 50, livenessTimeoutMs: 200 },
onSocketClosed: (record) => closes.push(record)
})
await connection.request('worktree.ps', undefined, 1000)
await vi.waitFor(() => expect(closes.length).toBeGreaterThan(0), { timeout: 5000 })
expect(closes[0]).toMatchObject({
cause: 'keepalive_timeout',
closeCode: null,
state: 'ready',
lastRttMs: null
})
expect(closes[0]?.socketUptimeMs).toBeGreaterThanOrEqual(200)
expect(closes[0]?.readyUptimeMs).toBeGreaterThanOrEqual(0)
connection.close()
})
it('measures keepalive round trips and reports a server close with its code', async () => {
const server = await createServer()
const closes: SharedControlSocketCloseRecord[] = []
const publishedCauses: unknown[] = []
const connection: RemoteRuntimeSharedControlConnection = new RemoteRuntimeSharedControlConnection(
server.pairing,
{
liveness: { pingIntervalMs: 50, livenessTimeoutMs: 1000 },
onSocketClosed: (record) => closes.push(record),
onDiagnosticsChanged: (diagnostics) => publishedCauses.push(diagnostics.lastCloseCause)
}
)
await connection.request('worktree.ps', undefined, 1000)
await vi.waitFor(() => expect(connection.getDiagnostics().lastRttMs).toBeGreaterThanOrEqual(0))
server.closeClients()
await vi.waitFor(() => expect(closes.length).toBeGreaterThan(0))
expect(closes[0]).toMatchObject({ cause: 'socket_close', closeCode: 4001 })
expect(closes[0]?.lastRttMs).toBeGreaterThanOrEqual(0)
expect(publishedCauses).toContain('socket_close')
connection.close()
})
it('reports an unexpected binary frame as a protocol error, not a transport failure', async () => {
const server = await createServer({ sendBinaryAfterAuth: true })
const closes: SharedControlSocketCloseRecord[] = []
@@ -1,5 +1,5 @@
import { createHash } from 'node:crypto'
import { describe, expect, it, vi } from 'vitest'
import { describe, expect, it } from 'vitest'
import {
encodeMobileE2EEV2Transcript,
validateMobileE2EEV2Handshake
@@ -7,11 +7,6 @@ import {
import { createMobileE2EEV2Fixture, MOBILE_E2EE_V2_VECTOR } from './mobile-e2ee-v2-fixtures'
describe('mobile E2EE v2 contract', () => {
it('validates the exact relay handshake and canonical encodings', () => {
const { hello, ready } = createMobileE2EEV2Fixture()
expect(validateMobileE2EEV2Handshake(hello, ready)).not.toBeNull()
})
it('rejects unknown fields, noncanonical base64, and an unechoed nonce', () => {
const { hello, ready } = createMobileE2EEV2Fixture()
expect(validateMobileE2EEV2Handshake({ ...hello, extra: true }, ready)).toBeNull()
@@ -29,30 +24,6 @@ describe('mobile E2EE v2 contract', () => {
).toBeNull()
})
it('rejects each oversized handshake field without decoding it', () => {
const { hello, ready } = createMobileE2EEV2Fixture()
const oversized = 'A'.repeat(48)
const decode = vi.spyOn(globalThis, 'atob')
try {
expect(validateMobileE2EEV2Handshake(hello, ready)).not.toBeNull()
for (const [invalidHello, invalidReady] of [
[{ ...hello, clientPublicKeyB64: oversized }, ready],
[hello, { ...ready, desktopPublicKeyB64: oversized }],
[
{ ...hello, clientNonceB64: oversized },
{ ...ready, clientNonceB64: oversized }
],
[hello, { ...ready, desktopNonceB64: oversized }]
]) {
decode.mockClear()
expect(validateMobileE2EEV2Handshake(invalidHello, invalidReady)).toBeNull()
expect(decode).not.toHaveBeenCalledWith(oversized)
}
} finally {
decode.mockRestore()
}
})
it('rejects context and capability-selection changes', () => {
const { hello, ready } = createMobileE2EEV2Fixture()
expect(
@@ -58,31 +58,4 @@ describe('mobile E2EE v2 framing', () => {
expect(attempt({ payloadKind: 'text' })).toBeNull()
expect(attempt({ sessionId: new Uint8Array(32).fill(9) })).toBeNull()
})
it('round-trips text and binary frames with successive counters', () => {
const kinds = ['text', 'binary', 'text'] as const
const frames = kinds.map((payloadKind, counter) =>
sealMobileE2EEV2Frame({
payload: new Uint8Array([counter]),
key,
sessionId,
direction: 'desktop-to-mobile',
payloadKind,
counter: BigInt(counter)
})
)
for (let counter = 0; counter < frames.length; counter++) {
expect(
openMobileE2EEV2Frame({
frame: frames[counter]!,
key,
sessionId,
direction: 'desktop-to-mobile',
payloadKind: kinds[counter]!,
expectedCounter: BigInt(counter)
})
).toEqual(new Uint8Array([counter]))
}
})
})