mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 02:27:43 +00:00
Desktop, CLI, web and runtime transports now take a PairedRuntimeConnection (endpoint, pinned host key, credential) instead of a PairingOffer. Offers stay on the pairing input path (codes, QR, links, environment add) and are converted with pairedRuntimeConnectionFromOffer. The credential is token-only today; request envelopes still carry the same deviceToken, so the wire is unchanged. This lets device-key and ticket credentials replace the token later without touching every connect call site again. Co-authored-by: m4air <m4air@Mac.localdomain>
149 lines
5.7 KiB
TypeScript
149 lines
5.7 KiB
TypeScript
import { pairedRuntimeConnectionFromOffer } from './paired-runtime-connection'
|
|
/**
|
|
* The subscribe path is the one that regressed: its connect failure rejects the subscribe
|
|
* promise rather than reaching `onError`, and that rejection crosses `ipcMain.handle`, which
|
|
* keeps only the message. So this file pins two things together — that the connect bound, not
|
|
* the subscription-start timer, is what fires against a silent host, and that the message it
|
|
* produces still classifies as recoverable once the code is gone. Split them and a future
|
|
* rewording passes both halves while dead-ending the terminal pane.
|
|
*/
|
|
import { createServer, type Server, type Socket } from 'node:net'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { generateKeyPair, publicKeyToBase64 } from './e2ee-crypto'
|
|
import { RemoteRuntimeClientError } from './remote-runtime-client-error'
|
|
import {
|
|
isRecoverableRemoteRuntimeConnectionError,
|
|
toRemoteRuntimeClientErrorLike
|
|
} from './remote-runtime-client-error-classification'
|
|
import { subscribeRemoteRuntimeTransport } from './remote-runtime-subscription-transport'
|
|
import { withRemoteRuntimeTailscaleHint } from './remote-runtime-tailscale-hint'
|
|
|
|
const servers = new Set<Server>()
|
|
const sockets = new Set<Socket>()
|
|
|
|
afterEach(async () => {
|
|
for (const socket of sockets) {
|
|
socket.destroy()
|
|
}
|
|
sockets.clear()
|
|
await Promise.all(
|
|
[...servers].map(
|
|
(server) =>
|
|
new Promise<void>((resolve) => {
|
|
server.close(() => resolve())
|
|
})
|
|
)
|
|
)
|
|
servers.clear()
|
|
})
|
|
|
|
/** Accepts TCP but never answers the upgrade — the same silence a black-holed host produces. */
|
|
async function listenSilentUpgradeServer(): Promise<string> {
|
|
const server = createServer((socket) => {
|
|
sockets.add(socket)
|
|
socket.once('close', () => sockets.delete(socket))
|
|
})
|
|
servers.add(server)
|
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
const address = server.address()
|
|
if (address === null || typeof address === 'string') {
|
|
throw new Error('expected a TCP address')
|
|
}
|
|
return `ws://127.0.0.1:${address.port}`
|
|
}
|
|
|
|
describe('remote runtime subscription connect bound', () => {
|
|
it('closes an unanswered real upgrade immediately when setup is cancelled', async () => {
|
|
const endpoint = await listenSilentUpgradeServer()
|
|
const keyPair = generateKeyPair()
|
|
const controller = new AbortController()
|
|
const pending = subscribeRemoteRuntimeTransport(
|
|
pairedRuntimeConnectionFromOffer({
|
|
v: 2,
|
|
endpoint,
|
|
deviceToken: 'device-token',
|
|
publicKeyB64: publicKeyToBase64(keyPair.publicKey)
|
|
}),
|
|
'files.search',
|
|
{ worktree: 'id:fixture', query: 'needle' },
|
|
15_000,
|
|
{ onResponse: vi.fn(), onError: vi.fn(), onClose: vi.fn() },
|
|
{ signal: controller.signal }
|
|
)
|
|
const rejection = expect(pending).rejects.toMatchObject({ name: 'AbortError' })
|
|
await vi.waitFor(() => expect(sockets.size).toBe(1))
|
|
for (const socket of sockets) {
|
|
socket.resume()
|
|
}
|
|
controller.abort()
|
|
await rejection
|
|
await vi.waitFor(() => expect(sockets.size).toBe(0))
|
|
})
|
|
|
|
it('creates no socket for a pre-aborted request', async () => {
|
|
const endpoint = await listenSilentUpgradeServer()
|
|
const keyPair = generateKeyPair()
|
|
const controller = new AbortController()
|
|
controller.abort()
|
|
await expect(
|
|
subscribeRemoteRuntimeTransport(
|
|
pairedRuntimeConnectionFromOffer({
|
|
v: 2,
|
|
endpoint,
|
|
deviceToken: 'device-token',
|
|
publicKeyB64: publicKeyToBase64(keyPair.publicKey)
|
|
}),
|
|
'files.search',
|
|
{},
|
|
15_000,
|
|
{ onResponse: vi.fn(), onError: vi.fn() },
|
|
{ signal: controller.signal }
|
|
)
|
|
).rejects.toMatchObject({ name: 'AbortError' })
|
|
expect(sockets.size).toBe(0)
|
|
})
|
|
|
|
it('fails an unanswered subscribe on the connect bound, with a message that survives the code strip', async () => {
|
|
const endpoint = await listenSilentUpgradeServer()
|
|
const keyPair = generateKeyPair()
|
|
|
|
const rejection = await subscribeRemoteRuntimeTransport(
|
|
pairedRuntimeConnectionFromOffer({
|
|
v: 2,
|
|
endpoint,
|
|
deviceToken: 'device-token',
|
|
publicKeyB64: publicKeyToBase64(keyPair.publicKey)
|
|
}),
|
|
'terminal.multiplex',
|
|
{},
|
|
// Why: comfortably longer than the connect bound, reproducing production's
|
|
// 12s < 15s ordering. If the bound stops firing, the start timer wins and
|
|
// the code/message assertions below change.
|
|
2_000,
|
|
{ onResponse: vi.fn(), onError: vi.fn(), onClose: vi.fn() },
|
|
{ connectTimeoutMs: 150 }
|
|
).then(
|
|
() => null,
|
|
(reason: unknown) => reason
|
|
)
|
|
|
|
expect(rejection).toBeInstanceOf(RemoteRuntimeClientError)
|
|
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: re-proved by the assertion above.
|
|
const error = rejection as RemoteRuntimeClientError
|
|
// The connect bound, not the subscription-start timer.
|
|
expect(error.code).toBe('remote_runtime_unavailable')
|
|
expect(error.code).not.toBe('runtime_timeout')
|
|
expect(error.message).toContain(endpoint)
|
|
|
|
// Loss of contact is never evidence the host's work stopped.
|
|
expect(error.message).toContain('unverifiable')
|
|
expect(error.message).not.toMatch(/\b(exited|gone|stopped|empty|no terminals)\b/i)
|
|
|
|
// What the renderer actually sees: ipcMain.handle forwards the message only.
|
|
const stripped = toRemoteRuntimeClientErrorLike(new Error(error.message))
|
|
expect(stripped.code).toBeUndefined()
|
|
expect(isRecoverableRemoteRuntimeConnectionError(stripped)).toBe(true)
|
|
expect(withRemoteRuntimeTailscaleHint(error.message, endpoint)).not.toBe(error.message)
|
|
})
|
|
})
|