mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: strengthen relay, persistence, and benchmark checks (#33563)
* test: strengthen relay, persistence, and benchmark checks * test: prune fixed journal wait allowance --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -165,7 +165,6 @@
|
||||
1 test-no-real-time-wait:mobile/src/terminal/terminal-web-document-mount.test.ts
|
||||
1 test-no-real-time-wait:mobile/src/terminal/terminal-webview-tap-routing.test.ts
|
||||
1 test-no-real-time-wait:mobile/src/transport/cellular-handshake-stall-real-socket.test.ts
|
||||
1 test-no-real-time-wait:mobile/src/transport/mobile-relay-pairing-journal-store.test.ts
|
||||
1 test-no-real-time-wait:mobile/src/transport/mobile-relay-runtime-failover.test.ts
|
||||
4 test-no-real-time-wait:mobile/src/transport/rpc-client-live-recovery.test.ts
|
||||
1 test-no-real-time-wait:mobile/src/transport/rpc-operation-barrier.test.ts
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
// Compare per-comparison localeCompare options with hoisted Intl.Collator on Source Control refresh; assert identical sorted output before timing.
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { build } from 'esbuild'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
@@ -26,26 +27,40 @@ function conflictRank(entry) {
|
||||
return 2
|
||||
}
|
||||
|
||||
// Pre-fix: resolves a collator per comparison.
|
||||
// Per-call baseline preserves production's locale and deterministic numeric ties.
|
||||
function compareBefore(a, b) {
|
||||
return (
|
||||
conflictRank(a) - conflictRank(b) || a.path.localeCompare(b.path, undefined, { numeric: true })
|
||||
conflictRank(a) - conflictRank(b) ||
|
||||
a.path.localeCompare(b.path, 'en', { numeric: true }) ||
|
||||
(a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
|
||||
)
|
||||
}
|
||||
|
||||
// Post-fix: one hoisted collator, mirroring source-control-status-sort.ts.
|
||||
const collator = new Intl.Collator(undefined, { numeric: true })
|
||||
function compareAfter(a, b) {
|
||||
return conflictRank(a) - conflictRank(b) || collator.compare(a.path, b.path)
|
||||
}
|
||||
const { outputFiles } = await build({
|
||||
entryPoints: [
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
'../../src/renderer/src/components/right-sidebar/source-control-status-sort.ts',
|
||||
import.meta.url
|
||||
)
|
||||
)
|
||||
],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false
|
||||
})
|
||||
const { compareGitStatusEntries: compareAfter } = await import(
|
||||
`data:text/javascript;base64,${Buffer.from(outputFiles[0].text).toString('base64')}`
|
||||
)
|
||||
|
||||
// Use real byte-sorted Git paths: shuffled input inflates win over nearly sorted production arrays.
|
||||
const REPO_PATHS = execFileSync('git', ['ls-files'], {
|
||||
const REPO_PATHS = execFileSync('git', ['ls-files', '-z'], {
|
||||
cwd: fileURLToPath(new URL('../..', import.meta.url)),
|
||||
maxBuffer: 256 * 1024 * 1024
|
||||
})
|
||||
.toString()
|
||||
.split('\n')
|
||||
.split('\0')
|
||||
.filter(Boolean)
|
||||
|
||||
function makeEntries(count) {
|
||||
@@ -88,7 +103,7 @@ for (const count of [3, 17, 26, 63, 308, 1000]) {
|
||||
const entries = makeEntries(count)
|
||||
const before = [...entries].sort(compareBefore).map((entry) => entry.path)
|
||||
const after = [...entries].sort(compareAfter).map((entry) => entry.path)
|
||||
if (before.join('\n') !== after.join('\n')) {
|
||||
if (before.length !== after.length || before.some((path, index) => path !== after[index])) {
|
||||
throw new Error(`sort order differs at ${count} files`)
|
||||
}
|
||||
const beforeMs = measure(compareBefore, entries)
|
||||
|
||||
@@ -11,20 +11,7 @@ assert(
|
||||
sources.every((source) => source.includes('export function buildRemovedSshTargetCleanupPatch'))
|
||||
)
|
||||
|
||||
async function load(source, instrument = false) {
|
||||
if (instrument) {
|
||||
const spread = '...nextTabsByWorktree'
|
||||
assert.equal(source.split(spread).length, 2)
|
||||
source = source.replace(spread, '...countTabMapCopy(nextTabsByWorktree)')
|
||||
source += `
|
||||
export const tabMapCopies = { count: 0, entries: 0 };
|
||||
function countTabMapCopy(map) {
|
||||
tabMapCopies.count++;
|
||||
tabMapCopies.entries += Reflect.ownKeys(map).length;
|
||||
return map;
|
||||
}
|
||||
`
|
||||
}
|
||||
async function load(source) {
|
||||
source += "\nexport { toAppSshPtyId } from '../../../../shared/ssh-pty-id';"
|
||||
const result = await build({
|
||||
entryPoints: [entry],
|
||||
@@ -62,7 +49,7 @@ function emptyState() {
|
||||
tabsByWorktree: {},
|
||||
ptyIdsByTabId: {},
|
||||
lastKnownRelayPtyIdByTabId: {},
|
||||
pendingCodexPaneRestartIds: {},
|
||||
pendingPaneAccountRestarts: {},
|
||||
codexRestartNoticeByPtyId: {},
|
||||
deferredSshSessionIdsByTabId: {},
|
||||
pendingReconnectPtyIdByTabId: {},
|
||||
@@ -72,6 +59,7 @@ function emptyState() {
|
||||
sshTargetLabels: new Map(),
|
||||
sshTargetGenerations: new Map(),
|
||||
portForwardsByConnection: {},
|
||||
unrestoredPortForwardsByConnection: {},
|
||||
detectedPortsByConnection: {},
|
||||
sshCredentialQueue: []
|
||||
}
|
||||
@@ -201,7 +189,7 @@ for (let iteration = 0; iteration < 3000; iteration++) {
|
||||
state.pendingReconnectPtyIdByTabId[id] = toAppSshPtyId(owner, 'reconnect')
|
||||
}
|
||||
if (ptyId) {
|
||||
state.pendingCodexPaneRestartIds[ptyId] = true
|
||||
state.pendingPaneAccountRestarts[ptyId] = 'codex'
|
||||
state.codexRestartNoticeByPtyId[ptyId] = {
|
||||
previousAccountLabel: 'old',
|
||||
nextAccountLabel: 'new'
|
||||
@@ -227,35 +215,13 @@ for (let iteration = 0; iteration < 3000; iteration++) {
|
||||
}
|
||||
console.log('3,000 frozen-state full-patch / identity differential cases passed')
|
||||
|
||||
// Instrument only the copy site for counts; timing arms above remain uninstrumented.
|
||||
const counted = await Promise.all(sources.map((source) => load(source, true)))
|
||||
for (const stride of [1, 10, 0]) {
|
||||
const state = timedState(100, stride, false)
|
||||
const expectedCopies = stride === 0 ? [0, 0] : [100 / stride, 1]
|
||||
counted.forEach((module, index) => {
|
||||
module.tabMapCopies.count = 0
|
||||
module.tabMapCopies.entries = 0
|
||||
assert.deepEqual(
|
||||
module.buildRemovedSshTargetCleanupPatch(state, 'removed'),
|
||||
arms[index](state, 'removed')
|
||||
)
|
||||
assert.deepEqual(module.tabMapCopies, {
|
||||
count: expectedCopies[index],
|
||||
entries: expectedCopies[index] * 100
|
||||
})
|
||||
})
|
||||
console.log(
|
||||
JSON.stringify({ stride, copiedEntries: counted.map((module) => module.tabMapCopies.entries) })
|
||||
)
|
||||
}
|
||||
|
||||
function sample(arm, state, repeats) {
|
||||
function sample(arm, state, repeats, expectedChanged) {
|
||||
const start = performance.now()
|
||||
let changed = 0
|
||||
for (let i = 0; i < repeats; i++) {
|
||||
changed += arm(state, 'removed') !== null ? 1 : 0
|
||||
}
|
||||
assert(changed === 0 || changed === repeats)
|
||||
assert.equal(changed, expectedChanged ? repeats : 0)
|
||||
return (performance.now() - start) / repeats
|
||||
}
|
||||
|
||||
@@ -281,19 +247,22 @@ for (const [count, stride, catalog, tabsPerWorkspace] of [
|
||||
[500, 1, true, 4]
|
||||
]) {
|
||||
const state = timedState(count, stride, catalog, tabsPerWorkspace)
|
||||
compare(state, 'removed')
|
||||
const expectedChanged = compare(state, 'removed')[0] !== null
|
||||
for (const arm of arms) {
|
||||
const until = performance.now() + 80
|
||||
while (performance.now() < until) {
|
||||
sample(arm, state, 1)
|
||||
sample(arm, state, 1, expectedChanged)
|
||||
}
|
||||
}
|
||||
const repeats = Math.max(1, Math.min(20000, Math.ceil(40 / sample(arms[0], state, 1))))
|
||||
const repeats = Math.max(
|
||||
1,
|
||||
Math.min(20000, Math.ceil(40 / sample(arms[0], state, 1, expectedChanged)))
|
||||
)
|
||||
/** @type {number[][]} */
|
||||
const samples = [[], []]
|
||||
for (let pair = 0; pair < 8; pair++) {
|
||||
for (const index of pair % 2 ? [1, 0] : [0, 1]) {
|
||||
samples[index].push(sample(arms[index], state, repeats))
|
||||
samples[index].push(sample(arms[index], state, repeats, expectedChanged))
|
||||
}
|
||||
}
|
||||
const median = samples.map((values) => {
|
||||
|
||||
@@ -72,10 +72,6 @@ function dependencies(journal: MobileRelayDirectUpgradeJournal | null = null) {
|
||||
describe('existing direct pairing relay upgrade', () => {
|
||||
it('persists pending material before install and publishes only after committed status', async () => {
|
||||
const deps = dependencies()
|
||||
let journal: MobileRelayDirectUpgradeJournal | null = null
|
||||
deps.writeJournal.mockImplementation(async (next) => {
|
||||
journal = next
|
||||
})
|
||||
const client = clientWith([
|
||||
success({ v: 1, relay }),
|
||||
success({
|
||||
@@ -105,13 +101,34 @@ describe('existing direct pairing relay upgrade', () => {
|
||||
|
||||
const result = await upgradeDirectMobileRelay({ client, host, dependencies: deps })
|
||||
|
||||
expect(journal).not.toBeNull()
|
||||
expect(deps.writeJournal).toHaveBeenCalledOnce()
|
||||
const journal = deps.writeJournal.mock.calls[0]?.[0]
|
||||
if (!journal) {
|
||||
throw new Error('Upgrade did not persist pending material')
|
||||
}
|
||||
expect(client.sendRequest).toHaveBeenNthCalledWith(1, 'pairing.getEndpoints', {
|
||||
installReqId: journal.reqId
|
||||
})
|
||||
expect(client.sendRequest).toHaveBeenNthCalledWith(3, 'pairing.getEndpoints', {
|
||||
installReqId: journal.reqId
|
||||
})
|
||||
expect(deps.writeBundle).toHaveBeenCalledWith({
|
||||
v: 1,
|
||||
hostId: host.id,
|
||||
deviceToken: host.deviceToken,
|
||||
current: {
|
||||
token: journal.pendingResumeToken,
|
||||
hash: journal.pendingResumeTokenHash,
|
||||
version: 1,
|
||||
expiresAt: 9_999_999
|
||||
}
|
||||
})
|
||||
expect(deps.writeJournal.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
vi.mocked(client.sendRequest).mock.invocationCallOrder[0]!
|
||||
)
|
||||
expect(client.sendRequest).toHaveBeenNthCalledWith(2, 'pairing.provisionRelay', {
|
||||
reqId: journal!.reqId,
|
||||
newResumeTokenHash: journal!.pendingResumeTokenHash
|
||||
reqId: journal.reqId,
|
||||
newResumeTokenHash: journal.pendingResumeTokenHash
|
||||
})
|
||||
expect(deps.writeBundle).toHaveBeenCalledBefore(deps.setRelayRouting)
|
||||
expect(deps.setRelayRouting).toHaveBeenCalledWith(host.id, relay)
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('pairing invite director resolution', () => {
|
||||
type: 'relay-moved',
|
||||
v: 1,
|
||||
cellUrl: 'https://relay-c2.onorca.dev',
|
||||
assignmentEpoch: 7,
|
||||
assignmentEpoch: 8,
|
||||
targetFromCell: true
|
||||
})
|
||||
})
|
||||
|
||||
@@ -114,6 +114,15 @@ describe('mobile relay pairing journal store', () => {
|
||||
expect(asyncStorage.setItem.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
secureStore.setItemAsync.mock.invocationCallOrder[0]!
|
||||
)
|
||||
expect(asyncStorage.setItem).toHaveBeenCalledWith(
|
||||
'orca:mobile-relay:pairing-journal:v1',
|
||||
JSON.stringify(journal.metadata)
|
||||
)
|
||||
expect(secureStore.setItemAsync).toHaveBeenCalledWith(
|
||||
'orca.mobile-relay.pairing-journal.v1',
|
||||
JSON.stringify(journal.secrets),
|
||||
expect.objectContaining({ keychainAccessible: 'WHEN_UNLOCKED_THIS_DEVICE_ONLY' })
|
||||
)
|
||||
expect(metadataRaw).not.toContain(offer.deviceToken)
|
||||
expect(metadataRaw).not.toContain(offer.relay.inviteToken)
|
||||
expect(metadataRaw).not.toContain(journal.secrets.pendingResumeToken)
|
||||
@@ -238,12 +247,17 @@ describe('mobile relay pairing journal store', () => {
|
||||
const secretReadGate = new Promise<void>((resolve) => {
|
||||
releaseSecretRead = resolve
|
||||
})
|
||||
let enteredSecretRead = () => {}
|
||||
const secretReadStarted = new Promise<void>((resolve) => {
|
||||
enteredSecretRead = resolve
|
||||
})
|
||||
secureStore.getItemAsync.mockImplementationOnce(async () => {
|
||||
enteredSecretRead()
|
||||
await secretReadGate
|
||||
return secretRaw
|
||||
})
|
||||
const loading = loadMobileRelayPairingJournal()
|
||||
await vi.waitFor(() => expect(secureStore.getItemAsync).toHaveBeenCalled())
|
||||
await secretReadStarted
|
||||
|
||||
const replacement = createMobileRelayPairingJournal({
|
||||
offer: offer as PairingOffer & { relay: NonNullable<PairingOffer['relay']> },
|
||||
@@ -379,33 +393,38 @@ describe('mobile relay pairing journal store', () => {
|
||||
})
|
||||
|
||||
it('makes a superseding scan wait for a publication in progress, and refuses a stale claim', async () => {
|
||||
const created = createMobileRelayPairingJournal({
|
||||
offer: offer as PairingOffer & { relay: NonNullable<PairingOffer['relay']> },
|
||||
hostId: 'host-1',
|
||||
hostName: 'Blue Whale',
|
||||
randomBytes: (length) => new Uint8Array(length).fill(10)
|
||||
})
|
||||
await saveMobileRelayPairingJournal(created)
|
||||
releaseMobileRelayPairingJournal(created.metadata.journalId)
|
||||
const release = await claimMobileRelayPairingJournalPublication(created.metadata.journalId)
|
||||
const replacement = createMobileRelayPairingJournal({
|
||||
offer: offer as PairingOffer & { relay: NonNullable<PairingOffer['relay']> },
|
||||
hostId: 'host-2',
|
||||
hostName: 'Red Panda',
|
||||
randomBytes: (length) => new Uint8Array(length).fill(12)
|
||||
})
|
||||
let saved = false
|
||||
const save = saveMobileRelayPairingJournal(replacement).then(() => (saved = true))
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
expect(saved).toBe(false)
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const created = createMobileRelayPairingJournal({
|
||||
offer: offer as PairingOffer & { relay: NonNullable<PairingOffer['relay']> },
|
||||
hostId: 'host-1',
|
||||
hostName: 'Blue Whale',
|
||||
randomBytes: (length) => new Uint8Array(length).fill(10)
|
||||
})
|
||||
await saveMobileRelayPairingJournal(created)
|
||||
releaseMobileRelayPairingJournal(created.metadata.journalId)
|
||||
const release = await claimMobileRelayPairingJournalPublication(created.metadata.journalId)
|
||||
const replacement = createMobileRelayPairingJournal({
|
||||
offer: offer as PairingOffer & { relay: NonNullable<PairingOffer['relay']> },
|
||||
hostId: 'host-2',
|
||||
hostName: 'Red Panda',
|
||||
randomBytes: (length) => new Uint8Array(length).fill(12)
|
||||
})
|
||||
let saved = false
|
||||
const save = saveMobileRelayPairingJournal(replacement).then(() => (saved = true))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(saved).toBe(false)
|
||||
|
||||
await clearMobileRelayPairingJournal(created.metadata.journalId)
|
||||
release()
|
||||
await save
|
||||
await expect(loadMobileRelayPairingJournal()).resolves.toEqual(replacement)
|
||||
await expect(
|
||||
claimMobileRelayPairingJournalPublication(created.metadata.journalId)
|
||||
).rejects.toThrow(/stale/)
|
||||
await clearMobileRelayPairingJournal(created.metadata.journalId)
|
||||
release()
|
||||
await save
|
||||
await expect(loadMobileRelayPairingJournal()).resolves.toEqual(replacement)
|
||||
await expect(
|
||||
claimMobileRelayPairingJournalPublication(created.metadata.journalId)
|
||||
).rejects.toThrow(/stale/)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('self-heals an undecryptable Android secret so the next QR scan can pair', async () => {
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('mobile relay resume director', () => {
|
||||
)
|
||||
await expect(
|
||||
resolveMobileRelayEndpoint({ relay, resumeToken: 'A'.repeat(43), fetchImpl: badTarget })
|
||||
).rejects.toThrow()
|
||||
).rejects.toThrow(/cellUrl/)
|
||||
|
||||
const oversized = vi.fn(
|
||||
async () =>
|
||||
@@ -62,6 +62,24 @@ describe('mobile relay resume director', () => {
|
||||
await expect(
|
||||
resolveMobileRelayEndpoint({ relay, resumeToken: 'A'.repeat(43), fetchImpl: oversized })
|
||||
).rejects.toThrow(/too large/)
|
||||
|
||||
const oversizedBody =
|
||||
' '.repeat(16 * 1024) +
|
||||
JSON.stringify({
|
||||
v: 1,
|
||||
cellUrl: 'https://relay-c2.onorca.dev',
|
||||
assignmentEpoch: 8,
|
||||
leaseExpiresAt: 1
|
||||
})
|
||||
for (const headers of [new Headers(), new Headers({ 'content-length': '1' })]) {
|
||||
await expect(
|
||||
resolveMobileRelayEndpoint({
|
||||
relay,
|
||||
resumeToken: 'A'.repeat(43),
|
||||
fetchImpl: async () => new Response(oversizedBody, { headers })
|
||||
})
|
||||
).rejects.toThrow(/too large/)
|
||||
}
|
||||
})
|
||||
|
||||
it('carries the bounded-overload Retry-After off a rejected resolve', async () => {
|
||||
|
||||
@@ -263,7 +263,7 @@ describe('relay runtime recovery without direct connectivity', () => {
|
||||
expect(openRelay).toHaveBeenCalledTimes(2)
|
||||
expect(openRelay).toHaveBeenLastCalledWith(
|
||||
relay,
|
||||
expect.objectContaining({ version: 3 }),
|
||||
expect.objectContaining({ token: 'token-v3'.padEnd(43, 'A'), version: 3 }),
|
||||
expect.any(String),
|
||||
expect.any(Function)
|
||||
)
|
||||
@@ -351,7 +351,7 @@ describe('relay runtime recovery without direct connectivity', () => {
|
||||
expect(deps.openRelay).toHaveBeenCalledOnce()
|
||||
expect(deps.openRelay).toHaveBeenLastCalledWith(
|
||||
relay,
|
||||
expect.objectContaining({ version: 2 }),
|
||||
expect.objectContaining({ token: 'token-v2'.padEnd(43, 'A'), version: 2 }),
|
||||
expect.any(String),
|
||||
expect.any(Function)
|
||||
)
|
||||
@@ -380,7 +380,7 @@ describe('relay runtime recovery without direct connectivity', () => {
|
||||
expect(openRelay).toHaveBeenCalledTimes(2)
|
||||
expect(openRelay).toHaveBeenLastCalledWith(
|
||||
relay,
|
||||
expect.objectContaining({ version: 1 }),
|
||||
expect.objectContaining({ token: 'token-v1'.padEnd(43, 'A'), version: 1 }),
|
||||
expect.any(String),
|
||||
expect.any(Function)
|
||||
)
|
||||
@@ -524,6 +524,7 @@ describe('failover with a real direct rpc-client', () => {
|
||||
await supervisor.start()
|
||||
await vi.advanceTimersByTimeAsync(3_000)
|
||||
|
||||
expect(DeadSocket.constructed).toContain(DIRECT_ENDPOINT)
|
||||
expect(deps.openRelay).toHaveBeenCalled()
|
||||
expect(logical.getActivePath()).toBe('relay')
|
||||
supervisor.stop()
|
||||
@@ -548,6 +549,7 @@ describe('failover with a real direct rpc-client', () => {
|
||||
await started
|
||||
await vi.advanceTimersByTimeAsync(3_000)
|
||||
|
||||
expect(DeadSocket.constructed).toContain(DIRECT_ENDPOINT)
|
||||
expect(deps.openRelay).toHaveBeenCalled()
|
||||
expect(logical.getActivePath()).toBe('relay')
|
||||
supervisor.stop()
|
||||
|
||||
@@ -90,6 +90,7 @@ describe('pairing keychain', () => {
|
||||
|
||||
const rotated = secureStoreMock.setItemAsync.mock.calls.at(-1)!
|
||||
expect(serviceOf(rotated[2] as Options)).toBe('orca.pairing.v1')
|
||||
expect(rotated[0]).toBe(TOKEN_KEY)
|
||||
expect(rotated[1]).toBe('token')
|
||||
expect(generationRecord).toBe('1')
|
||||
})
|
||||
@@ -209,6 +210,7 @@ describe('pairing keychain', () => {
|
||||
serviceOf(options) === 'orca.pairing.v1' ? 'token' : null
|
||||
)
|
||||
await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBe('token')
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object))
|
||||
})
|
||||
|
||||
it('reads through the rotated service once a rotation has been committed', async () => {
|
||||
@@ -218,6 +220,7 @@ describe('pairing keychain', () => {
|
||||
)
|
||||
|
||||
await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBe('rotated-token')
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object))
|
||||
})
|
||||
|
||||
it('falls back to a retired service so rotation does not orphan a still-readable token', async () => {
|
||||
@@ -227,6 +230,7 @@ describe('pairing keychain', () => {
|
||||
)
|
||||
|
||||
await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBe('legacy-token')
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object))
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
@@ -241,6 +245,7 @@ describe('pairing keychain', () => {
|
||||
})
|
||||
|
||||
await expect(readPairingKeychainItem(TOKEN_KEY)).rejects.toBe(currentError)
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object))
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@@ -280,11 +285,13 @@ describe('pairing keychain', () => {
|
||||
|
||||
// Treat Android’s indistinguishable absent/undecryptable null as absent so callers can clean orphans.
|
||||
await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBeNull()
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object))
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Keep presence so later reads cannot resurrect superseded legacy secrets.
|
||||
expect(presenceRecord).toBe('1')
|
||||
await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBeNull()
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object))
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
@@ -295,6 +302,7 @@ describe('pairing keychain', () => {
|
||||
)
|
||||
|
||||
await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBe('legacy-token')
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object))
|
||||
})
|
||||
|
||||
it('probes every bounded service when the generation record is temporarily unreadable', async () => {
|
||||
@@ -309,6 +317,7 @@ describe('pairing keychain', () => {
|
||||
)
|
||||
|
||||
await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBe('rotated-token')
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object))
|
||||
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenCalledTimes(7)
|
||||
})
|
||||
@@ -328,10 +337,13 @@ describe('pairing keychain', () => {
|
||||
|
||||
await deletePairingKeychainItem(TOKEN_KEY)
|
||||
|
||||
const services = secureStoreMock.deleteItemAsync.mock.calls.map((call) =>
|
||||
serviceOf(call[1] as Options)
|
||||
)
|
||||
expect(services).toEqual(['orca.pairing.v2', 'orca.pairing.v1', undefined])
|
||||
expect(
|
||||
secureStoreMock.deleteItemAsync.mock.calls.map(([key, options]) => [key, serviceOf(options)])
|
||||
).toEqual([
|
||||
[TOKEN_KEY, 'orca.pairing.v2'],
|
||||
[TOKEN_KEY, 'orca.pairing.v1'],
|
||||
[TOKEN_KEY, undefined]
|
||||
])
|
||||
expect(asyncStorageMock.removeItem).toHaveBeenCalledWith(TOKEN_PRESENCE_KEY)
|
||||
})
|
||||
|
||||
@@ -345,25 +357,49 @@ describe('pairing keychain', () => {
|
||||
})
|
||||
|
||||
await expect(deletePairingKeychainItem(TOKEN_KEY)).rejects.toBe(deleteError)
|
||||
expect(secureStoreMock.deleteItemAsync).toHaveBeenCalledTimes(3)
|
||||
expect(
|
||||
secureStoreMock.deleteItemAsync.mock.calls.map(([key, options]) => [key, serviceOf(options)])
|
||||
).toEqual([
|
||||
[TOKEN_KEY, 'orca.pairing.v2'],
|
||||
[TOKEN_KEY, 'orca.pairing.v1'],
|
||||
[TOKEN_KEY, undefined]
|
||||
])
|
||||
expect(asyncStorageMock.removeItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('serializes writes that share the global generation', async () => {
|
||||
let releaseFirst!: () => void
|
||||
const firstWrite = new Promise<void>((resolve) => {
|
||||
releaseFirst = resolve
|
||||
vi.useFakeTimers()
|
||||
const entered = Promise.withResolvers<void>()
|
||||
const held = Promise.withResolvers<void>()
|
||||
const order: string[] = []
|
||||
secureStoreMock.setItemAsync.mockImplementation(async (key: string, value: string) => {
|
||||
order.push(`start:${key}:${value}`)
|
||||
if (key === TOKEN_KEY) {
|
||||
entered.resolve()
|
||||
await held.promise
|
||||
}
|
||||
order.push(`finish:${key}:${value}`)
|
||||
})
|
||||
secureStoreMock.setItemAsync.mockImplementationOnce(async () => firstWrite)
|
||||
try {
|
||||
const first = writePairingKeychainItem(TOKEN_KEY, 'first')
|
||||
const second = writePairingKeychainItem(`${TOKEN_KEY}.second`, 'second')
|
||||
await entered.promise
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(order).toEqual([`start:${TOKEN_KEY}:first`])
|
||||
|
||||
const first = writePairingKeychainItem(TOKEN_KEY, 'first')
|
||||
const second = writePairingKeychainItem(`${TOKEN_KEY}.second`, 'second')
|
||||
await vi.waitFor(() => expect(secureStoreMock.setItemAsync).toHaveBeenCalledTimes(1))
|
||||
held.resolve()
|
||||
await Promise.all([first, second])
|
||||
|
||||
releaseFirst()
|
||||
await Promise.all([first, second])
|
||||
|
||||
expect(secureStoreMock.setItemAsync).toHaveBeenCalledTimes(2)
|
||||
expect(order).toEqual([
|
||||
`start:${TOKEN_KEY}:first`,
|
||||
`finish:${TOKEN_KEY}:first`,
|
||||
`start:${TOKEN_KEY}.second:second`,
|
||||
`finish:${TOKEN_KEY}.second:second`
|
||||
])
|
||||
} finally {
|
||||
held.resolve()
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['not-a-number', '', ' ', '0:pending'])(
|
||||
@@ -375,6 +411,7 @@ describe('pairing keychain', () => {
|
||||
)
|
||||
|
||||
await expect(readPairingKeychainItem(TOKEN_KEY)).resolves.toBe('rotated-token')
|
||||
expect(secureStoreMock.getItemAsync).toHaveBeenLastCalledWith(TOKEN_KEY, expect.any(Object))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -397,7 +434,22 @@ describe('pairing keychain', () => {
|
||||
|
||||
await deletePairingKeychainItem(TOKEN_KEY)
|
||||
|
||||
expect(secureStoreMock.deleteItemAsync).toHaveBeenCalledTimes(9)
|
||||
expect(
|
||||
secureStoreMock.deleteItemAsync.mock.calls.map(([key, options]) => [
|
||||
key,
|
||||
serviceOf(options)
|
||||
])
|
||||
).toEqual([
|
||||
[TOKEN_KEY, 'orca.pairing.v8'],
|
||||
[TOKEN_KEY, 'orca.pairing.v7'],
|
||||
[TOKEN_KEY, 'orca.pairing.v6'],
|
||||
[TOKEN_KEY, 'orca.pairing.v5'],
|
||||
[TOKEN_KEY, 'orca.pairing.v4'],
|
||||
[TOKEN_KEY, 'orca.pairing.v3'],
|
||||
[TOKEN_KEY, 'orca.pairing.v2'],
|
||||
[TOKEN_KEY, 'orca.pairing.v1'],
|
||||
[TOKEN_KEY, undefined]
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -291,7 +291,10 @@ describe('recovering pairing relay candidate', () => {
|
||||
})
|
||||
|
||||
await expect(candidate.sendRequest('status.get')).resolves.toEqual(success())
|
||||
expect(resolveDirector).toHaveBeenCalledOnce()
|
||||
expect(resolveDirector).toHaveBeenCalledExactlyOnceWith({
|
||||
...journal.metadata.relay,
|
||||
inviteToken: journal.secrets.inviteToken
|
||||
})
|
||||
})
|
||||
|
||||
it('bounds director recovery and applies full jitter to failures and target retries', async () => {
|
||||
|
||||
@@ -47,7 +47,10 @@ describe('served relay pairing recovery', () => {
|
||||
})
|
||||
|
||||
await expect(candidate.sendRequest('status.get')).resolves.toMatchObject({ ok: true })
|
||||
expect(resolveDirector).toHaveBeenCalledOnce()
|
||||
expect(resolveDirector).toHaveBeenCalledExactlyOnceWith({
|
||||
...journal.metadata.relay,
|
||||
inviteToken: journal.secrets.inviteToken
|
||||
})
|
||||
expect(persistMove).toHaveBeenCalledWith(resolvedRelay)
|
||||
})
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ function makeRuntimeWithSlowFirstWrite() {
|
||||
const firstWriteDurable = new Promise<void>((resolve) => {
|
||||
releaseFirstWrite = resolve
|
||||
})
|
||||
const firstWriteEntered = Promise.withResolvers<void>()
|
||||
let writes = 0
|
||||
const applyLayoutCommand = vi.fn(
|
||||
async (
|
||||
@@ -59,6 +60,7 @@ function makeRuntimeWithSlowFirstWrite() {
|
||||
}
|
||||
writes += 1
|
||||
if (writes === 1) {
|
||||
firstWriteEntered.resolve()
|
||||
await firstWriteDurable
|
||||
}
|
||||
return { ok: true, result: {} }
|
||||
@@ -66,7 +68,7 @@ function makeRuntimeWithSlowFirstWrite() {
|
||||
)
|
||||
const runtime = new OrcaRuntimeService(Object.assign(runtimeStore, { applyLayoutCommand }))
|
||||
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
|
||||
return { runtime, getSession, releaseFirstWrite }
|
||||
return { runtime, getSession, releaseFirstWrite, firstWriteEntered: firstWriteEntered.promise }
|
||||
}
|
||||
|
||||
function sleepCapture(updatedAt: number): WorktreeSleepResumeCapture {
|
||||
@@ -98,11 +100,11 @@ function sleepCapture(updatedAt: number): WorktreeSleepResumeCapture {
|
||||
|
||||
describe('a queued hook status and a workspace sleep', () => {
|
||||
it('keeps the sleep’s record when a status accepted before the sleep is written after it', async () => {
|
||||
const { runtime, getSession, releaseFirstWrite } = makeRuntimeWithSlowFirstWrite()
|
||||
const { runtime, getSession, releaseFirstWrite, firstWriteEntered } =
|
||||
makeRuntimeWithSlowFirstWrite()
|
||||
runtime.recordHookStatusResume(codexRow())
|
||||
await vi.waitFor(() =>
|
||||
expect(getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY]).toBeDefined()
|
||||
)
|
||||
await firstWriteEntered
|
||||
expect(getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY]).toBeDefined()
|
||||
runtime.recordHookStatusResume(codexRow({ state: 'blocked', receivedAt: 11 }))
|
||||
|
||||
const slept = runtime['sleepResumeRecords'].persist(TEST_WORKTREE_ID, sleepCapture(11), [
|
||||
@@ -120,11 +122,11 @@ describe('a queued hook status and a workspace sleep', () => {
|
||||
})
|
||||
|
||||
it('keeps a window sleep’s record when a status accepted before it is written after it', async () => {
|
||||
const { runtime, getSession, releaseFirstWrite } = makeRuntimeWithSlowFirstWrite()
|
||||
const { runtime, getSession, releaseFirstWrite, firstWriteEntered } =
|
||||
makeRuntimeWithSlowFirstWrite()
|
||||
runtime.recordHookStatusResume(codexRow())
|
||||
await vi.waitFor(() =>
|
||||
expect(getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY]).toBeDefined()
|
||||
)
|
||||
await firstWriteEntered
|
||||
expect(getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY]).toBeDefined()
|
||||
runtime.recordHookStatusResume(codexRow({ state: 'blocked', receivedAt: 11 }))
|
||||
|
||||
const [record] = sleepCapture(11).get(PTY_ID)!.records
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('HostAccountSignInFlows', () => {
|
||||
const flows = new HostAccountSignInFlows(runner.run, 'linux')
|
||||
const started = flows.start('device-a', 'claude')
|
||||
expect(started.phase).toBe('starting')
|
||||
await vi.waitFor(() => expect(runner.run).toHaveBeenCalledOnce())
|
||||
expect(runner.run).toHaveBeenCalledOnce()
|
||||
|
||||
runner.channel().onPrompt({ ...PROMPT, needsCode: true })
|
||||
const waiting = await flows.wait('device-a', started.flowId, started.revision)
|
||||
@@ -77,7 +77,7 @@ describe('HostAccountSignInFlows', () => {
|
||||
const runner = controllableRunner()
|
||||
const flows = new HostAccountSignInFlows(runner.run, 'linux')
|
||||
const { flowId } = flows.start('device-a', 'claude')
|
||||
await vi.waitFor(() => expect(runner.run).toHaveBeenCalledOnce())
|
||||
expect(runner.run).toHaveBeenCalledOnce()
|
||||
expect((await flows.cancel('device-a', flowId)).phase).toBe('cancelled')
|
||||
expect(runner.channel().signal.aborted).toBe(true)
|
||||
await expect(runner.channel().nextCode()).rejects.toThrow()
|
||||
@@ -115,7 +115,7 @@ describe('HostAccountSignInFlows', () => {
|
||||
const runner = controllableRunner()
|
||||
const flows = new HostAccountSignInFlows(runner.run, 'linux')
|
||||
const { flowId, revision } = flows.start('device-a', 'claude')
|
||||
await vi.waitFor(() => expect(runner.run).toHaveBeenCalledOnce())
|
||||
expect(runner.run).toHaveBeenCalledOnce()
|
||||
runner
|
||||
.channel()
|
||||
.onPrompt({ url: 'https://evil.test/?code=true', userCode: null, needsCode: true })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
import { RpcDispatcher } from './rpc/dispatcher'
|
||||
import type { RpcRequest } from './rpc/core'
|
||||
@@ -46,10 +46,12 @@ function graph(ptyId: string | null, extraLeafPtyId?: string) {
|
||||
|
||||
function makeHost(options: { livePtyIds: Set<string> }) {
|
||||
const writes: string[] = []
|
||||
const inputWritten = Promise.withResolvers<void>()
|
||||
const runtime = new OrcaRuntimeService(null)
|
||||
runtime.setPtyController({
|
||||
write: (_ptyId, data) => {
|
||||
writes.push(data)
|
||||
inputWritten.resolve()
|
||||
return true
|
||||
},
|
||||
kill: () => true,
|
||||
@@ -57,7 +59,7 @@ function makeHost(options: { livePtyIds: Set<string> }) {
|
||||
hasPty: (ptyId) => options.livePtyIds.has(ptyId)
|
||||
})
|
||||
runtime.attachWindow(1)
|
||||
return { runtime, writes }
|
||||
return { runtime, writes, inputWritten: inputWritten.promise }
|
||||
}
|
||||
|
||||
async function handleFor(runtime: OrcaRuntimeService): Promise<string> {
|
||||
@@ -67,6 +69,9 @@ async function handleFor(runtime: OrcaRuntimeService): Promise<string> {
|
||||
|
||||
function startMultiplex(runtime: OrcaRuntimeService) {
|
||||
const binaryFrames: StreamFrame[] = []
|
||||
const controlRegistered = Promise.withResolvers<void>()
|
||||
const subscribed = Promise.withResolvers<void>()
|
||||
const writeUnavailable = Promise.withResolvers<void>()
|
||||
const handlers = new Map<number, (frame: StreamFrame) => void>()
|
||||
const messages: { result?: { type?: string; streamId?: number } }[] = []
|
||||
const request: RpcRequest = {
|
||||
@@ -77,18 +82,29 @@ function startMultiplex(runtime: OrcaRuntimeService) {
|
||||
}
|
||||
void new RpcDispatcher({ runtime, methods: TERMINAL_METHODS }).dispatchStreaming(
|
||||
request,
|
||||
(message) => messages.push(JSON.parse(message)),
|
||||
(message) => {
|
||||
messages.push(JSON.parse(message))
|
||||
if (messages.at(-1)?.result?.type === 'subscribed') {
|
||||
subscribed.resolve()
|
||||
}
|
||||
},
|
||||
{
|
||||
connectionId: 'paired-client',
|
||||
sendBinary: (bytes: Uint8Array<ArrayBufferLike>) => {
|
||||
const frame = decodeTerminalStreamFrame(bytes)
|
||||
if (frame) {
|
||||
binaryFrames.push(frame)
|
||||
if (frame.opcode === TerminalStreamOpcode.WriteUnavailable) {
|
||||
writeUnavailable.resolve()
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
registerBinaryStreamHandler: (streamId: number, handler: (frame: StreamFrame) => void) => {
|
||||
handlers.set(streamId, handler)
|
||||
if (streamId === 0) {
|
||||
controlRegistered.resolve()
|
||||
}
|
||||
return () => handlers.delete(streamId)
|
||||
}
|
||||
}
|
||||
@@ -97,17 +113,26 @@ function startMultiplex(runtime: OrcaRuntimeService) {
|
||||
handlers.get(streamId)?.(
|
||||
decodeTerminalStreamFrame(encodeTerminalStreamFrame({ opcode, streamId, seq: 0, payload }))!
|
||||
)
|
||||
return { binaryFrames, handlers, messages, send }
|
||||
return {
|
||||
binaryFrames,
|
||||
handlers,
|
||||
messages,
|
||||
send,
|
||||
controlRegistered: controlRegistered.promise,
|
||||
subscribed: subscribed.promise,
|
||||
writeUnavailable: writeUnavailable.promise
|
||||
}
|
||||
}
|
||||
|
||||
describe('a host pane remounting onto its same live PTY (#23759)', () => {
|
||||
it('keeps the handle a paired client is typing into', async () => {
|
||||
const { runtime, writes } = makeHost({ livePtyIds: new Set([PTY_ID]) })
|
||||
const { runtime, writes, inputWritten } = makeHost({ livePtyIds: new Set([PTY_ID]) })
|
||||
runtime.syncWindowGraph(1, graph(PTY_ID))
|
||||
const handle = await handleFor(runtime)
|
||||
|
||||
const client = startMultiplex(runtime)
|
||||
await vi.waitFor(() => expect(client.handlers.has(0)).toBe(true))
|
||||
await client.controlRegistered
|
||||
expect(client.handlers.has(0)).toBe(true)
|
||||
client.send(
|
||||
0,
|
||||
TerminalStreamOpcode.Subscribe,
|
||||
@@ -118,16 +143,16 @@ describe('a host pane remounting onto its same live PTY (#23759)', () => {
|
||||
capabilities: { writeUnavailable: 1 }
|
||||
})
|
||||
)
|
||||
await vi.waitFor(() =>
|
||||
expect(client.messages.some((message) => message.result?.type === 'subscribed')).toBe(true)
|
||||
)
|
||||
await client.subscribed
|
||||
expect(client.messages.some((message) => message.result?.type === 'subscribed')).toBe(true)
|
||||
|
||||
runtime.syncWindowGraph(1, graph(null))
|
||||
runtime.syncWindowGraph(1, graph(PTY_ID))
|
||||
expect(await handleFor(runtime)).toBe(handle)
|
||||
|
||||
client.send(1, TerminalStreamOpcode.Input, encodeTerminalStreamText('ls\r'))
|
||||
await vi.waitFor(() => expect(writes).toEqual(['ls\r']))
|
||||
await Promise.race([inputWritten, client.writeUnavailable])
|
||||
expect(writes).toEqual(['ls\r'])
|
||||
expect(
|
||||
client.binaryFrames.some((frame) => frame.opcode === TerminalStreamOpcode.WriteUnavailable)
|
||||
).toBe(false)
|
||||
|
||||
Reference in New Issue
Block a user